diff options
Diffstat (limited to 'hyperkitty')
76 files changed, 16006 insertions, 0 deletions
diff --git a/hyperkitty/__init__.py b/hyperkitty/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/hyperkitty/__init__.py diff --git a/hyperkitty/apache/README.rst b/hyperkitty/apache/README.rst new file mode 100644 index 0000000..e06893a --- /dev/null +++ b/hyperkitty/apache/README.rst @@ -0,0 +1,17 @@ +Create logs directory +--------------------- +**mkdir -p logs** + +Create python egg directory +--------------------------- +**mkdir -p .python-egg** +chmod -R 777 .python-egg/ + + + +Edit httpd.conf +--------------- + +Add the following line in your httpd.conf (generally present at /etc/apache2/httpd.conf) + +Include "/path/to/application/apache/apache_django_wsgi.conf" diff --git a/hyperkitty/apache/__init__.py b/hyperkitty/apache/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/hyperkitty/apache/__init__.py diff --git a/hyperkitty/apache/apache_django_wsgi.conf b/hyperkitty/apache/apache_django_wsgi.conf new file mode 100644 index 0000000..7f1ccaf --- /dev/null +++ b/hyperkitty/apache/apache_django_wsgi.conf @@ -0,0 +1,23 @@ +<VirtualHost *:80> + + Alias /robots.txt /home/akhan/gsoc/robots.txt + Alias /favicon.ico /home/akhan/gsoc/favicon.ico + Alias /static /home/akhan/gsoc/static + + ErrorLog /home/akhan/gsoc/logs/error.log + CustomLog /home/akhan/gsoc/logs/access.log combined + + WSGIScriptAlias / /home/akhan/gsoc/apache/django.wsgi + WSGIDaemonProcess akhan user=akhan group=users threads=25 + + <Directory "/home/akhan/gsoc/apache"> + Order deny,allow + Allow from all + </Directory> + + <Directory "/home/akhan/gsoc"> + Order allow,deny + Allow from all + </Directory> + +</VirtualHost> diff --git a/hyperkitty/apache/django.wsgi b/hyperkitty/apache/django.wsgi new file mode 100755 index 0000000..cc699a4 --- /dev/null +++ b/hyperkitty/apache/django.wsgi @@ -0,0 +1,42 @@ +import os +import sys +import site + +STAGING=True + +if STAGING: + # staging virtual environment + vepath = '/home/akhan/.virtualenvs/wackyenv/lib/python2.7/site-packages' +else: + # live virtual environment + vepath = '/home/akhan/.virtualenvs/live-server/lib/python2.7/site-packages' + +prev_sys_path = list(sys.path) + +# add the site-packages of our virtualenv as a site dir +site.addsitedir(vepath) + +# add the app's directory to the PYTHONPATH +sys.path.append('/home/akhan/gsoc') + +# reorder sys.path so new directories from the addsitedir show up first +new_sys_path = [p for p in sys.path if p not in prev_sys_path] + +for item in new_sys_path: + sys.path.remove(item) +sys.path[:0] = new_sys_path + + +#Calculate the path based on the location of the WSGI script. +apache_configuration= os.path.dirname(__file__) +project = os.path.dirname(apache_configuration) +workspace = os.path.dirname(project) +sys.path.append(workspace) + + +os.environ['DJANGO_SETTINGS_MODULE'] = 'gsoc.apache.settings_production' +# make sure this directory is writable by wsgi process +os.environ['PYTHON_EGG_CACHE'] = '/home/akhan/gsoc/.python-egg' + +from django.core.handlers.wsgi import WSGIHandler +application = WSGIHandler() diff --git a/hyperkitty/apache/settings_production.py b/hyperkitty/apache/settings_production.py new file mode 100644 index 0000000..1090b7a --- /dev/null +++ b/hyperkitty/apache/settings_production.py @@ -0,0 +1,195 @@ +import os + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +# Django settings for hyperkitty project. + +DEBUG = False +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + ('Aamir Khan', 'syst3m.w0rm+hk@gmail.com'), +) + +MANAGERS = ADMINS + +MAILMAN_API_URL=r'http://%(username)s:%(password)s@localhost:8001/3.0/' +MAILMAN_USER='mailmanapi' +MAILMAN_PASS='88ffd62d1094a6248415c59d7538793f3df5de2f04d244087952394e689e902a' + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': 'hk', # Or path to database file if using sqlite3. + 'USER': 'root', # Not used with sqlite3. + 'PASSWORD': 'rootroot', # Not used with sqlite3. + 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '', # Set to empty string for default. Not used with sqlite3. + } +} + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# On Unix systems, a value of None will cause Django to use the same +# timezone as the operating system. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'America/Chicago' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale +USE_L10N = True + +# Absolute filesystem path to the directory that will hold user-uploaded files. +# Example: "/home/media/media.lawrence.com/media/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash. +# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" +MEDIA_URL = '' + +# Absolute path to the directory static files should be collected to. +# Don't put anything in this directory yourself; store your static files +# in apps' "static/" subdirectories and in STATICFILES_DIRS. +# Example: "/home/media/media.lawrence.com/static/" +#STATIC_ROOT = '' +STATIC_ROOT = BASE_DIR + '/static_files/' + +# URL prefix for static files. +# Example: "http://media.lawrence.com/static/" +STATIC_URL = '/static/' + +# URL prefix for admin static files -- CSS, JavaScript and images. +# Make sure to use a trailing slash. +# Examples: "http://foo.com/static/admin/", "/static/admin/". +ADMIN_MEDIA_PREFIX = '/static/admin/' + +# Additional locations of static files +STATICFILES_DIRS = ( + # Put strings here, like "/home/html/static" or "C:/www/django/static". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. + BASE_DIR + '/static/', +) + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +# 'django.contrib.staticfiles.finders.DefaultStorageFinder', +) + +# Make this unique, and don't share it with anybody. +SECRET_KEY = 'dtc3%x(k#mzpe32dmhtsb6!3p(izk84f7nuw1-+4x8zsxwsa^z' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.Loader', +) + + +TEMPLATE_CONTEXT_PROCESSORS = ( + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "django.core.context_processors.debug", + "django.core.context_processors.i18n", + "django.core.context_processors.media", + "django.core.context_processors.static", + "django.core.context_processors.csrf", + "django.contrib.messages.context_processors.messages", + "gsoc.context_processors.app_name", +) + + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +) + +ROOT_URLCONF = 'gsoc.apache.urls_production' + +TEMPLATE_DIRS = ( + # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. + BASE_DIR + '/templates', +) + +AUTHENTICATION_BACKENDS = ( + 'social_auth.backends.google.GoogleBackend', + 'social_auth.backends.yahoo.YahooBackend', + 'social_auth.backends.browserid.BrowserIDBackend', + 'social_auth.backends.OpenIDBackend', + 'django.contrib.auth.backends.ModelBackend', +) + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', +# 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.admin', + # 'django.contrib.admindocs', + 'gsoc', + 'social_auth', + 'djangorestframework', + 'gravatar', +) + + +LOGIN_URL = '/accounts/login/' +LOGIN_REDIRECT_URL = '/' +LOGIN_ERROR_URL = '/accounts/login/' +SOCIAL_AUTH_COMPLETE_URL_NAME = 'socialauth_complete' +SOCIAL_AUTH_ASSOCIATE_URL_NAME = 'socialauth_associate_complete' +SOCIAL_AUTH_DEFAULT_USERNAME = 'new_social_auth_user' +SOCIAL_AUTH_UUID_LENGTH = 16 + +AUTH_PROFILE_MODULE = 'gsoc.UserProfile' + + +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# the site admins on every HTTP 500 error. +# See http://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + } +} + +SOCIAL_AUTH_LAST_LOGIN = 'social_auth_last_login_backend' +APP_NAME = 'Fedora Mailman App' +KITTYSTORE_URL = 'postgres://mm3:mm3@localhost/mm3' diff --git a/hyperkitty/apache/urls_production.py b/hyperkitty/apache/urls_production.py new file mode 100644 index 0000000..3bb4a65 --- /dev/null +++ b/hyperkitty/apache/urls_production.py @@ -0,0 +1,87 @@ +from django.conf.urls.defaults import patterns, include, url +from django.views.generic.simple import direct_to_template +from gsoc.api import EmailResource, ThreadResource, SearchResource + +urlpatterns = patterns('', + # Account + url(r'^accounts/login/$', 'views.accounts.user_login', name='user_login'), + url(r'^accounts/logout/$', 'views.accounts.user_logout', name='user_logout'), + url(r'^accounts/profile/$', 'views.accounts.user_profile', name='user_profile'), + url(r'^accounts/register/$', 'views.accounts.user_registration', name='user_registration'), + + + # Index + url(r'^/$', 'views.pages.index', name='index'), + url(r'^$', 'views.pages.index', name='index'), + + # Archives + url(r'^archives/(?P<mlist_fqdn>.*@.*)/(?P<year>\d{4})/(?P<month>\d\d?)/(?P<day>\d\d?)/$', + 'views.list.archives'), + url(r'^archives/(?P<mlist_fqdn>.*@.*)/(?P<year>\d{4})/(?P<month>\d\d?)/$', + 'views.list.archives'), + url(r'^archives/(?P<mlist_fqdn>.*@.*)/$', + 'views.list.archives'), + + # Threads + url(r'^thread/(?P<mlist_fqdn>.*@.*)/(?P<threadid>.+)/$', + 'views.thread.thread_index'), + + + # Lists + url(r'^list/$', 'views.pages.index'), # Can I remove this URL? + url(r'^list/(?P<mlist_fqdn>.*@.*)/$', + 'views.list.list'), + + # Search Tag + url(r'^tag/(?P<mlist_fqdn>.*@.*)\/(?P<tag>.*)\/(?P<page>\d+)/$', + 'views.list.search_tag'), + url(r'^tag/(?P<mlist_fqdn>.*@.*)\/(?P<tag>.*)/$', + 'views.list.search_tag'), + + # Search + # If page number is present in URL + url(r'^search/(?P<mlist_fqdn>.*@.*)\/(?P<target>.*)\/(?P<keyword>.*)\/(?P<page>\d+)/$', + 'views.list.search_keyword'), + # Show the first page as default when no page number is present in URL + url(r'^search/(?P<mlist_fqdn>.*@.*)\/(?P<target>.*)\/(?P<keyword>.*)/$', + 'views.list.search_keyword'), + url(r'^search/(?P<mlist_fqdn>.*@.*)/$', + 'views.list.search'), + + + ### MESSAGE LEVEL VIEWS ### + # Vote a message + url(r'^message/(?P<mlist_fqdn>.*@.*)/(?P<messageid>.+)/$', + 'views.message.index'), + + url(r'^vote/(?P<mlist_fqdn>.*@.*)/$', + 'views.message.vote'), + ### MESSAGE LEVEL VIEW ENDS ### + + + + ### THREAD LEVEL VIEWS ### + # Thread view page + url(r'^thread/(?P<mlist_fqdn>.*@.*)/(?P<threadid>.+)/$', + 'views.thread.thread_index'), + + # Add Tag to a thread + url(r'^addtag/(?P<mlist_fqdn>.*@.*)\/(?P<email_id>.*)/$', + 'views.thread.add_tag'), + ### THREAD LEVEL VIEW ENDS ### + + + # REST API + url(r'^api/$', 'views.api.api'), + url(r'^api/email\/(?P<mlist_fqdn>.*@.*)\/(?P<messageid>.*)/', + EmailResource.as_view()), + url(r'^api/thread\/(?P<mlist_fqdn>.*@.*)\/(?P<threadid>.*)/', + ThreadResource.as_view()), + url(r'^api/search\/(?P<mlist_fqdn>.*@.*)\/(?P<field>.*)\/(?P<keyword>.*)/', + SearchResource.as_view()), + + + # Social Auth + url(r'', include('social_auth.urls')), + +) diff --git a/hyperkitty/api.py b/hyperkitty/api.py new file mode 100644 index 0000000..fe1d715 --- /dev/null +++ b/hyperkitty/api.py @@ -0,0 +1,69 @@ +#-*- coding: utf-8 -*- + +from djangorestframework.views import View +from django.conf.urls.defaults import url +from django.conf import settings +from django.http import HttpResponseNotModified, HttpResponse +from kittystore.kittysastore import KittySAStore +import json +import re + +from gsoc.utils import log + +STORE = KittySAStore(settings.KITTYSTORE_URL) + + +class EmailResource(View): + """ Resource used to retrieve emails from the archives using the + REST API. + """ + + def get(self, request, mlist_fqdn, messageid): + list_name = mlist_fqdn.split('@')[0] + email = STORE.get_email(list_name, messageid) + if not email: + return HttpResponse(status=404) + else: + return email + + +class ThreadResource(View): + """ Resource used to retrieve threads from the archives using the + REST API. + """ + + def get(self, request, mlist_fqdn, threadid): + list_name = mlist_fqdn.split('@')[0] + thread = STORE.get_thread(list_name, threadid) + if not thread: + return HttpResponse(status=404) + else: + return thread + + +class SearchResource(View): + """ Resource used to search the archives using the REST API. + """ + + def get(self, request, mlist_fqdn, field, keyword): + list_name = mlist_fqdn.split('@')[0] + + if field not in ['Subject', 'Content', 'SubjectContent', 'From']: + return HttpResponse(status=404) + + regex = '.*%s.*' % keyword + if field == 'SubjectContent': + query_string = {'$or' : [ + {'Subject': re.compile(regex, re.IGNORECASE)}, + {'Content': re.compile(regex, re.IGNORECASE)} + ]} + else: + query_string = {field.capitalize(): + re.compile(regex, re.IGNORECASE)} + + print query_string, field, keyword + threads = STORE.search_archives(list_name, query_string) + if not threads: + return HttpResponse(status=404) + else: + return threads diff --git a/hyperkitty/context_processors.py b/hyperkitty/context_processors.py new file mode 100644 index 0000000..5c39319 --- /dev/null +++ b/hyperkitty/context_processors.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. +# +# This file is part of HyperKitty. +# +# HyperKitty is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# HyperKitty is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# HyperKitty. If not, see <http://www.gnu.org/licenses/>. +# +# Author: Aamir Khan <syst3m.w0rm@gmail.com> +# + +from django.conf import settings + +def app_name(context): + return {'app_name' : settings.APP_NAME}
\ No newline at end of file diff --git a/hyperkitty/lib/__init__.py b/hyperkitty/lib/__init__.py new file mode 100644 index 0000000..aa5a3d9 --- /dev/null +++ b/hyperkitty/lib/__init__.py @@ -0,0 +1,14 @@ +#-*- coding: utf-8 -*- + +from hashlib import md5 +import urllib + + +def gravatar_url(email): + '''Return a gravatar url for an email address''' + size = 64 + default = "http://fedoraproject.org/static/images/" + \ + "fedora_infinity_%ix%i.png" % (size, size) + query_string = urllib.urlencode({'s': size, 'd': default}) + identifier = md5(email).hexdigest() + return 'http://www.gravatar.com/avatar/%s?%s' % (identifier, query_string) diff --git a/hyperkitty/lib/mockup.py b/hyperkitty/lib/mockup.py new file mode 100644 index 0000000..6dfe298 --- /dev/null +++ b/hyperkitty/lib/mockup.py @@ -0,0 +1,167 @@ +#-*- coding: utf-8 -*- + + +class Email(object): + """ Email class containing the information needed to store and + display email threads. + """ + + def __init__(self): + """ Constructor. + Instanciate the default attributes of the object. + """ + self.email_id = '' + self.title = '' + self.body = '' + self.tags = [] + self.category = 'question' + self.category_tag = None + self.participants = set(['Pierre-Yves Chibon']) + self.answers = [] + self.liked = 0 + self.author = '' + self.avatar = None + +class Author(object): + """ Author class containing the information needed to get the top + author of the month! + """ + + def __init__(self): + """ Constructor. + Instanciate the default attributes of the object. + """ + self.name = None + self.kudos = 0 + self.avatar = None + + +def get_email_tag(tag): + threads = generate_random_thread() + output = [] + for email in threads: + if tag in email.tags or tag in email.category: + output.append(email) + elif email.category_tag and tag in email.category_tag: + output.append(email) + return output + + +def generate_thread_per_category(): + threads = generate_random_thread() + categories = {} + for thread in threads: + category = thread.category + if thread.category_tag: + category = thread.category_tag + if category in categories.keys(): + categories[category].append(thread) + else: + categories[category] = [thread] + return categories + +def generate_top_author(): + authors = [] + + author = Author() + author.name = 'Pierre-Yves Chibon' + author.avatar = 'https://secure.gravatar.com/avatar/072b4416fbfad867a44bc7a5be5eddb9' + author.kudos = 3 + authors.append(author) + + author = Author() + author.name = 'Stanislav OchotnickĂ½' + author.avatar = 'http://sochotni.fedorapeople.org/sochotni.jpg' + author.kudos = 4 + authors.append(author) + + author = Author() + author.name = 'Toshio Kuratomi' + author.avatar = 'https://secure.gravatar.com/avatar/7a9c1d88f484c9806bceca0d6d91e948' + author.kudos = 5 + authors.append(author) + + return authors + +def generate_random_thread(): + threads = [] + + ## 1 + email = Email() + email.email_id = 1 + email.title = 'Headsup! krb5 ccache defaults are changing in Rawhide' + email.age = '6 days' + email.body = '''Dear fellow developers, +with the upcoming Fedora 18 release (currently Rawhide) we are going to change the place where krb5 credential cache files are saved by default. + +The new default for credential caches will be the /run/user/username directory. +''' + email.tags.extend(['rawhide', 'krb5']) + email.participants = set(['Stephen Gallagher', 'Toshio Kuratomi', 'Kevin Fenzi', 'Seth Vidal']) + email.answers.extend([1,2,3,4,5,6,7,8,9,10,11,12]) + email.liked = 1 + email.author = 'Stephen Gallagher' + email.avatar = 'http://fedorapeople.org/~sgallagh/karrde712.png' + threads.append(email) + + ## 2 + email = Email() + email.email_id = 2 + email.title = 'Problem in packaging kicad' + email.age = '6 days' + email.body = '''Paragraph 1: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ''' + email.tags.extend(['packaging', 'kicad']) + email.participants = set(['Pierre-Yves Chibon', 'Tom "spot" Callaway', 'Toshio Kuratomi', 'Kevin Fenzi']) + email.answers.extend([1,2,3,4,5,6,7,8,9,10,11,12]) + email.liked = 0 + email.author = 'Pierre-Yves Chibon' + email.avatar = 'https://secure.gravatar.com/avatar/072b4416fbfad867a44bc7a5be5eddb9' + threads.append(email) + + ## 3 + email = Email() + email.email_id = 3 + email.title = 'Update Java Guideline' + email.age = '6 days' + email.body = '''Paragraph 1: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ''' + email.tags.extend(['rawhide', 'krb5']) + email.participants = set(['Stanislav OchotnickĂ½', 'Tom "spot" Callaway', 'Stephen Gallagher', 'Jason Tibbitts', 'Rex Dieter', 'Toshio Kuratomi']) + email.answers.extend([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19]) + email.liked = 5 + email.category = 'todo' + email.author = 'Stanislav OchotnickĂ½' + email.avatar = 'http://sochotni.fedorapeople.org/sochotni.jpg' + threads.append(email) + + ## 4 + email = Email() + email.email_id = 4 + email.title = 'Agenda for the next Board Meeting' + email.age = '6 days' + email.body = '''Paragraph 1: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ''' + email.tags.extend(['agenda', 'board']) + email.participants = set(['Toshio Kuratomi', 'Tom "spot" Callaway', 'Robyn Bergeron', 'Max Spevack']) + email.answers.extend([1,2,3,4,5,6,7,8,9,10,11,12]) + email.liked = 20 + email.category = 'agenda' + email.author = 'Toshio Kuratomi' + email.avatar = 'https://secure.gravatar.com/avatar/7a9c1d88f484c9806bceca0d6d91e948' + threads.append(email) + + ## 5 + email = Email() + email.email_id = 5 + email.title = 'I told you so! ' + email.age = '6 days' + email.body = '''Paragraph 1: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ''' + email.tags.extend(['systemd', 'mp3', 'pulseaudio']) + email.participants = set(['Pierre-Yves Chibon']) + email.answers.extend([1,2,3,4,5,6,7,8,9,10,11,12]) + email.liked = 0 + email.author = 'Pierre-Yves Chibon' + email.avatar = 'https://secure.gravatar.com/avatar/072b4416fbfad867a44bc7a5be5eddb9' + email.category = 'shut down' + email.category_tag = 'dead' + threads.append(email) + + return threads diff --git a/hyperkitty/models.py b/hyperkitty/models.py new file mode 100644 index 0000000..588363d --- /dev/null +++ b/hyperkitty/models.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. +# +# This file is part of HyperKitty. +# +# HyperKitty is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# HyperKitty is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# HyperKitty. If not, see <http://www.gnu.org/licenses/>. +# +# Author: Aamir Khan <syst3m.w0rm@gmail.com> +# + +from django.db import models +from django.contrib.auth.models import User +from django.conf import settings + +from kittystore.kittysastore import KittySAStore + +from gsoc.utils import log + +STORE = KittySAStore(settings.KITTYSTORE_URL) + + +class Rating(models.Model): + # @TODO: instead of list_address, user list model from kittystore? + list_address = models.CharField(max_length=50) + + # @TODO: instead of messsageid, use message model from kittystore? + messageid = models.CharField(max_length=100) + + user = models.ForeignKey(User) + + vote = models.SmallIntegerField() + + def __unicode__(self): + """Unicode representation""" + if self.vote == 1: + return u'id = %s : %s voted up %s' % (self.id, unicode(self.user), self.messageid) + else: + return u'id = %s : %s voted down %s' % (self.id, unicode(self.user), self.messageid) + + +class UserProfile(models.Model): + # User Object + user = models.OneToOneField(User) + + karma = models.IntegerField(default=1) + + def _get_votes(self): + "Returns all the votes by a user" + # Extract all the votes by this user + try: + votes = Rating.objects.filter(user = self.user) + except Rating.DoesNotExist: + votes = {} + + for vote in votes: + list_name = vote.list_address.split('@')[0] + message = STORE.get_email(list_name, vote.messageid) + vote.message = message + + return votes + + votes = property(_get_votes) + + def __unicode__(self): + """Unicode representation""" + return u'%s' % (unicode(self.user)) diff --git a/hyperkitty/robots.txt b/hyperkitty/robots.txt new file mode 100644 index 0000000..5fe5da1 --- /dev/null +++ b/hyperkitty/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Disallow: /accounts/ +Disallow: /admin/ +Disallow: /vote/ diff --git a/hyperkitty/settings.py b/hyperkitty/settings.py new file mode 100644 index 0000000..23e3578 --- /dev/null +++ b/hyperkitty/settings.py @@ -0,0 +1,195 @@ +import os + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +# Django settings for hyperkitty project. + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + ('Aamir Khan', 'syst3m.w0rm+hk@gmail.com'), +) + +MANAGERS = ADMINS + +MAILMAN_API_URL=r'http://%(username)s:%(password)s@localhost:8001/3.0/' +MAILMAN_USER='mailmanapi' +MAILMAN_PASS='88ffd62d1094a6248415c59d7538793f3df5de2f04d244087952394e689e902a' + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': 'hk', # Or path to database file if using sqlite3. + 'USER': 'root', # Not used with sqlite3. + 'PASSWORD': 'rootroot', # Not used with sqlite3. + 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '', # Set to empty string for default. Not used with sqlite3. + } +} + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# On Unix systems, a value of None will cause Django to use the same +# timezone as the operating system. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'America/Chicago' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale +USE_L10N = True + +# Absolute filesystem path to the directory that will hold user-uploaded files. +# Example: "/home/media/media.lawrence.com/media/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash. +# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" +MEDIA_URL = '' + +# Absolute path to the directory static files should be collected to. +# Don't put anything in this directory yourself; store your static files +# in apps' "static/" subdirectories and in STATICFILES_DIRS. +# Example: "/home/media/media.lawrence.com/static/" +#STATIC_ROOT = '' +STATIC_ROOT = BASE_DIR + '/static_files/' + +# URL prefix for static files. +# Example: "http://media.lawrence.com/static/" +STATIC_URL = '/static/' + +# URL prefix for admin static files -- CSS, JavaScript and images. +# Make sure to use a trailing slash. +# Examples: "http://foo.com/static/admin/", "/static/admin/". +ADMIN_MEDIA_PREFIX = '/static/admin/' + +# Additional locations of static files +STATICFILES_DIRS = ( + # Put strings here, like "/home/html/static" or "C:/www/django/static". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. + BASE_DIR + '/static/', +) + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +# 'django.contrib.staticfiles.finders.DefaultStorageFinder', +) + +# Make this unique, and don't share it with anybody. +SECRET_KEY = 'dtc3%x(k#mzpe32dmhtsb6!3p(izk84f7nuw1-+4x8zsxwsa^z' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.Loader', +) + + +TEMPLATE_CONTEXT_PROCESSORS = ( + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "django.core.context_processors.debug", + "django.core.context_processors.i18n", + "django.core.context_processors.media", + "django.core.context_processors.static", + "django.core.context_processors.csrf", + "django.contrib.messages.context_processors.messages", + "gsoc.context_processors.app_name", +) + + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +) + +ROOT_URLCONF = 'urls' + +TEMPLATE_DIRS = ( + # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. + BASE_DIR + '/templates', +) + +AUTHENTICATION_BACKENDS = ( + 'social_auth.backends.google.GoogleBackend', + 'social_auth.backends.yahoo.YahooBackend', + 'social_auth.backends.browserid.BrowserIDBackend', + 'social_auth.backends.OpenIDBackend', + 'django.contrib.auth.backends.ModelBackend', +) + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', +# 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.admin', + # 'django.contrib.admindocs', + 'gsoc', + 'social_auth', + 'djangorestframework', + 'gravatar', +) + + +LOGIN_URL = '/accounts/login/' +LOGIN_REDIRECT_URL = '/' +LOGIN_ERROR_URL = '/accounts/login/' +SOCIAL_AUTH_COMPLETE_URL_NAME = 'socialauth_complete' +SOCIAL_AUTH_ASSOCIATE_URL_NAME = 'socialauth_associate_complete' +SOCIAL_AUTH_DEFAULT_USERNAME = 'new_social_auth_user' +SOCIAL_AUTH_UUID_LENGTH = 16 + +AUTH_PROFILE_MODULE = 'gsoc.UserProfile' + + +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# the site admins on every HTTP 500 error. +# See http://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + } +} + +SOCIAL_AUTH_LAST_LOGIN = 'social_auth_last_login_backend' +APP_NAME = 'Fedora Mailman App' +KITTYSTORE_URL = 'postgres://mm3:mm3@localhost/mm3' diff --git a/hyperkitty/static/css/bootstrap.css b/hyperkitty/static/css/bootstrap.css new file mode 100644 index 0000000..c3e0c00 --- /dev/null +++ b/hyperkitty/static/css/bootstrap.css @@ -0,0 +1,3496 @@ +/*! + * Bootstrap v2.0.1 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section { + display: block; +} +audio, canvas, video { + display: inline-block; + *display: inline; + *zoom: 1; +} +audio:not([controls]) { + display: none; +} +html { + font-size: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} +a:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +a:hover, a:active { + outline: 0; +} +sub, sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} +sup { + top: -0.5em; +} +sub { + bottom: -0.25em; +} +img { + max-width: 100%; + height: auto; + border: 0; + -ms-interpolation-mode: bicubic; +} +button, +input, +select, +textarea { + margin: 0; + font-size: 100%; + vertical-align: middle; +} +button, input { + *overflow: visible; + line-height: normal; +} +button::-moz-focus-inner, input::-moz-focus-inner { + padding: 0; + border: 0; +} +button, +input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; +} +input[type="search"] { + -webkit-appearance: textfield; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; +} +textarea { + overflow: auto; + vertical-align: top; +} +.clearfix { + *zoom: 1; +} +.clearfix:before, .clearfix:after { + display: table; + content: ""; +} +.clearfix:after { + clear: both; +} +body { + margin: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + line-height: 18px; + color: #333333; + background-color: #ffffff; +} +a { + color: #0088cc; + text-decoration: none; +} +a:hover { + color: #005580; + text-decoration: underline; +} +.row { + margin-left: -20px; + *zoom: 1; +} +.row:before, .row:after { + display: table; + content: ""; +} +.row:after { + clear: both; +} +[class*="span"] { + float: left; + margin-left: 20px; +} +.span1 { + width: 60px; +} +.span2 { + width: 140px; +} +.span3 { + width: 220px; +} +.span4 { + width: 300px; +} +.span5 { + width: 380px; +} +.span6 { + width: 460px; +} +.span7 { + width: 540px; +} +.span8 { + width: 620px; +} +.span9 { + width: 700px; +} +.span10 { + width: 780px; +} +.span11 { + width: 860px; +} +.span12, .container { + width: 940px; +} +.offset1 { + margin-left: 100px; +} +.offset2 { + margin-left: 180px; +} +.offset3 { + margin-left: 260px; +} +.offset4 { + margin-left: 340px; +} +.offset5 { + margin-left: 420px; +} +.offset6 { + margin-left: 500px; +} +.offset7 { + margin-left: 580px; +} +.offset8 { + margin-left: 660px; +} +.offset9 { + margin-left: 740px; +} +.offset10 { + margin-left: 820px; +} +.offset11 { + margin-left: 900px; +} +.row-fluid { + width: 100%; + *zoom: 1; +} +.row-fluid:before, .row-fluid:after { + display: table; + content: ""; +} +.row-fluid:after { + clear: both; +} +.row-fluid > [class*="span"] { + float: left; + margin-left: 2.127659574%; +} +.row-fluid > [class*="span"]:first-child { + margin-left: 0; +} +.row-fluid > .span1 { + width: 6.382978723%; +} +.row-fluid > .span2 { + width: 14.89361702%; +} +.row-fluid > .span3 { + width: 23.404255317%; +} +.row-fluid > .span4 { + width: 31.914893614%; +} +.row-fluid > .span5 { + width: 40.425531911%; +} +.row-fluid > .span6 { + width: 48.93617020799999%; +} +.row-fluid > .span7 { + width: 57.446808505%; +} +.row-fluid > .span8 { + width: 65.95744680199999%; +} +.row-fluid > .span9 { + width: 74.468085099%; +} +.row-fluid > .span10 { + width: 82.97872339599999%; +} +.row-fluid > .span11 { + width: 91.489361693%; +} +.row-fluid > .span12 { + width: 99.99999998999999%; +} +.container { + width: 940px; + margin-left: auto; + margin-right: auto; + *zoom: 1; +} +.container:before, .container:after { + display: table; + content: ""; +} +.container:after { + clear: both; +} +.container-fluid { + padding-left: 20px; + padding-right: 20px; + *zoom: 1; +} +.container-fluid:before, .container-fluid:after { + display: table; + content: ""; +} +.container-fluid:after { + clear: both; +} +p { + margin: 0 0 9px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + line-height: 18px; +} +p small { + font-size: 11px; + color: #999999; +} +.lead { + margin-bottom: 18px; + font-size: 20px; + font-weight: 200; + line-height: 27px; +} +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 0; + font-weight: bold; + color: #333333; + text-rendering: optimizelegibility; +} +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small { + font-weight: normal; + color: #999999; +} +h1 { + font-size: 30px; + line-height: 36px; +} +h1 small { + font-size: 18px; +} +h2 { + font-size: 24px; + line-height: 36px; +} +h2 small { + font-size: 18px; +} +h3 { + line-height: 27px; + font-size: 18px; +} +h3 small { + font-size: 14px; +} +h4, h5, h6 { + line-height: 18px; +} +h4 { + font-size: 14px; +} +h4 small { + font-size: 12px; +} +h5 { + font-size: 12px; +} +h6 { + font-size: 11px; + color: #999999; + text-transform: uppercase; +} +.page-header { + padding-bottom: 17px; + margin: 18px 0; + border-bottom: 1px solid #eeeeee; +} +.page-header h1 { + line-height: 1; +} +ul, ol { + padding: 0; + margin: 0 0 9px 25px; +} +ul ul, +ul ol, +ol ol, +ol ul { + margin-bottom: 0; +} +ul { + list-style: disc; +} +ol { + list-style: decimal; +} +li { + line-height: 18px; +} +ul.unstyled, ol.unstyled { + margin-left: 0; + list-style: none; +} +dl { + margin-bottom: 18px; +} +dt, dd { + line-height: 18px; +} +dt { + font-weight: bold; +} +dd { + margin-left: 9px; +} +hr { + margin: 18px 0; + border: 0; + border-top: 1px solid #eeeeee; + border-bottom: 1px solid #ffffff; +} +strong { + font-weight: bold; +} +em { + font-style: italic; +} +.muted { + color: #999999; +} +abbr { + font-size: 90%; + text-transform: uppercase; + border-bottom: 1px dotted #ddd; + cursor: help; +} +blockquote { + padding: 0 0 0 15px; + margin: 0 0 18px; + border-left: 5px solid #eeeeee; +} +blockquote p { + margin-bottom: 0; + font-size: 16px; + font-weight: 300; + line-height: 22.5px; +} +blockquote small { + display: block; + line-height: 18px; + color: #999999; +} +blockquote small:before { + content: '\2014 \00A0'; +} +blockquote.pull-right { + float: right; + padding-left: 0; + padding-right: 15px; + border-left: 0; + border-right: 5px solid #eeeeee; +} +blockquote.pull-right p, blockquote.pull-right small { + text-align: right; +} +q:before, +q:after, +blockquote:before, +blockquote:after { + content: ""; +} +address { + display: block; + margin-bottom: 18px; + line-height: 18px; + font-style: normal; +} +small { + font-size: 100%; +} +cite { + font-style: normal; +} +code, pre { + padding: 0 3px 2px; + font-family: Menlo, Monaco, "Courier New", monospace; + font-size: 12px; + color: #333333; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +code { + padding: 3px 4px; + color: #d14; + background-color: #f7f7f9; + border: 1px solid #e1e1e8; +} +pre { + display: block; + padding: 8.5px; + margin: 0 0 9px; + font-size: 12px; + line-height: 18px; + background-color: #f5f5f5; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.15); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + white-space: pre; + white-space: pre-wrap; + word-break: break-all; + word-wrap: break-word; +} +pre.prettyprint { + margin-bottom: 18px; +} +pre code { + padding: 0; + color: inherit; + background-color: transparent; + border: 0; +} +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} +form { + margin: 0 0 18px; +} +fieldset { + padding: 0; + margin: 0; + border: 0; +} +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 27px; + font-size: 19.5px; + line-height: 36px; + color: #333333; + border: 0; + border-bottom: 1px solid #eee; +} +legend small { + font-size: 13.5px; + color: #999999; +} +label, +input, +button, +select, +textarea { + font-size: 13px; + font-weight: normal; + line-height: 18px; +} +input, +button, +select, +textarea { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; +} +label { + display: block; + margin-bottom: 5px; + color: #333333; +} +input, +textarea, +select, +.uneditable-input { + display: inline-block; + width: 210px; + height: 18px; + padding: 4px; + margin-bottom: 9px; + font-size: 13px; + line-height: 18px; + color: #555555; + border: 1px solid #ccc; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.uneditable-textarea { + width: auto; + height: auto; +} +label input, label textarea, label select { + display: block; +} +input[type="image"], input[type="checkbox"], input[type="radio"] { + width: auto; + height: auto; + padding: 0; + margin: 3px 0; + *margin-top: 0; + /* IE7 */ + + line-height: normal; + cursor: pointer; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; + border: 0 \9; + /* IE9 and down */ + +} +input[type="image"] { + border: 0; +} +input[type="file"] { + width: auto; + padding: initial; + line-height: initial; + border: initial; + background-color: #ffffff; + background-color: initial; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} +input[type="button"], input[type="reset"], input[type="submit"] { + width: auto; + height: auto; +} +select, input[type="file"] { + height: 28px; + /* In IE7, the height of the select element cannot be changed by height, only font-size */ + + *margin-top: 4px; + /* For IE7, add top margin to align select with labels */ + + line-height: 28px; +} +input[type="file"] { + line-height: 18px \9; +} +select { + width: 220px; + background-color: #ffffff; +} +select[multiple], select[size] { + height: auto; +} +input[type="image"] { + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} +textarea { + height: auto; +} +input[type="hidden"] { + display: none; +} +.radio, .checkbox { + padding-left: 18px; +} +.radio input[type="radio"], .checkbox input[type="checkbox"] { + float: left; + margin-left: -18px; +} +.controls > .radio:first-child, .controls > .checkbox:first-child { + padding-top: 5px; +} +.radio.inline, .checkbox.inline { + display: inline-block; + padding-top: 5px; + margin-bottom: 0; + vertical-align: middle; +} +.radio.inline + .radio.inline, .checkbox.inline + .checkbox.inline { + margin-left: 10px; +} +input, textarea { + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + -moz-transition: border linear 0.2s, box-shadow linear 0.2s; + -ms-transition: border linear 0.2s, box-shadow linear 0.2s; + -o-transition: border linear 0.2s, box-shadow linear 0.2s; + transition: border linear 0.2s, box-shadow linear 0.2s; +} +input:focus, textarea:focus { + border-color: rgba(82, 168, 236, 0.8); + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + outline: 0; + outline: thin dotted \9; + /* IE6-9 */ + +} +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus, +select:focus { + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.input-mini { + width: 60px; +} +.input-small { + width: 90px; +} +.input-medium { + width: 150px; +} +.input-large { + width: 210px; +} +.input-xlarge { + width: 270px; +} +.input-xxlarge { + width: 530px; +} +input[class*="span"], +select[class*="span"], +textarea[class*="span"], +.uneditable-input { + float: none; + margin-left: 0; +} +input.span1, textarea.span1, .uneditable-input.span1 { + width: 50px; +} +input.span2, textarea.span2, .uneditable-input.span2 { + width: 130px; +} +input.span3, textarea.span3, .uneditable-input.span3 { + width: 210px; +} +input.span4, textarea.span4, .uneditable-input.span4 { + width: 290px; +} +input.span5, textarea.span5, .uneditable-input.span5 { + width: 370px; +} +input.span6, textarea.span6, .uneditable-input.span6 { + width: 450px; +} +input.span7, textarea.span7, .uneditable-input.span7 { + width: 530px; +} +input.span8, textarea.span8, .uneditable-input.span8 { + width: 610px; +} +input.span9, textarea.span9, .uneditable-input.span9 { + width: 690px; +} +input.span10, textarea.span10, .uneditable-input.span10 { + width: 770px; +} +input.span11, textarea.span11, .uneditable-input.span11 { + width: 850px; +} +input.span12, textarea.span12, .uneditable-input.span12 { + width: 930px; +} +input[disabled], +select[disabled], +textarea[disabled], +input[readonly], +select[readonly], +textarea[readonly] { + background-color: #f5f5f5; + border-color: #ddd; + cursor: not-allowed; +} +.control-group.warning > label, .control-group.warning .help-block, .control-group.warning .help-inline { + color: #c09853; +} +.control-group.warning input, .control-group.warning select, .control-group.warning textarea { + color: #c09853; + border-color: #c09853; +} +.control-group.warning input:focus, .control-group.warning select:focus, .control-group.warning textarea:focus { + border-color: #a47e3c; + -webkit-box-shadow: 0 0 6px #dbc59e; + -moz-box-shadow: 0 0 6px #dbc59e; + box-shadow: 0 0 6px #dbc59e; +} +.control-group.warning .input-prepend .add-on, .control-group.warning .input-append .add-on { + color: #c09853; + background-color: #fcf8e3; + border-color: #c09853; +} +.control-group.error > label, .control-group.error .help-block, .control-group.error .help-inline { + color: #b94a48; +} +.control-group.error input, .control-group.error select, .control-group.error textarea { + color: #b94a48; + border-color: #b94a48; +} +.control-group.error input:focus, .control-group.error select:focus, .control-group.error textarea:focus { + border-color: #953b39; + -webkit-box-shadow: 0 0 6px #d59392; + -moz-box-shadow: 0 0 6px #d59392; + box-shadow: 0 0 6px #d59392; +} +.control-group.error .input-prepend .add-on, .control-group.error .input-append .add-on { + color: #b94a48; + background-color: #f2dede; + border-color: #b94a48; +} +.control-group.success > label, .control-group.success .help-block, .control-group.success .help-inline { + color: #468847; +} +.control-group.success input, .control-group.success select, .control-group.success textarea { + color: #468847; + border-color: #468847; +} +.control-group.success input:focus, .control-group.success select:focus, .control-group.success textarea:focus { + border-color: #356635; + -webkit-box-shadow: 0 0 6px #7aba7b; + -moz-box-shadow: 0 0 6px #7aba7b; + box-shadow: 0 0 6px #7aba7b; +} +.control-group.success .input-prepend .add-on, .control-group.success .input-append .add-on { + color: #468847; + background-color: #dff0d8; + border-color: #468847; +} +input:focus:required:invalid, textarea:focus:required:invalid, select:focus:required:invalid { + color: #b94a48; + border-color: #ee5f5b; +} +input:focus:required:invalid:focus, textarea:focus:required:invalid:focus, select:focus:required:invalid:focus { + border-color: #e9322d; + -webkit-box-shadow: 0 0 6px #f8b9b7; + -moz-box-shadow: 0 0 6px #f8b9b7; + box-shadow: 0 0 6px #f8b9b7; +} +.form-actions { + padding: 17px 20px 18px; + margin-top: 18px; + margin-bottom: 18px; + background-color: #f5f5f5; + border-top: 1px solid #ddd; +} +.uneditable-input { + display: block; + background-color: #ffffff; + border-color: #eee; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + cursor: not-allowed; +} +:-moz-placeholder { + color: #999999; +} +::-webkit-input-placeholder { + color: #999999; +} +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 0; + color: #999999; +} +.help-inline { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; + margin-bottom: 9px; + vertical-align: middle; + padding-left: 5px; +} +.input-prepend, .input-append { + margin-bottom: 5px; + *zoom: 1; +} +.input-prepend:before, +.input-append:before, +.input-prepend:after, +.input-append:after { + display: table; + content: ""; +} +.input-prepend:after, .input-append:after { + clear: both; +} +.input-prepend input, +.input-append input, +.input-prepend .uneditable-input, +.input-append .uneditable-input { + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} +.input-prepend input:focus, +.input-append input:focus, +.input-prepend .uneditable-input:focus, +.input-append .uneditable-input:focus { + position: relative; + z-index: 2; +} +.input-prepend .uneditable-input, .input-append .uneditable-input { + border-left-color: #ccc; +} +.input-prepend .add-on, .input-append .add-on { + float: left; + display: block; + width: auto; + min-width: 16px; + height: 18px; + margin-right: -1px; + padding: 4px 5px; + font-weight: normal; + line-height: 18px; + color: #999999; + text-align: center; + text-shadow: 0 1px 0 #ffffff; + background-color: #f5f5f5; + border: 1px solid #ccc; + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} +.input-prepend .active, .input-append .active { + background-color: #a9dba9; + border-color: #46a546; +} +.input-prepend .add-on { + *margin-top: 1px; + /* IE6-7 */ + +} +.input-append input, .input-append .uneditable-input { + float: left; + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} +.input-append .uneditable-input { + border-left-color: #eee; + border-right-color: #ccc; +} +.input-append .add-on { + margin-right: 0; + margin-left: -1px; + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} +.input-append input:first-child { + *margin-left: -160px; +} +.input-append input:first-child + .add-on { + *margin-left: -21px; +} +.search-query { + padding-left: 14px; + padding-right: 14px; + margin-bottom: 0; + -webkit-border-radius: 14px; + -moz-border-radius: 14px; + border-radius: 14px; +} +.form-search input, +.form-inline input, +.form-horizontal input, +.form-search textarea, +.form-inline textarea, +.form-horizontal textarea, +.form-search select, +.form-inline select, +.form-horizontal select, +.form-search .help-inline, +.form-inline .help-inline, +.form-horizontal .help-inline, +.form-search .uneditable-input, +.form-inline .uneditable-input, +.form-horizontal .uneditable-input { + display: inline-block; + margin-bottom: 0; +} +.form-search .hide, .form-inline .hide, .form-horizontal .hide { + display: none; +} +.form-search label, +.form-inline label, +.form-search .input-append, +.form-inline .input-append, +.form-search .input-prepend, +.form-inline .input-prepend { + display: inline-block; +} +.form-search .input-append .add-on, +.form-inline .input-prepend .add-on, +.form-search .input-append .add-on, +.form-inline .input-prepend .add-on { + vertical-align: middle; +} +.form-search .radio, +.form-inline .radio, +.form-search .checkbox, +.form-inline .checkbox { + margin-bottom: 0; + vertical-align: middle; +} +.control-group { + margin-bottom: 9px; +} +legend + .control-group { + margin-top: 18px; + -webkit-margin-top-collapse: separate; +} +.form-horizontal .control-group { + margin-bottom: 18px; + *zoom: 1; +} +.form-horizontal .control-group:before, .form-horizontal .control-group:after { + display: table; + content: ""; +} +.form-horizontal .control-group:after { + clear: both; +} +.form-horizontal .control-label { + float: left; + width: 140px; + padding-top: 5px; + text-align: right; +} +.form-horizontal .controls { + margin-left: 160px; +} +.form-horizontal .form-actions { + padding-left: 160px; +} +table { + max-width: 100%; + border-collapse: collapse; + border-spacing: 0; +} +.table { + width: 100%; + margin-bottom: 18px; +} +.table th, .table td { + padding: 8px; + line-height: 18px; + text-align: left; + vertical-align: top; + border-top: 1px solid #ddd; +} +.table th { + font-weight: bold; +} +.table thead th { + vertical-align: bottom; +} +.table thead:first-child tr th, .table thead:first-child tr td { + border-top: 0; +} +.table tbody + tbody { + border-top: 2px solid #ddd; +} +.table-condensed th, .table-condensed td { + padding: 4px 5px; +} +.table-bordered { + border: 1px solid #ddd; + border-collapse: separate; + *border-collapse: collapsed; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.table-bordered th + th, +.table-bordered td + td, +.table-bordered th + td, +.table-bordered td + th { + border-left: 1px solid #ddd; +} +.table-bordered thead:first-child tr:first-child th, .table-bordered tbody:first-child tr:first-child th, .table-bordered tbody:first-child tr:first-child td { + border-top: 0; +} +.table-bordered thead:first-child tr:first-child th:first-child, .table-bordered tbody:first-child tr:first-child td:first-child { + -webkit-border-radius: 4px 0 0 0; + -moz-border-radius: 4px 0 0 0; + border-radius: 4px 0 0 0; +} +.table-bordered thead:first-child tr:first-child th:last-child, .table-bordered tbody:first-child tr:first-child td:last-child { + -webkit-border-radius: 0 4px 0 0; + -moz-border-radius: 0 4px 0 0; + border-radius: 0 4px 0 0; +} +.table-bordered thead:last-child tr:last-child th:first-child, .table-bordered tbody:last-child tr:last-child td:first-child { + -webkit-border-radius: 0 0 0 4px; + -moz-border-radius: 0 0 0 4px; + border-radius: 0 0 0 4px; +} +.table-bordered thead:last-child tr:last-child th:last-child, .table-bordered tbody:last-child tr:last-child td:last-child { + -webkit-border-radius: 0 0 4px 0; + -moz-border-radius: 0 0 4px 0; + border-radius: 0 0 4px 0; +} +.table-striped tbody tr:nth-child(odd) td, .table-striped tbody tr:nth-child(odd) th { + background-color: #f9f9f9; +} +.table tbody tr:hover td, .table tbody tr:hover th { + background-color: #f5f5f5; +} +table .span1 { + float: none; + width: 44px; + margin-left: 0; +} +table .span2 { + float: none; + width: 124px; + margin-left: 0; +} +table .span3 { + float: none; + width: 204px; + margin-left: 0; +} +table .span4 { + float: none; + width: 284px; + margin-left: 0; +} +table .span5 { + float: none; + width: 364px; + margin-left: 0; +} +table .span6 { + float: none; + width: 444px; + margin-left: 0; +} +table .span7 { + float: none; + width: 524px; + margin-left: 0; +} +table .span8 { + float: none; + width: 604px; + margin-left: 0; +} +table .span9 { + float: none; + width: 684px; + margin-left: 0; +} +table .span10 { + float: none; + width: 764px; + margin-left: 0; +} +table .span11 { + float: none; + width: 844px; + margin-left: 0; +} +table .span12 { + float: none; + width: 924px; + margin-left: 0; +} +[class^="icon-"], [class*=" icon-"] { + display: inline-block; + width: 14px; + height: 14px; + line-height: 14px; + vertical-align: text-top; + background-image: url("../img/glyphicons-halflings.png"); + background-position: 14px 14px; + background-repeat: no-repeat; + *margin-right: .3em; +} +[class^="icon-"]:last-child, [class*=" icon-"]:last-child { + *margin-left: 0; +} +.icon-white { + background-image: url("../img/glyphicons-halflings-white.png"); +} +.icon-glass { + background-position: 0 0; +} +.icon-music { + background-position: -24px 0; +} +.icon-search { + background-position: -48px 0; +} +.icon-envelope { + background-position: -72px 0; +} +.icon-heart { + background-position: -96px 0; +} +.icon-star { + background-position: -120px 0; +} +.icon-star-empty { + background-position: -144px 0; +} +.icon-user { + background-position: -168px 0; +} +.icon-film { + background-position: -192px 0; +} +.icon-th-large { + background-position: -216px 0; +} +.icon-th { + background-position: -240px 0; +} +.icon-th-list { + background-position: -264px 0; +} +.icon-ok { + background-position: -288px 0; +} +.icon-remove { + background-position: -312px 0; +} +.icon-zoom-in { + background-position: -336px 0; +} +.icon-zoom-out { + background-position: -360px 0; +} +.icon-off { + background-position: -384px 0; +} +.icon-signal { + background-position: -408px 0; +} +.icon-cog { + background-position: -432px 0; +} +.icon-trash { + background-position: -456px 0; +} +.icon-home { + background-position: 0 -24px; +} +.icon-file { + background-position: -24px -24px; +} +.icon-time { + background-position: -48px -24px; +} +.icon-road { + background-position: -72px -24px; +} +.icon-download-alt { + background-position: -96px -24px; +} +.icon-download { + background-position: -120px -24px; +} +.icon-upload { + background-position: -144px -24px; +} +.icon-inbox { + background-position: -168px -24px; +} +.icon-play-circle { + background-position: -192px -24px; +} +.icon-repeat { + background-position: -216px -24px; +} +.icon-refresh { + background-position: -240px -24px; +} +.icon-list-alt { + background-position: -264px -24px; +} +.icon-lock { + background-position: -287px -24px; +} +.icon-flag { + background-position: -312px -24px; +} +.icon-headphones { + background-position: -336px -24px; +} +.icon-volume-off { + background-position: -360px -24px; +} +.icon-volume-down { + background-position: -384px -24px; +} +.icon-volume-up { + background-position: -408px -24px; +} +.icon-qrcode { + background-position: -432px -24px; +} +.icon-barcode { + background-position: -456px -24px; +} +.icon-tag { + background-position: 0 -48px; +} +.icon-tags { + background-position: -25px -48px; +} +.icon-book { + background-position: -48px -48px; +} +.icon-bookmark { + background-position: -72px -48px; +} +.icon-print { + background-position: -96px -48px; +} +.icon-camera { + background-position: -120px -48px; +} +.icon-font { + background-position: -144px -48px; +} +.icon-bold { + background-position: -167px -48px; +} +.icon-italic { + background-position: -192px -48px; +} +.icon-text-height { + background-position: -216px -48px; +} +.icon-text-width { + background-position: -240px -48px; +} +.icon-align-left { + background-position: -264px -48px; +} +.icon-align-center { + background-position: -288px -48px; +} +.icon-align-right { + background-position: -312px -48px; +} +.icon-align-justify { + background-position: -336px -48px; +} +.icon-list { + background-position: -360px -48px; +} +.icon-indent-left { + background-position: -384px -48px; +} +.icon-indent-right { + background-position: -408px -48px; +} +.icon-facetime-video { + background-position: -432px -48px; +} +.icon-picture { + background-position: -456px -48px; +} +.icon-pencil { + background-position: 0 -72px; +} +.icon-map-marker { + background-position: -24px -72px; +} +.icon-adjust { + background-position: -48px -72px; +} +.icon-tint { + background-position: -72px -72px; +} +.icon-edit { + background-position: -96px -72px; +} +.icon-share { + background-position: -120px -72px; +} +.icon-check { + background-position: -144px -72px; +} +.icon-move { + background-position: -168px -72px; +} +.icon-step-backward { + background-position: -192px -72px; +} +.icon-fast-backward { + background-position: -216px -72px; +} +.icon-backward { + background-position: -240px -72px; +} +.icon-play { + background-position: -264px -72px; +} +.icon-pause { + background-position: -288px -72px; +} +.icon-stop { + background-position: -312px -72px; +} +.icon-forward { + background-position: -336px -72px; +} +.icon-fast-forward { + background-position: -360px -72px; +} +.icon-step-forward { + background-position: -384px -72px; +} +.icon-eject { + background-position: -408px -72px; +} +.icon-chevron-left { + background-position: -432px -72px; +} +.icon-chevron-right { + background-position: -456px -72px; +} +.icon-plus-sign { + background-position: 0 -96px; +} +.icon-minus-sign { + background-position: -24px -96px; +} +.icon-remove-sign { + background-position: -48px -96px; +} +.icon-ok-sign { + background-position: -72px -96px; +} +.icon-question-sign { + background-position: -96px -96px; +} +.icon-info-sign { + background-position: -120px -96px; +} +.icon-screenshot { + background-position: -144px -96px; +} +.icon-remove-circle { + background-position: -168px -96px; +} +.icon-ok-circle { + background-position: -192px -96px; +} +.icon-ban-circle { + background-position: -216px -96px; +} +.icon-arrow-left { + background-position: -240px -96px; +} +.icon-arrow-right { + background-position: -264px -96px; +} +.icon-arrow-up { + background-position: -289px -96px; +} +.icon-arrow-down { + background-position: -312px -96px; +} +.icon-share-alt { + background-position: -336px -96px; +} +.icon-resize-full { + background-position: -360px -96px; +} +.icon-resize-small { + background-position: -384px -96px; +} +.icon-plus { + background-position: -408px -96px; +} +.icon-minus { + background-position: -433px -96px; +} +.icon-asterisk { + background-position: -456px -96px; +} +.icon-exclamation-sign { + background-position: 0 -120px; +} +.icon-gift { + background-position: -24px -120px; +} +.icon-leaf { + background-position: -48px -120px; +} +.icon-fire { + background-position: -72px -120px; +} +.icon-eye-open { + background-position: -96px -120px; +} +.icon-eye-close { + background-position: -120px -120px; +} +.icon-warning-sign { + background-position: -144px -120px; +} +.icon-plane { + background-position: -168px -120px; +} +.icon-calendar { + background-position: -192px -120px; +} +.icon-random { + background-position: -216px -120px; +} +.icon-comment { + background-position: -240px -120px; +} +.icon-magnet { + background-position: -264px -120px; +} +.icon-chevron-up { + background-position: -288px -120px; +} +.icon-chevron-down { + background-position: -313px -119px; +} +.icon-retweet { + background-position: -336px -120px; +} +.icon-shopping-cart { + background-position: -360px -120px; +} +.icon-folder-close { + background-position: -384px -120px; +} +.icon-folder-open { + background-position: -408px -120px; +} +.icon-resize-vertical { + background-position: -432px -119px; +} +.icon-resize-horizontal { + background-position: -456px -118px; +} +.dropdown { + position: relative; +} +.dropdown-toggle { + *margin-bottom: -3px; +} +.dropdown-toggle:active, .open .dropdown-toggle { + outline: 0; +} +.caret { + display: inline-block; + width: 0; + height: 0; + text-indent: -99999px; + *text-indent: 0; + vertical-align: top; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #000000; + opacity: 0.3; + filter: alpha(opacity=30); + content: "\2193"; +} +.dropdown .caret { + margin-top: 8px; + margin-left: 2px; +} +.dropdown:hover .caret, .open.dropdown .caret { + opacity: 1; + filter: alpha(opacity=100); +} +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + float: left; + display: none; + min-width: 160px; + _width: 160px; + padding: 4px 0; + margin: 0; + list-style: none; + background-color: #ffffff; + border-color: #ccc; + border-color: rgba(0, 0, 0, 0.2); + border-style: solid; + border-width: 1px; + -webkit-border-radius: 0 0 5px 5px; + -moz-border-radius: 0 0 5px 5px; + border-radius: 0 0 5px 5px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; + *border-right-width: 2px; + *border-bottom-width: 2px; +} +.dropdown-menu.bottom-up { + top: auto; + bottom: 100%; + margin-bottom: 2px; +} +.dropdown-menu .divider { + height: 1px; + margin: 5px 1px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; + *width: 100%; + *margin: -5px 0 5px; +} +.dropdown-menu a { + display: block; + padding: 3px 15px; + clear: both; + font-weight: normal; + line-height: 18px; + color: #555555; + white-space: nowrap; +} +.dropdown-menu li > a:hover, .dropdown-menu .active > a, .dropdown-menu .active > a:hover { + color: #ffffff; + text-decoration: none; + background-color: #0088cc; +} +.dropdown.open { + *z-index: 1000; +} +.dropdown.open .dropdown-toggle { + color: #ffffff; + background: #ccc; + background: rgba(0, 0, 0, 0.3); +} +.dropdown.open .dropdown-menu { + display: block; +} +.typeahead { + margin-top: 2px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #eee; + border: 1px solid rgba(0, 0, 0, 0.05); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); +} +.fade { + -webkit-transition: opacity 0.15s linear; + -moz-transition: opacity 0.15s linear; + -ms-transition: opacity 0.15s linear; + -o-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; + opacity: 0; +} +.fade.in { + opacity: 1; +} +.collapse { + -webkit-transition: height 0.35s ease; + -moz-transition: height 0.35s ease; + -ms-transition: height 0.35s ease; + -o-transition: height 0.35s ease; + transition: height 0.35s ease; + position: relative; + overflow: hidden; + height: 0; +} +.collapse.in { + height: auto; +} +.close { + float: right; + font-size: 20px; + font-weight: bold; + line-height: 18px; + color: #000000; + text-shadow: 0 1px 0 #ffffff; + opacity: 0.2; + filter: alpha(opacity=20); +} +.close:hover { + color: #000000; + text-decoration: none; + opacity: 0.4; + filter: alpha(opacity=40); + cursor: pointer; +} +.btn { + display: inline-block; + padding: 4px 10px 4px; + margin-bottom: 0; + font-size: 13px; + line-height: 18px; + color: #333333; + text-align: center; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + vertical-align: middle; + background-color: #f5f5f5; + background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -ms-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); + background-image: linear-gradient(top, #ffffff, #e6e6e6); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0); + border-color: #e6e6e6 #e6e6e6 #bfbfbf; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + border: 1px solid #ccc; + border-bottom-color: #bbb; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + cursor: pointer; + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + *margin-left: .3em; +} +.btn:hover, +.btn:active, +.btn.active, +.btn.disabled, +.btn[disabled] { + background-color: #e6e6e6; +} +.btn:active, .btn.active { + background-color: #cccccc \9; +} +.btn:first-child { + *margin-left: 0; +} +.btn:hover { + color: #333333; + text-decoration: none; + background-color: #e6e6e6; + background-position: 0 -15px; + -webkit-transition: background-position 0.1s linear; + -moz-transition: background-position 0.1s linear; + -ms-transition: background-position 0.1s linear; + -o-transition: background-position 0.1s linear; + transition: background-position 0.1s linear; +} +.btn:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.btn.active, .btn:active { + background-image: none; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + background-color: #e6e6e6; + background-color: #d9d9d9 \9; + outline: 0; +} +.btn.disabled, .btn[disabled] { + cursor: default; + background-image: none; + background-color: #e6e6e6; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} +.btn-large { + padding: 9px 14px; + font-size: 15px; + line-height: normal; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +.btn-large [class^="icon-"] { + margin-top: 1px; +} +.btn-small { + padding: 5px 9px; + font-size: 11px; + line-height: 16px; +} +.btn-small [class^="icon-"] { + margin-top: -1px; +} +.btn-mini { + padding: 2px 6px; + font-size: 11px; + line-height: 14px; +} +.btn-primary, +.btn-primary:hover, +.btn-warning, +.btn-warning:hover, +.btn-danger, +.btn-danger:hover, +.btn-success, +.btn-success:hover, +.btn-info, +.btn-info:hover, +.btn-inverse, +.btn-inverse:hover { + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + color: #ffffff; +} +.btn-primary.active, +.btn-warning.active, +.btn-danger.active, +.btn-success.active, +.btn-info.active, +.btn-dark.active { + color: rgba(255, 255, 255, 0.75); +} +.btn-primary { + background-color: #006dcc; + background-image: -moz-linear-gradient(top, #0088cc, #0044cc); + background-image: -ms-linear-gradient(top, #0088cc, #0044cc); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); + background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); + background-image: -o-linear-gradient(top, #0088cc, #0044cc); + background-image: linear-gradient(top, #0088cc, #0044cc); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0); + border-color: #0044cc #0044cc #002a80; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.btn-primary:hover, +.btn-primary:active, +.btn-primary.active, +.btn-primary.disabled, +.btn-primary[disabled] { + background-color: #0044cc; +} +.btn-primary:active, .btn-primary.active { + background-color: #003399 \9; +} +.btn-warning { + background-color: #faa732; + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: -ms-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(top, #fbb450, #f89406); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0); + border-color: #f89406 #f89406 #ad6704; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.btn-warning:hover, +.btn-warning:active, +.btn-warning.active, +.btn-warning.disabled, +.btn-warning[disabled] { + background-color: #f89406; +} +.btn-warning:active, .btn-warning.active { + background-color: #c67605 \9; +} +.btn-danger { + background-color: #da4f49; + background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -ms-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); + background-image: linear-gradient(top, #ee5f5b, #bd362f); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#bd362f', GradientType=0); + border-color: #bd362f #bd362f #802420; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.btn-danger:hover, +.btn-danger:active, +.btn-danger.active, +.btn-danger.disabled, +.btn-danger[disabled] { + background-color: #bd362f; +} +.btn-danger:active, .btn-danger.active { + background-color: #942a25 \9; +} +.btn-success { + background-color: #5bb75b; + background-image: -moz-linear-gradient(top, #62c462, #51a351); + background-image: -ms-linear-gradient(top, #62c462, #51a351); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); + background-image: -webkit-linear-gradient(top, #62c462, #51a351); + background-image: -o-linear-gradient(top, #62c462, #51a351); + background-image: linear-gradient(top, #62c462, #51a351); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0); + border-color: #51a351 #51a351 #387038; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.btn-success:hover, +.btn-success:active, +.btn-success.active, +.btn-success.disabled, +.btn-success[disabled] { + background-color: #51a351; +} +.btn-success:active, .btn-success.active { + background-color: #408140 \9; +} +.btn-info { + background-color: #49afcd; + background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -ms-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); + background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); + background-image: linear-gradient(top, #5bc0de, #2f96b4); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#2f96b4', GradientType=0); + border-color: #2f96b4 #2f96b4 #1f6377; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.btn-info:hover, +.btn-info:active, +.btn-info.active, +.btn-info.disabled, +.btn-info[disabled] { + background-color: #2f96b4; +} +.btn-info:active, .btn-info.active { + background-color: #24748c \9; +} +.btn-inverse { + background-color: #393939; + background-image: -moz-linear-gradient(top, #454545, #262626); + background-image: -ms-linear-gradient(top, #454545, #262626); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#454545), to(#262626)); + background-image: -webkit-linear-gradient(top, #454545, #262626); + background-image: -o-linear-gradient(top, #454545, #262626); + background-image: linear-gradient(top, #454545, #262626); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#454545', endColorstr='#262626', GradientType=0); + border-color: #262626 #262626 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.btn-inverse:hover, +.btn-inverse:active, +.btn-inverse.active, +.btn-inverse.disabled, +.btn-inverse[disabled] { + background-color: #262626; +} +.btn-inverse:active, .btn-inverse.active { + background-color: #0c0c0c \9; +} +button.btn, input[type="submit"].btn { + *padding-top: 2px; + *padding-bottom: 2px; +} +button.btn::-moz-focus-inner, input[type="submit"].btn::-moz-focus-inner { + padding: 0; + border: 0; +} +button.btn.large, input[type="submit"].btn.large { + *padding-top: 7px; + *padding-bottom: 7px; +} +button.btn.small, input[type="submit"].btn.small { + *padding-top: 3px; + *padding-bottom: 3px; +} +.btn-group { + position: relative; + *zoom: 1; + *margin-left: .3em; +} +.btn-group:before, .btn-group:after { + display: table; + content: ""; +} +.btn-group:after { + clear: both; +} +.btn-group:first-child { + *margin-left: 0; +} +.btn-group + .btn-group { + margin-left: 5px; +} +.btn-toolbar { + margin-top: 9px; + margin-bottom: 9px; +} +.btn-toolbar .btn-group { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; +} +.btn-group .btn { + position: relative; + float: left; + margin-left: -1px; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +.btn-group .btn:first-child { + margin-left: 0; + -webkit-border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; + border-top-left-radius: 4px; + -webkit-border-bottom-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; + border-bottom-left-radius: 4px; +} +.btn-group .btn:last-child, .btn-group .dropdown-toggle { + -webkit-border-top-right-radius: 4px; + -moz-border-radius-topright: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + -moz-border-radius-bottomright: 4px; + border-bottom-right-radius: 4px; +} +.btn-group .btn.large:first-child { + margin-left: 0; + -webkit-border-top-left-radius: 6px; + -moz-border-radius-topleft: 6px; + border-top-left-radius: 6px; + -webkit-border-bottom-left-radius: 6px; + -moz-border-radius-bottomleft: 6px; + border-bottom-left-radius: 6px; +} +.btn-group .btn.large:last-child, .btn-group .large.dropdown-toggle { + -webkit-border-top-right-radius: 6px; + -moz-border-radius-topright: 6px; + border-top-right-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + -moz-border-radius-bottomright: 6px; + border-bottom-right-radius: 6px; +} +.btn-group .btn:hover, +.btn-group .btn:focus, +.btn-group .btn:active, +.btn-group .btn.active { + z-index: 2; +} +.btn-group .dropdown-toggle:active, .btn-group.open .dropdown-toggle { + outline: 0; +} +.btn-group .dropdown-toggle { + padding-left: 8px; + padding-right: 8px; + -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + *padding-top: 5px; + *padding-bottom: 5px; +} +.btn-group.open { + *z-index: 1000; +} +.btn-group.open .dropdown-menu { + display: block; + margin-top: 1px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +.btn-group.open .dropdown-toggle { + background-image: none; + -webkit-box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} +.btn .caret { + margin-top: 7px; + margin-left: 0; +} +.btn:hover .caret, .open.btn-group .caret { + opacity: 1; + filter: alpha(opacity=100); +} +.btn-primary .caret, +.btn-danger .caret, +.btn-info .caret, +.btn-success .caret, +.btn-inverse .caret { + border-top-color: #ffffff; + opacity: 0.75; + filter: alpha(opacity=75); +} +.btn-small .caret { + margin-top: 4px; +} +.alert { + padding: 8px 35px 8px 14px; + margin-bottom: 18px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + background-color: #fcf8e3; + border: 1px solid #fbeed5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.alert, .alert-heading { + color: #c09853; +} +.alert .close { + position: relative; + top: -2px; + right: -21px; + line-height: 18px; +} +.alert-success { + background-color: #dff0d8; + border-color: #d6e9c6; +} +.alert-success, .alert-success .alert-heading { + color: #468847; +} +.alert-danger, .alert-error { + background-color: #f2dede; + border-color: #eed3d7; +} +.alert-danger, +.alert-error, +.alert-danger .alert-heading, +.alert-error .alert-heading { + color: #b94a48; +} +.alert-info { + background-color: #d9edf7; + border-color: #bce8f1; +} +.alert-info, .alert-info .alert-heading { + color: #3a87ad; +} +.alert-block { + padding-top: 14px; + padding-bottom: 14px; +} +.alert-block > p, .alert-block > ul { + margin-bottom: 0; +} +.alert-block p + p { + margin-top: 5px; +} +.nav { + margin-left: 0; + margin-bottom: 18px; + list-style: none; +} +.nav > li > a { + display: block; +} +.nav > li > a:hover { + text-decoration: none; + background-color: #eeeeee; +} +.nav .nav-header { + display: block; + padding: 3px 15px; + font-size: 11px; + font-weight: bold; + line-height: 18px; + color: #999999; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-transform: uppercase; +} +.nav li + .nav-header { + margin-top: 9px; +} +.nav-list { + padding-left: 14px; + padding-right: 14px; + margin-bottom: 0; +} +.nav-list > li > a, .nav-list .nav-header { + margin-left: -15px; + margin-right: -15px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); +} +.nav-list > li > a { + padding: 3px 15px; +} +.nav-list .active > a, .nav-list .active > a:hover { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); + background-color: #0088cc; +} +.nav-list [class^="icon-"] { + margin-right: 2px; +} +.nav-tabs, .nav-pills { + *zoom: 1; +} +.nav-tabs:before, +.nav-pills:before, +.nav-tabs:after, +.nav-pills:after { + display: table; + content: ""; +} +.nav-tabs:after, .nav-pills:after { + clear: both; +} +.nav-tabs > li, .nav-pills > li { + float: left; +} +.nav-tabs > li > a, .nav-pills > li > a { + padding-right: 12px; + padding-left: 12px; + margin-right: 2px; + line-height: 14px; +} +.nav-tabs { + border-bottom: 1px solid #ddd; +} +.nav-tabs > li { + margin-bottom: -1px; +} +.nav-tabs > li > a { + padding-top: 9px; + padding-bottom: 9px; + border: 1px solid transparent; + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} +.nav-tabs > li > a:hover { + border-color: #eeeeee #eeeeee #dddddd; +} +.nav-tabs > .active > a, .nav-tabs > .active > a:hover { + color: #555555; + background-color: #ffffff; + border: 1px solid #ddd; + border-bottom-color: transparent; + cursor: default; +} +.nav-pills > li > a { + padding-top: 8px; + padding-bottom: 8px; + margin-top: 2px; + margin-bottom: 2px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +.nav-pills .active > a, .nav-pills .active > a:hover { + color: #ffffff; + background-color: #0088cc; +} +.nav-stacked > li { + float: none; +} +.nav-stacked > li > a { + margin-right: 0; +} +.nav-tabs.nav-stacked { + border-bottom: 0; +} +.nav-tabs.nav-stacked > li > a { + border: 1px solid #ddd; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +.nav-tabs.nav-stacked > li:first-child > a { + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} +.nav-tabs.nav-stacked > li:last-child > a { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} +.nav-tabs.nav-stacked > li > a:hover { + border-color: #ddd; + z-index: 2; +} +.nav-pills.nav-stacked > li > a { + margin-bottom: 3px; +} +.nav-pills.nav-stacked > li:last-child > a { + margin-bottom: 1px; +} +.nav-tabs .dropdown-menu, .nav-pills .dropdown-menu { + margin-top: 1px; + border-width: 1px; +} +.nav-pills .dropdown-menu { + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.nav-tabs .dropdown-toggle .caret, .nav-pills .dropdown-toggle .caret { + border-top-color: #0088cc; + margin-top: 6px; +} +.nav-tabs .dropdown-toggle:hover .caret, .nav-pills .dropdown-toggle:hover .caret { + border-top-color: #005580; +} +.nav-tabs .active .dropdown-toggle .caret, .nav-pills .active .dropdown-toggle .caret { + border-top-color: #333333; +} +.nav > .dropdown.active > a:hover { + color: #000000; + cursor: pointer; +} +.nav-tabs .open .dropdown-toggle, .nav-pills .open .dropdown-toggle, .nav > .open.active > a:hover { + color: #ffffff; + background-color: #999999; + border-color: #999999; +} +.nav .open .caret, .nav .open.active .caret, .nav .open a:hover .caret { + border-top-color: #ffffff; + opacity: 1; + filter: alpha(opacity=100); +} +.tabs-stacked .open > a:hover { + border-color: #999999; +} +.tabbable { + *zoom: 1; +} +.tabbable:before, .tabbable:after { + display: table; + content: ""; +} +.tabbable:after { + clear: both; +} +.tab-content { + overflow: hidden; +} +.tabs-below .nav-tabs, .tabs-right .nav-tabs, .tabs-left .nav-tabs { + border-bottom: 0; +} +.tab-content > .tab-pane, .pill-content > .pill-pane { + display: none; +} +.tab-content > .active, .pill-content > .active { + display: block; +} +.tabs-below .nav-tabs { + border-top: 1px solid #ddd; +} +.tabs-below .nav-tabs > li { + margin-top: -1px; + margin-bottom: 0; +} +.tabs-below .nav-tabs > li > a { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} +.tabs-below .nav-tabs > li > a:hover { + border-bottom-color: transparent; + border-top-color: #ddd; +} +.tabs-below .nav-tabs .active > a, .tabs-below .nav-tabs .active > a:hover { + border-color: transparent #ddd #ddd #ddd; +} +.tabs-left .nav-tabs > li, .tabs-right .nav-tabs > li { + float: none; +} +.tabs-left .nav-tabs > li > a, .tabs-right .nav-tabs > li > a { + min-width: 74px; + margin-right: 0; + margin-bottom: 3px; +} +.tabs-left .nav-tabs { + float: left; + margin-right: 19px; + border-right: 1px solid #ddd; +} +.tabs-left .nav-tabs > li > a { + margin-right: -1px; + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} +.tabs-left .nav-tabs > li > a:hover { + border-color: #eeeeee #dddddd #eeeeee #eeeeee; +} +.tabs-left .nav-tabs .active > a, .tabs-left .nav-tabs .active > a:hover { + border-color: #ddd transparent #ddd #ddd; + *border-right-color: #ffffff; +} +.tabs-right .nav-tabs { + float: right; + margin-left: 19px; + border-left: 1px solid #ddd; +} +.tabs-right .nav-tabs > li > a { + margin-left: -1px; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} +.tabs-right .nav-tabs > li > a:hover { + border-color: #eeeeee #eeeeee #eeeeee #dddddd; +} +.tabs-right .nav-tabs .active > a, .tabs-right .nav-tabs .active > a:hover { + border-color: #ddd #ddd #ddd transparent; + *border-left-color: #ffffff; +} +.navbar { + overflow: visible; + margin-bottom: 18px; +} +.navbar-inner { + padding-left: 20px; + padding-right: 20px; + background-color: #2c2c2c; + background-image: -moz-linear-gradient(top, #333333, #222222); + background-image: -ms-linear-gradient(top, #333333, #222222); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222)); + background-image: -webkit-linear-gradient(top, #333333, #222222); + background-image: -o-linear-gradient(top, #333333, #222222); + background-image: linear-gradient(top, #333333, #222222); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); +} +.btn-navbar { + display: none; + float: right; + padding: 7px 10px; + margin-left: 5px; + margin-right: 5px; + background-color: #2c2c2c; + background-image: -moz-linear-gradient(top, #333333, #222222); + background-image: -ms-linear-gradient(top, #333333, #222222); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222)); + background-image: -webkit-linear-gradient(top, #333333, #222222); + background-image: -o-linear-gradient(top, #333333, #222222); + background-image: linear-gradient(top, #333333, #222222); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0); + border-color: #222222 #222222 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); +} +.btn-navbar:hover, +.btn-navbar:active, +.btn-navbar.active, +.btn-navbar.disabled, +.btn-navbar[disabled] { + background-color: #222222; +} +.btn-navbar:active, .btn-navbar.active { + background-color: #080808 \9; +} +.btn-navbar .icon-bar { + display: block; + width: 18px; + height: 2px; + background-color: #f5f5f5; + -webkit-border-radius: 1px; + -moz-border-radius: 1px; + border-radius: 1px; + -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); +} +.btn-navbar .icon-bar + .icon-bar { + margin-top: 3px; +} +.nav-collapse.collapse { + height: auto; +} +.navbar .brand:hover { + text-decoration: none; +} +.navbar .brand { + float: left; + display: block; + padding: 8px 20px 12px; + margin-left: -20px; + font-size: 20px; + font-weight: 200; + line-height: 1; + color: #ffffff; +} +.navbar .navbar-text { + margin-bottom: 0; + line-height: 40px; + color: #999999; +} +.navbar .navbar-text a:hover { + color: #ffffff; + background-color: transparent; +} +.navbar .btn, .navbar .btn-group { + margin-top: 5px; +} +.navbar .btn-group .btn { + margin-top: 0; +} +.navbar-form { + margin-bottom: 0; + *zoom: 1; +} +.navbar-form:before, .navbar-form:after { + display: table; + content: ""; +} +.navbar-form:after { + clear: both; +} +.navbar-form input, .navbar-form select { + display: inline-block; + margin-top: 5px; + margin-bottom: 0; +} +.navbar-form .radio, .navbar-form .checkbox { + margin-top: 5px; +} +.navbar-form input[type="image"], .navbar-form input[type="checkbox"], .navbar-form input[type="radio"] { + margin-top: 3px; +} +.navbar-form .input-append, .navbar-form .input-prepend { + margin-top: 6px; + white-space: nowrap; +} +.navbar-form .input-append input, .navbar-form .input-prepend input { + margin-top: 0; +} +.navbar-search { + position: relative; + float: left; + margin-top: 6px; + margin-bottom: 0; +} +.navbar-search .search-query { + padding: 4px 9px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + font-weight: normal; + line-height: 1; + color: #ffffff; + color: rgba(255, 255, 255, 0.75); + background: #666; + background: rgba(255, 255, 255, 0.3); + border: 1px solid #111; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.15); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.15); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.15); + -webkit-transition: none; + -moz-transition: none; + -ms-transition: none; + -o-transition: none; + transition: none; +} +.navbar-search .search-query :-moz-placeholder { + color: #eeeeee; +} +.navbar-search .search-query::-webkit-input-placeholder { + color: #eeeeee; +} +.navbar-search .search-query:hover { + color: #ffffff; + background-color: #999999; + background-color: rgba(255, 255, 255, 0.5); +} +.navbar-search .search-query:focus, .navbar-search .search-query.focused { + padding: 5px 10px; + color: #333333; + text-shadow: 0 1px 0 #ffffff; + background-color: #ffffff; + border: 0; + -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + outline: 0; +} +.navbar-fixed-top { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 1030; +} +.navbar-fixed-top .navbar-inner { + padding-left: 0; + padding-right: 0; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +.navbar .nav { + position: relative; + left: 0; + display: block; + float: left; + margin: 0 10px 0 0; +} +.navbar .nav.pull-right { + float: right; +} +.navbar .nav > li { + display: block; + float: left; +} +.navbar .nav > li > a { + float: none; + padding: 10px 10px 11px; + line-height: 19px; + color: #999999; + text-decoration: none; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} +.navbar .nav > li > a:hover { + background-color: transparent; + color: #ffffff; + text-decoration: none; +} +.navbar .nav .active > a, .navbar .nav .active > a:hover { + color: #ffffff; + text-decoration: none; + background-color: #222222; +} +.navbar .divider-vertical { + height: 40px; + width: 1px; + margin: 0 9px; + overflow: hidden; + background-color: #222222; + border-right: 1px solid #333333; +} +.navbar .nav.pull-right { + margin-left: 10px; + margin-right: 0; +} +.navbar .dropdown-menu { + margin-top: 1px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.navbar .dropdown-menu:before { + content: ''; + display: inline-block; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-bottom-color: rgba(0, 0, 0, 0.2); + position: absolute; + top: -7px; + left: 9px; +} +.navbar .dropdown-menu:after { + content: ''; + display: inline-block; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid #ffffff; + position: absolute; + top: -6px; + left: 10px; +} +.navbar .nav .dropdown-toggle .caret, .navbar .nav .open.dropdown .caret { + border-top-color: #ffffff; +} +.navbar .nav .active .caret { + opacity: 1; + filter: alpha(opacity=100); +} +.navbar .nav .open > .dropdown-toggle, .navbar .nav .active > .dropdown-toggle, .navbar .nav .open.active > .dropdown-toggle { + background-color: transparent; +} +.navbar .nav .active > .dropdown-toggle:hover { + color: #ffffff; +} +.navbar .nav.pull-right .dropdown-menu { + left: auto; + right: 0; +} +.navbar .nav.pull-right .dropdown-menu:before { + left: auto; + right: 12px; +} +.navbar .nav.pull-right .dropdown-menu:after { + left: auto; + right: 13px; +} +.breadcrumb { + padding: 7px 14px; + margin: 0 0 18px; + background-color: #fbfbfb; + background-image: -moz-linear-gradient(top, #ffffff, #f5f5f5); + background-image: -ms-linear-gradient(top, #ffffff, #f5f5f5); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f5f5f5)); + background-image: -webkit-linear-gradient(top, #ffffff, #f5f5f5); + background-image: -o-linear-gradient(top, #ffffff, #f5f5f5); + background-image: linear-gradient(top, #ffffff, #f5f5f5); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f5f5f5', GradientType=0); + border: 1px solid #ddd; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + -webkit-box-shadow: inset 0 1px 0 #ffffff; + -moz-box-shadow: inset 0 1px 0 #ffffff; + box-shadow: inset 0 1px 0 #ffffff; +} +.breadcrumb li { + display: inline-block; + text-shadow: 0 1px 0 #ffffff; +} +.breadcrumb .divider { + padding: 0 5px; + color: #999999; +} +.breadcrumb .active a { + color: #333333; +} +.pagination { + height: 36px; + margin: 18px 0; +} +.pagination ul { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; + margin-left: 0; + margin-bottom: 0; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} +.pagination li { + display: inline; +} +.pagination a { + float: left; + padding: 0 14px; + line-height: 34px; + text-decoration: none; + border: 1px solid #ddd; + border-left-width: 0; +} +.pagination a:hover, .pagination .active a { + background-color: #f5f5f5; +} +.pagination .active a { + color: #999999; + cursor: default; +} +.pagination .disabled a, .pagination .disabled a:hover { + color: #999999; + background-color: transparent; + cursor: default; +} +.pagination li:first-child a { + border-left-width: 1px; + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} +.pagination li:last-child a { + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} +.pagination-centered { + text-align: center; +} +.pagination-right { + text-align: right; +} +.pager { + margin-left: 0; + margin-bottom: 18px; + list-style: none; + text-align: center; + *zoom: 1; +} +.pager:before, .pager:after { + display: table; + content: ""; +} +.pager:after { + clear: both; +} +.pager li { + display: inline; +} +.pager a { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} +.pager a:hover { + text-decoration: none; + background-color: #f5f5f5; +} +.pager .next a { + float: right; +} +.pager .previous a { + float: left; +} +.modal-open .dropdown-menu { + z-index: 2050; +} +.modal-open .dropdown.open { + *z-index: 2050; +} +.modal-open .popover { + z-index: 2060; +} +.modal-open .tooltip { + z-index: 2070; +} +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000000; +} +.modal-backdrop.fade { + opacity: 0; +} +.modal-backdrop, .modal-backdrop.fade.in { + opacity: 0.8; + filter: alpha(opacity=80); +} +.modal { + position: fixed; + top: 50%; + left: 50%; + z-index: 1050; + max-height: 500px; + overflow: auto; + width: 560px; + margin: -250px 0 0 -280px; + background-color: #ffffff; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, 0.3); + *border: 1px solid #999; + /* IE6-7 */ + + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -webkit-background-clip: padding-box; + -moz-background-clip: padding-box; + background-clip: padding-box; +} +.modal.fade { + -webkit-transition: opacity .3s linear, top .3s ease-out; + -moz-transition: opacity .3s linear, top .3s ease-out; + -ms-transition: opacity .3s linear, top .3s ease-out; + -o-transition: opacity .3s linear, top .3s ease-out; + transition: opacity .3s linear, top .3s ease-out; + top: -25%; +} +.modal.fade.in { + top: 50%; +} +.modal-header { + padding: 9px 15px; + border-bottom: 1px solid #eee; +} +.modal-header .close { + margin-top: 2px; +} +.modal-body { + padding: 15px; +} +.modal-body .modal-form { + margin-bottom: 0; +} +.modal-footer { + padding: 14px 15px 15px; + margin-bottom: 0; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; + -webkit-box-shadow: inset 0 1px 0 #ffffff; + -moz-box-shadow: inset 0 1px 0 #ffffff; + box-shadow: inset 0 1px 0 #ffffff; + *zoom: 1; +} +.modal-footer:before, .modal-footer:after { + display: table; + content: ""; +} +.modal-footer:after { + clear: both; +} +.modal-footer .btn { + float: right; + margin-left: 5px; + margin-bottom: 0; +} +.tooltip { + position: absolute; + z-index: 1020; + display: block; + visibility: visible; + padding: 5px; + font-size: 11px; + opacity: 0; + filter: alpha(opacity=0); +} +.tooltip.in { + opacity: 0.8; + filter: alpha(opacity=80); +} +.tooltip.top { + margin-top: -2px; +} +.tooltip.right { + margin-left: 2px; +} +.tooltip.bottom { + margin-top: 2px; +} +.tooltip.left { + margin-left: -2px; +} +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid #000000; +} +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 5px solid #000000; +} +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 5px solid #000000; +} +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-right: 5px solid #000000; +} +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #ffffff; + text-align: center; + text-decoration: none; + background-color: #000000; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; +} +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1010; + display: none; + padding: 5px; +} +.popover.top { + margin-top: -5px; +} +.popover.right { + margin-left: 5px; +} +.popover.bottom { + margin-top: 5px; +} +.popover.left { + margin-left: -5px; +} +.popover.top .arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid #000000; +} +.popover.right .arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-right: 5px solid #000000; +} +.popover.bottom .arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 5px solid #000000; +} +.popover.left .arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 5px solid #000000; +} +.popover .arrow { + position: absolute; + width: 0; + height: 0; +} +.popover-inner { + padding: 3px; + width: 280px; + overflow: hidden; + background: #000000; + background: rgba(0, 0, 0, 0.8); + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); +} +.popover-title { + padding: 9px 15px; + line-height: 1; + background-color: #f5f5f5; + border-bottom: 1px solid #eee; + -webkit-border-radius: 3px 3px 0 0; + -moz-border-radius: 3px 3px 0 0; + border-radius: 3px 3px 0 0; +} +.popover-content { + padding: 14px; + background-color: #ffffff; + -webkit-border-radius: 0 0 3px 3px; + -moz-border-radius: 0 0 3px 3px; + border-radius: 0 0 3px 3px; + -webkit-background-clip: padding-box; + -moz-background-clip: padding-box; + background-clip: padding-box; +} +.popover-content p, .popover-content ul, .popover-content ol { + margin-bottom: 0; +} +.thumbnails { + margin-left: -20px; + list-style: none; + *zoom: 1; +} +.thumbnails:before, .thumbnails:after { + display: table; + content: ""; +} +.thumbnails:after { + clear: both; +} +.thumbnails > li { + float: left; + margin: 0 0 18px 20px; +} +.thumbnail { + display: block; + padding: 4px; + line-height: 1; + border: 1px solid #ddd; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); +} +a.thumbnail:hover { + border-color: #0088cc; + -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); +} +.thumbnail > img { + display: block; + max-width: 100%; + margin-left: auto; + margin-right: auto; +} +.thumbnail .caption { + padding: 9px; +} +.label { + padding: 2px 4px 3px; + font-size: 11.049999999999999px; + font-weight: bold; + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #999999; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.label:hover { + color: #ffffff; + text-decoration: none; +} +.label-important { + background-color: #b94a48; +} +.label-important:hover { + background-color: #953b39; +} +.label-warning { + background-color: #f89406; +} +.label-warning:hover { + background-color: #c67605; +} +.label-success { + background-color: #468847; +} +.label-success:hover { + background-color: #356635; +} +.label-info { + background-color: #3a87ad; +} +.label-info:hover { + background-color: #2d6987; +} +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} +@-moz-keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} +@keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} +.progress { + overflow: hidden; + height: 18px; + margin-bottom: 18px; + background-color: #f7f7f7; + background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -ms-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); + background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: linear-gradient(top, #f5f5f5, #f9f9f9); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f5f5f5', endColorstr='#f9f9f9', GradientType=0); + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.progress .bar { + width: 0%; + height: 18px; + color: #ffffff; + font-size: 12px; + text-align: center; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #0e90d2; + background-image: -moz-linear-gradient(top, #149bdf, #0480be); + background-image: -ms-linear-gradient(top, #149bdf, #0480be); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); + background-image: -webkit-linear-gradient(top, #149bdf, #0480be); + background-image: -o-linear-gradient(top, #149bdf, #0480be); + background-image: linear-gradient(top, #149bdf, #0480be); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#149bdf', endColorstr='#0480be', GradientType=0); + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-transition: width 0.6s ease; + -moz-transition: width 0.6s ease; + -ms-transition: width 0.6s ease; + -o-transition: width 0.6s ease; + transition: width 0.6s ease; +} +.progress-striped .bar { + background-color: #62c462; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + -moz-background-size: 40px 40px; + -o-background-size: 40px 40px; + background-size: 40px 40px; +} +.progress.active .bar { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -moz-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} +.progress-danger .bar { + background-color: #dd514c; + background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); + background-image: linear-gradient(top, #ee5f5b, #c43c35); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0); +} +.progress-danger.progress-striped .bar { + background-color: #ee5f5b; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.progress-success .bar { + background-color: #5eb95e; + background-image: -moz-linear-gradient(top, #62c462, #57a957); + background-image: -ms-linear-gradient(top, #62c462, #57a957); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957)); + background-image: -webkit-linear-gradient(top, #62c462, #57a957); + background-image: -o-linear-gradient(top, #62c462, #57a957); + background-image: linear-gradient(top, #62c462, #57a957); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0); +} +.progress-success.progress-striped .bar { + background-color: #62c462; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.progress-info .bar { + background-color: #4bb1cf; + background-image: -moz-linear-gradient(top, #5bc0de, #339bb9); + background-image: -ms-linear-gradient(top, #5bc0de, #339bb9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9)); + background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9); + background-image: -o-linear-gradient(top, #5bc0de, #339bb9); + background-image: linear-gradient(top, #5bc0de, #339bb9); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0); +} +.progress-info.progress-striped .bar { + background-color: #5bc0de; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.accordion { + margin-bottom: 18px; +} +.accordion-group { + margin-bottom: 2px; + border: 1px solid #e5e5e5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.accordion-heading { + border-bottom: 0; +} +.accordion-heading .accordion-toggle { + display: block; + padding: 8px 15px; +} +.accordion-inner { + padding: 9px 15px; + border-top: 1px solid #e5e5e5; +} +.carousel { + position: relative; + margin-bottom: 18px; + line-height: 1; +} +.carousel-inner { + overflow: hidden; + width: 100%; + position: relative; +} +.carousel .item { + display: none; + position: relative; + -webkit-transition: 0.6s ease-in-out left; + -moz-transition: 0.6s ease-in-out left; + -ms-transition: 0.6s ease-in-out left; + -o-transition: 0.6s ease-in-out left; + transition: 0.6s ease-in-out left; +} +.carousel .item > img { + display: block; + line-height: 1; +} +.carousel .active, .carousel .next, .carousel .prev { + display: block; +} +.carousel .active { + left: 0; +} +.carousel .next, .carousel .prev { + position: absolute; + top: 0; + width: 100%; +} +.carousel .next { + left: 100%; +} +.carousel .prev { + left: -100%; +} +.carousel .next.left, .carousel .prev.right { + left: 0; +} +.carousel .active.left { + left: -100%; +} +.carousel .active.right { + left: 100%; +} +.carousel-control { + position: absolute; + top: 40%; + left: 15px; + width: 40px; + height: 40px; + margin-top: -20px; + font-size: 60px; + font-weight: 100; + line-height: 30px; + color: #ffffff; + text-align: center; + background: #222222; + border: 3px solid #ffffff; + -webkit-border-radius: 23px; + -moz-border-radius: 23px; + border-radius: 23px; + opacity: 0.5; + filter: alpha(opacity=50); +} +.carousel-control.right { + left: auto; + right: 15px; +} +.carousel-control:hover { + color: #ffffff; + text-decoration: none; + opacity: 0.9; + filter: alpha(opacity=90); +} +.carousel-caption { + position: absolute; + left: 0; + right: 0; + bottom: 0; + padding: 10px 15px 5px; + background: #333333; + background: rgba(0, 0, 0, 0.75); +} +.carousel-caption h4, .carousel-caption p { + color: #ffffff; +} +.hero-unit { + padding: 60px; + margin-bottom: 30px; + background-color: #f5f5f5; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} +.hero-unit h1 { + margin-bottom: 0; + font-size: 60px; + line-height: 1; + letter-spacing: -1px; +} +.hero-unit p { + font-size: 18px; + font-weight: 200; + line-height: 27px; +} +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.hide { + display: none; +} +.show { + display: block; +} +.invisible { + visibility: hidden; +} diff --git a/hyperkitty/static/css/normalize.css b/hyperkitty/static/css/normalize.css new file mode 100644 index 0000000..f056d58 --- /dev/null +++ b/hyperkitty/static/css/normalize.css @@ -0,0 +1,504 @@ +/*! normalize.css 2012-03-06T10:21 UTC - http://github.com/necolas/normalize.css */ + +/* ============================================================================= + HTML5 display definitions + ========================================================================== */ + +/* + * Corrects block display not defined in IE6/7/8/9 & FF3 + */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section, +summary { + display: block; +} + +/* + * Corrects inline-block display not defined in IE6/7/8/9 & FF3 + */ + +audio, +canvas, +video { + display: inline-block; + *display: inline; + *zoom: 1; +} + +/* + * Prevents modern browsers from displaying 'audio' without controls + * Remove excess height in iOS5 devices + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/* + * Addresses styling for 'hidden' attribute not present in IE7/8/9, FF3, S4 + * Known issue: no IE6 support + */ + +[hidden] { + display: none; +} + + +/* ============================================================================= + Base + ========================================================================== */ + +/* + * 1. Corrects text resizing oddly in IE6/7 when body font-size is set using em units + * http://clagnut.com/blog/348/#c790 + * 2. Prevents iOS text size adjust after orientation change, without disabling user zoom + * www.456bereastreet.com/archive/201012/controlling_text_size_in_safari_for_ios_without_disabling_user_zoom/ + */ + +html { + font-size: 100%; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ + -ms-text-size-adjust: 100%; /* 2 */ +} + +/* + * Addresses font-family inconsistency between 'textarea' and other form elements. + */ + +html, +button, +input, +select, +textarea { + font-family: sans-serif; +} + +/* + * Addresses margins handled incorrectly in IE6/7 + */ + +body { + margin: 0; +} + + +/* ============================================================================= + Links + ========================================================================== */ + +/* + * Addresses outline displayed oddly in Chrome + */ + +a:focus { + outline: thin dotted; +} + +/* + * Improves readability when focused and also mouse hovered in all browsers + * people.opera.com/patrickl/experiments/keyboard/test + */ + +a:hover, +a:active { + outline: 0; +} + + +/* ============================================================================= + Typography + ========================================================================== */ + +/* + * Addresses font sizes and margins set differently in IE6/7 + * Addresses font sizes within 'section' and 'article' in FF4+, Chrome, S5 + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +h2 { + font-size: 1.5em; + margin: 0.83em 0; +} + +h3 { + font-size: 1.17em; + margin: 1em 0; +} + +h4 { + font-size: 1em; + margin: 1.33em 0; +} + +h5 { + font-size: 0.83em; + margin: 1.67em 0; +} + +h6 { + font-size: 0.75em; + margin: 2.33em 0; +} + +/* + * Addresses styling not present in IE7/8/9, S5, Chrome + */ + +abbr[title] { + border-bottom: 1px dotted; +} + +/* + * Addresses style set to 'bolder' in FF3+, S4/5, Chrome +*/ + +b, +strong { + font-weight: bold; +} + +blockquote { + margin: 1em 40px; +} + +/* + * Addresses styling not present in S5, Chrome + */ + +dfn { + font-style: italic; +} + +/* + * Addresses styling not present in IE6/7/8/9 + */ + +mark { + background: #ff0; + color: #000; +} + +/* + * Addresses margins set differently in IE6/7 + */ + +p, +pre { + margin: 1em 0; +} + +/* + * Corrects font family set oddly in IE6, S4/5, Chrome + * en.wikipedia.org/wiki/User:Davidgothberg/Test59 + */ + +pre, +code, +kbd, +samp { + font-family: monospace, serif; + _font-family: 'courier new', monospace; + font-size: 1em; +} + +/* + * Improves readability of pre-formatted text in all browsers + */ + +pre { + white-space: pre; + white-space: pre-wrap; + word-wrap: break-word; +} + +/* + * 1. Addresses CSS quotes not supported in IE6/7 + * 2. Addresses quote property not supported in S4 + */ + +/* 1 */ + +q { + quotes: none; +} + +/* 2 */ + +q:before, +q:after { + content: ''; + content: none; +} + +small { + font-size: 75%; +} + +/* + * Prevents sub and sup affecting line-height in all browsers + * gist.github.com/413930 + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + + +/* ============================================================================= + Lists + ========================================================================== */ + +/* + * Addresses margins set differently in IE6/7 + */ + +dl, +menu, +ol, +ul { + margin: 1em 0; +} + +dd { + margin: 0 0 0 40px; +} + +/* + * Addresses paddings set differently in IE6/7 + */ + +menu, +ol, +ul { + padding: 0 0 0 40px; +} + +/* + * Corrects list images handled incorrectly in IE7 + */ + +nav ul, +nav ol { + list-style: none; + list-style-image: none; +} + + +/* ============================================================================= + Embedded content + ========================================================================== */ + +/* + * 1. Removes border when inside 'a' element in IE6/7/8/9, FF3 + * 2. Improves image quality when scaled in IE7 + * code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ + */ + +img { + border: 0; /* 1 */ + -ms-interpolation-mode: bicubic; /* 2 */ +} + +/* + * Corrects overflow displayed oddly in IE9 + */ + +svg:not(:root) { + overflow: hidden; +} + + +/* ============================================================================= + Figures + ========================================================================== */ + +/* + * Addresses margin not present in IE6/7/8/9, S5, O11 + */ + +figure { + margin: 0; +} + + +/* ============================================================================= + Forms + ========================================================================== */ + +/* + * Corrects margin displayed oddly in IE6/7 + */ + +form { + margin: 0; +} + +/* + * Define consistent border, margin, and padding + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/* + * 1. Corrects color not being inherited in IE6/7/8/9 + * 2. Corrects text not wrapping in FF3 + * 3. Corrects alignment displayed oddly in IE6/7 + */ + +legend { + border: 0; /* 1 */ + padding: 0; + white-space: normal; /* 2 */ + *margin-left: -7px; /* 3 */ +} + +/* + * 1. Corrects font size not being inherited in all browsers + * 2. Addresses margins set differently in IE6/7, FF3+, S5, Chrome + * 3. Improves appearance and consistency in all browsers + */ + +button, +input, +select, +textarea { + font-size: 100%; /* 1 */ + margin: 0; /* 2 */ + vertical-align: baseline; /* 3 */ + *vertical-align: middle; /* 3 */ +} + +/* + * Addresses FF3/4 setting line-height on 'input' using !important in the UA stylesheet + */ + +button, +input { + line-height: normal; /* 1 */ +} + +/* + * 1. Improves usability and consistency of cursor style between image-type 'input' and others + * 2. Corrects inability to style clickable 'input' types in iOS + * 3. Removes inner spacing in IE7 without affecting normal text inputs + * Known issue: inner spacing remains in IE6 + */ + +button, +input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; /* 1 */ + -webkit-appearance: button; /* 2 */ + *overflow: visible; /* 3 */ +} + +/* + * Re-set default cursor for disabled elements + */ + +button[disabled], +input[disabled] { + cursor: default; +} + +/* + * 1. Addresses box sizing set to content-box in IE8/9 + * 2. Removes excess padding in IE8/9 + * 3. Removes excess padding in IE7 + Known issue: excess padding remains in IE6 + */ + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ + *height: 13px; /* 3 */ + *width: 13px; /* 3 */ +} + +/* + * 1. Addresses appearance set to searchfield in S5, Chrome + * 2. Addresses box-sizing set to border-box in S5, Chrome (include -moz to future-proof) + */ + +input[type="search"] { + -webkit-appearance: textfield; /* 1 */ + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; /* 2 */ + box-sizing: content-box; +} + +/* + * Removes inner padding and search cancel button in S5, Chrome on OS X + */ + +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +/* + * Removes inner padding and border in FF3+ + * www.sitepen.com/blog/2008/05/14/the-devils-in-the-details-fixing-dojos-toolbar-buttons/ + */ + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/* + * 1. Removes default vertical scrollbar in IE6/7/8/9 + * 2. Improves readability and alignment in all browsers + */ + +textarea { + overflow: auto; /* 1 */ + vertical-align: top; /* 2 */ +} + + +/* ============================================================================= + Tables + ========================================================================== */ + +/* + * Remove most spacing between table cells + */ + +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/hyperkitty/static/css/stats.css b/hyperkitty/static/css/stats.css new file mode 100644 index 0000000..ec1bf4d --- /dev/null +++ b/hyperkitty/static/css/stats.css @@ -0,0 +1,137 @@ +h2 { + margin-top: 0px; + padding-top: 10px; +} +/* Add icons to some text */ +.neutral { + background: url("../img/neutral.png") no-repeat scroll left; + padding-left: 20px; + padding-right: 20px; + font-weight: bold; +} + +.like { + background: url("../img/like.png") no-repeat scroll left; + padding-left: 20px; + padding-right: 20px; + font-weight: bold; +} + +.likealot { + background: url("../img/likealot.png") no-repeat scroll left; + padding-left: 20px; + padding-right: 20px; + font-weight: bold; +} + +/* The content section of the page */ +.content { + width: 1024px; + margin: auto; +} + +#graph { + vertical-align: middle; +} + +#fig { + position: relative; + margin: auto; + width: 540px; + height: 330px; +} + +#top_discussion { + width: 45%; + margin-right: 22px; + margin-left: 10px; +} + +#discussion_by_topic { + width: 45%; + margin-top: 20px; + margin-right: 22px; + margin-left: 10px; +} + +#most_active { + float: right; + width: 45%; +} + +#discussion_marker { + float: right; + width: 45%; + margin-top: 20px; +} + +.thread { + white-space: nowrap; +} + +.thread * { + white-space: normal; +} + +.thread_id { + font-weight: bold; + font-size: 125%; + color: rgb(102, 102, 102); + vertical-align: top; + padding-right: 10px; +} + +.thread_title{ + padding-right:20px; + color: rgb(102, 102, 102); + display: inline-block; +} + +.thread_stats ul li { + margin-right:10px; +} + +.category { + font-variant: small-caps; + font-weight: bold; + color: white; + -webkit-border-radius: 5px 5px 5px 5px; + -moz-border-radius: 5px 5px 5px 5px; + border-radius: 5px 5px 5px 5px; + vertical-align: top; + margin-bottom: 10px; + padding-top: 0px; + padding-left: 10px; +} + +.category_entry { + list-style-type: circle; + margin-top: 0px; + padding-bottom: 10px; + padding-left: 25px; +} + +.category_entry li { + padding-bottom: 10px; +} + +.maker { + color: rgb(102, 102, 102); + padding-right: 10px; + padding-bottom: 20px; +} + +.maker_id, .marker_name{ + font-weight: bold; + font-size: 115%; + vertical-align: top; + padding-right: 20px; +} + +.gravatar { + padding-right: 20px; +} + +.score{ + font-weight: bold; +} diff --git a/hyperkitty/static/css/style.css b/hyperkitty/static/css/style.css new file mode 100644 index 0000000..30535a2 --- /dev/null +++ b/hyperkitty/static/css/style.css @@ -0,0 +1,382 @@ +body { + position: relative; + padding-top: 90px; + background-color: white; +} + +.Sb { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + background-color: white; + clear: both; + font-size: 13px; + line-height: 1.4; + margin: 20px 0 20px 68px; + outline: none; + position: relative; + width: 497px; + word-wrap: break-word; +} + +.ZX { + color: #999; + height: 40px; + margin: 0 2px; + position: relative; + bottom: -3px; + background-color: #F8F8F8; + border: 1px solid #CCC; + +} + +.socialLogin { + list-style: none; + margin: 0px; +} + +.socialLogin li { + float: left; + padding: 5px; +} + +.right { + text-align: right; +} + +.inline-block { + display: inline-block; +} + +.inline li, .inline-block li { + display: inline-block; + list-style-type: none; +} + +/* Add icons to some text */ +.participant { + background: url("../img/participant.png") no-repeat scroll left top; + padding-left: 20px; +} + +.discussion { + background: url("../img/discussion.png") no-repeat scroll left top; + padding-left: 20px; +} + +.saved { + background: url("../img/saved.png") no-repeat scroll left top; + padding-left: 20px; +} + +.notsaved { + background: url("../img/notsaved.png") no-repeat scroll left top; + padding-left: 20px; +} + +.gravatar { + vertical-align: top; + width:40px; + font-size: 70%; +} + +.gravatar img { + width: 40px; +} + +.neutral { + background: url("../img/neutral.png") no-repeat scroll left; + padding-left: 20px; + padding-right: 20px; + font-weight: bold; +} + +.like { + background: url("../img/like.png") no-repeat scroll left; + padding-left: 20px; + padding-right: 20px; + font-weight: bold; +} + +.likealot { + background: url("../img/likealot.png") no-repeat scroll left; + padding-left: 20px; + padding-right: 20px; + font-weight: bold; +} + +.youlike { + background: url("../img/youlike.png") no-repeat scroll left; + padding-left: 15px; + padding-right: 5px; +} + +.youdislike { + background: url("../img/youdislike.png") no-repeat scroll left; + padding-left: 15px; + padding-right: 5px; +} + +.showdiscussion { + background-image: linear-gradient(bottom, rgb(204,204,204) 11%, rgb(255,255,255) 100%); + background-image: -o-linear-gradient(bottom, rgb(204,204,204) 11%, rgb(255,255,255) 100%); + background-image: -moz-linear-gradient(bottom, rgb(204,204,204) 11%, rgb(255,255,255) 100%); + background-image: -webkit-linear-gradient(bottom, rgb(204,204,204) 11%, rgb(255,255,255) 100%); + background-image: -ms-linear-gradient(bottom, rgb(204,204,204) 11%, rgb(255,255,255) 100%); + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0.11, rgb(204,204,204)), + color-stop(1, rgb(255,255,255)) + ); + padding: 3px 7px 3px 7px; + -webkit-border-radius: 5px 5px 5px 5px; + -moz-border-radius: 5px 5px 5px 5px; + border-radius: 5px 5px 5px 5px; + border-style: solid; + border-width: 1px; + border-color: rgb(179, 179, 179); +} + +.showdiscussion a { + color: rgb(77, 77, 77); +} + + +/* Top of the page -- header */ +.header { + background-color: rgb(236, 236, 236); + min-height : 100px; +} + +#white { + color: rgb(255, 255, 255); + background-color: rgb(255, 255, 255); + margin-bottom: 0px; +} + +#headline { + min-height: 50px; +} + +.list_nav { + float: left; + list-style: none; + margin: 0; + padding: 5px 0 0 0; +} + +.list_nav li { + float: left; + margin-left: 20px; +} + +.user_nav { + float: right; + list-style: none; + margin: 0; + padding: 5px 0 0 0; +} + +.user_nav a { + float: none; + padding: 10px 10px 11px; + line-height: 19px; + color: #999; + text-decoration: none; + text-shadow: 0 -1px 0 + rgba(0, 0, 0, 0.25); +} + +.user_nav li { + float: left; + margin-left: 20px; +} + +#thread_content { + width: 70%; +} + +.email_date .date { + font-weight: bold; +} + +#top_right { + position: absolute; + right: 20px; + bottom: 0; + color: rgb(102, 102, 102); +} + +#top_right li { + margin-left:10px; +} + +#list_name { + font-weight: bold; +} + +#page_date { + font-size: 150%; +} + + +#searchbox { + text-align:right; + padding-right: 20px; +} + +#searchbox input { + width: 250px +} + +#searchbox input::-webkit-input-placeholder { + font-style: italic; +} + +#searchbox input:-moz-placeholder { + font-style: italic; +} + +#recent_activities{ + width: 88%; + margin-top: 20px; + margin-right: 10px; + float: right; +} + +#archives{ + width: 9%; + margin-left: 10px; + margin-top: 20px; + float: left; +/* + margin-right: 2px; +*/ +} + +#archives ul { + padding: 0; + margin: 0; +} + +#archives li { + list-style-type: none; +} + +/* Thread list */ + +.thread_title { + font-weight: bold; + font-size: 125%; +} + +.thread_date { + font-style: italic; + font-size: 70%; + color: rgb(128, 0, 0); +} + +.thread_content { + margin-top:10px; +} + +.thread_info { + text-align:right; + padding-right: 50px; +} + +.tags { + text-align:left; + margin: 0px 0px 0px 200px; + padding: 0px; +} + +/* Part containing the body of the mail which can be shown/hidden */ +.expander { + width: 665px; + background-image: linear-gradient(bottom, rgb(236,236,236) 11%, rgb(255,255,255) 100%); + background-image: -o-linear-gradient(bottom, rgb(236,236,236) 11%, rgb(255,255,255) 100%); + background-image: -moz-linear-gradient(bottom, rgb(236,236,236) 11%, rgb(255,255,255) 100%); + background-image: -webkit-linear-gradient(bottom, rgb(236,236,236) 11%, rgb(255,255,255) 100%); + background-image: -ms-linear-gradient(bottom, rgb(236,236,236) 11%, rgb(255,255,255) 100%); + + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0.11, rgb(236,236,236)), + color-stop(1, rgb(255,255,255)) + ); + border-style: solid; + border-width: 1px; + border-color: rgb(236,236,236); + -webkit-border-radius: 5px 5px 5px 5px; + -moz-border-radius: 5px 5px 5px 5px; + border-radius: 5px 5px 5px 5px; + padding-left: 20px; + margin-left: 21px; + display: inline-block; + vertical-align: top; + white-space: pre; +} + +.expander a { + float: right; + padding: 20px 10px 0px 0px; +} + +/* Thread types */ +.type { + font-variant: small-caps; + font-weight: bold; + color: white; + padding: 3px; + -webkit-border-radius: 5px 5px 5px 5px; + -moz-border-radius: 5px 5px 5px 5px; + border-radius: 5px 5px 5px 5px; + vertical-align: top; + width: 110px; + text-align:center; +} + +.type a { + color: white; +} + +.type_question { + background-color: rgb(179, 128, 255); +} + +.type_agenda { + background-color: rgb(42, 127, 255); +} + +.type_todo { + background-color: rgb(200, 171, 55); +} + +.type_dead { + background-color: rgb(0, 0, 0); +} + +.type_announcement { + background-color: rgb(170, 212, 0); +} + +.type_policy { + background-color: rgb(200, 55, 171); +} + +.type_test { + background-color: rgb(200, 171, 55); +} + +.invisible { + visibility: hidden; +} + +.removed { + display: none; +}[ diff --git a/hyperkitty/static/css/thread.css b/hyperkitty/static/css/thread.css new file mode 100644 index 0000000..4815ace --- /dev/null +++ b/hyperkitty/static/css/thread.css @@ -0,0 +1,239 @@ +#thread_nav{ + margin:auto; + width:100%; + text-align:center; +} + +#thread_nav * { + vertical-align: middle; +} + +#thread_nav .thread_title{ + margin:auto; + width: 50%; +} + +#thread_nav br { + margin-top: 10px; +} + +#thread_nav .thread_info { + margin-top:10px; + margin-bottom:10px; + font-size: 70%; + font-weight: normal; + text-align: center; +} + +#thread_nav .thread_info li { + margin-left:3em; +} + +#olderhread, #newewthread { + font-size: 70%; + color: rgb(167, 169, 172); +} + +#olderhread { + float: right; + margin-top: 2em; + margin-right: 20px; +} + +/* Define the two columns */ +#thread_content { + width: 70%; + margin-right: 22px; +} + +#thread_overview_info { + float: right; + width: 22%; +} + +/* Thread general information column */ +#days_old { + margin-left:1em; +} + +.days_text { + font-size: 70%; +} + +.days_num { + font-size: 200%; +} + +#add_to_fav a{ + color: rgb(167, 169, 172); +} + +#grey { + color: rgb(167, 169, 172); + background-color: rgb(167, 169, 172); + margin: 0px; + border: 0 none; + height: 1px; +} + +#tags { + color: rgb(167, 169, 172); + margin-top: 20px; +} + +#tag_title { + color: rgb(77, 77, 77); + text-transform: uppercase; +} + +#tags ul { + padding: 10px 0px 10px 0px; + margin: 0; +} + +#add_tag_field { + width:70%; +} + +#participants { + margin-top: 20px; + color: rgb(167, 169, 172); +} + +#participants_title { + color: rgb(77, 77, 77); + text-transform: uppercase; +} + +#participants ul { + padding: 10px 0px 10px 0px; + margin: 0; +} + +#participants li { + list-style-type: none; +} + +#participants img { + width: 20px; +} + +/* Main section with the whole thread */ + +/* First email of the thread. */ + +.first_email { +} + +.email_header { + position:relative; + margin-top: 20px; + margin-bottom: 20px; +} + +.email_header img { + width: 40px; +} + +.email_author .name{ + color: rgb(55, 113, 200); + font-weight: bold; +} + +.email_author .rank{ + color: rgb(167, 169, 172); + font-size: 80%; + font-weight: bold; +} + +.email_date { + position: absolute; + right: 20px; + bottom: 0px; +} + +.email_date .date { + font-weight: bold; +} + +.email_date .time { + color: rgb(167, 169, 172); +} + +.email_info { + padding: 0px; +} + +.add_comment { + float: right; +} + +/* The email thread */ +.even { + background-color: rgb(246, 246, 246); + border-top: 1px solid rgb(179, 179, 179); + padding-left: 20px; + margin: 20px 0px 20px 0px; +} + +.odd { + background-color: rgb(238, 238, 238); + border-top: 1px solid rgb(179, 179, 179); + padding-left: 20px; + margin: 20px 0px 20px 0px; +} + +.email { +} + +.email .email_header { + margin-top: 10px; + margin-bottom: 10px; +} + +.email .email_author { + font-size: 90%; +} + +.email .email_date, .email .email_date .date { + font-size: 90%; +} + +.email_body{ + -webkit-border-radius: 5px 5px 5px 5px; + -moz-border-radius: 5px 5px 5px 5px; + border-radius: 5px 5px 5px 5px; + border-style: solid; + border-width: 1px; + border-color: rgb(179, 179, 179); + padding: 5px; + min-height: 40px; + background-color: rgb(255, 255, 255); + white-space: pre; + display: inline-block; +} + +#first_email_body { + white-space: pre; + display: inline-block; +} + + +.email_body a { + float: right; + padding: 3px 10px 0px 0px; +} + +.email_info { + padding: 0px; + margin-top: 5px; +} + +.thread_email { + padding-left: 20px; + margin-left: 21px; + display: inline-block; + vertical-align: top; + white-space: pre; +} + diff --git a/hyperkitty/static/img/button_newer.png b/hyperkitty/static/img/button_newer.png Binary files differnew file mode 100644 index 0000000..14cfaa6 --- /dev/null +++ b/hyperkitty/static/img/button_newer.png diff --git a/hyperkitty/static/img/button_older.png b/hyperkitty/static/img/button_older.png Binary files differnew file mode 100644 index 0000000..6c3c950 --- /dev/null +++ b/hyperkitty/static/img/button_older.png diff --git a/hyperkitty/static/img/discussion.png b/hyperkitty/static/img/discussion.png Binary files differnew file mode 100644 index 0000000..26e60d9 --- /dev/null +++ b/hyperkitty/static/img/discussion.png diff --git a/hyperkitty/static/img/email_bg.png b/hyperkitty/static/img/email_bg.png Binary files differnew file mode 100644 index 0000000..f3ae7b7 --- /dev/null +++ b/hyperkitty/static/img/email_bg.png diff --git a/hyperkitty/static/img/like.png b/hyperkitty/static/img/like.png Binary files differnew file mode 100644 index 0000000..7406cdd --- /dev/null +++ b/hyperkitty/static/img/like.png diff --git a/hyperkitty/static/img/likealot.png b/hyperkitty/static/img/likealot.png Binary files differnew file mode 100644 index 0000000..5ce4b88 --- /dev/null +++ b/hyperkitty/static/img/likealot.png diff --git a/hyperkitty/static/img/login/browserid.png b/hyperkitty/static/img/login/browserid.png Binary files differnew file mode 100644 index 0000000..919a5c7 --- /dev/null +++ b/hyperkitty/static/img/login/browserid.png diff --git a/hyperkitty/static/img/login/facebook.png b/hyperkitty/static/img/login/facebook.png Binary files differnew file mode 100644 index 0000000..551100d --- /dev/null +++ b/hyperkitty/static/img/login/facebook.png diff --git a/hyperkitty/static/img/login/google.png b/hyperkitty/static/img/login/google.png Binary files differnew file mode 100644 index 0000000..b840860 --- /dev/null +++ b/hyperkitty/static/img/login/google.png diff --git a/hyperkitty/static/img/login/openid.png b/hyperkitty/static/img/login/openid.png Binary files differnew file mode 100644 index 0000000..bc81687 --- /dev/null +++ b/hyperkitty/static/img/login/openid.png diff --git a/hyperkitty/static/img/login/twitter.png b/hyperkitty/static/img/login/twitter.png Binary files differnew file mode 100644 index 0000000..14960b8 --- /dev/null +++ b/hyperkitty/static/img/login/twitter.png diff --git a/hyperkitty/static/img/login/yahoo.png b/hyperkitty/static/img/login/yahoo.png Binary files differnew file mode 100644 index 0000000..e9deaf2 --- /dev/null +++ b/hyperkitty/static/img/login/yahoo.png diff --git a/hyperkitty/static/img/neutral.png b/hyperkitty/static/img/neutral.png Binary files differnew file mode 100644 index 0000000..392f8c7 --- /dev/null +++ b/hyperkitty/static/img/neutral.png diff --git a/hyperkitty/static/img/newthread.png b/hyperkitty/static/img/newthread.png Binary files differnew file mode 100644 index 0000000..e61b871 --- /dev/null +++ b/hyperkitty/static/img/newthread.png diff --git a/hyperkitty/static/img/notsaved.png b/hyperkitty/static/img/notsaved.png Binary files differnew file mode 100644 index 0000000..a427a91 --- /dev/null +++ b/hyperkitty/static/img/notsaved.png diff --git a/hyperkitty/static/img/participant.png b/hyperkitty/static/img/participant.png Binary files differnew file mode 100644 index 0000000..f2d700b --- /dev/null +++ b/hyperkitty/static/img/participant.png diff --git a/hyperkitty/static/img/saved.png b/hyperkitty/static/img/saved.png Binary files differnew file mode 100644 index 0000000..b240cd5 --- /dev/null +++ b/hyperkitty/static/img/saved.png diff --git a/hyperkitty/static/img/show_discussion.png b/hyperkitty/static/img/show_discussion.png Binary files differnew file mode 100644 index 0000000..f7f42f1 --- /dev/null +++ b/hyperkitty/static/img/show_discussion.png diff --git a/hyperkitty/static/img/youdislike.png b/hyperkitty/static/img/youdislike.png Binary files differnew file mode 100644 index 0000000..0c6387b --- /dev/null +++ b/hyperkitty/static/img/youdislike.png diff --git a/hyperkitty/static/img/youlike.png b/hyperkitty/static/img/youlike.png Binary files differnew file mode 100644 index 0000000..affe451 --- /dev/null +++ b/hyperkitty/static/img/youlike.png diff --git a/hyperkitty/static/jquery.expander.js b/hyperkitty/static/jquery.expander.js new file mode 100644 index 0000000..9eabab4 --- /dev/null +++ b/hyperkitty/static/jquery.expander.js @@ -0,0 +1,382 @@ +/*! + * jQuery Expander Plugin v1.4 + * + * Date: Sun Dec 11 15:08:42 2011 EST + * Requires: jQuery v1.3+ + * + * Copyright 2011, Karl Swedberg + * Dual licensed under the MIT and GPL licenses (just like jQuery): + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * + * + * +*/ + +(function($) { + $.expander = { + version: '1.4', + defaults: { + // the number of characters at which the contents will be sliced into two parts. + slicePoint: 100, + + // whether to keep the last word of the summary whole (true) or let it slice in the middle of a word (false) + preserveWords: true, + + // a threshold of sorts for whether to initially hide/collapse part of the element's contents. + // If after slicing the contents in two there are fewer words in the second part than + // the value set by widow, we won't bother hiding/collapsing anything. + widow: 4, + + // text displayed in a link instead of the hidden part of the element. + // clicking this will expand/show the hidden/collapsed text + expandText: 'read more', + expandPrefix: '… ', + + expandAfterSummary: false, + + // class names for summary element and detail element + summaryClass: 'summary', + detailClass: 'details', + + // class names for <span> around "read-more" link and "read-less" link + moreClass: 'read-more', + lessClass: 'read-less', + + // number of milliseconds after text has been expanded at which to collapse the text again. + // when 0, no auto-collapsing + collapseTimer: 0, + + // effects for expanding and collapsing + expandEffect: 'fadeIn', + expandSpeed: 250, + collapseEffect: 'fadeOut', + collapseSpeed: 200, + + // allow the user to re-collapse the expanded text. + userCollapse: true, + + // text to use for the link to re-collapse the text + userCollapseText: 'read less', + userCollapsePrefix: ' ', + + + // all callback functions have the this keyword mapped to the element in the jQuery set when .expander() is called + + onSlice: null, // function() {} + beforeExpand: null, // function() {}, + afterExpand: null, // function() {}, + onCollapse: null // function(byUser) {} + } + }; + + $.fn.expander = function(options) { + var meth = 'init'; + + if (typeof options == 'string') { + meth = options; + options = {}; + } + + var opts = $.extend({}, $.expander.defaults, options), + rSelfClose = /^<(?:area|br|col|embed|hr|img|input|link|meta|param).*>$/i, + rAmpWordEnd = /(&(?:[^;]+;)?|\w+)$/, + rOpenCloseTag = /<\/?(\w+)[^>]*>/g, + rOpenTag = /<(\w+)[^>]*>/g, + rCloseTag = /<\/(\w+)>/g, + rLastCloseTag = /(<\/[^>]+>)\s*$/, + rTagPlus = /^<[^>]+>.?/, + delayedCollapse; + + var methods = { + init: function() { + this.each(function() { + var i, l, tmp, summTagLess, summOpens, summCloses, lastCloseTag, detailText, + $thisDetails, $readMore, + openTagsForDetails = [], + closeTagsForsummaryText = [], + defined = {}, + thisEl = this, + $this = $(this), + $summEl = $([]), + o = $.meta ? $.extend({}, opts, $this.data()) : opts, + hasDetails = !!$this.find('.' + o.detailClass).length, + hasBlocks = !!$this.find('*').filter(function() { + var display = $(this).css('display'); + return (/^block|table|list/).test(display); + }).length, + el = hasBlocks ? 'div' : 'span', + detailSelector = el + '.' + o.detailClass, + moreSelector = 'span.' + o.moreClass, + expandSpeed = o.expandSpeed || 0, + allHtml = $.trim( $this.html() ), + allText = $.trim( $this.text() ), + summaryText = allHtml.slice(0, o.slicePoint); + + // bail out if we've already set up the expander on this element + if ( $.data(this, 'expander') ) { + return; + } + $.data(this, 'expander', true); + + // determine which callback functions are defined + $.each(['onSlice','beforeExpand', 'afterExpand', 'onCollapse'], function(index, val) { + defined[val] = $.isFunction(o[val]); + }); + + // back up if we're in the middle of a tag or word + summaryText = backup(summaryText); + + // summary text sans tags length + summTagless = summaryText.replace(rOpenCloseTag, '').length; + + // add more characters to the summary, one for each character in the tags + while (summTagless < o.slicePoint) { + newChar = allHtml.charAt(summaryText.length); + if (newChar == '<') { + newChar = allHtml.slice(summaryText.length).match(rTagPlus)[0]; + } + summaryText += newChar; + summTagless++; + } + + summaryText = backup(summaryText, o.preserveWords); + + // separate open tags from close tags and clean up the lists + summOpens = summaryText.match(rOpenTag) || []; + summCloses = summaryText.match(rCloseTag) || []; + + // filter out self-closing tags + tmp = []; + $.each(summOpens, function(index, val) { + if ( !rSelfClose.test(val) ) { + tmp.push(val); + } + }); + summOpens = tmp; + + // strip close tags to just the tag name + l = summCloses.length; + for (i = 0; i < l; i++) { + summCloses[i] = summCloses[i].replace(rCloseTag, '$1'); + } + + // tags that start in summary and end in detail need: + // a). close tag at end of summary + // b). open tag at beginning of detail + $.each(summOpens, function(index, val) { + var thisTagName = val.replace(rOpenTag, '$1'); + var closePosition = $.inArray(thisTagName, summCloses); + if (closePosition === -1) { + openTagsForDetails.push(val); + closeTagsForsummaryText.push('</' + thisTagName + '>'); + + } else { + summCloses.splice(closePosition, 1); + } + }); + + // reverse the order of the close tags for the summary so they line up right + closeTagsForsummaryText.reverse(); + + // create necessary summary and detail elements if they don't already exist + if ( !hasDetails ) { + + // end script if there is no detail text or if detail has fewer words than widow option + detailText = allHtml.slice(summaryText.length); + + if ( detailText === '' || detailText.split(/\s+/).length < o.widow ) { + return; + } + + // otherwise, continue... + lastCloseTag = closeTagsForsummaryText.pop() || ''; + summaryText += closeTagsForsummaryText.join(''); + detailText = openTagsForDetails.join('') + detailText; + + } else { + // assume that even if there are details, we still need readMore/readLess/summary elements + // (we already bailed out earlier when readMore el was found) + // but we need to create els differently + + // remove the detail from the rest of the content + detailText = $this.find(detailSelector).remove().html(); + + // The summary is what's left + summaryText = $this.html(); + + // allHtml is the summary and detail combined (this is needed when content has block-level elements) + allHtml = summaryText + detailText; + + lastCloseTag = ''; + } + o.moreLabel = $this.find(moreSelector).length ? '' : buildMoreLabel(o); + + if (hasBlocks) { + detailText = allHtml; + } + summaryText += lastCloseTag; + + // onSlice callback + o.summary = summaryText; + o.details = detailText; + o.lastCloseTag = lastCloseTag; + + if (defined.onSlice) { + // user can choose to return a modified options object + // one last chance for user to change the options. sneaky, huh? + // but could be tricky so use at your own risk. + tmp = o.onSlice.call(thisEl, o); + + // so, if the returned value from the onSlice function is an object with a details property, we'll use that! + o = tmp && tmp.details ? tmp : o; + } + + // build the html with summary and detail and use it to replace old contents + var html = buildHTML(o, hasBlocks); + + $this.html( html ); + + // set up details and summary for expanding/collapsing + $thisDetails = $this.find(detailSelector); + $readMore = $this.find(moreSelector); + $thisDetails.hide(); + $readMore.find('a').unbind('click.expander').bind('click.expander', expand); + + $summEl = $this.find('div.' + o.summaryClass); + + if ( o.userCollapse && !$this.find('span.' + o.lessClass).length ) { + $this + .find(detailSelector) + .append('<span class="' + o.lessClass + '">' + o.userCollapsePrefix + '<a href="#">' + o.userCollapseText + '</a></span>'); + } + + $this + .find('span.' + o.lessClass + ' a') + .unbind('click.expander') + .bind('click.expander', function(event) { + event.preventDefault(); + clearTimeout(delayedCollapse); + var $detailsCollapsed = $(this).closest(detailSelector); + reCollapse(o, $detailsCollapsed); + if (defined.onCollapse) { + o.onCollapse.call(thisEl, true); + } + }); + + function expand(event) { + event.preventDefault(); + $readMore.hide(); + $summEl.hide(); + if (defined.beforeExpand) { + o.beforeExpand.call(thisEl); + } + + $thisDetails.stop(false, true)[o.expandEffect](expandSpeed, function() { + $thisDetails.css({zoom: ''}); + if (defined.afterExpand) {o.afterExpand.call(thisEl);} + delayCollapse(o, $thisDetails, thisEl); + }); + } + + }); // this.each + }, + destroy: function() { + if ( !this.data('expander') ) { + return; + } + this.removeData('expander'); + this.each(function() { + var $this = $(this), + o = $.meta ? $.extend({}, opts, $this.data()) : opts, + details = $this.find('.' + o.detailClass).contents(); + + $this.find('.' + o.moreClass).remove(); + $this.find('.' + o.summaryClass).remove(); + $this.find('.' + o.detailClass).after(details).remove(); + $this.find('.' + o.lessClass).remove(); + + }); + } + }; + + // run the methods (almost always "init") + if ( methods[meth] ) { + methods[ meth ].call(this); + } + + // utility functions + function buildHTML(o, blocks) { + var el = 'span', + summary = o.summary; + if ( blocks ) { + el = 'div'; + // if summary ends with a close tag, tuck the moreLabel inside it + if ( rLastCloseTag.test(summary) && !o.expandAfterSummary) { + summary = summary.replace(rLastCloseTag, o.moreLabel + '$1'); + } else { + // otherwise (e.g. if ends with self-closing tag) just add moreLabel after summary + // fixes #19 + summary += o.moreLabel; + } + + // and wrap it in a div + summary = '<div class="' + o.summaryClass + '">' + summary + '</div>'; + } else { + summary += o.moreLabel; + } + + return [ + summary, + '<', + el + ' class="' + o.detailClass + '"', + '>', + o.details, + '</' + el + '>' + ].join(''); + } + + function buildMoreLabel(o) { + var ret = '<span class="' + o.moreClass + '">' + o.expandPrefix; + ret += '<a href="#">' + o.expandText + '</a></span>'; + return ret; + } + + function backup(txt, preserveWords) { + if ( txt.lastIndexOf('<') > txt.lastIndexOf('>') ) { + txt = txt.slice( 0, txt.lastIndexOf('<') ); + } + if (preserveWords) { + txt = txt.replace(rAmpWordEnd,''); + } + return txt; + } + + function reCollapse(o, el) { + el.stop(true, true)[o.collapseEffect](o.collapseSpeed, function() { + var prevMore = el.prev('span.' + o.moreClass).show(); + if (!prevMore.length) { + el.parent().children('div.' + o.summaryClass).show() + .find('span.' + o.moreClass).show(); + } + }); + } + + function delayCollapse(option, $collapseEl, thisEl) { + if (option.collapseTimer) { + delayedCollapse = setTimeout(function() { + reCollapse(option, $collapseEl); + if ( $.isFunction(option.onCollapse) ) { + option.onCollapse.call(thisEl, false); + } + }, option.collapseTimer); + } + } + + return this; + }; + + // plugin defaults + $.fn.expander.defaults = $.expander.defaults; +})(jQuery); diff --git a/hyperkitty/static/js/libs/jquery-1.7.1.min.js b/hyperkitty/static/js/libs/jquery-1.7.1.min.js new file mode 100644 index 0000000..198b3ff --- /dev/null +++ b/hyperkitty/static/js/libs/jquery-1.7.1.min.js @@ -0,0 +1,4 @@ +/*! jQuery v1.7.1 jquery.com | jquery.org/license */ +(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cv(a){if(!ck[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){cl||(cl=c.createElement("iframe"),cl.frameBorder=cl.width=cl.height=0),b.appendChild(cl);if(!cm||!cl.createElement)cm=(cl.contentWindow||cl.contentDocument).document,cm.write((c.compatMode==="CSS1Compat"?"<!doctype html>":"")+"<html><body>"),cm.close();d=cm.createElement(a),cm.body.appendChild(d),e=f.css(d,"display"),b.removeChild(cl)}ck[a]=e}return ck[a]}function cu(a,b){var c={};f.each(cq.concat.apply([],cq.slice(0,b)),function(){c[this]=a});return c}function ct(){cr=b}function cs(){setTimeout(ct,0);return cr=f.now()}function cj(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ci(){try{return new a.XMLHttpRequest}catch(b){}}function cc(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g<i;g++){if(g===1)for(h in a.converters)typeof h=="string"&&(e[h.toLowerCase()]=a.converters[h]);l=k,k=d[g];if(k==="*")k=l;else if(l!=="*"&&l!==k){m=l+" "+k,n=e[m]||e["* "+k];if(!n){p=b;for(o in e){j=o.split(" ");if(j[0]===l||j[0]==="*"){p=e[j[1]+" "+k];if(p){o=e[o],o===!0?n=p:p===!0&&(n=o);break}}}}!n&&!p&&f.error("No conversion from "+m.replace(" "," to ")),n!==!0&&(c=n?n(c):p(o(c)))}}return c}function cb(a,c,d){var e=a.contents,f=a.dataTypes,g=a.responseFields,h,i,j,k;for(i in g)i in d&&(c[g[i]]=d[i]);while(f[0]==="*")f.shift(),h===b&&(h=a.mimeType||c.getResponseHeader("content-type"));if(h)for(i in e)if(e[i]&&e[i].test(h)){f.unshift(i);break}if(f[0]in d)j=f[0];else{for(i in d){if(!f[0]||a.converters[i+" "+f[0]]){j=i;break}k||(k=i)}j=j||k}if(j){j!==f[0]&&f.unshift(j);return d[j]}}function ca(a,b,c,d){if(f.isArray(b))f.each(b,function(b,e){c||bE.test(a)?d(a,e):ca(a+"["+(typeof e=="object"||f.isArray(e)?b:"")+"]",e,c,d)});else if(!c&&b!=null&&typeof b=="object")for(var e in b)ca(a+"["+e+"]",b[e],c,d);else d(a,b)}function b_(a,c){var d,e,g=f.ajaxSettings.flatOptions||{};for(d in c)c[d]!==b&&((g[d]?a:e||(e={}))[d]=c[d]);e&&f.extend(!0,a,e)}function b$(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h=a[f],i=0,j=h?h.length:0,k=a===bT,l;for(;i<j&&(k||!l);i++)l=h[i](c,d,e),typeof l=="string"&&(!k||g[l]?l=b:(c.dataTypes.unshift(l),l=b$(a,c,d,e,l,g)));(k||!l)&&!g["*"]&&(l=b$(a,c,d,e,"*",g));return l}function bZ(a){return function(b,c){typeof b!="string"&&(c=b,b="*");if(f.isFunction(c)){var d=b.toLowerCase().split(bP),e=0,g=d.length,h,i,j;for(;e<g;e++)h=d[e],j=/^\+/.test(h),j&&(h=h.substr(1)||"*"),i=a[h]=a[h]||[],i[j?"unshift":"push"](c)}}}function bC(a,b,c){var d=b==="width"?a.offsetWidth:a.offsetHeight,e=b==="width"?bx:by,g=0,h=e.length;if(d>0){if(c!=="border")for(;g<h;g++)c||(d-=parseFloat(f.css(a,"padding"+e[g]))||0),c==="margin"?d+=parseFloat(f.css(a,c+e[g]))||0:d-=parseFloat(f.css(a,"border"+e[g]+"Width"))||0;return d+"px"}d=bz(a,b,b);if(d<0||d==null)d=a.style[b]||0;d=parseFloat(d)||0;if(c)for(;g<h;g++)d+=parseFloat(f.css(a,"padding"+e[g]))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+e[g]+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+e[g]))||0);return d+"px"}function bp(a,b){b.src?f.ajax({url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(bf,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)}function bo(a){var b=c.createElement("div");bh.appendChild(b),b.innerHTML=a.outerHTML;return b.firstChild}function bn(a){var b=(a.nodeName||"").toLowerCase();b==="input"?bm(a):b!=="script"&&typeof a.getElementsByTagName!="undefined"&&f.grep(a.getElementsByTagName("input"),bm)}function bm(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bl(a){return typeof a.getElementsByTagName!="undefined"?a.getElementsByTagName("*"):typeof a.querySelectorAll!="undefined"?a.querySelectorAll("*"):[]}function bk(a,b){var c;if(b.nodeType===1){b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase();if(c==="object")b.outerHTML=a.outerHTML;else if(c!=="input"||a.type!=="checkbox"&&a.type!=="radio"){if(c==="option")b.selected=a.defaultSelected;else if(c==="input"||c==="textarea")b.defaultValue=a.defaultValue}else a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value);b.removeAttribute(f.expando)}}function bj(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c,d,e,g=f._data(a),h=f._data(b,g),i=g.events;if(i){delete h.handle,h.events={};for(c in i)for(d=0,e=i[c].length;d<e;d++)f.event.add(b,c+(i[c][d].namespace?".":"")+i[c][d].namespace,i[c][d],i[c][d].data)}h.data&&(h.data=f.extend({},h.data))}}function bi(a,b){return f.nodeName(a,"table")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function U(a){var b=V.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function T(a,b,c){b=b||0;if(f.isFunction(b))return f.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return f.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=f.grep(a,function(a){return a.nodeType===1});if(O.test(b))return f.filter(b,d,!c);b=f.filter(b,d)}return f.grep(a,function(a,d){return f.inArray(a,b)>=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?parseFloat(d):j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c<d;c++)b[a[c]]=!0;return b}var c=a.document,d=a.navigator,e=a.location,f=function(){function J(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(J,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.1",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j<k;j++)if((a=arguments[j])!=null)for(c in a){d=i[c],f=a[c];if(i===f)continue;l&&f&&(e.isPlainObject(f)||(g=e.isArray(f)))?(g?(g=!1,h=d&&e.isArray(d)?d:[]):h=d&&e.isPlainObject(d)?d:{},i[c]=e.extend(l,h,f)):f!==b&&(i[c]=f)}return i},e.extend({noConflict:function(b){a.$===e&&(a.$=g),b&&a.jQuery===e&&(a.jQuery=f);return e},isReady:!1,readyWait:1,holdReady:function(a){a?e.readyWait++:e.ready(!0)},ready:function(a){if(a===!0&&!--e.readyWait||a!==!0&&!e.isReady){if(!c.body)return setTimeout(e.ready,1);e.isReady=!0;if(a!==!0&&--e.readyWait>0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g<h;)if(c.apply(a[g++],d)===!1)break}else if(i){for(f in a)if(c.call(a[f],f,a[f])===!1)break}else for(;g<h;)if(c.call(a[g],g,a[g++])===!1)break;return a},trim:G?function(a){return a==null?"":G.call(a)}:function(a){return a==null?"":(a+"").replace(k,"").replace(l,"")},makeArray:function(a,b){var c=b||[];if(a!=null){var d=e.type(a);a.length==null||d==="string"||d==="function"||d==="regexp"||e.isWindow(a)?E.call(c,a):e.merge(c,a)}return c},inArray:function(a,b,c){var d;if(b){if(H)return H.call(b,a,c);d=b.length,c=c?c<0?Math.max(0,d+c):c:0;for(;c<d;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,c){var d=a.length,e=0;if(typeof c.length=="number")for(var f=c.length;e<f;e++)a[d++]=c[e];else while(c[e]!==b)a[d++]=c[e++];a.length=d;return a},grep:function(a,b,c){var d=[],e;c=!!c;for(var f=0,g=a.length;f<g;f++)e=!!b(a[f],f),c!==e&&d.push(a[f]);return d},map:function(a,c,d){var f,g,h=[],i=0,j=a.length,k=a instanceof e||j!==b&&typeof j=="number"&&(j>0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i<j;i++)f=c(a[i],i,d),f!=null&&(h[h.length]=f);else for(g in a)f=c(a[g],g,d),f!=null&&(h[h.length]=f);return h.concat.apply([],h)},guid:1,proxy:function(a,c){if(typeof c=="string"){var d=a[c];c=a,a=d}if(!e.isFunction(a))return b;var f=F.call(arguments,2),g=function(){return a.apply(c,f.concat(F.call(arguments)))};g.guid=a.guid=a.guid||g.guid||e.guid++;return g},access:function(a,c,d,f,g,h){var i=a.length;if(typeof c=="object"){for(var j in c)e.access(a,j,c[j],f,g,d);return a}if(d!==b){f=!h&&f&&e.isFunction(d);for(var k=0;k<i;k++)g(a[k],c,f?d.call(a[k],k,g(a[k],c)):d,h);return a}return i?g(a[0],c):b},now:function(){return(new Date).getTime()},uaMatch:function(a){a=a.toLowerCase();var b=r.exec(a)||s.exec(a)||t.exec(a)||a.indexOf("compatible")<0&&u.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},sub:function(){function a(b,c){return new a.fn.init(b,c)}e.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function(d,f){f&&f instanceof e&&!(f instanceof a)&&(f=a(f));return e.fn.init.call(this,d,f,b)},a.fn.init.prototype=a.fn;var b=a(c);return a},browser:{}}),e.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){I["[object "+b+"]"]=b.toLowerCase()}),z=e.uaMatch(y),z.browser&&(e.browser[z.browser]=!0,e.browser.version=z.version),e.browser.webkit&&(e.browser.safari=!0),j.test(" ")&&(k=/^[\s\xA0]+/,l=/[\s\xA0]+$/),h=e(c),c.addEventListener?B=function(){c.removeEventListener("DOMContentLoaded",B,!1),e.ready()}:c.attachEvent&&(B=function(){c.readyState==="complete"&&(c.detachEvent("onreadystatechange",B),e.ready())});return e}(),g={};f.Callbacks=function(a){a=a?g[a]||h(a):{};var c=[],d=[],e,i,j,k,l,m=function(b){var d,e,g,h,i;for(d=0,e=b.length;d<e;d++)g=b[d],h=f.type(g),h==="array"?m(g):h==="function"&&(!a.unique||!o.has(g))&&c.push(g)},n=function(b,f){f=f||[],e=!a.memory||[b,f],i=!0,l=j||0,j=0,k=c.length;for(;c&&l<k;l++)if(c[l].apply(b,f)===!1&&a.stopOnFalse){e=!0;break}i=!1,c&&(a.once?e===!0?o.disable():c=[]:d&&d.length&&(e=d.shift(),o.fireWith(e[0],e[1])))},o={add:function(){if(c){var a=c.length;m(arguments),i?k=c.length:e&&e!==!0&&(j=a,n(e[0],e[1]))}return this},remove:function(){if(c){var b=arguments,d=0,e=b.length;for(;d<e;d++)for(var f=0;f<c.length;f++)if(b[d]===c[f]){i&&f<=k&&(k--,f<=l&&l--),c.splice(f--,1);if(a.unique)break}}return this},has:function(a){if(c){var b=0,d=c.length;for(;b<d;b++)if(a===c[b])return!0}return!1},empty:function(){c=[];return this},disable:function(){c=d=e=b;return this},disabled:function(){return!c},lock:function(){d=b,(!e||e===!0)&&o.disable();return this},locked:function(){return!d},fireWith:function(b,c){d&&(i?a.once||d.push([b,c]):(!a.once||!e)&&n(b,c));return this},fire:function(){o.fireWith(this,arguments);return this},fired:function(){return!!e}};return o};var i=[].slice;f.extend({Deferred:function(a){var b=f.Callbacks("once memory"),c=f.Callbacks("once memory"),d=f.Callbacks("memory"),e="pending",g={resolve:b,reject:c,notify:d},h={done:b.add,fail:c.add,progress:d.add,state:function(){return e},isResolved:b.fired,isRejected:c.fired,then:function(a,b,c){i.done(a).fail(b).progress(c);return this},always:function(){i.done.apply(i,arguments).fail.apply(i,arguments);return this},pipe:function(a,b,c){return f.Deferred(function(d){f.each({done:[a,"resolve"],fail:[b,"reject"],progress:[c,"notify"]},function(a,b){var c=b[0],e=b[1],g;f.isFunction(c)?i[a](function(){g=c.apply(this,arguments),g&&f.isFunction(g.promise)?g.promise().then(d.resolve,d.reject,d.notify):d[e+"With"](this===i?d:this,[g])}):i[a](d[e])})}).promise()},promise:function(a){if(a==null)a=h;else for(var b in h)a[b]=h[b];return a}},i=h.promise({}),j;for(j in g)i[j]=g[j].fire,i[j+"With"]=g[j].fireWith;i.done(function(){e="resolved"},c.disable,d.lock).fail(function(){e="rejected"},b.disable,d.lock),a&&a.call(i,i);return i},when:function(a){function m(a){return function(b){e[a]=arguments.length>1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c<d;c++)b[c]&&b[c].promise&&f.isFunction(b[c].promise)?b[c].promise().then(l(c),j.reject,m(c)):--g;g||j.resolveWith(j,b)}else j!==a&&j.resolveWith(j,d?[a]:[]);return k}}),f.support=function(){var b,d,e,g,h,i,j,k,l,m,n,o,p,q=c.createElement("div"),r=c.documentElement;q.setAttribute("className","t"),q.innerHTML=" <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/>",d=q.getElementsByTagName("*"),e=q.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=q.getElementsByTagName("input")[0],b={leadingWhitespace:q.firstChild.nodeType===3,tbody:!q.getElementsByTagName("tbody").length,htmlSerialize:!!q.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:q.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav></:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete q.test}catch(s){b.deleteExpando=!1}!q.addEventListener&&q.attachEvent&&q.fireEvent&&(q.attachEvent("onclick",function(){b.noCloneEvent=!1}),q.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),q.appendChild(i),k=c.createDocumentFragment(),k.appendChild(q.lastChild),b.checkClone=k.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,k.removeChild(i),k.appendChild(q),q.innerHTML="",a.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",q.style.width="2px",q.appendChild(j),b.reliableMarginRight=(parseInt((a.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0);if(q.attachEvent)for(o in{submit:1,change:1,focusin:1})n="on"+o,p=n in q,p||(q.setAttribute(n,"return;"),p=typeof q[n]=="function"),b[o+"Bubbles"]=p;k.removeChild(q),k=g=h=j=q=i=null,f(function(){var a,d,e,g,h,i,j,k,m,n,o,r=c.getElementsByTagName("body")[0];!r||(j=1,k="position:absolute;top:0;left:0;width:1px;height:1px;margin:0;",m="visibility:hidden;border:0;",n="style='"+k+"border:5px solid #000;padding:0;'",o="<div "+n+"><div></div></div>"+"<table "+n+" cellpadding='0' cellspacing='0'>"+"<tr><td></td></tr></table>",a=c.createElement("div"),a.style.cssText=m+"width:0;height:0;position:static;top:0;margin-top:"+j+"px",r.insertBefore(a,r.firstChild),q=c.createElement("div"),a.appendChild(q),q.innerHTML="<table><tr><td style='padding:0;border:0;display:none'></td><td>t</td></tr></table>",l=q.getElementsByTagName("td"),p=l[0].offsetHeight===0,l[0].style.display="",l[1].style.display="none",b.reliableHiddenOffsets=p&&l[0].offsetHeight===0,q.innerHTML="",q.style.width=q.style.paddingLeft="1px",f.boxModel=b.boxModel=q.offsetWidth===2,typeof q.style.zoom!="undefined"&&(q.style.display="inline",q.style.zoom=1,b.inlineBlockNeedsLayout=q.offsetWidth===2,q.style.display="",q.innerHTML="<div style='width:4px;'></div>",b.shrinkWrapBlocks=q.offsetWidth!==2),q.style.cssText=k+m,q.innerHTML=o,d=q.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,i={doesNotAddBorder:e.offsetTop!==5,doesAddBorderForTableAndCells:h.offsetTop===5},e.style.position="fixed",e.style.top="20px",i.fixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",i.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,i.doesNotIncludeMarginInBodyOffset=r.offsetTop!==j,r.removeChild(a),q=a=null,f.extend(b,i))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e<g;e++)delete d[b[e]];if(!(c?m:f.isEmptyObject)(d))return}}if(!c){delete j[k].data;if(!m(j[k]))return}f.support.deleteExpando||!j.setInterval?delete j[k]:j[k]=null,i&&(f.support.deleteExpando?delete a[h]:a.removeAttribute?a.removeAttribute(h):a[h]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d,e,g,h=null;if(typeof a=="undefined"){if(this.length){h=f.data(this[0]);if(this[0].nodeType===1&&!f._data(this[0],"parsedAttrs")){e=this[0].attributes;for(var i=0,j=e.length;i<j;i++)g=e[i].name,g.indexOf("data-")===0&&(g=f.camelCase(g.substring(5)),l(this[0],g,h[g]));f._data(this[0],"parsedAttrs",!0)}}return h}if(typeof a=="object")return this.each(function(){f.data(this,a)});d=a.split("."),d[1]=d[1]?"."+d[1]:"";if(c===b){h=this.triggerHandler("getData"+d[1]+"!",[d[0]]),h===b&&this.length&&(h=f.data(this[0],a),h=l(this[0],a,h));return h===b&&d[1]?this.data(d[0]):h}return this.each(function(){var b=f(this),e=[d[0],c];b.triggerHandler("setData"+d[1]+"!",e),f.data(this,a,c),b.triggerHandler("changeData"+d[1]+"!",e)})},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,b){a&&(b=(b||"fx")+"mark",f._data(a,b,(f._data(a,b)||0)+1))},_unmark:function(a,b,c){a!==!0&&(c=b,b=a,a=!1);if(b){c=c||"fx";var d=c+"mark",e=a?0:(f._data(b,d)||1)-1;e?f._data(b,d,e):(f.removeData(b,d,!0),n(b,c,"mark"))}},queue:function(a,b,c){var d;if(a){b=(b||"fx")+"queue",d=f._data(a,b),c&&(!d||f.isArray(c)?d=f._data(a,b,f.makeArray(c)):d.push(c));return d||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e={};d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),f._data(a,b+".run",e),d.call(a,function(){f.dequeue(a,b)},e)),c.length||(f.removeData(a,b+"queue "+b+".run",!0),n(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){typeof a!="string"&&(c=a,a="fx");if(c===b)return f.queue(this[0],a);return this.each(function(){var b=f.queue(this,a,c);a==="fx"&&b[0]!=="inprogress"&&f.dequeue(this,a)})},dequeue:function(a){return this.each(function(){f.dequeue(this,a)})},delay:function(a,b){a=f.fx?f.fx.speeds[a]||a:a,b=b||"fx";return this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,c){function m(){--h||d.resolveWith(e,[e])}typeof a!="string"&&(c=a,a=b),a=a||"fx";var d=f.Deferred(),e=this,g=e.length,h=1,i=a+"defer",j=a+"queue",k=a+"mark",l;while(g--)if(l=f.data(e[g],i,b,!0)||(f.data(e[g],j,b,!0)||f.data(e[g],k,b,!0))&&f.data(e[g],i,f.Callbacks("once memory"),!0))h++,l.add(m);m();return d.promise()}});var o=/[\n\t\r]/g,p=/\s+/,q=/\r/g,r=/^(?:button|input)$/i,s=/^(?:button|input|object|select|textarea)$/i,t=/^a(?:rea)?$/i,u=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,v=f.support.getSetAttribute,w,x,y;f.fn.extend({attr:function(a,b){return f.access(this,a,b,!0,f.attr)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,a,b,!0,f.prop)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(p);for(c=0,d=this.length;c<d;c++){e=this[c];if(e.nodeType===1)if(!e.className&&b.length===1)e.className=a;else{g=" "+e.className+" ";for(h=0,i=b.length;h<i;h++)~g.indexOf(" "+b[h]+" ")||(g+=b[h]+" ");e.className=f.trim(g)}}}return this},removeClass:function(a){var c,d,e,g,h,i,j;if(f.isFunction(a))return this.each(function(b){f(this).removeClass(a.call(this,b,this.className))});if(a&&typeof a=="string"||a===b){c=(a||"").split(p);for(d=0,e=this.length;d<e;d++){g=this[d];if(g.nodeType===1&&g.className)if(a){h=(" "+g.className+" ").replace(o," ");for(i=0,j=c.length;i<j;i++)h=h.replace(" "+c[i]+" "," ");g.className=f.trim(h)}else g.className=""}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";if(f.isFunction(a))return this.each(function(c){f(this).toggleClass(a.call(this,c,this.className,b),b)});return this.each(function(){if(c==="string"){var e,g=0,h=f(this),i=b,j=a.split(p);while(e=j[g++])i=d?i:!h.hasClass(e),h[i?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&f._data(this,"__className__",this.className),this.className=this.className||a===!1?"":f._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ",c=0,d=this.length;for(;c<d;c++)if(this[c].nodeType===1&&(" "+this[c].className+" ").replace(o," ").indexOf(b)>-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.nodeName.toLowerCase()]||f.valHooks[g.type];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c<d;c++){e=i[c];if(e.selected&&(f.support.optDisabled?!e.disabled:e.getAttribute("disabled")===null)&&(!e.parentNode.disabled||!f.nodeName(e.parentNode,"optgroup"))){b=f(e).val();if(j)return b;h.push(b)}}if(j&&!h.length&&i.length)return f(i[g]).val();return h},set:function(a,b){var c=f.makeArray(b);f(a).find("option").each(function(){this.selected=f.inArray(f(this).val(),c)>=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;h<g;h++)e=d[h],e&&(c=f.propFix[e]||e,f.attr(a,e,""),a.removeAttribute(v?e:c),u.test(e)&&c in a&&(a[c]=!1))}},attrHooks:{type:{set:function(a,b){if(r.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.value;a.setAttribute("type",b),c&&(a.value=c);return b}}},value:{get:function(a,b){if(w&&f.nodeName(a,"button"))return w.get(a,b);return b in a?a.value:null},set:function(a,b,c){if(w&&f.nodeName(a,"button"))return w.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e,g,h,i=a.nodeType;if(!!a&&i!==3&&i!==8&&i!==2){h=i!==1||!f.isXMLDoc(a),h&&(c=f.propFix[c]||c,g=f.propHooks[c]);return d!==b?g&&"set"in g&&(e=g.set(a,d,c))!==b?e:a[c]=d:g&&"get"in g&&(e=g.get(a,c))!==null?e:a[c]}},propHooks:{tabIndex:{get:function(a){var c=a.getAttributeNode("tabindex");return c&&c.specified?parseInt(c.value,10):s.test(a.nodeName)||t.test(a.nodeName)&&a.href?0:b}}}}),f.attrHooks.tabindex=f.propHooks.tabIndex,x={get:function(a,c){var d,e=f.prop(a,c);return e===!0||typeof e!="boolean"&&(d=a.getAttributeNode(c))&&d.nodeValue!==!1?c.toLowerCase():b},set:function(a,b,c){var d;b===!1?f.removeAttr(a,c):(d=f.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase()));return c}},v||(y={name:!0,id:!0},w=f.valHooks.button={get:function(a,c){var d;d=a.getAttributeNode(c);return d&&(y[c]?d.nodeValue!=="":d.specified)?d.nodeValue:b},set:function(a,b,d){var e=a.getAttributeNode(d);e||(e=c.createAttribute(d),a.setAttributeNode(e));return e.nodeValue=b+""}},f.attrHooks.tabindex.set=w.set,f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})}),f.attrHooks.contenteditable={get:w.get,set:function(a,b,c){b===""&&(b="false"),w.set(a,b,c)}}),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex);return null}})),f.support.enctype||(f.propFix.enctype="encoding"),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/\bhover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function(a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")}; +f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k<c.length;k++){l=A.exec(c[k])||[],m=l[1],n=(l[2]||"").split(".").sort(),s=f.event.special[m]||{},m=(g?s.delegateType:s.bindType)||m,s=f.event.special[m]||{},o=f.extend({type:m,origType:l[1],data:e,handler:d,guid:d.guid,selector:g,quick:G(g),namespace:n.join(".")},p),r=j[m];if(!r){r=j[m]=[],r.delegateCount=0;if(!s.setup||s.setup.call(a,e,n,i)===!1)a.addEventListener?a.addEventListener(m,i,!1):a.attachEvent&&a.attachEvent("on"+m,i)}s.add&&(s.add.call(a,o),o.handler.guid||(o.handler.guid=d.guid)),g?r.splice(r.delegateCount++,0,o):r.push(o),f.event.global[m]=!0}a=null}},global:{},remove:function(a,b,c,d,e){var g=f.hasData(a)&&f._data(a),h,i,j,k,l,m,n,o,p,q,r,s;if(!!g&&!!(o=g.events)){b=f.trim(I(b||"")).split(" ");for(h=0;h<b.length;h++){i=A.exec(b[h])||[],j=k=i[1],l=i[2];if(!j){for(j in o)f.event.remove(a,j+b[h],c,d,!0);continue}p=f.event.special[j]||{},j=(d?p.delegateType:p.bindType)||j,r=o[j]||[],m=r.length,l=l?new RegExp("(^|\\.)"+l.split(".").sort().join("\\.(?:.*\\.)?")+"(\\.|$)"):null;for(n=0;n<r.length;n++)s=r[n],(e||k===s.origType)&&(!c||c.guid===s.guid)&&(!l||l.test(s.namespace))&&(!d||d===s.selector||d==="**"&&s.selector)&&(r.splice(n--,1),s.selector&&r.delegateCount--,p.remove&&p.remove.call(a,s));r.length===0&&m!==r.length&&((!p.teardown||p.teardown.call(a,l)===!1)&&f.removeEvent(a,j,g.handle),delete o[j])}f.isEmptyObject(o)&&(q=g.handle,q&&(q.elem=null),f.removeData(a,["events","handle"],!0))}},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(c,d,e,g){if(!e||e.nodeType!==3&&e.nodeType!==8){var h=c.type||c,i=[],j,k,l,m,n,o,p,q,r,s;if(E.test(h+f.event.triggered))return;h.indexOf("!")>=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;l<r.length&&!c.isPropagationStopped();l++)m=r[l][0],c.type=r[l][1],q=(f._data(m,"events")||{})[c.type]&&f._data(m,"handle"),q&&q.apply(m,d),q=o&&m[o],q&&f.acceptData(m)&&q.apply(m,d)===!1&&c.preventDefault();c.type=h,!g&&!c.isDefaultPrevented()&&(!p._default||p._default.apply(e.ownerDocument,d)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)&&o&&e[h]&&(h!=="focus"&&h!=="blur"||c.target.offsetWidth!==0)&&!f.isWindow(e)&&(n=e[o],n&&(e[o]=null),f.event.triggered=h,e[h](),f.event.triggered=b,n&&(e[o]=n));return c.result}},dispatch:function(c){c=f.event.fix(c||a.event);var d=(f._data(this,"events")||{})[c.type]||[],e=d.delegateCount,g=[].slice.call(arguments,0),h=!c.exclusive&&!c.namespace,i=[],j,k,l,m,n,o,p,q,r,s,t;g[0]=c,c.delegateTarget=this;if(e&&!c.target.disabled&&(!c.button||c.type!=="click")){m=f(this),m.context=this.ownerDocument||this;for(l=c.target;l!=this;l=l.parentNode||this){o={},q=[],m[0]=l;for(j=0;j<e;j++)r=d[j],s=r.selector,o[s]===b&&(o[s]=r.quick?H(l,r.quick):m.is(s)),o[s]&&q.push(r);q.length&&i.push({elem:l,matches:q})}}d.length>e&&i.push({elem:this,matches:d.slice(e)});for(j=0;j<i.length&&!c.isPropagationStopped();j++){p=i[j],c.currentTarget=p.elem;for(k=0;k<p.matches.length&&!c.isImmediatePropagationStopped();k++){r=p.matches[k];if(h||!c.namespace&&!r.namespace||c.namespace_re&&c.namespace_re.test(r.namespace))c.data=r.data,c.handleObj=r,n=((f.event.special[r.origType]||{}).handle||r.handler).apply(p.elem,g),n!==b&&(c.result=n,n===!1&&(c.preventDefault(),c.stopPropagation()))}}return c.result},props:"attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){a.which==null&&(a.which=b.charCode!=null?b.charCode:b.keyCode);return a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,d){var e,f,g,h=d.button,i=d.fromElement;a.pageX==null&&d.clientX!=null&&(e=a.target.ownerDocument||c,f=e.documentElement,g=e.body,a.pageX=d.clientX+(f&&f.scrollLeft||g&&g.scrollLeft||0)-(f&&f.clientLeft||g&&g.clientLeft||0),a.pageY=d.clientY+(f&&f.scrollTop||g&&g.scrollTop||0)-(f&&f.clientTop||g&&g.clientTop||0)),!a.relatedTarget&&i&&(a.relatedTarget=i===a.target?d.toElement:i),!a.which&&h!==b&&(a.which=h&1?1:h&2?3:h&4?2:0);return a}},fix:function(a){if(a[f.expando])return a;var d,e,g=a,h=f.event.fixHooks[a.type]||{},i=h.props?this.props.concat(h.props):this.props;a=f.Event(g);for(d=i.length;d;)e=i[--d],a[e]=g[e];a.target||(a.target=g.srcElement||c),a.target.nodeType===3&&(a.target=a.target.parentNode),a.metaKey===b&&(a.metaKey=a.ctrlKey);return h.filter?h.filter(a,g):a},special:{ready:{setup:f.bindReady},load:{noBubble:!0},focus:{delegateType:"focusin"},blur:{delegateType:"focusout"},beforeunload:{setup:function(a,b,c){f.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}},simulate:function(a,b,c,d){var e=f.extend(new f.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?f.event.trigger(e,null,b):f.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},f.event.handle=f.event.dispatch,f.removeEvent=c.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){a.detachEvent&&a.detachEvent("on"+b,c)},f.Event=function(a,b){if(!(this instanceof f.Event))return new f.Event(a,b);a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?K:J):this.type=a,b&&f.extend(this,b),this.timeStamp=a&&a.timeStamp||f.now(),this[f.expando]=!0},f.Event.prototype={preventDefault:function(){this.isDefaultPrevented=K;var a=this.originalEvent;!a||(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){this.isPropagationStopped=K;var a=this.originalEvent;!a||(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=K,this.stopPropagation()},isDefaultPrevented:J,isPropagationStopped:J,isImmediatePropagationStopped:J},f.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){f.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c=this,d=a.relatedTarget,e=a.handleObj,g=e.selector,h;if(!d||d!==c&&!f.contains(c,d))a.type=e.origType,h=e.handler.apply(this,arguments),a.type=b;return h}}}),f.support.submitBubbles||(f.event.special.submit={setup:function(){if(f.nodeName(this,"form"))return!1;f.event.add(this,"click._submit keypress._submit",function(a){var c=a.target,d=f.nodeName(c,"input")||f.nodeName(c,"button")?c.form:b;d&&!d._submit_attached&&(f.event.add(d,"submit._submit",function(a){this.parentNode&&!a.isTrigger&&f.event.simulate("submit",this.parentNode,a,!0)}),d._submit_attached=!0)})},teardown:function(){if(f.nodeName(this,"form"))return!1;f.event.remove(this,"._submit")}}),f.support.changeBubbles||(f.event.special.change={setup:function(){if(z.test(this.nodeName)){if(this.type==="checkbox"||this.type==="radio")f.event.add(this,"propertychange._change",function(a){a.originalEvent.propertyName==="checked"&&(this._just_changed=!0)}),f.event.add(this,"click._change",function(a){this._just_changed&&!a.isTrigger&&(this._just_changed=!1,f.event.simulate("change",this,a,!0))});return!1}f.event.add(this,"beforeactivate._change",function(a){var b=a.target;z.test(b.nodeName)&&!b._change_attached&&(f.event.add(b,"change._change",function(a){this.parentNode&&!a.isSimulated&&!a.isTrigger&&f.event.simulate("change",this.parentNode,a,!0)}),b._change_attached=!0)})},handle:function(a){var b=a.target;if(this!==b||a.isSimulated||a.isTrigger||b.type!=="radio"&&b.type!=="checkbox")return a.handleObj.handler.apply(this,arguments)},teardown:function(){f.event.remove(this,"._change");return z.test(this.nodeName)}}),f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){var d=0,e=function(a){f.event.simulate(b,a.target,f.event.fix(a),!0)};f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.fn.extend({on:function(a,c,d,e,g){var h,i;if(typeof a=="object"){typeof c!="string"&&(d=c,c=b);for(i in a)this.on(i,c,d,a[i],g);return this}d==null&&e==null?(e=c,d=c=b):e==null&&(typeof c=="string"?(e=d,d=b):(e=d,d=c,c=b));if(e===!1)e=J;else if(!e)return this;g===1&&(h=e,e=function(a){f().off(a);return h.apply(this,arguments)},e.guid=h.guid||(h.guid=f.guid++));return this.each(function(){f.event.add(this,a,e,d,c)})},one:function(a,b,c,d){return this.on.call(this,a,b,c,d,1)},off:function(a,c,d){if(a&&a.preventDefault&&a.handleObj){var e=a.handleObj;f(a.delegateTarget).off(e.namespace?e.type+"."+e.namespace:e.type,e.selector,e.handler);return this}if(typeof a=="object"){for(var g in a)this.off(g,c,a[g]);return this}if(c===!1||typeof c=="function")d=c,c=b;d===!1&&(d=J);return this.each(function(){f.event.remove(this,a,d,c)})},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},live:function(a,b,c){f(this.context).on(a,this.selector,b,c);return this},die:function(a,b){f(this.context).off(a,this.selector||"**",b);return this},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return arguments.length==1?this.off(a,"**"):this.off(b,a,c)},trigger:function(a,b){return this.each(function(){f.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0])return f.event.trigger(a,b,this[0],!0)},toggle:function(a){var b=arguments,c=a.guid||f.guid++,d=0,e=function(c){var e=(f._data(this,"lastToggle"+a.guid)||0)%d;f._data(this,"lastToggle"+a.guid,e+1),c.preventDefault();return b[e].apply(this,arguments)||!1};e.guid=c;while(d<b.length)b[d++].guid=c;return this.click(e)},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),f.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){f.fn[b]=function(a,c){c==null&&(c=a,a=null);return arguments.length>0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h<i;h++){var j=e[h];if(j){var k=!1;j=j[a];while(j){if(j[d]===c){k=e[j.sizset];break}if(j.nodeType===1){g||(j[d]=c,j.sizset=h);if(typeof b!="string"){if(j===b){k=!0;break}}else if(m.filter(b,[j]).length>0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h<i;h++){var j=e[h];if(j){var k=!1;j=j[a];while(j){if(j[d]===c){k=e[j.sizset];break}j.nodeType===1&&!g&&(j[d]=c,j.sizset=h);if(j.nodeName.toLowerCase()===b){k=j;break}j=j[a]}e[h]=k}}}var a=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b<a.length;b++)a[b]===a[b-1]&&a.splice(b--,1)}return a},m.matches=function(a,b){return m(a,null,null,b)},m.matchesSelector=function(a,b){return m(b,null,null,[a]).length>0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e<f;e++){h=o.order[e];if(g=o.leftMatch[h].exec(a)){i=g[1],g.splice(1,1);if(i.substr(i.length-1)!=="\\"){g[1]=(g[1]||"").replace(j,""),d=o.find[h](g,b,c);if(d!=null){a=a.replace(o.match[h],"");break}}}}d||(d=typeof b.getElementsByTagName!="undefined"?b.getElementsByTagName("*"):[]);return{set:d,expr:a}},m.filter=function(a,c,d,e){var f,g,h,i,j,k,l,n,p,q=a,r=[],s=c,t=c&&c[0]&&m.isXML(c[0]);while(a&&c.length){for(h in o.filter)if((f=o.leftMatch[h].exec(a))!=null&&f[2]){k=o.filter[h],l=f[1],g=!1,f.splice(1,1);if(l.substr(l.length-1)==="\\")continue;s===r&&(r=[]);if(o.preFilter[h]){f=o.preFilter[h](f,s,d,r,e,t);if(!f)g=i=!0;else if(f===!0)continue}if(f)for(n=0;(j=s[n])!=null;n++)j&&(i=k(j,f,n,s),p=e^i,d&&i!=null?p?g=!0:s[n]=!1:p&&(r.push(j),g=!0));if(i!==b){d||(s=r),a=a.replace(o.match[h],"");if(!g)return[];break}}if(a===q)if(g==null)m.error(a);else break;q=a}return s},m.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)};var n=m.getText=function(a){var b,c,d=a.nodeType,e="";if(d){if(d===1||d===9){if(typeof a.textContent=="string")return a.textContent;if(typeof a.innerText=="string")return a.innerText.replace(k,"");for(a=a.firstChild;a;a=a.nextSibling)e+=n(a)}else if(d===3||d===4)return a.nodeValue}else for(b=0;c=a[b];b++)c.nodeType!==8&&(e+=n(c));return e},o=m.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(a){return a.getAttribute("href")},type:function(a){return a.getAttribute("type")}},relative:{"+":function(a,b){var c=typeof b=="string",d=c&&!l.test(b),e=c&&!d;d&&(b=b.toLowerCase());for(var f=0,g=a.length,h;f<g;f++)if(h=a[f]){while((h=h.previousSibling)&&h.nodeType!==1);a[f]=e||h&&h.nodeName.toLowerCase()===b?h||!1:h===b}e&&m.filter(b,a,!0)},">":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e<f;e++){c=a[e];if(c){var g=c.parentNode;a[e]=g.nodeName.toLowerCase()===b?g:!1}}}else{for(;e<f;e++)c=a[e],c&&(a[e]=d?c.parentNode:c.parentNode===b);d&&m.filter(b,a,!0)}},"":function(a,b,c){var d,f=e++,g=x;typeof b=="string"&&!l.test(b)&&(b=b.toLowerCase(),d=b,g=w),g("parentNode",b,f,a,d,c)},"~":function(a,b,c){var d,f=e++,g=x;typeof b=="string"&&!l.test(b)&&(b=b.toLowerCase(),d=b,g=w),g("previousSibling",b,f,a,d,c)}},find:{ID:function(a,b,c){if(typeof b.getElementById!="undefined"&&!c){var d=b.getElementById(a[1]);return d&&d.parentNode?[d]:[]}},NAME:function(a,b){if(typeof b.getElementsByName!="undefined"){var c=[],d=b.getElementsByName(a[1]);for(var e=0,f=d.length;e<f;e++)d[e].getAttribute("name")===a[1]&&c.push(d[e]);return c.length===0?null:c}},TAG:function(a,b){if(typeof b.getElementsByTagName!="undefined")return b.getElementsByTagName(a[1])}},preFilter:{CLASS:function(a,b,c,d,e,f){a=" "+a[1].replace(j,"")+" ";if(f)return a;for(var g=0,h;(h=b[g])!=null;g++)h&&(e^(h.className&&(" "+h.className+" ").replace(/[\t\n\r]/g," ").indexOf(a)>=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return b<c[3]-0},gt:function(a,b,c){return b>c[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h<i;h++)if(g[h]===a)return!1;return!0}m.error(e)},CHILD:function(a,b){var c,e,f,g,h,i,j,k=b[1],l=a;switch(k){case"only":case"first":while(l=l.previousSibling)if(l.nodeType===1)return!1;if(k==="first")return!0;l=a;case"last":while(l=l.nextSibling)if(l.nodeType===1)return!1;return!0;case"nth":c=b[2],e=b[3];if(c===1&&e===0)return!0;f=b[0],g=a.parentNode;if(g&&(g[d]!==f||!a.nodeIndex)){i=0;for(l=g.firstChild;l;l=l.nextSibling)l.nodeType===1&&(l.nodeIndex=++i);g[d]=f}j=a.nodeIndex-e;return c===0?j===0:j%c===0&&j/c>=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c<e;c++)d.push(a[c]);else for(;a[c];c++)d.push(a[c]);return d}}var u,v;c.documentElement.compareDocumentPosition?u=function(a,b){if(a===b){h=!0;return 0}if(!a.compareDocumentPosition||!b.compareDocumentPosition)return a.compareDocumentPosition?-1:1;return a.compareDocumentPosition(b)&4?-1:1}:(u=function(a,b){if(a===b){h=!0;return 0}if(a.sourceIndex&&b.sourceIndex)return a.sourceIndex-b.sourceIndex;var c,d,e=[],f=[],g=a.parentNode,i=b.parentNode,j=g;if(g===i)return v(a,b);if(!g)return-1;if(!i)return 1;while(j)e.unshift(j),j=j.parentNode;j=i;while(j)f.unshift(j),j=j.parentNode;c=e.length,d=f.length;for(var k=0;k<c&&k<d;k++)if(e[k]!==f[k])return v(e[k],f[k]);return k===c?v(a,f[k],-1):v(e[k],b,1)},v=function(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}),function(){var a=c.createElement("div"),d="script"+(new Date).getTime(),e=c.documentElement;a.innerHTML="<a name='"+d+"'/>",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="<p class='TEST'></p>";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="<div class='test e'></div><div class='test'></div>";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h<i;h++)m(a,g[h],e,c);return m.filter(f,e)};m.attr=f.attr,m.selectors.attrMap={},f.find=m,f.expr=m.selectors,f.expr[":"]=f.expr.filters,f.unique=m.uniqueSort,f.text=m.getText,f.isXMLDoc=m.isXML,f.contains=m.contains}();var L=/Until$/,M=/^(?:parents|prevUntil|prevAll)/,N=/,/,O=/^.[^:#\[\.,]*$/,P=Array.prototype.slice,Q=f.expr.match.POS,R={children:!0,contents:!0,next:!0,prev:!0};f.fn.extend({find:function(a){var b=this,c,d;if(typeof a!="string")return f(a).filter(function(){for(c=0,d=b.length;c<d;c++)if(f.contains(b[c],this))return!0});var e=this.pushStack("","find",a),g,h,i;for(c=0,d=this.length;c<d;c++){g=e.length,f.find(a,this[c],e);if(c>0)for(h=g;h<e.length;h++)for(i=0;i<g;i++)if(e[i]===e[h]){e.splice(h--,1);break}}return e},has:function(a){var b=f(a);return this.filter(function(){for(var a=0,c=b.length;a<c;a++)if(f.contains(this,b[a]))return!0})},not:function(a){return this.pushStack(T(this,a,!1),"not",a)},filter:function(a){return this.pushStack(T(this,a,!0),"filter",a)},is:function(a){return!!a&&(typeof a=="string"?Q.test(a)?f(a,this.context).index(this[0])>=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d<a.length;d++)f(g).is(a[d])&&c.push({selector:a[d],elem:g,level:h});g=g.parentNode,h++}return c}var i=Q.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d<e;d++){g=this[d];while(g){if(i?i.index(g)>-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/<tbody/i,_=/<|&#?\w+;/,ba=/<(?:script|style)/i,bb=/<(?:script|object|embed|option|style)/i,bc=new RegExp("<(?:"+V+")","i"),bd=/checked\s*(?:[^=]|=\s*.checked.)/i,be=/\/(java|ecma)script/i,bf=/^\s*<!(?:\[CDATA\[|\-\-)/,bg={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div<div>","</div>"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function() +{for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1></$2>");try{for(var c=0,d=this.length;c<d;c++)this[c].nodeType===1&&(f.cleanData(this[c].getElementsByTagName("*")),this[c].innerHTML=a)}catch(e){this.empty().append(a)}}else f.isFunction(a)?this.each(function(b){var c=f(this);c.html(a.call(this,b,c.html()))}):this.empty().append(a);return this},replaceWith:function(a){if(this[0]&&this[0].parentNode){if(f.isFunction(a))return this.each(function(b){var c=f(this),d=c.html();c.replaceWith(a.call(this,b,d))});typeof a!="string"&&(a=f(a).detach());return this.each(function(){var b=this.nextSibling,c=this.parentNode;f(this).remove(),b?f(b).before(a):f(c).append(a)})}return this.length?this.pushStack(f(f.isFunction(a)?a():a),"replaceWith",a):this},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,d){var e,g,h,i,j=a[0],k=[];if(!f.support.checkClone&&arguments.length===3&&typeof j=="string"&&bd.test(j))return this.each(function(){f(this).domManip(a,c,d,!0)});if(f.isFunction(j))return this.each(function(e){var g=f(this);a[0]=j.call(this,e,c?g.html():b),g.domManip(a,c,d)});if(this[0]){i=j&&j.parentNode,f.support.parentNode&&i&&i.nodeType===11&&i.childNodes.length===this.length?e={fragment:i}:e=f.buildFragment(a,this,k),h=e.fragment,h.childNodes.length===1?g=h=h.firstChild:g=h.firstChild;if(g){c=c&&f.nodeName(g,"tr");for(var l=0,m=this.length,n=m-1;l<m;l++)d.call(c?bi(this[l],g):this[l],e.cacheable||m>1&&l<n?f.clone(h,!0,!0):h)}k.length&&f.each(k,bp)}return this}}),f.buildFragment=function(a,b,d){var e,g,h,i,j=a[0];b&&b[0]&&(i=b[0].ownerDocument||b[0]),i.createDocumentFragment||(i=c),a.length===1&&typeof j=="string"&&j.length<512&&i===c&&j.charAt(0)==="<"&&!bb.test(j)&&(f.support.checkClone||!bd.test(j))&&(f.support.html5Clone||!bc.test(j))&&(g=!0,h=f.fragments[j],h&&h!==1&&(e=h)),e||(e=i.createDocumentFragment(),f.clean(a,i,e,d)),g&&(f.fragments[j]=h?e:1);return{fragment:e,cacheable:g}},f.fragments={},f.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){f.fn[a]=function(c){var d=[],e=f(c),g=this.length===1&&this[0].parentNode;if(g&&g.nodeType===11&&g.childNodes.length===1&&e.length===1){e[b](this[0]);return this}for(var h=0,i=e.length;h<i;h++){var j=(h>0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||!bc.test("<"+a.nodeName)?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!_.test(k))k=b.createTextNode(k);else{k=k.replace(Y,"<$1></$2>");var l=(Z.exec(k)||["",""])[1].toLowerCase(),m=bg[l]||bg._default,n=m[0],o=b.createElement("div");b===c?bh.appendChild(o):U(b).appendChild(o),o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=$.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]==="<table>"&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&X.test(k)&&o.insertBefore(b.createTextNode(X.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i<r;i++)bn(k[i]);else bn(k);k.nodeType?h.push(k):h=f.merge(h,k)}if(d){g=function(a){return!a.type||be.test(a.type)};for(j=0;h[j];j++)if(e&&f.nodeName(h[j],"script")&&(!h[j].type||h[j].type.toLowerCase()==="text/javascript"))e.push(h[j].parentNode?h[j].parentNode.removeChild(h[j]):h[j]);else{if(h[j].nodeType===1){var s=f.grep(h[j].getElementsByTagName("script"),g);h.splice.apply(h,[j+1,0].concat(s))}d.appendChild(h[j])}}return h},cleanData:function(a){var b,c,d=f.cache,e=f.event.special,g=f.support.deleteExpando;for(var h=0,i;(i=a[h])!=null;h++){if(i.nodeName&&f.noData[i.nodeName.toLowerCase()])continue;c=i[f.expando];if(c){b=d[c];if(b&&b.events){for(var j in b.events)e[j]?f.event.remove(i,j):f.removeEvent(i,j,b.handle);b.handle&&(b.handle.elem=null)}g?delete i[f.expando]:i.removeAttribute&&i.removeAttribute(f.expando),delete d[c]}}}});var bq=/alpha\([^)]*\)/i,br=/opacity=([^)]*)/,bs=/([A-Z]|^ms)/g,bt=/^-?\d+(?:px)?$/i,bu=/^-?\d/,bv=/^([\-+])=([\-+.\de]+)/,bw={position:"absolute",visibility:"hidden",display:"block"},bx=["Left","Right"],by=["Top","Bottom"],bz,bA,bB;f.fn.css=function(a,c){if(arguments.length===2&&c===b)return this;return f.access(this,a,c,!0,function(a,c,d){return d!==b?f.style(a,c,d):f.css(a,c)})},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bz(a,"opacity","opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d,h==="string"&&(g=bv.exec(d))&&(d=+(g[1]+1)*+g[2]+parseFloat(f.css(a,c)),h="number");if(d==null||h==="number"&&isNaN(d))return;h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(bz)return bz(a,c)},swap:function(a,b,c){var d={};for(var e in b)d[e]=a.style[e],a.style[e]=b[e];c.call(a);for(e in b)a.style[e]=d[e]}}),f.curCSS=f.css,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){var e;if(c){if(a.offsetWidth!==0)return bC(a,b,d);f.swap(a,bw,function(){e=bC(a,b,d)});return e}},set:function(a,b){if(!bt.test(b))return b;b=parseFloat(b);if(b>=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return br.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bq,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bq.test(g)?g.replace(bq,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bz(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bA=function(a,b){var c,d,e;b=b.replace(bs,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b)));return c}),c.documentElement.currentStyle&&(bB=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f===null&&g&&(e=g[b])&&(f=e),!bt.test(f)&&bu.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f||0,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),bz=bA||bB,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bD=/%20/g,bE=/\[\]$/,bF=/\r?\n/g,bG=/#.*$/,bH=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bI=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bJ=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bK=/^(?:GET|HEAD)$/,bL=/^\/\//,bM=/\?/,bN=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,bO=/^(?:select|textarea)/i,bP=/\s+/,bQ=/([?&])_=[^&]*/,bR=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bS=f.fn.load,bT={},bU={},bV,bW,bX=["*/"]+["*"];try{bV=e.href}catch(bY){bV=c.createElement("a"),bV.href="",bV=bV.href}bW=bR.exec(bV.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bS)return bS.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("<div>").append(c.replace(bN,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bO.test(this.nodeName)||bI.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bF,"\r\n")}}):{name:b.name,value:c.replace(bF,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b_(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b_(a,b);return a},ajaxSettings:{url:bV,isLocal:bJ.test(bW[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bX},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bZ(bT),ajaxTransport:bZ(bU),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?cb(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cc(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bH.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bG,"").replace(bL,bW[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bP),d.crossDomain==null&&(r=bR.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bW[1]&&r[2]==bW[2]&&(r[3]||(r[1]==="http:"?80:443))==(bW[3]||(bW[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),b$(bT,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bK.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bM.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bQ,"$1_="+x);d.url=y+(y===d.url?(bM.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bX+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=b$(bU,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)ca(g,a[g],c,e);return d.join("&").replace(bD,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cd=f.now(),ce=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cd++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(ce.test(b.url)||e&&ce.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(ce,l),b.url===j&&(e&&(k=k.replace(ce,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cf=a.ActiveXObject?function(){for(var a in ch)ch[a](0,1)}:!1,cg=0,ch;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ci()||cj()}:ci,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cf&&delete ch[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cg,cf&&(ch||(ch={},f(a).unload(cf)),ch[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var ck={},cl,cm,cn=/^(?:toggle|show|hide)$/,co=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cp,cq=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cr;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cu("show",3),a,b,c);for(var g=0,h=this.length;g<h;g++)d=this[g],d.style&&(e=d.style.display,!f._data(d,"olddisplay")&&e==="none"&&(e=d.style.display=""),e===""&&f.css(d,"display")==="none"&&f._data(d,"olddisplay",cv(d.nodeName)));for(g=0;g<h;g++){d=this[g];if(d.style){e=d.style.display;if(e===""||e==="none")d.style.display=f._data(d,"olddisplay")||""}}return this},hide:function(a,b,c){if(a||a===0)return this.animate(cu("hide",3),a,b,c);var d,e,g=0,h=this.length;for(;g<h;g++)d=this[g],d.style&&(e=f.css(d,"display"),e!=="none"&&!f._data(d,"olddisplay")&&f._data(d,"olddisplay",e));for(g=0;g<h;g++)this[g].style&&(this[g].style.display="none");return this},_toggle:f.fn.toggle,toggle:function(a,b,c){var d=typeof a=="boolean";f.isFunction(a)&&f.isFunction(b)?this._toggle.apply(this,arguments):a==null||d?this.each(function(){var b=d?a:f(this).is(":hidden");f(this)[b?"show":"hide"]()}):this.animate(cu("toggle",3),a,b,c);return this},fadeTo:function(a,b,c,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){function g(){e.queue===!1&&f._mark(this);var b=f.extend({},e),c=this.nodeType===1,d=c&&f(this).is(":hidden"),g,h,i,j,k,l,m,n,o;b.animatedProperties={};for(i in a){g=f.camelCase(i),i!==g&&(a[g]=a[i],delete a[i]),h=a[g],f.isArray(h)?(b.animatedProperties[g]=h[1],h=a[g]=h[0]):b.animatedProperties[g]=b.specialEasing&&b.specialEasing[g]||b.easing||"swing";if(h==="hide"&&d||h==="show"&&!d)return b.complete.call(this);c&&(g==="height"||g==="width")&&(b.overflow=[this.style.overflow,this.style.overflowX,this.style.overflowY],f.css(this,"display")==="inline"&&f.css(this,"float")==="none"&&(!f.support.inlineBlockNeedsLayout||cv(this.nodeName)==="inline"?this.style.display="inline-block":this.style.zoom=1))}b.overflow!=null&&(this.style.overflow="hidden");for(i in a)j=new f.fx(this,b,i),h=a[i],cn.test(h)?(o=f._data(this,"toggle"+i)||(h==="toggle"?d?"show":"hide":0),o?(f._data(this,"toggle"+i,o==="show"?"hide":"show"),j[o]()):j[h]()):(k=co.exec(h),l=j.cur(),k?(m=parseFloat(k[2]),n=k[3]||(f.cssNumber[i]?"":"px"),n!=="px"&&(f.style(this,i,(m||1)+n),l=(m||1)/j.cur()*l,f.style(this,i,l+n)),k[1]&&(m=(k[1]==="-="?-1:1)*m+l),j.custom(l,m,n)):j.custom(l,h,""));return!0}var e=f.speed(b,c,d);if(f.isEmptyObject(a))return this.each(e.complete,[!1]);a=f.extend({},a);return e.queue===!1?this.each(g):this.queue(e.queue,g)},stop:function(a,c,d){typeof a!="string"&&(d=c,c=a,a=b),c&&a!==!1&&this.queue(a||"fx",[]);return this.each(function(){function h(a,b,c){var e=b[c];f.removeData(a,c,!0),e.stop(d)}var b,c=!1,e=f.timers,g=f._data(this);d||f._unmark(!0,this);if(a==null)for(b in g)g[b]&&g[b].stop&&b.indexOf(".run")===b.length-4&&h(this,g,b);else g[b=a+".run"]&&g[b].stop&&h(this,g,b);for(b=e.length;b--;)e[b].elem===this&&(a==null||e[b].queue===a)&&(d?e[b](!0):e[b].saveState(),c=!0,e.splice(b,1));(!d||!c)&&f.dequeue(this,a)})}}),f.each({slideDown:cu("show",1),slideUp:cu("hide",1),slideToggle:cu("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){f.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),f.extend({speed:function(a,b,c){var d=a&&typeof a=="object"?f.extend({},a):{complete:c||!c&&b||f.isFunction(a)&&a,duration:a,easing:c&&b||b&&!f.isFunction(b)&&b};d.duration=f.fx.off?0:typeof d.duration=="number"?d.duration:d.duration in f.fx.speeds?f.fx.speeds[d.duration]:f.fx.speeds._default;if(d.queue==null||d.queue===!0)d.queue="fx";d.old=d.complete,d.complete=function(a){f.isFunction(d.old)&&d.old.call(this),d.queue?f.dequeue(this,d.queue):a!==!1&&f._unmark(this)};return d},easing:{linear:function(a,b,c,d){return c+d*a},swing:function(a,b,c,d){return(-Math.cos(a*Math.PI)/2+.5)*d+c}},timers:[],fx:function(a,b,c){this.options=b,this.elem=a,this.prop=c,b.orig=b.orig||{}}}),f.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this),(f.fx.step[this.prop]||f.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a,b=f.css(this.elem,this.prop);return isNaN(a=parseFloat(b))?!b||b==="auto"?0:b:a},custom:function(a,c,d){function h(a){return e.step(a)}var e=this,g=f.fx;this.startTime=cr||cs(),this.end=c,this.now=this.start=a,this.pos=this.state=0,this.unit=d||this.unit||(f.cssNumber[this.prop]?"":"px"),h.queue=this.options.queue,h.elem=this.elem,h.saveState=function(){e.options.hide&&f._data(e.elem,"fxshow"+e.prop)===b&&f._data(e.elem,"fxshow"+e.prop,e.start)},h()&&f.timers.push(h)&&!cp&&(cp=setInterval(g.tick,g.interval))},show:function(){var a=f._data(this.elem,"fxshow"+this.prop);this.options.orig[this.prop]=a||f.style(this.elem,this.prop),this.options.show=!0,a!==b?this.custom(this.cur(),a):this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur()),f(this.elem).show()},hide:function(){this.options.orig[this.prop]=f._data(this.elem,"fxshow"+this.prop)||f.style(this.elem,this.prop),this.options.hide=!0,this.custom(this.cur(),0)},step:function(a){var b,c,d,e=cr||cs(),g=!0,h=this.elem,i=this.options;if(a||e>=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c<b.length;c++)a=b[c],!a()&&b[c]===a&&b.splice(c--,1);b.length||f.fx.stop()},interval:13,stop:function(){clearInterval(cp),cp=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){f.style(a.elem,"opacity",a.now)},_default:function(a){a.elem.style&&a.elem.style[a.prop]!=null?a.elem.style[a.prop]=a.now+a.unit:a.elem[a.prop]=a.now}}}),f.each(["width","height"],function(a,b){f.fx.step[b]=function(a){f.style(a.elem,b,Math.max(0,a.now)+a.unit)}}),f.expr&&f.expr.filters&&(f.expr.filters.animated=function(a){return f.grep(f.timers,function(b){return a===b.elem}).length});var cw=/^t(?:able|d|h)$/i,cx=/^(?:body|html)$/i;"getBoundingClientRect"in c.documentElement?f.fn.offset=function(a){var b=this[0],c;if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);try{c=b.getBoundingClientRect()}catch(d){}var e=b.ownerDocument,g=e.documentElement;if(!c||!f.contains(g,b))return c?{top:c.top,left:c.left}:{top:0,left:0};var h=e.body,i=cy(e),j=g.clientTop||h.clientTop||0,k=g.clientLeft||h.clientLeft||0,l=i.pageYOffset||f.support.boxModel&&g.scrollTop||h.scrollTop,m=i.pageXOffset||f.support.boxModel&&g.scrollLeft||h.scrollLeft,n=c.top+l-j,o=c.left+m-k;return{top:n,left:o}}:f.fn.offset=function(a){var b=this[0];if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);var c,d=b.offsetParent,e=b,g=b.ownerDocument,h=g.documentElement,i=g.body,j=g.defaultView,k=j?j.getComputedStyle(b,null):b.currentStyle,l=b.offsetTop,m=b.offsetLeft;while((b=b.parentNode)&&b!==i&&b!==h){if(f.support.fixedPosition&&k.position==="fixed")break;c=j?j.getComputedStyle(b,null):b.currentStyle,l-=b.scrollTop,m-=b.scrollLeft,b===d&&(l+=b.offsetTop,m+=b.offsetLeft,f.support.doesNotAddBorder&&(!f.support.doesAddBorderForTableAndCells||!cw.test(b.nodeName))&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),e=d,d=b.offsetParent),f.support.subtractsBorderForOverflowNotVisible&&c.overflow!=="visible"&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),k=c}if(k.position==="relative"||k.position==="static")l+=i.offsetTop,m+=i.offsetLeft;f.support.fixedPosition&&k.position==="fixed"&&(l+=Math.max(h.scrollTop,i.scrollTop),m+=Math.max(h.scrollLeft,i.scrollLeft));return{top:l,left:m}},f.offset={bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.support.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cy(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cy(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,d,"padding")):this[d]():null},f.fn["outer"+c]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,d,a?"margin":"border")):this[d]():null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c],h=e.document.body;return e.document.compatMode==="CSS1Compat"&&g||h&&h["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var i=f.css(e,d),j=parseFloat(i);return f.isNumeric(j)?j:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window);
\ No newline at end of file diff --git a/hyperkitty/static/protovis-d3.1.js b/hyperkitty/static/protovis-d3.1.js new file mode 100644 index 0000000..af56eac --- /dev/null +++ b/hyperkitty/static/protovis-d3.1.js @@ -0,0 +1,7725 @@ +/** + * @class The built-in Array class. + * @name Array + */ + +if (!Array.prototype.map) { + /** + * Creates a new array with the results of calling a provided function on + * every element in this array. Implemented in Javascript 1.6. + * + * @see <a + * href="https://developer.mozilla.org/En/Core_JavaScript_1.5_Reference/Objects/Array/Map">map</a> + * documentation. + * @param {function} f function that produces an element of the new Array from + * an element of the current one. + * @param [o] object to use as <tt>this</tt> when executing <tt>f</tt>. + */ + Array.prototype.map = function(f, o) { + var n = this.length; + var result = new Array(n); + for (var i = 0; i < n; i++) { + if (i in this) { + result[i] = f.call(o, this[i], i, this); + } + } + return result; + }; +} + +if (!Array.prototype.filter) { + /** + * Creates a new array with all elements that pass the test implemented by the + * provided function. Implemented in Javascript 1.6. + * + * @see <a + * href="https://developer.mozilla.org/En/Core_JavaScript_1.5_Reference/Objects/Array/filter">filter</a> + * documentation. + * @param {function} f function to test each element of the array. + * @param [o] object to use as <tt>this</tt> when executing <tt>f</tt>. + */ + Array.prototype.filter = function(f, o) { + var n = this.length; + var result = new Array(); + for (var i = 0; i < n; i++) { + if (i in this) { + var v = this[i]; + if (f.call(o, v, i, this)) result.push(v); + } + } + return result; + }; +} + +if (!Array.prototype.forEach) { + /** + * Executes a provided function once per array element. Implemented in + * Javascript 1.6. + * + * @see <a + * href="https://developer.mozilla.org/En/Core_JavaScript_1.5_Reference/Objects/Array/ForEach">forEach</a> + * documentation. + * @param {function} f function to execute for each element. + * @param [o] object to use as <tt>this</tt> when executing <tt>f</tt>. + */ + Array.prototype.forEach = function(f, o) { + var n = this.length >>> 0; + for (var i = 0; i < n; i++) { + if (i in this) f.call(o, this[i], i, this); + } + }; +} + +if (!Array.prototype.reduce) { + /** + * Apply a function against an accumulator and each value of the array (from + * left-to-right) as to reduce it to a single value. Implemented in Javascript + * 1.8. + * + * @see <a + * href="https://developer.mozilla.org/En/Core_JavaScript_1.5_Reference/Objects/Array/Reduce">reduce</a> + * documentation. + * @param {function} f function to execute on each value in the array. + * @param [v] object to use as the first argument to the first call of + * <tt>t</tt>. + */ + Array.prototype.reduce = function(f, v) { + var len = this.length; + if (!len && (arguments.length == 1)) { + throw new Error("reduce: empty array, no initial value"); + } + + var i = 0; + if (arguments.length < 2) { + while (true) { + if (i in this) { + v = this[i++]; + break; + } + if (++i >= len) { + throw new Error("reduce: no values, no initial value"); + } + } + } + + for (; i < len; i++) { + if (i in this) { + v = f(v, this[i], i, this); + } + } + return v; + }; +} +/** + * @class The built-in Date class. + * @name Date + */ + +Date.__parse__ = Date.parse; + +/** + * Parses a date from a string, optionally using the specified formatting. If + * only a single argument is specified (i.e., <tt>format</tt> is not specified), + * this method invokes the native implementation to guarantee + * backwards-compatibility. + * + * <p>The format string is in the same format expected by the <tt>strptime</tt> + * function in C. The following conversion specifications are supported:<ul> + * + * <li>%b - abbreviated month names.</li> + * <li>%B - full month names.</li> + * <li>%h - same as %b.</li> + * <li>%d - day of month [1,31].</li> + * <li>%e - same as %d.</li> + * <li>%H - hour (24-hour clock) [0,23].</li> + * <li>%m - month number [1,12].</li> + * <li>%M - minute [0,59].</li> + * <li>%S - second [0,61].</li> + * <li>%y - year with century [0,99].</li> + * <li>%Y - year including century.</li> + * <li>%% - %.</li> + * + * </ul>The following conversion specifications are <i>unsupported</i> (for now):<ul> + * + * <li>%a - day of week, either abbreviated or full name.</li> + * <li>%A - same as %a.</li> + * <li>%c - locale's appropriate date and time.</li> + * <li>%C - century number.</li> + * <li>%D - same as %m/%d/%y.</li> + * <li>%I - hour (12-hour clock) [1,12].</li> + * <li>%j - day number [1,366].</li> + * <li>%n - any white space.</li> + * <li>%p - locale's equivalent of a.m. or p.m.</li> + * <li>%r - same as %I:%M:%S %p.</li> + * <li>%R - same as %H:%M.</li> + * <li>%t - same as %n.</li> + * <li>%T - same as %H:%M:%S.</li> + * <li>%U - week number [0,53].</li> + * <li>%w - weekday [0,6].</li> + * <li>%W - week number [0,53].</li> + * <li>%x - locale's equivalent to %m/%d/%y.</li> + * <li>%X - locale's equivalent to %I:%M:%S %p.</li> + * + * </ul> + * + * @see <a + * href="http://www.opengroup.org/onlinepubs/007908799/xsh/strptime.html">strptime</a> + * documentation. + * @param {string} s the string to parse as a date. + * @param {string} [format] an optional format string. + * @returns {Date} the parsed date. + */ +Date.parse = function(s, format) { + if (arguments.length == 1) { + return Date.__parse__(s); + } + + var year = 1970, month = 0, date = 1, hour = 0, minute = 0, second = 0; + var fields = [function() {}]; + format = format.replace(/[\\\^\$\*\+\?\[\]\(\)\.\{\}]/g, "\\$&"); + format = format.replace(/%[a-zA-Z0-9]/g, function(s) { + switch (s) { + // TODO %a: day of week, either abbreviated or full name + // TODO %A: same as %a + case '%b': { + fields.push(function(x) { month = { + Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5, Jul: 6, Aug: 7, + Sep: 8, Oct: 9, Nov: 10, Dec: 11 + }[x]; }); + return "([A-Za-z]+)"; + } + case '%h': + case '%B': { + fields.push(function(x) { month = { + January: 0, February: 1, March: 2, April: 3, May: 4, June: 5, + July: 6, August: 7, September: 8, October: 9, November: 10, + December: 11 + }[x]; }); + return "([A-Za-z]+)"; + } + // TODO %c: locale's appropriate date and time + // TODO %C: century number[0,99] + case '%e': + case '%d': { + fields.push(function(x) { date = x; }); + return "([0-9]+)"; + } + // TODO %D: same as %m/%d/%y + case '%H': { + fields.push(function(x) { hour = x; }); + return "([0-9]+)"; + } + // TODO %I: hour (12-hour clock) [1,12] + // TODO %j: day number [1,366] + case '%m': { + fields.push(function(x) { month = x - 1; }); + return "([0-9]+)"; + } + case '%M': { + fields.push(function(x) { minute = x; }); + return "([0-9]+)"; + } + // TODO %n: any white space + // TODO %p: locale's equivalent of a.m. or p.m. + // TODO %r: %I:%M:%S %p + // TODO %R: %H:%M + case '%S': { + fields.push(function(x) { second = x; }); + return "([0-9]+)"; + } + // TODO %t: any white space + // TODO %T: %H:%M:%S + // TODO %U: week number [00,53] + // TODO %w: weekday [0,6] + // TODO %W: week number [00, 53] + // TODO %x: locale date (%m/%d/%y) + // TODO %X: locale time (%I:%M:%S %p) + case '%y': { + fields.push(function(x) { + x = Number(x); + year = x + (((0 <= x) && (x < 69)) ? 2000 + : (((x >= 69) && (x < 100) ? 1900 : 0))); + }); + return "([0-9]+)"; + } + case '%Y': { + fields.push(function(x) { year = x; }); + return "([0-9]+)"; + } + case '%%': { + fields.push(function() {}); + return "%"; + } + } + return s; + }); + + var match = s.match(format); + if (match) match.forEach(function(m, i) { fields[i](m); }); + return new Date(year, month, date, hour, minute, second); +}; + +if (Date.prototype.toLocaleFormat) { + Date.prototype.format = Date.prototype.toLocaleFormat; +} else { + +/** + * Converts a date to a string using the specified formatting. If the + * <tt>Date</tt> object already supports the <tt>toLocaleFormat</tt> method, as + * in Firefox, this is simply an alias to the built-in method. + * + * <p>The format string is in the same format expected by the <tt>strftime</tt> + * function in C. The following conversion specifications are supported:<ul> + * + * <li>%a - abbreviated weekday name.</li> + * <li>%A - full weekday name.</li> + * <li>%b - abbreviated month names.</li> + * <li>%B - full month names.</li> + * <li>%c - locale's appropriate date and time.</li> + * <li>%C - century number.</li> + * <li>%d - day of month [01,31] (zero padded).</li> + * <li>%D - same as %m/%d/%y.</li> + * <li>%e - day of month [ 1,31] (space padded).</li> + * <li>%h - same as %b.</li> + * <li>%H - hour (24-hour clock) [00,23] (zero padded).</li> + * <li>%I - hour (12-hour clock) [01,12] (zero padded).</li> + * <li>%m - month number [01,12] (zero padded).</li> + * <li>%M - minute [0,59] (zero padded).</li> + * <li>%n - newline character.</li> + * <li>%p - locale's equivalent of a.m. or p.m.</li> + * <li>%r - same as %I:%M:%S %p.</li> + * <li>%R - same as %H:%M.</li> + * <li>%S - second [00,61] (zero padded).</li> + * <li>%t - tab character.</li> + * <li>%T - same as %H:%M:%S.</li> + * <li>%x - same as %m/%d/%y.</li> + * <li>%X - same as %I:%M:%S %p.</li> + * <li>%y - year with century [00,99] (zero padded).</li> + * <li>%Y - year including century.</li> + * <li>%% - %.</li> + * + * </ul>The following conversion specifications are <i>unsupported</i> (for now):<ul> + * + * <li>%j - day number [1,366].</li> + * <li>%u - weekday number [1,7].</li> + * <li>%U - week number [00,53].</li> + * <li>%V - week number [01,53].</li> + * <li>%w - weekday number [0,6].</li> + * <li>%W - week number [00,53].</li> + * <li>%Z - timezone name or abbreviation.</li> + * + * </ul> + * + * @see <a + * href="http://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/Date/toLocaleFormat">Date.toLocaleFormat</a> + * documentation. + * @see <a + * href="http://www.opengroup.org/onlinepubs/007908799/xsh/strftime.html">strftime</a> + * documentation. + * @param {string} format a format string. + * @returns {string} the formatted date. + */ +Date.prototype.format = function(format) { + function pad(n, p) { return (n < 10) ? (p || "0") + n : n; } + var d = this; + return format.replace(/%[a-zA-Z0-9]/g, function(s) { + switch (s) { + case '%a': return [ + "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" + ][d.getDay()]; + case '%A': return [ + "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", + "Saturday" + ][d.getDay()]; + case '%h': + case '%b': return [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", + "Oct", "Nov", "Dec" + ][d.getMonth()]; + case '%B': return [ + "January", "February", "March", "April", "May", "June", "July", + "August", "September", "October", "November", "December" + ][d.getMonth()]; + case '%c': return d.toLocaleString(); + case '%C': return pad(Math.floor(d.getFullYear() / 100) % 100); + case '%d': return pad(d.getDate()); + case '%x': + case '%D': return pad(d.getMonth() + 1) + + "/" + pad(d.getDate()) + + "/" + pad(d.getFullYear() % 100); + case '%e': return pad(d.getDate(), " "); + case '%H': return pad(d.getHours()); + case '%I': { + var h = d.getHours() % 12; + return h ? pad(h) : 12; + } + // TODO %j: day of year as a decimal number [001,366] + case '%m': return pad(d.getMonth() + 1); + case '%M': return pad(d.getMinutes()); + case '%n': return "\n"; + case '%p': return d.getHours() < 12 ? "AM" : "PM"; + case '%T': + case '%X': + case '%r': { + var h = d.getHours() % 12; + return (h ? pad(h) : 12) + + ":" + pad(d.getMinutes()) + + ":" + pad(d.getSeconds()) + + " " + (d.getHours() < 12 ? "AM" : "PM"); + } + case '%R': return pad(d.getHours()) + ":" + pad(d.getMinutes()); + case '%S': return pad(d.getSeconds()); + case '%t': return "\t"; + case '%u': { + var w = d.getDay(); + return w ? w : 1; + } + // TODO %U: week number (sunday first day) [00,53] + // TODO %V: week number (monday first day) [01,53] ... with weirdness + case '%w': return d.getDay(); + // TODO %W: week number (monday first day) [00,53] ... with weirdness + case '%y': return pad(d.getFullYear() % 100); + case '%Y': return d.getFullYear(); + // TODO %Z: timezone name or abbreviation + case '%%': return "%"; + } + return s; + }); + }; +} +var pv = function() {/** + * The top-level Protovis namespace. All public methods and fields should be + * registered on this object. Note that core Protovis source is surrounded by an + * anonymous function, so any other declared globals will not be visible outside + * of core methods. This also allows multiple versions of Protovis to coexist, + * since each version will see their own <tt>pv</tt> namespace. + * + * @namespace The top-level Protovis namespace, <tt>pv</tt>. + */ +var pv = {}; + +/** + * @private Returns a prototype object suitable for extending the given class + * <tt>f</tt>. Rather than constructing a new instance of <tt>f</tt> to serve as + * the prototype (which unnecessarily runs the constructor on the created + * prototype object, potentially polluting it), an anonymous function is + * generated internally that shares the same prototype: + * + * <pre>function g() {} + * g.prototype = f.prototype; + * return new g();</pre> + * + * For more details, see Douglas Crockford's essay on prototypal inheritance. + * + * @param {function} f a constructor. + * @returns a suitable prototype object. + * @see Douglas Crockford's essay on <a + * href="http://javascript.crockford.com/prototypal.html">prototypal + * inheritance</a>. + */ +pv.extend = function(f) { + function g() {} + g.prototype = f.prototype || f; + return new g(); +}; + +try { + eval("pv.parse = function(x) x;"); // native support +} catch (e) { + +/** + * @private Parses a Protovis specification, which may use JavaScript 1.8 + * function expresses, replacing those function expressions with proper + * functions such that the code can be run by a JavaScript 1.6 interpreter. This + * hack only supports function expressions (using clumsy regular expressions, no + * less), and not other JavaScript 1.8 features such as let expressions. + * + * @param {string} s a Protovis specification (i.e., a string of JavaScript 1.8 + * source code). + * @returns {string} a conformant JavaScript 1.6 source code. + */ + pv.parse = function(js) { // hacky regex support + var re = new RegExp("function(\\s+\\w+)?\\([^)]*\\)\\s*", "mg"), m, d, i = 0, s = ""; + while (m = re.exec(js)) { + var j = m.index + m[0].length; + if (js.charAt(j--) != '{') { + s += js.substring(i, j) + "{return "; + i = j; + for (var p = 0; p >= 0 && j < js.length; j++) { + var c = js.charAt(j); + switch (c) { + case '"': case '\'': { + while (++j < js.length && (d = js.charAt(j)) != c) { + if (d == '\\') j++; + } + break; + } + case '[': case '(': p++; break; + case ']': case ')': p--; break; + case ';': + case ',': if (p == 0) p--; break; + } + } + s += pv.parse(js.substring(i, --j)) + ";}"; + i = j; + } + re.lastIndex = j; + } + s += js.substring(i); + return s; + }; +} + +/** + * Returns the passed-in argument, <tt>x</tt>; the identity function. This method + * is provided for convenience since it is used as the default behavior for a + * number of property functions. + * + * @param x a value. + * @returns the value <tt>x</tt>. + */ +pv.identity = function(x) { return x; }; + +/** + * Returns <tt>this.index</tt>. This method is provided for convenience for use + * with scales. For example, to color bars by their index, say: + * + * <pre>.fillStyle(pv.Colors.category10().by(pv.index))</pre> + * + * This method is equivalent to <tt>function() this.index</tt>, but more + * succinct. Note that the <tt>index</tt> property is also supported for + * accessor functions with {@link pv.max}, {@link pv.min} and other array + * utility methods. + * + * @see pv.Scale + * @see pv.Mark#index + */ +pv.index = function() { return this.index; }; + +/** + * Returns <tt>this.childIndex</tt>. This method is provided for convenience for + * use with scales. For example, to color bars by their child index, say: + * + * <pre>.fillStyle(pv.Colors.category10().by(pv.child))</pre> + * + * This method is equivalent to <tt>function() this.childIndex</tt>, but more + * succinct. + * + * @see pv.Scale + * @see pv.Mark#childIndex + */ +pv.child = function() { return this.childIndex; }; + +/** + * Returns <tt>this.parent.index</tt>. This method is provided for convenience + * for use with scales. This method is provided for convenience for use with + * scales. For example, to color bars by their parent index, say: + * + * <pre>.fillStyle(pv.Colors.category10().by(pv.parent))</pre> + * + * Tthis method is equivalent to <tt>function() this.parent.index</tt>, but more + * succinct. + * + * @see pv.Scale + * @see pv.Mark#index + */ +pv.parent = function() { return this.parent.index; }; + +/** + * Returns an array of numbers, starting at <tt>start</tt>, incrementing by + * <tt>step</tt>, until <tt>stop</tt> is reached. The stop value is exclusive. If + * only a single argument is specified, this value is interpeted as the + * <i>stop</i> value, with the <i>start</i> value as zero. If only two arguments + * are specified, the step value is implied to be one. + * + * <p>The method is modeled after the built-in <tt>range</tt> method from + * Python. See the Python documentation for more details. + * + * @see <a href="http://docs.python.org/library/functions.html#range">Python range</a> + * @param {number} [start] the start value. + * @param {number} stop the stop value. + * @param {number} [step] the step value. + * @returns {number[]} an array of numbers. + */ +pv.range = function(start, stop, step) { + if (arguments.length == 1) { + stop = start; + start = 0; + } + if (step == undefined) step = 1; + else if (!step) throw new Error("step must be non-zero"); + var array = [], i = 0, j; + if (step < 0) { + while ((j = start + step * i++) > stop) { + array.push(j); + } + } else { + while ((j = start + step * i++) < stop) { + array.push(j); + } + } + return array; +}; + +/** + * Returns a random number in the range [<tt>min</tt>, <tt>max</tt>) that is a + * multiple of <tt>step</tt>. More specifically, the returned number is of the + * form <tt>min</tt> + <i>n</i> * <tt>step</tt>, where <i>n</i> is a nonnegative + * integer. If <tt>step</tt> is not specified, it defaults to 1, returning a + * random integer if <tt>min</tt> is also an integer. + * + * @param min {number} minimum value. + * @param [max] {number} maximum value. + * @param [step] {numbeR} step value. + */ +pv.random = function(min, max, step) { + if (arguments.length == 1) { + max = min; + min = 0; + } + if (step == undefined) { + step = 1; + } + return step + ? (Math.floor(Math.random() * (max - min) / step) * step + min) + : (Math.random() * (max - min) + min); +}; + +/** + * Concatenates the specified array with itself <i>n</i> times. For example, + * <tt>pv.repeat([1, 2])</tt> returns [1, 2, 1, 2]. + * + * @param {array} a an array. + * @param {number} [n] the number of times to repeat; defaults to two. + * @returns {array} an array that repeats the specified array. + */ +pv.repeat = function(array, n) { + if (arguments.length == 1) n = 2; + return pv.blend(pv.range(n).map(function() { return array; })); +}; + +/** + * Given two arrays <tt>a</tt> and <tt>b</tt>, <style + * type="text/css">sub{line-height:0}</style> returns an array of all possible + * pairs of elements [a<sub>i</sub>, b<sub>j</sub>]. The outer loop is on array + * <i>a</i>, while the inner loop is on <i>b</i>, such that the order of + * returned elements is [a<sub>0</sub>, b<sub>0</sub>], [a<sub>0</sub>, + * b<sub>1</sub>], ... [a<sub>0</sub>, b<sub>m</sub>], [a<sub>1</sub>, + * b<sub>0</sub>], [a<sub>1</sub>, b<sub>1</sub>], ... [a<sub>1</sub>, + * b<sub>m</sub>], ... [a<sub>n</sub>, b<sub>m</sub>]. If either array is empty, + * an empty array is returned. + * + * @param {array} a an array. + * @param {array} b an array. + * @returns {array} an array of pairs of elements in <tt>a</tt> and <tt>b</tt>. + */ +pv.cross = function(a, b) { + var array = []; + for (var i = 0, n = a.length, m = b.length; i < n; i++) { + for (var j = 0, x = a[i]; j < m; j++) { + array.push([x, b[j]]); + } + } + return array; +}; + +/** + * Given the specified array of arrays, concatenates the arrays into a single + * array. If the individual arrays are explicitly known, an alternative to blend + * is to use JavaScript's <tt>concat</tt> method directly. These two equivalent + * expressions:<ul> + * + * <li><tt>pv.blend([[1, 2, 3], ["a", "b", "c"]])</tt> + * <li><tt>[1, 2, 3].concat(["a", "b", "c"])</tt> + * + * </ul>return [1, 2, 3, "a", "b", "c"]. + * + * @param {array[]} arrays an array of arrays. + * @returns {array} an array containing all the elements of each array in + * <tt>arrays</tt>. + */ +pv.blend = function(arrays) { + return Array.prototype.concat.apply([], arrays); +}; + +/** + * Given the specified array of arrays, <style + * type="text/css">sub{line-height:0}</style> transposes each element + * array<sub>ij</sub> with array<sub>ji</sub>. If the array has dimensions + * <i>n</i>×<i>m</i>, it will have dimensions <i>m</i>×<i>n</i> + * after this method returns. This method transposes the elements of the array + * in place, mutating the array, and returning a reference to the array. + * + * @param {array[]} arrays an array of arrays. + * @returns {array[]} the passed-in array, after transposing the elements. + */ +pv.transpose = function(arrays) { + var n = arrays.length, m = pv.max(arrays, function(d) { return d.length; }); + + if (m > n) { + arrays.length = m; + for (var i = n; i < m; i++) { + arrays[i] = new Array(n); + } + for (var i = 0; i < n; i++) { + for (var j = i + 1; j < m; j++) { + var t = arrays[i][j]; + arrays[i][j] = arrays[j][i]; + arrays[j][i] = t; + } + } + } else { + for (var i = 0; i < m; i++) { + arrays[i].length = n; + } + for (var i = 0; i < n; i++) { + for (var j = 0; j < i; j++) { + var t = arrays[i][j]; + arrays[i][j] = arrays[j][i]; + arrays[j][i] = t; + } + } + } + + arrays.length = m; + for (var i = 0; i < m; i++) { + arrays[i].length = n; + } + + return arrays; +}; + +/** + * Returns all of the property names (keys) of the specified object (a map). The + * order of the returned array is not defined. + * + * @param map an object. + * @returns {string[]} an array of strings corresponding to the keys. + * @see #entries + */ +pv.keys = function(map) { + var array = []; + for (var key in map) { + array.push(key); + } + return array; +}; + +/** + * Returns all of the entries (key-value pairs) of the specified object (a + * map). The order of the returned array is not defined. Each key-value pair is + * represented as an object with <tt>key</tt> and <tt>value</tt> attributes, + * e.g., <tt>{key: "foo", value: 42}</tt>. + * + * @param map an object. + * @returns {array} an array of key-value pairs corresponding to the keys. + */ +pv.entries = function(map) { + var array = []; + for (var key in map) { + array.push({ key: key, value: map[key] }); + } + return array; +}; + +/** + * Returns all of the values (attribute values) of the specified object (a + * map). The order of the returned array is not defined. + * + * @param map an object. + * @returns {array} an array of objects corresponding to the values. + * @see #entries + */ +pv.values = function(map) { + var array = []; + for (var key in map) { + array.push(map[key]); + } + return array; +}; + +/** + * @private A private variant of Array.prototype.map that supports the index + * property. + */ +function map(array, f) { + var o = {}; + return f + ? array.map(function(d, i) { o.index = i; return f.call(o, d); }) + : array.slice(); +}; + +/** + * Returns a normalized copy of the specified array, such that the sum of the + * returned elements sum to one. If the specified array is not an array of + * numbers, an optional accessor function <tt>f</tt> can be specified to map the + * elements to numbers. For example, if <tt>array</tt> is an array of objects, + * and each object has a numeric property "foo", the expression + * + * <pre>pv.normalize(array, function(d) d.foo)</pre> + * + * returns a normalized array on the "foo" property. If an accessor function is + * not specified, the identity function is used. Accessor functions can refer to + * <tt>this.index</tt>. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number[]} an array of numbers that sums to one. + */ +pv.normalize = function(array, f) { + var norm = map(array, f), sum = pv.sum(norm); + for (var i = 0; i < norm.length; i++) norm[i] /= sum; + return norm; +}; + +/** + * Returns the sum of the specified array. If the specified array is not an + * array of numbers, an optional accessor function <tt>f</tt> can be specified + * to map the elements to numbers. See {@link #normalize} for an example. + * Accessor functions can refer to <tt>this.index</tt>. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number} the sum of the specified array. + */ +pv.sum = function(array, f) { + var o = {}; + return array.reduce(f + ? function(p, d, i) { o.index = i; return p + f.call(o, d); } + : function(p, d) { return p + d; }, 0); +}; + +/** + * Returns the maximum value of the specified array. If the specified array is + * not an array of numbers, an optional accessor function <tt>f</tt> can be + * specified to map the elements to numbers. See {@link #normalize} for an + * example. Accessor functions can refer to <tt>this.index</tt>. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number} the maximum value of the specified array. + */ +pv.max = function(array, f) { + if (f == pv.index) return array.length - 1; + return Math.max.apply(null, f ? map(array, f) : array); +}; + +/** + * Returns the index of the maximum value of the specified array. If the + * specified array is not an array of numbers, an optional accessor function + * <tt>f</tt> can be specified to map the elements to numbers. See + * {@link #normalize} for an example. Accessor functions can refer to + * <tt>this.index</tt>. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number} the index of the maximum value of the specified array. + */ +pv.max.index = function(array, f) { + if (f == pv.index) return array.length - 1; + if (!f) f = pv.identity; + var maxi = -1, maxx = -Infinity, o = {}; + for (var i = 0; i < array.length; i++) { + o.index = i; + var x = f.call(o, array[i]); + if (x > maxx) { + maxx = x; + maxi = i; + } + } + return maxi; +} + +/** + * Returns the minimum value of the specified array of numbers. If the specified + * array is not an array of numbers, an optional accessor function <tt>f</tt> + * can be specified to map the elements to numbers. See {@link #normalize} for + * an example. Accessor functions can refer to <tt>this.index</tt>. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number} the minimum value of the specified array. + */ +pv.min = function(array, f) { + if (f == pv.index) return 0; + return Math.min.apply(null, f ? map(array, f) : array); +}; + +/** + * Returns the index of the minimum value of the specified array. If the + * specified array is not an array of numbers, an optional accessor function + * <tt>f</tt> can be specified to map the elements to numbers. See + * {@link #normalize} for an example. Accessor functions can refer to + * <tt>this.index</tt>. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number} the index of the minimum value of the specified array. + */ +pv.min.index = function(array, f) { + if (f == pv.index) return 0; + if (!f) f = pv.identity; + var mini = -1, minx = Infinity, o = {}; + for (var i = 0; i < array.length; i++) { + o.index = i; + var x = f.call(o, array[i]); + if (x < minx) { + minx = x; + mini = i; + } + } + return mini; +} + +/** + * Returns the arithmetic mean, or average, of the specified array. If the + * specified array is not an array of numbers, an optional accessor function + * <tt>f</tt> can be specified to map the elements to numbers. See + * {@link #normalize} for an example. Accessor functions can refer to + * <tt>this.index</tt>. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number} the mean of the specified array. + */ +pv.mean = function(array, f) { + return pv.sum(array, f) / array.length; +}; + +/** + * Returns the median of the specified array. If the specified array is not an + * array of numbers, an optional accessor function <tt>f</tt> can be specified + * to map the elements to numbers. See {@link #normalize} for an example. + * Accessor functions can refer to <tt>this.index</tt>. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number} the median of the specified array. + */ +pv.median = function(array, f) { + if (f == pv.index) return (array.length - 1) / 2; + array = map(array, f).sort(pv.naturalOrder); + if (array.length % 2) return array[Math.floor(array.length / 2)]; + var i = array.length / 2; + return (array[i - 1] + array[i]) / 2; +}; + +/** + * Returns a map constructed from the specified <tt>keys</tt>, using the + * function <tt>f</tt> to compute the value for each key. The single argument to + * the value function is the key. The callback is invoked only for indexes of + * the array which have assigned values; it is not invoked for indexes which + * have been deleted or which have never been assigned values. + * + * <p>For example, this expression creates a map from strings to string length: + * + * <pre>pv.dict(["one", "three", "seventeen"], function(s) s.length)</pre> + * + * The returned value is <tt>{one: 3, three: 5, seventeen: 9}</tt>. Accessor + * functions can refer to <tt>this.index</tt>. + * + * @param {array} keys an array. + * @param {function} f a value function. + * @returns a map from keys to values. + */ +pv.dict = function(keys, f) { + var m = {}, o = {}; + for (var i = 0; i < keys.length; i++) { + if (i in keys) { + var k = keys[i]; + o.index = i; + m[k] = f.call(o, k); + } + } + return m; +}; + +/** + * Returns a permutation of the specified array, using the specified array of + * indexes. The returned array contains the corresponding element in + * <tt>array</tt> for each index in <tt>indexes</tt>, in order. For example, + * + * <pre>pv.permute(["a", "b", "c"], [1, 2, 0])</pre> + * + * returns <tt>["b", "c", "a"]</tt>. It is acceptable for the array of indexes + * to be a different length from the array of elements, and for indexes to be + * duplicated or omitted. The optional accessor function <tt>f</tt> can be used + * to perform a simultaneous mapping of the array elements. Accessor functions + * can refer to <tt>this.index</tt>. + * + * @param {array} array an array. + * @param {number[]} indexes an array of indexes into <tt>array</tt>. + * @param {function} [f] an optional accessor function. + * @returns {array} an array of elements from <tt>array</tt>; a permutation. + */ +pv.permute = function(array, indexes, f) { + if (!f) f = pv.identity; + var p = new Array(indexes.length), o = {}; + indexes.forEach(function(j, i) { o.index = j; p[i] = f.call(o, array[j]); }); + return p; +}; + +/** + * Returns a map from key to index for the specified <tt>keys</tt> array. For + * example, + * + * <pre>pv.numerate(["a", "b", "c"])</pre> + * + * returns <tt>{a: 0, b: 1, c: 2}</tt>. Note that since JavaScript maps only + * support string keys, <tt>keys</tt> must contain strings, or other values that + * naturally map to distinct string values. Alternatively, an optional accessor + * function <tt>f</tt> can be specified to compute the string key for the given + * element. Accessor functions can refer to <tt>this.index</tt>. + * + * @param {array} keys an array, usually of string keys. + * @param {function} [f] an optional key function. + * @returns a map from key to index. + */ +pv.numerate = function(keys, f) { + if (!f) f = pv.identity; + var map = {}, o = {}; + keys.forEach(function(x, i) { o.index = i; map[f.call(o, x)] = i; }); + return map; +}; + +/** + * The comparator function for natural order. This can be used in conjunction with + * the built-in array <tt>sort</tt> method to sort elements by their natural + * order, ascending. Note that if no comparator function is specified to the + * built-in <tt>sort</tt> method, the default order is lexicographic, <i>not</i> + * natural! + * + * @see <a + * href="http://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/Array/sort">Array.sort</a>. + * @param a an element to compare. + * @param b an element to compare. + * @returns {number} negative if a < b; positive if a > b; otherwise 0. + */ +pv.naturalOrder = function(a, b) { + return (a < b) ? -1 : ((a > b) ? 1 : 0); +}; + +/** + * The comparator function for reverse natural order. This can be used in + * conjunction with the built-in array <tt>sort</tt> method to sort elements by + * their natural order, descending. Note that if no comparator function is + * specified to the built-in <tt>sort</tt> method, the default order is + * lexicographic, <i>not</i> natural! + * + * @see #naturalOrder + * @param a an element to compare. + * @param b an element to compare. + * @returns {number} negative if a < b; positive if a > b; otherwise 0. + */ +pv.reverseOrder = function(b, a) { + return (a < b) ? -1 : ((a > b) ? 1 : 0); +}; + +/** + * @private Computes the value of the specified CSS property <tt>p</tt> on the + * specified element <tt>e</tt>. + * + * @param {string} p the name of the CSS property. + * @param e the element on which to compute the CSS property. + */ +pv.css = function(e, p) { + return window.getComputedStyle + ? window.getComputedStyle(e, null).getPropertyValue(p) + : e.currentStyle[p]; +}; + +/** + * Namespace constants for SVG, XMLNS, and XLINK. + * + * @namespace Namespace constants for SVG, XMLNS, and XLINK. + */ +pv.ns = { + /** + * The SVG namespace, "http://www.w3.org/2000/svg". + * + * @type string + * @constant + */ + svg: "http://www.w3.org/2000/svg", + + /** + * The XMLNS namespace, "http://www.w3.org/2000/xmlns". + * + * @type string + * @constant + */ + xmlns: "http://www.w3.org/2000/xmlns", + + /** + * The XLINK namespace, "http://www.w3.org/1999/xlink". + * + * @type string + * @constant + */ + xlink: "http://www.w3.org/1999/xlink" +}; + +/** + * Protovis major and minor version numbers. + * + * @namespace Protovis major and minor version numbers. + */ +pv.version = { + /** + * The major version number. + * + * @type number + * @constant + */ + major: 3, + + /** + * The minor version number. + * + * @type number + * @constant + */ + minor: 1 +}; + +/** + * @private Reports the specified error to the JavaScript console. Mozilla only + * allows logging to the console for privileged code; if the console is + * unavailable, the alert dialog box is used instead. + * + * @param e the exception that triggered the error. + */ +pv.error = function(e) { + (typeof console == "undefined") ? alert(e) : console.error(e); +}; + +/** + * @private Registers the specified listener for events of the specified type on + * the specified target. For standards-compliant browsers, this method uses + * <tt>addEventListener</tt>; for Internet Explorer, <tt>attachEvent</tt>. + * + * @param target a DOM element. + * @param {string} type the type of event, such as "click". + * @param {function} the listener callback function. + */ +pv.listen = function(target, type, listener) { + return target.addEventListener + ? target.addEventListener(type, listener, false) + : target.attachEvent("on" + type, listener); +}; + +/** + * Returns the logarithm with a given base value. + * + * @param {number} x the number for which to compute the logarithm. + * @param {number} b the base of the logarithm. + * @returns {number} the logarithm value. + */ +pv.log = function(x, b) { + return Math.log(x) / Math.log(b); +}; + +/** + * Computes a zero-symmetric logarithm. Computes the logarithm of the absolute + * value of the input, and determines the sign of the output according to the + * sign of the input value. + * + * @param {number} x the number for which to compute the logarithm. + * @param {number} b the base of the logarithm. + * @returns {number} the symmetric log value. + */ +pv.logSymmetric = function(x, b) { + return (x == 0) ? 0 : ((x < 0) ? -pv.log(-x, b) : pv.log(x, b)); +}; + +/** + * Computes a zero-symmetric logarithm, with adjustment to values between zero + * and the logarithm base. This adjustment introduces distortion for values less + * than the base number, but enables simultaneous plotting of log-transformed + * data involving both positive and negative numbers. + * + * @param {number} x the number for which to compute the logarithm. + * @param {number} b the base of the logarithm. + * @returns {number} the adjusted, symmetric log value. + */ +pv.logAdjusted = function(x, b) { + var negative = x < 0; + if (x < b) x += (b - x) / b; + return negative ? -pv.log(x, b) : pv.log(x, b); +}; + +/** + * Rounds an input value down according to its logarithm. The method takes the + * floor of the logarithm of the value and then uses the resulting value as an + * exponent for the base value. + * + * @param {number} x the number for which to compute the logarithm floor. + * @param {number} b the base of the logarithm. + * @return {number} the rounded-by-logarithm value. + */ +pv.logFloor = function(x, b) { + return (x > 0) + ? Math.pow(b, Math.floor(pv.log(x, b))) + : -Math.pow(b, -Math.floor(-pv.log(-x, b))); +}; + +/** + * Rounds an input value up according to its logarithm. The method takes the + * ceiling of the logarithm of the value and then uses the resulting value as an + * exponent for the base value. + * + * @param {number} x the number for which to compute the logarithm ceiling. + * @param {number} b the base of the logarithm. + * @return {number} the rounded-by-logarithm value. + */ +pv.logCeil = function(x, b) { + return (x > 0) + ? Math.pow(b, Math.ceil(pv.log(x, b))) + : -Math.pow(b, -Math.ceil(-pv.log(-x, b))); +}; + +/** + * Searches the specified array of numbers for the specified value using the + * binary search algorithm. The array must be sorted (as by the <tt>sort</tt> + * method) prior to making this call. If it is not sorted, the results are + * undefined. If the array contains multiple elements with the specified value, + * there is no guarantee which one will be found. + * + * <p>The <i>insertion point</i> is defined as the point at which the value + * would be inserted into the array: the index of the first element greater than + * the value, or <tt>array.length</tt>, if all elements in the array are less + * than the specified value. Note that this guarantees that the return value + * will be nonnegative if and only if the value is found. + * + * @param {number[]} array the array to be searched. + * @param {number} value the value to be searched for. + * @returns the index of the search value, if it is contained in the array; + * otherwise, (-(<i>insertion point</i>) - 1). + * @param {function} [f] an optional key function. + */ +pv.search = function(array, value, f) { + if (!f) f = pv.identity; + var low = 0, high = array.length - 1; + while (low <= high) { + var mid = (low + high) >> 1, midValue = f(array[mid]); + if (midValue < value) low = mid + 1; + else if (midValue > value) high = mid - 1; + else return mid; + } + return -low - 1; +}; + +pv.search.index = function(array, value, f) { + var i = pv.search(array, value, f); + return (i < 0) ? (-i - 1) : i; +}; +/** + * Returns a {@link pv.Tree} operator for the specified array. This is a + * convenience factory method, equivalent to <tt>new pv.Tree(array)</tt>. + * + * @see pv.Tree + * @param {array} array an array from which to construct a tree. + * @returns {pv.Tree} a tree operator for the specified array. + */ +pv.tree = function(array) { + return new pv.Tree(array); +}; + +/** + * Constructs a tree operator for the specified array. This constructor should + * not be invoked directly; use {@link pv.tree} instead. + * + * @class Represents a tree operator for the specified array. The tree operator + * allows a hierarchical map to be constructed from an array; it is similar to + * the {@link pv.Nest} operator, except the hierarchy is derived dynamically + * from the array elements. + * + * <p>For example, given an array of size information for ActionScript classes: + * + * <pre>{ name: "flare.flex.FlareVis", size: 4116 }, + * { name: "flare.physics.DragForce", size: 1082 }, + * { name: "flare.physics.GravityForce", size: 1336 }, ...</pre> + * + * To facilitate visualization, it may be useful to nest the elements by their + * package hierarchy: + * + * <pre>var tree = pv.tree(classes) + * .keys(function(d) d.name.split(".")) + * .map();</pre> + * + * The resulting tree is: + * + * <pre>{ flare: { + * flex: { + * FlareVis: { + * name: "flare.flex.FlareVis", + * size: 4116 } }, + * physics: { + * DragForce: { + * name: "flare.physics.DragForce", + * size: 1082 }, + * GravityForce: { + * name: "flare.physics.GravityForce", + * size: 1336 } }, + * ... } }</pre> + * + * By specifying a value function, + * + * <pre>var tree = pv.tree(classes) + * .keys(function(d) d.name.split(".")) + * .value(function(d) d.size) + * .map();</pre> + * + * we can further eliminate redundant data: + * + * <pre>{ flare: { + * flex: { + * FlareVis: 4116 }, + * physics: { + * DragForce: 1082, + * GravityForce: 1336 }, + * ... } }</pre> + * + * For visualizations with large data sets, performance improvements may be seen + * by storing the data in a tree format, and then flattening it into an array at + * runtime with {@link pv.Flatten}. + * + * @param {array} array an array from which to construct a tree. + */ +pv.Tree = function(array) { + this.array = array; +}; + +/** + * Assigns a <i>keys</i> function to this operator; required. The keys function + * returns an array of <tt>string</tt>s for each element in the associated + * array; these keys determine how the elements are nested in the tree. The + * returned keys should be unique for each element in the array; otherwise, the + * behavior of this operator is undefined. + * + * @param {function} k the keys function. + * @returns {pv.Tree} this. + */ +pv.Tree.prototype.keys = function(k) { + this.k = k; + return this; +}; + +/** + * Assigns a <i>value</i> function to this operator; optional. The value + * function specifies an optional transformation of the element in the array + * before it is inserted into the map. If no value function is specified, it is + * equivalent to using the identity function. + * + * @param {function} k the value function. + * @returns {pv.Tree} this. + */ +pv.Tree.prototype.value = function(v) { + this.v = v; + return this; +}; + +/** + * Returns a hierarchical map of values. The hierarchy is determined by the keys + * function; the values in the map are determined by the value function. + * + * @returns a hierarchical map of values. + */ +pv.Tree.prototype.map = function() { + var map = {}, o = {}; + for (var i = 0; i < this.array.length; i++) { + o.index = i; + var value = this.array[i], keys = this.k.call(o, value), node = map; + for (var j = 0; j < keys.length - 1; j++) { + node = node[keys[j]] || (node[keys[j]] = {}); + } + node[keys[j]] = this.v ? this.v.call(o, value) : value; + } + return map; +}; +/** + * Returns a {@link pv.Nest} operator for the specified array. This is a + * convenience factory method, equivalent to <tt>new pv.Nest(array)</tt>. + * + * @see pv.Nest + * @param {array} array an array of elements to nest. + * @returns {pv.Nest} a nest operator for the specified array. + */ +pv.nest = function(array) { + return new pv.Nest(array); +}; + +/** + * Constructs a nest operator for the specified array. This constructor should + * not be invoked directly; use {@link pv.nest} instead. + * + * @class Represents a {@link Nest} operator for the specified array. Nesting + * allows elements in an array to be grouped into a hierarchical tree + * structure. The levels in the tree are specified by <i>key</i> functions. The + * leaf nodes of the tree can be sorted by value, while the internal nodes can + * be sorted by key. Finally, the tree can be returned either has a + * multidimensional array via {@link #entries}, or as a hierarchical map via + * {@link #map}. The {@link #rollup} routine similarly returns a map, collapsing + * the elements in each leaf node using a summary function. + * + * <p>For example, consider the following tabular data structure of Barley + * yields, from various sites in Minnesota during 1931-2: + * + * <pre>{ yield: 27.00, variety: "Manchuria", year: 1931, site: "University Farm" }, + * { yield: 48.87, variety: "Manchuria", year: 1931, site: "Waseca" }, + * { yield: 27.43, variety: "Manchuria", year: 1931, site: "Morris" }, ...</pre> + * + * To facilitate visualization, it may be useful to nest the elements first by + * year, and then by variety, as follows: + * + * <pre>var nest = pv.nest(yields) + * .key(function(d) d.year) + * .key(function(d) d.variety) + * .entries();</pre> + * + * This returns a nested array. Each element of the outer array is a key-values + * pair, listing the values for each distinct key: + * + * <pre>{ key: 1931, values: [ + * { key: "Manchuria", values: [ + * { yield: 27.00, variety: "Manchuria", year: 1931, site: "University Farm" }, + * { yield: 48.87, variety: "Manchuria", year: 1931, site: "Waseca" }, + * { yield: 27.43, variety: "Manchuria", year: 1931, site: "Morris" }, + * ... + * ] }, + * { key: "Glabron", values: [ + * { yield: 43.07, variety: "Glabron", year: 1931, site: "University Farm" }, + * { yield: 55.20, variety: "Glabron", year: 1931, site: "Waseca" }, + * ... + * ] }, + * ] }, + * { key: 1932, values: ... }</pre> + * + * Further details, including sorting and rollup, is provided below on the + * corresponding methods. + * + * @param {array} array an array of elements to nest. + */ +pv.Nest = function(array) { + this.array = array; + this.keys = []; +}; + +/** + * Nests using the specified key function. Multiple keys may be added to the + * nest; the array elements will be nested in the order keys are specified. + * + * @param {function} key a key function; must return a string or suitable map + * key. + * @return {pv.Nest} this. + */ +pv.Nest.prototype.key = function(key) { + this.keys.push(key); + return this; +}; + +/** + * Sorts the previously-added keys. The natural sort order is used by default + * (see {@link pv.naturalOrder}); if an alternative order is desired, + * <tt>order</tt> should be a comparator function. If this method is not called + * (i.e., keys are <i>unsorted</i>), keys will appear in the order they appear + * in the underlying elements array. For example, + * + * <pre>pv.nest(yields) + * .key(function(d) d.year) + * .key(function(d) d.variety) + * .sortKeys() + * .entries()</pre> + * + * groups yield data by year, then variety, and sorts the variety groups + * lexicographically (since the variety attribute is a string). + * + * <p>Key sort order is only used in conjunction with {@link #entries}, which + * returns an array of key-values pairs. If the nest is used to construct a + * {@link #map} instead, keys are unsorted. + * + * @param {function} [order] an optional comparator function. + * @returns {pv.Nest} this. + */ +pv.Nest.prototype.sortKeys = function(order) { + this.keys[this.keys.length - 1].order = order || pv.naturalOrder; + return this; +}; + +/** + * Sorts the leaf values. The natural sort order is used by default (see + * {@link pv.naturalOrder}); if an alternative order is desired, <tt>order</tt> + * should be a comparator function. If this method is not called (i.e., values + * are <i>unsorted</i>), values will appear in the order they appear in the + * underlying elements array. For example, + * + * <pre>pv.nest(yields) + * .key(function(d) d.year) + * .key(function(d) d.variety) + * .sortValues(function(a, b) a.yield - b.yield) + * .entries()</pre> + * + * groups yield data by year, then variety, and sorts the values for each + * variety group by yield. + * + * <p>Value sort order, unlike keys, applies to both {@link #entries} and + * {@link #map}. It has no effect on {@link #rollup}. + * + * @param {function} [order] an optional comparator function. + * @return {pv.Nest} this. + */ +pv.Nest.prototype.sortValues = function(order) { + this.order = order || pv.naturalOrder; + return this; +}; + +/** + * Returns a hierarchical map of values. Each key adds one level to the + * hierarchy. With only a single key, the returned map will have a key for each + * distinct value of the key function; the correspond value with be an array of + * elements with that key value. If a second key is added, this will be a nested + * map. For example: + * + * <pre>pv.nest(yields) + * .key(function(d) d.variety) + * .key(function(d) d.site) + * .map()</pre> + * + * returns a map <tt>m</tt> such that <tt>m[variety][site]</tt> is an array, a subset of + * <tt>yields</tt>, with each element having the given variety and site. + * + * @returns a hierarchical map of values. + */ +pv.Nest.prototype.map = function() { + var map = {}, values = []; + + /* Build the map. */ + for (var i, j = 0; j < this.array.length; j++) { + var x = this.array[j]; + var m = map; + for (i = 0; i < this.keys.length - 1; i++) { + var k = this.keys[i](x); + if (!m[k]) m[k] = {}; + m = m[k]; + } + k = this.keys[i](x); + if (!m[k]) { + var a = []; + values.push(a); + m[k] = a; + } + m[k].push(x); + } + + /* Sort each leaf array. */ + if (this.order) { + for (var i = 0; i < values.length; i++) { + values[i].sort(this.order); + } + } + + return map; +}; + +/** + * Returns a hierarchical nested array. This method is similar to + * {@link pv.entries}, but works recursively on the entire hierarchy. Rather + * than returning a map like {@link #map}, this method returns a nested + * array. Each element of the array has a <tt>key</tt> and <tt>values</tt> + * field. For leaf nodes, the <tt>values</tt> array will be a subset of the + * underlying elements array; for non-leaf nodes, the <tt>values</tt> array will + * contain more key-values pairs. + * + * <p>For an example usage, see the {@link Nest} constructor. + * + * @returns a hierarchical nested array. + */ +pv.Nest.prototype.entries = function() { + + /** Recursively extracts the entries for the given map. */ + function entries(map) { + var array = []; + for (var k in map) { + var v = map[k]; + array.push({ key: k, values: (v instanceof Array) ? v : entries(v) }); + }; + return array; + } + + /** Recursively sorts the values for the given key-values array. */ + function sort(array, i) { + var o = this.keys[i].order; + if (o) array.sort(function(a, b) { return o(a.key, b.key); }); + if (++i < this.keys.length) { + for (var j = 0; j < array.length; j++) { + sort.call(this, array[j].values, i); + } + } + return array; + } + + return sort.call(this, entries(this.map()), 0); +}; + +/** + * Returns a rollup map. The behavior of this method is the same as + * {@link #map}, except that the leaf values are replaced with the return value + * of the specified rollup function <tt>f</tt>. For example, + * + * <pre>pv.nest(yields) + * .key(function(d) d.site) + * .rollup(function(v) pv.median(v, function(d) d.yield))</pre> + * + * first groups yield data by site, and then returns a map from site to median + * yield for the given site. + * + * @see #map + * @param {function} f a rollup function. + * @returns a hierarchical map, with the leaf values computed by <tt>f</tt>. + */ +pv.Nest.prototype.rollup = function(f) { + + /** Recursively descends to the leaf nodes (arrays) and does rollup. */ + function rollup(map) { + for (var key in map) { + var value = map[key]; + if (value instanceof Array) { + map[key] = f(value); + } else { + rollup(value); + } + } + return map; + } + + return rollup(this.map()); +}; +/** + * Returns a {@link pv.Flatten} operator for the specified map. This is a + * convenience factory method, equivalent to <tt>new pv.Flatten(map)</tt>. + * + * @see pv.Flatten + * @param map a map to flatten. + * @returns {pv.Flatten} a flatten operator for the specified map. + */ +pv.flatten = function(map) { + return new pv.Flatten(map); +}; + +/** + * Constructs a flatten operator for the specified map. This constructor should + * not be invoked directly; use {@link pv.flatten} instead. + * + * @class Represents a flatten operator for the specified array. Flattening + * allows hierarchical maps to be flattened into an array. The levels in the + * input tree are specified by <i>key</i> functions. + * + * <p>For example, consider the following hierarchical data structure of Barley + * yields, from various sites in Minnesota during 1931-2: + * + * <pre>{ 1931: { + * Manchuria: { + * "University Farm": 27.00, + * "Waseca": 48.87, + * "Morris": 27.43, + * ... }, + * Glabron: { + * "University Farm": 43.07, + * "Waseca": 55.20, + * ... } }, + * 1932: { + * ... } }</pre> + * + * To facilitate visualization, it may be useful to flatten the tree into a + * tabular array: + * + * <pre>var array = pv.flatten(yields) + * .key("year") + * .key("variety") + * .key("site") + * .key("yield") + * .array();</pre> + * + * This returns an array of object elements. Each element in the array has + * attributes corresponding to this flatten operator's keys: + * + * <pre>{ site: "University Farm", variety: "Manchuria", year: 1931, yield: 27 }, + * { site: "Waseca", variety: "Manchuria", year: 1931, yield: 48.87 }, + * { site: "Morris", variety: "Manchuria", year: 1931, yield: 27.43 }, + * { site: "University Farm", variety: "Glabron", year: 1931, yield: 43.07 }, + * { site: "Waseca", variety: "Glabron", year: 1931, yield: 55.2 }, ...</pre> + * + * <p>The flatten operator is roughly the inverse of the {@link pv.Nest} and + * {@link pv.Tree} operators. + * + * @param map a map to flatten. + */ +pv.Flatten = function(map) { + this.map = map; + this.keys = []; +}; + +/** + * Flattens using the specified key function. Multiple keys may be added to the + * flatten; the tiers of the underlying tree must correspond to the specified + * keys, in order. The order of the returned array is undefined; however, you + * can easily sort it. + * + * @param {string} key the key name. + * @param {function} [f] an optional value map function. + * @return {pv.Nest} this. + */ +pv.Flatten.prototype.key = function(key, f) { + this.keys.push({name: key, value: f}); + return this; +}; + +/** + * Returns the flattened array. Each entry in the array is an object; each + * object has attributes corresponding to this flatten operator's keys. + * + * @returns an array of elements from the flattened map. + */ +pv.Flatten.prototype.array = function() { + var entries = [], stack = [], keys = this.keys; + + /* Recursively visits the specified value. */ + function visit(value, i) { + if (i < keys.length - 1) { + for (var key in value) { + stack.push(key); + visit(value[key], i + 1); + stack.pop(); + } + } else { + entries.push(stack.concat(value)); + } + } + + visit(this.map, 0); + return entries.map(function(stack) { + var m = {}; + for (var i = 0; i < keys.length; i++) { + var k = keys[i], v = stack[i]; + m[k.name] = k.value ? k.value.call(null, v) : v; + } + return m; + }); +}; +/** + * Returns a {@link pv.Vector} for the specified <i>x</i> and <i>y</i> + * coordinate. This is a convenience factory method, equivalent to <tt>new + * pv.Vector(x, y)</tt>. + * + * @see pv.Vector + * @param {number} x the <i>x</i> coordinate. + * @param {number} y the <i>y</i> coordinate. + * @returns {pv.Vector} a vector for the specified coordinates. + */ +pv.vector = function(x, y) { + return new pv.Vector(x, y); +}; + +/** + * Constructs a {@link pv.Vector} for the specified <i>x</i> and <i>y</i> + * coordinate. This constructor should not be invoked directly; use + * {@link pv.vector} instead. + * + * @class Represents a two-dimensional vector; a 2-tuple <i>⟨x, + * y⟩</i>. + * + * @param {number} x the <i>x</i> coordinate. + * @param {number} y the <i>y</i> coordinate. + */ +pv.Vector = function(x, y) { + this.x = x; + this.y = y; +}; + +/** + * Returns a vector perpendicular to this vector: <i>⟨-y, x⟩</i>. + * + * @returns {pv.Vector} a perpendicular vector. + */ +pv.Vector.prototype.perp = function() { + return new pv.Vector(-this.y, this.x); +}; + +/** + * Returns a normalized copy of this vector: a vector with the same direction, + * but unit length. If this vector has zero length this method returns a copy of + * this vector. + * + * @returns {pv.Vector} a unit vector. + */ +pv.Vector.prototype.norm = function() { + var l = this.length(); + return this.times(l ? (1 / l) : 1); +}; + +/** + * Returns the magnitude of this vector, defined as <i>sqrt(x * x + y * y)</i>. + * + * @returns {number} a length. + */ +pv.Vector.prototype.length = function() { + return Math.sqrt(this.x * this.x + this.y * this.y); +}; + +/** + * Returns a scaled copy of this vector: <i>⟨x * k, y * k⟩</i>. + * To perform the equivalent divide operation, use <i>1 / k</i>. + * + * @param {number} k the scale factor. + * @returns {pv.Vector} a scaled vector. + */ +pv.Vector.prototype.times = function(k) { + return new pv.Vector(this.x * k, this.y * k); +}; + +/** + * Returns this vector plus the vector <i>v</i>: <i>⟨x + v.x, y + + * v.y⟩</i>. If only one argument is specified, it is interpreted as the + * vector <i>v</i>. + * + * @param {number} x the <i>x</i> coordinate to add. + * @param {number} y the <i>y</i> coordinate to add. + * @returns {pv.Vector} a new vector. + */ +pv.Vector.prototype.plus = function(x, y) { + return (arguments.length == 1) + ? new pv.Vector(this.x + x.x, this.y + x.y) + : new pv.Vector(this.x + x, this.y + y); +}; + +/** + * Returns this vector minus the vector <i>v</i>: <i>⟨x - v.x, y - + * v.y⟩</i>. If only one argument is specified, it is interpreted as the + * vector <i>v</i>. + * + * @param {number} x the <i>x</i> coordinate to subtract. + * @param {number} y the <i>y</i> coordinate to subtract. + * @returns {pv.Vector} a new vector. + */ +pv.Vector.prototype.minus = function(x, y) { + return (arguments.length == 1) + ? new pv.Vector(this.x - x.x, this.y - x.y) + : new pv.Vector(this.x - x, this.y - y); +}; + +/** + * Returns the dot product of this vector and the vector <i>v</i>: <i>x * v.x + + * y * v.y</i>. If only one argument is specified, it is interpreted as the + * vector <i>v</i>. + * + * @param {number} x the <i>x</i> coordinate to dot. + * @param {number} y the <i>y</i> coordinate to dot. + * @returns {number} a dot product. + */ +pv.Vector.prototype.dot = function(x, y) { + return (arguments.length == 1) + ? this.x * x.x + this.y * x.y + : this.x * x + this.y * y; +}; +// TODO code-sharing between scales + +/** + * @ignore + * @class + */ +pv.Scale = function() {}; + +/** + * @private Returns a function that interpolators from the start value to the + * end value, given a parameter <i>t</i> in [0, 1]. + * + * @param start the start value. + * @param end the end value. + */ +pv.Scale.interpolator = function(start, end) { + if (typeof start == "number") { + return function(t) { + return t * (end - start) + start; + }; + } + + /* For now, assume color. */ + start = pv.color(start).rgb(); + end = pv.color(end).rgb(); + return function(t) { + var a = start.a * (1 - t) + end.a * t; + if (a < 1e-5) a = 0; // avoid scientific notation + return (start.a == 0) ? pv.rgb(end.r, end.g, end.b, a) + : ((end.a == 0) ? pv.rgb(start.r, start.g, start.b, a) + : pv.rgb( + Math.round(start.r * (1 - t) + end.r * t), + Math.round(start.g * (1 - t) + end.g * t), + Math.round(start.b * (1 - t) + end.b * t), a)); + }; +}; +/** + * Returns a linear scale for the specified domain. The arguments to this + * constructor are optional, and equivalent to calling {@link #domain}. + * + * @class Represents a linear scale. <style + * type="text/css">sub{line-height:0}</style> <img src="../linear.png" + * width="180" height="175" align="right"> Most commonly, a linear scale + * represents a 1-dimensional linear transformation from a numeric domain of + * input data [<i>d<sub>0</sub></i>, <i>d<sub>1</sub></i>] to a numeric range of + * pixels [<i>r<sub>0</sub></i>, <i>r<sub>1</sub></i>]. The equation for such a + * scale is: + * + * <blockquote><i>f(x) = (x - d<sub>0</sub>) / (d<sub>1</sub> - d<sub>0</sub>) * + * (r<sub>1</sub> - r<sub>0</sub>) + r<sub>0</sub></i></blockquote> + * + * For example, a linear scale from the domain [0, 100] to range [0, 640]: + * + * <blockquote><i>f(x) = (x - 0) / (100 - 0) * (640 - 0) + 0</i><br> + * <i>f(x) = x / 100 * 640</i><br> + * <i>f(x) = x * 6.4</i><br> + * </blockquote> + * + * Thus, saying + * + * <pre>.height(function(d) d * 6.4)</pre> + * + * is identical to + * + * <pre>.height(pv.Scale.linear(0, 100).range(0, 640))</pre> + * + * As you can see, scales do not always make code smaller, but they should make + * code more explicit and easier to maintain. In addition to readability, scales + * offer several useful features: + * + * <p>1. The range can be expressed in colors, rather than pixels. Changing the + * example above to + * + * <pre>.fillStyle(pv.Scale.linear(0, 100).range("red", "green"))</pre> + * + * will cause it to fill the marks "red" on an input value of 0, "green" on an + * input value of 100, and some color in-between for intermediate values. + * + * <p>2. The domain and range can be subdivided for a "poly-linear" + * transformation. For example, you may want a diverging color scale that is + * increasingly red for negative values, and increasingly green for positive + * values: + * + * <pre>.fillStyle(pv.Scale.linear(-1, 0, 1).range("red", "white", "green"))</pre> + * + * The domain can be specified as a series of <i>n</i> monotonically-increasing + * values; the range must also be specified as <i>n</i> values, resulting in + * <i>n - 1</i> contiguous linear scales. + * + * <p>3. Linear scales can be inverted for interaction. The {@link #invert} + * method takes a value in the output range, and returns the corresponding value + * in the input domain. This is frequently used to convert the mouse location + * (see {@link pv.Mark#mouse}) to a value in the input domain. Note that + * inversion is only supported for numeric ranges, and not colors. + * + * <p>4. A scale can be queried for reasonable "tick" values. The {@link #ticks} + * method provides a convenient way to get a series of evenly-spaced rounded + * values in the input domain. Frequently these are used in conjunction with + * {@link pv.Rule} to display tick marks or grid lines. + * + * <p>5. A scale can be "niced" to extend the domain to suitable rounded + * numbers. If the minimum and maximum of the domain are messy because they are + * derived from data, you can use {@link #nice} to round these values down and + * up to even numbers. + * + * @param {number...} domain... domain values. + * @returns {pv.Scale.linear} a linear scale. + */ +pv.Scale.linear = function() { + var d = [0, 1], r = [0, 1], i = [pv.identity], precision = 0; + + /** @private */ + function scale(x) { + var j = pv.search(d, x); + if (j < 0) j = -j - 2; + j = Math.max(0, Math.min(i.length - 1, j)); + return i[j]((x - d[j]) / (d[j + 1] - d[j])); + } + + /** + * Sets or gets the input domain. This method can be invoked several ways: + * + * <p>1. <tt>domain(min, ..., max)</tt> + * + * <p>Specifying the domain as a series of numbers is the most explicit and + * recommended approach. Most commonly, two numbers are specified: the minimum + * and maximum value. However, for a diverging scale, or other subdivided + * poly-linear scales, multiple values can be specified. Values can be derived + * from data using {@link pv.min} and {@link pv.max}. For example: + * + * <pre>.domain(0, pv.max(array))</pre> + * + * An alternative method for deriving minimum and maximum values from data + * follows. + * + * <p>2. <tt>domain(array, minf, maxf)</tt> + * + * <p>When both the minimum and maximum value are derived from data, the + * arguments to the <tt>domain</tt> method can be specified as the array of + * data, followed by zero, one or two accessor functions. For example, if the + * array of data is just an array of numbers: + * + * <pre>.domain(array)</pre> + * + * On the other hand, if the array elements are objects representing stock + * values per day, and the domain should consider the stock's daily low and + * daily high: + * + * <pre>.domain(array, function(d) d.low, function(d) d.high)</pre> + * + * The first method of setting the domain is preferred because it is more + * explicit; setting the domain using this second method should be used only + * if brevity is required. + * + * <p>3. <tt>domain()</tt> + * + * <p>Invoking the <tt>domain</tt> method with no arguments returns the + * current domain as an array of numbers. + * + * @function + * @name pv.Scale.linear.prototype.domain + * @param {number...} domain... domain values. + * @returns {pv.Scale.linear} <tt>this</tt>, or the current domain. + */ + scale.domain = function(array, min, max) { + if (arguments.length) { + if (array instanceof Array) { + if (arguments.length < 2) min = pv.identity; + if (arguments.length < 3) max = min; + d = [pv.min(array, min), pv.max(array, max)]; + } else { + d = Array.prototype.slice.call(arguments); + } + return this; + } + return d; + }; + + /** + * Sets or gets the output range. This method can be invoked several ways: + * + * <p>1. <tt>range(min, ..., max)</tt> + * + * <p>The range may be specified as a series of numbers or colors. Most + * commonly, two numbers are specified: the minimum and maximum pixel values. + * For a color scale, values may be specified as {@link pv.Color}s or + * equivalent strings. For a diverging scale, or other subdivided poly-linear + * scales, multiple values can be specified. For example: + * + * <pre>.range("red", "white", "green")</pre> + * + * <p>Currently, only numbers and colors are supported as range values. The + * number of range values must exactly match the number of domain values, or + * the behavior of the scale is undefined. + * + * <p>2. <tt>range()</tt> + * + * <p>Invoking the <tt>range</tt> method with no arguments returns the current + * range as an array of numbers or colors. + * + * @function + * @name pv.Scale.linear.prototype.range + * @param {...} range... range values. + * @returns {pv.Scale.linear} <tt>this</tt>, or the current range. + */ + scale.range = function() { + if (arguments.length) { + r = Array.prototype.slice.call(arguments); + i = []; + for (var j = 0; j < r.length - 1; j++) { + i.push(pv.Scale.interpolator(r[j], r[j + 1])); + } + return this; + } + return r; + }; + + /** + * Inverts the specified value in the output range, returning the + * corresponding value in the input domain. This is frequently used to convert + * the mouse location (see {@link pv.Mark#mouse}) to a value in the input + * domain. Inversion is only supported for numeric ranges, and not colors. + * + * <p>Note that this method does not do any rounding or bounds checking. If + * the input domain is discrete (e.g., an array index), the returned value + * should be rounded. If the specified <tt>y</tt> value is outside the range, + * the returned value may be equivalently outside the input domain. + * + * @function + * @name pv.Scale.linear.prototype.invert + * @param {number} y a value in the output range (a pixel location). + * @returns {number} a value in the input domain. + */ + scale.invert = function(y) { + var j = pv.search(r, y); + if (j < 0) j = -j - 2; + j = Math.max(0, Math.min(i.length - 1, j)); + return (y - r[j]) / (r[j + 1] - r[j]) * (d[j + 1] - d[j]) + d[j]; + }; + + /** + * Returns an array of evenly-spaced, suitably-rounded values in the input + * domain. This method attempts to return between 5 and 10 tick values. These + * values are frequently used in conjunction with {@link pv.Rule} to display + * tick marks or grid lines. + * + * @function + * @name pv.Scale.linear.prototype.ticks + * @returns {number[]} an array input domain values to use as ticks. + */ + scale.ticks = function() { + var min = d[0], + max = d[d.length - 1], + span = max - min, + step = pv.logCeil(span / 10, 10); + if (span / step < 2) step /= 5; + else if (span / step < 5) step /= 2; + var start = Math.ceil(min / step) * step, + end = Math.floor(max / step) * step; + precision = Math.max(0, -Math.floor(pv.log(step, 10) + .01)); + return pv.range(start, end + step, step); + }; + + /** + * Formats the specified tick value using the appropriate precision, based on + * the step interval between tick marks. + * + * @function + * @name pv.Scale.linear.prototype.tickFormat + * @param {number} t a tick value. + * @return {string} a formatted tick value. + */ + scale.tickFormat = function(t) { + return t.toFixed(precision); + }; + + /** + * "Nices" this scale, extending the bounds of the input domain to + * evenly-rounded values. Nicing is useful if the domain is computed + * dynamically from data, and may be irregular. For example, given a domain of + * [0.20147987687960267, 0.996679553296417], a call to <tt>nice()</tt> might + * extend the domain to [0.2, 1]. + * + * <p>This method must be invoked each time after setting the domain. + * + * @function + * @name pv.Scale.linear.prototype.nice + * @returns {pv.Scale.linear} <tt>this</tt>. + */ + scale.nice = function() { + var min = d[0], + max = d[d.length - 1], + step = Math.pow(10, Math.round(Math.log(max - min) / Math.log(10)) - 1); + d = [Math.floor(min / step) * step, Math.ceil(max / step) * step]; + return this; + }; + + /** + * Returns a view of this scale by the specified accessor function <tt>f</tt>. + * Given a scale <tt>y</tt>, <tt>y.by(function(d) d.foo)</tt> is equivalent to + * <tt>function(d) y(d.foo)</tt>. + * + * <p>This method is provided for convenience, such that scales can be + * succinctly defined inline. For example, given an array of data elements + * that have a <tt>score</tt> attribute with the domain [0, 1], the height + * property could be specified as: + * + * <pre>.height(pv.Scale.linear().range(0, 480).by(function(d) d.score))</pre> + * + * This is equivalent to: + * + * <pre>.height(function(d) d.score * 480)</pre> + * + * This method should be used judiciously; it is typically more clear to + * invoke the scale directly, passing in the value to be scaled. + * + * @function + * @name pv.Scale.linear.prototype.by + * @param {function} f an accessor function. + * @returns {pv.Scale.linear} a view of this scale by the specified accessor + * function. + */ + scale.by = function(f) { + function by() { return scale(f.apply(this, arguments)); } + for (var method in scale) by[method] = scale[method]; + return by; + }; + + scale.domain.apply(scale, arguments); + return scale; +}; +/** + * Returns a log scale for the specified domain. The arguments to this + * constructor are optional, and equivalent to calling {@link #domain}. + * + * @class Represents a log scale. <style + * type="text/css">sub{line-height:0}</style> <img src="../log.png" + * width="190" height="175" align="right"> Most commonly, a log scale represents + * a 1-dimensional log transformation from a numeric domain of input data + * [<i>d<sub>0</sub></i>, <i>d<sub>1</sub></i>] to a numeric range of pixels + * [<i>r<sub>0</sub></i>, <i>r<sub>1</sub></i>]. The equation for such a scale + * is: + * + * <blockquote><i>f(x) = (log(x) - log(d<sub>0</sub>)) / (log(d<sub>1</sub>) - + * log(d<sub>0</sub>)) * (r<sub>1</sub> - r<sub>0</sub>) + + * r<sub>0</sub></i></blockquote> + * + * where <i>log(x)</i> represents the zero-symmetric logarthim of <i>x</i> using + * the scale's associated base (default: 10, see {@link pv.logSymmetric}). For + * example, a log scale from the domain [1, 100] to range [0, 640]: + * + * <blockquote><i>f(x) = (log(x) - log(1)) / (log(100) - log(1)) * (640 - 0) + 0</i><br> + * <i>f(x) = log(x) / 2 * 640</i><br> + * <i>f(x) = log(x) * 320</i><br> + * </blockquote> + * + * Thus, saying + * + * <pre>.height(function(d) Math.log(d) * 138.974)</pre> + * + * is equivalent to + * + * <pre>.height(pv.Scale.log(1, 100).range(0, 640))</pre> + * + * As you can see, scales do not always make code smaller, but they should make + * code more explicit and easier to maintain. In addition to readability, scales + * offer several useful features: + * + * <p>1. The range can be expressed in colors, rather than pixels. Changing the + * example above to + * + * <pre>.fillStyle(pv.Scale.log(1, 100).range("red", "green"))</pre> + * + * will cause it to fill the marks "red" on an input value of 1, "green" on an + * input value of 100, and some color in-between for intermediate values. + * + * <p>2. The domain and range can be subdivided for a "poly-log" + * transformation. For example, you may want a diverging color scale that is + * increasingly red for small values, and increasingly green for large values: + * + * <pre>.fillStyle(pv.Scale.log(1, 10, 100).range("red", "white", "green"))</pre> + * + * The domain can be specified as a series of <i>n</i> monotonically-increasing + * values; the range must also be specified as <i>n</i> values, resulting in + * <i>n - 1</i> contiguous log scales. + * + * <p>3. Log scales can be inverted for interaction. The {@link #invert} method + * takes a value in the output range, and returns the corresponding value in the + * input domain. This is frequently used to convert the mouse location (see + * {@link pv.Mark#mouse}) to a value in the input domain. Note that inversion is + * only supported for numeric ranges, and not colors. + * + * <p>4. A scale can be queried for reasonable "tick" values. The {@link #ticks} + * method provides a convenient way to get a series of evenly-spaced rounded + * values in the input domain. Frequently these are used in conjunction with + * {@link pv.Rule} to display tick marks or grid lines. + * + * <p>5. A scale can be "niced" to extend the domain to suitable rounded + * numbers. If the minimum and maximum of the domain are messy because they are + * derived from data, you can use {@link #nice} to round these values down and + * up to even numbers. + * + * @param {number...} domain... domain values. + * @returns {pv.Scale.log} a log scale. + */ +pv.Scale.log = function() { + var d = [1, 10], l = [0, 1], b = 10, r = [0, 1], i = [pv.identity]; + + /** @private */ + function scale(x) { + var j = pv.search(d, x); + if (j < 0) j = -j - 2; + j = Math.max(0, Math.min(i.length - 1, j)); + return i[j]((log(x) - l[j]) / (l[j + 1] - l[j])); + } + + /** @private */ + function log(x) { + return pv.logSymmetric(x, b); + } + + /** + * Sets or gets the input domain. This method can be invoked several ways: + * + * <p>1. <tt>domain(min, ..., max)</tt> + * + * <p>Specifying the domain as a series of numbers is the most explicit and + * recommended approach. Most commonly, two numbers are specified: the minimum + * and maximum value. However, for a diverging scale, or other subdivided + * poly-log scales, multiple values can be specified. Values can be derived + * from data using {@link pv.min} and {@link pv.max}. For example: + * + * <pre>.domain(1, pv.max(array))</pre> + * + * An alternative method for deriving minimum and maximum values from data + * follows. + * + * <p>2. <tt>domain(array, minf, maxf)</tt> + * + * <p>When both the minimum and maximum value are derived from data, the + * arguments to the <tt>domain</tt> method can be specified as the array of + * data, followed by zero, one or two accessor functions. For example, if the + * array of data is just an array of numbers: + * + * <pre>.domain(array)</pre> + * + * On the other hand, if the array elements are objects representing stock + * values per day, and the domain should consider the stock's daily low and + * daily high: + * + * <pre>.domain(array, function(d) d.low, function(d) d.high)</pre> + * + * The first method of setting the domain is preferred because it is more + * explicit; setting the domain using this second method should be used only + * if brevity is required. + * + * <p>3. <tt>domain()</tt> + * + * <p>Invoking the <tt>domain</tt> method with no arguments returns the + * current domain as an array of numbers. + * + * @function + * @name pv.Scale.log.prototype.domain + * @param {number...} domain... domain values. + * @returns {pv.Scale.log} <tt>this</tt>, or the current domain. + */ + scale.domain = function(array, min, max) { + if (arguments.length) { + if (array instanceof Array) { + if (arguments.length < 2) min = pv.identity; + if (arguments.length < 3) max = min; + d = [pv.min(array, min), pv.max(array, max)]; + } else { + d = Array.prototype.slice.call(arguments); + } + l = d.map(log); + return this; + } + return d; + }; + + /** + * @function + * @name pv.Scale.log.prototype.range + * @param {...} range... range values. + * @returns {pv.Scale.log} <tt>this</tt>. + */ + scale.range = function() { + if (arguments.length) { + r = Array.prototype.slice.call(arguments); + i = []; + for (var j = 0; j < r.length - 1; j++) { + i.push(pv.Scale.interpolator(r[j], r[j + 1])); + } + return this; + } + return r; + }; + + /** + * Sets or gets the output range. This method can be invoked several ways: + * + * <p>1. <tt>range(min, ..., max)</tt> + * + * <p>The range may be specified as a series of numbers or colors. Most + * commonly, two numbers are specified: the minimum and maximum pixel values. + * For a color scale, values may be specified as {@link pv.Color}s or + * equivalent strings. For a diverging scale, or other subdivided poly-log + * scales, multiple values can be specified. For example: + * + * <pre>.range("red", "white", "green")</pre> + * + * <p>Currently, only numbers and colors are supported as range values. The + * number of range values must exactly match the number of domain values, or + * the behavior of the scale is undefined. + * + * <p>2. <tt>range()</tt> + * + * <p>Invoking the <tt>range</tt> method with no arguments returns the current + * range as an array of numbers or colors. + * + * @function + * @name pv.Scale.log.prototype.invert + * @param {...} range... range values. + * @returns {pv.Scale.log} <tt>this</tt>, or the current range. + */ + scale.invert = function(y) { + var j = pv.search(r, y); + if (j < 0) j = -j - 2; + j = Math.max(0, Math.min(i.length - 1, j)); + var t = l[j] + (y - r[j]) / (r[j + 1] - r[j]) * (l[j + 1] - l[j]); + return (d[j] < 0) ? -Math.pow(b, -t) : Math.pow(b, t); + }; + + /** + * Returns an array of evenly-spaced, suitably-rounded values in the input + * domain. These values are frequently used in conjunction with {@link + * pv.Rule} to display tick marks or grid lines. + * + * @function + * @name pv.Scale.log.prototype.ticks + * @returns {number[]} an array input domain values to use as ticks. + */ + scale.ticks = function() { + // TODO: support multiple domains + var start = Math.floor(l[0]), + end = Math.ceil(l[1]), + ticks = []; + for (var i = start; i < end; i++) { + var x = Math.pow(b, i); + if (d[0] < 0) x = -x; + for (var j = 1; j < b; j++) { + ticks.push(x * j); + } + } + ticks.push(Math.pow(b, end)); + if (ticks[0] < d[0]) ticks.shift(); + if (ticks[ticks.length - 1] > d[1]) ticks.pop(); + return ticks; + }; + + /** + * Formats the specified tick value using the appropriate precision, assuming + * base 10. + * + * @function + * @name pv.Scale.log.prototype.tickFormat + * @param {number} t a tick value. + * @return {string} a formatted tick value. + */ + scale.tickFormat = function(t) { + return t.toPrecision(1); + }; + + /** + * "Nices" this scale, extending the bounds of the input domain to + * evenly-rounded values. This method uses {@link pv.logFloor} and {@link + * pv.logCeil}. Nicing is useful if the domain is computed dynamically from + * data, and may be irregular. For example, given a domain of + * [0.20147987687960267, 0.996679553296417], a call to <tt>nice()</tt> might + * extend the domain to [0.1, 1]. + * + * <p>This method must be invoked each time after setting the domain (and + * base). + * + * @function + * @name pv.Scale.log.prototype.nice + * @returns {pv.Scale.log} <tt>this</tt>. + */ + scale.nice = function() { + // TODO: support multiple domains + d = [pv.logFloor(d[0], b), pv.logCeil(d[1], b)]; + l = d.map(log); + return this; + }; + + /** + * Sets or gets the logarithm base. Defaults to 10. + * + * @function + * @name pv.Scale.log.prototype.base + * @param {number} [v] the new base. + * @returns {pv.Scale.log} <tt>this</tt>, or the current base. + */ + scale.base = function(v) { + if (arguments.length) { + b = v; + l = d.map(log); + return this; + } + return b; + }; + + /** + * Returns a view of this scale by the specified accessor function <tt>f</tt>. + * Given a scale <tt>y</tt>, <tt>y.by(function(d) d.foo)</tt> is equivalent to + * <tt>function(d) y(d.foo)</tt>. + * + * <p>This method is provided for convenience, such that scales can be + * succinctly defined inline. For example, given an array of data elements + * that have a <tt>score</tt> attribute with the domain [0, 1], the height + * property could be specified as: + * + * <pre>.height(pv.Scale.log().range(0, 480).by(function(d) d.score))</pre> + * + * This is equivalent to: + * + * <pre>.height(function(d) d.score * 480)</pre> + * + * This method should be used judiciously; it is typically more clear to + * invoke the scale directly, passing in the value to be scaled. + * + * @function + * @name pv.Scale.log.prototype.by + * @param {function} f an accessor function. + * @returns {pv.Scale.log} a view of this scale by the specified accessor + * function. + */ + scale.by = function(f) { + function by() { return scale(f.apply(this, arguments)); } + for (var method in scale) by[method] = scale[method]; + return by; + }; + + scale.domain.apply(scale, arguments); + return scale; +}; +/** + * Returns an ordinal scale for the specified domain. The arguments to this + * constructor are optional, and equivalent to calling {@link #domain}. + * + * @class Represents an ordinal scale. <style + * type="text/css">sub{line-height:0}</style> An ordinal scale represents a + * pairwise mapping from <i>n</i> discrete values in the input domain to + * <i>n</i> discrete values in the output range. For example, an ordinal scale + * might map a domain of species ["setosa", "versicolor", "virginica"] to colors + * ["red", "green", "blue"]. Thus, saying + * + * <pre>.fillStyle(function(d) { + * switch (d.species) { + * case "setosa": return "red"; + * case "versicolor": return "green"; + * case "virginica": return "blue"; + * } + * })</pre> + * + * is equivalent to + * + * <pre>.fillStyle(pv.Scale.ordinal("setosa", "versicolor", "virginica") + * .range("red", "green", "blue") + * .by(function(d) d.species))</pre> + * + * If the mapping from species to color does not need to be specified + * explicitly, the domain can be omitted. In this case it will be inferred + * lazily from the data: + * + * <pre>.fillStyle(pv.colors("red", "green", "blue") + * .by(function(d) d.species))</pre> + * + * When the domain is inferred, the first time the scale is invoked, the first + * element from the range will be returned. Subsequent calls with unique values + * will return subsequent elements from the range. If the inferred domain grows + * larger than the range, range values will be reused. However, it is strongly + * recommended that the domain and the range contain the same number of + * elements. + * + * <p>A range can be discretized from a continuous interval (e.g., for pixel + * positioning) by using {@link #split}, {@link #splitFlush} or + * {@link #splitBanded} after the domain has been set. For example, if + * <tt>states</tt> is an array of the fifty U.S. state names, the state name can + * be encoded in the left position: + * + * <pre>.left(pv.Scale.ordinal(states) + * .split(0, 640) + * .by(function(d) d.state))</pre> + * + * <p>N.B.: ordinal scales are not invertible (at least not yet), since the + * domain and range and discontinuous. A workaround is to use a linear scale. + * + * @param {...} domain... domain values. + * @returns {pv.Scale.ordinal} an ordinal scale. + * @see pv.colors + */ +pv.Scale.ordinal = function() { + var d = [], i = {}, r = [], band = 0; + + /** @private */ + function scale(x) { + if (!(x in i)) i[x] = d.push(x) - 1; + return r[i[x] % r.length]; + } + + /** + * Sets or gets the input domain. This method can be invoked several ways: + * + * <p>1. <tt>domain(values...)</tt> + * + * <p>Specifying the domain as a series of values is the most explicit and + * recommended approach. However, if the domain values are derived from data, + * you may find the second method more appropriate. + * + * <p>2. <tt>domain(array, f)</tt> + * + * <p>Rather than enumerating the domain values as explicit arguments to this + * method, you can specify a single argument of an array. In addition, you can + * specify an optional accessor function to extract the domain values from the + * array. + * + * <p>3. <tt>domain()</tt> + * + * <p>Invoking the <tt>domain</tt> method with no arguments returns the + * current domain as an array. + * + * @function + * @name pv.Scale.ordinal.prototype.domain + * @param {...} domain... domain values. + * @returns {pv.Scale.ordinal} <tt>this</tt>, or the current domain. + */ + scale.domain = function(array, f) { + if (arguments.length) { + array = (array instanceof Array) + ? ((arguments.length > 1) ? map(array, f) : array) + : Array.prototype.slice.call(arguments); + + /* Filter the specified ordinals to their unique values. */ + d = []; + var seen = {}; + for (var j = 0; j < array.length; j++) { + var o = array[j]; + if (!(o in seen)) { + seen[o] = true; + d.push(o); + } + } + + i = pv.numerate(d); + return this; + } + return d; + }; + + /** + * Sets or gets the output range. This method can be invoked several ways: + * + * <p>1. <tt>range(values...)</tt> + * + * <p>Specifying the range as a series of values is the most explicit and + * recommended approach. However, if the range values are derived from data, + * you may find the second method more appropriate. + * + * <p>2. <tt>range(array, f)</tt> + * + * <p>Rather than enumerating the range values as explicit arguments to this + * method, you can specify a single argument of an array. In addition, you can + * specify an optional accessor function to extract the range values from the + * array. + * + * <p>3. <tt>range()</tt> + * + * <p>Invoking the <tt>range</tt> method with no arguments returns the + * current range as an array. + * + * @function + * @name pv.Scale.ordinal.prototype.range + * @param {...} range... range values. + * @returns {pv.Scale.ordinal} <tt>this</tt>, or the current range. + */ + scale.range = function(array, f) { + if (arguments.length) { + r = (array instanceof Array) + ? ((arguments.length > 1) ? map(array, f) : array) + : Array.prototype.slice.call(arguments); + if (typeof r[0] == "string") r = r.map(pv.color); + return this; + } + return r; + }; + + /** + * Sets the range from the given continuous interval. The interval + * [<i>min</i>, <i>max</i>] is subdivided into <i>n</i> equispaced points, + * where <i>n</i> is the number of (unique) values in the domain. The first + * and last point are offset from the edge of the range by half the distance + * between points. + * + * <p>This method must be called <i>after</i> the domain is set. + * + * @function + * @name pv.Scale.ordinal.prototype.split + * @param {number} min minimum value of the output range. + * @param {number} max maximum value of the output range. + * @returns {pv.Scale.ordinal} <tt>this</tt>. + * @see #splitFlush + * @see #splitBanded + */ + scale.split = function(min, max) { + var step = (max - min) / this.domain().length; + r = pv.range(min + step / 2, max, step); + return this; + }; + + /** + * Sets the range from the given continuous interval. The interval + * [<i>min</i>, <i>max</i>] is subdivided into <i>n</i> equispaced points, + * where <i>n</i> is the number of (unique) values in the domain. The first + * and last point are exactly on the edge of the range. + * + * <p>This method must be called <i>after</i> the domain is set. + * + * @function + * @name pv.Scale.ordinal.prototype.splitFlush + * @param {number} min minimum value of the output range. + * @param {number} max maximum value of the output range. + * @returns {pv.Scale.ordinal} <tt>this</tt>. + * @see #split + */ + scale.splitFlush = function(min, max) { + var n = this.domain().length, step = (max - min) / (n - 1); + r = (n == 1) ? [(min + max) / 2] + : pv.range(min, max + step / 2, step); + return this; + }; + + /** + * Sets the range from the given continuous interval. The interval + * [<i>min</i>, <i>max</i>] is subdivided into <i>n</i> equispaced bands, + * where <i>n</i> is the number of (unique) values in the domain. The first + * and last band are offset from the edge of the range by the distance between + * bands. + * + * <p>The band width argument, <tt>band</tt>, is typically in the range [0, 1] + * and defaults to 1. This fraction corresponds to the amount of space in the + * range to allocate to the bands, as opposed to padding. A value of 0.5 means + * that the band width will be equal to the padding width. The computed + * absolute band width can be retrieved from the range as + * <tt>scale.range().band</tt>. + * + * <p>If the band width argument is negative, this method will allocate bands + * of a <i>fixed</i> width <tt>-band</tt>, rather than a relative fraction of + * the available space. + * + * <p>Tip: to inset the bands by a fixed amount <tt>p</tt>, specify a minimum + * value of <tt>min + p</tt> (or simply <tt>p</tt>, if <tt>min</tt> is + * 0). Then set the mark width to <tt>scale.range().band - p</tt>. + * + * <p>This method must be called <i>after</i> the domain is set. + * + * @function + * @name pv.Scale.ordinal.prototype.splitBanded + * @param {number} min minimum value of the output range. + * @param {number} max maximum value of the output range. + * @param {number} [band] the fractional band width in [0, 1]; defaults to 1. + * @returns {pv.Scale.ordinal} <tt>this</tt>. + * @see #split + */ + scale.splitBanded = function(min, max, band) { + if (arguments.length < 3) band = 1; + if (band < 0) { + var n = this.domain().length, + total = -band * n, + remaining = max - min - total, + padding = remaining / (n + 1); + r = pv.range(min + padding, max, padding - band); + r.band = -band; + } else { + var step = (max - min) / (this.domain().length + (1 - band)); + r = pv.range(min + step * (1 - band), max, step); + r.band = step * band; + } + return this; + }; + + /** + * Returns a view of this scale by the specified accessor function <tt>f</tt>. + * Given a scale <tt>y</tt>, <tt>y.by(function(d) d.foo)</tt> is equivalent to + * <tt>function(d) y(d.foo)</tt>. This method should be used judiciously; it + * is typically more clear to invoke the scale directly, passing in the value + * to be scaled. + * + * @function + * @name pv.Scale.ordinal.prototype.by + * @param {function} f an accessor function. + * @returns {pv.Scale.ordinal} a view of this scale by the specified accessor + * function. + */ + scale.by = function(f) { + function by() { return scale(f.apply(this, arguments)); } + for (var method in scale) by[method] = scale[method]; + return by; + }; + + scale.domain.apply(scale, arguments); + return scale; +}; +/** + * Returns the {@link pv.Color} for the specified color format string. Colors + * may have an associated opacity, or alpha channel. Color formats are specified + * by CSS Color Modular Level 3, using either in RGB or HSL color space. For + * example:<ul> + * + * <li>#f00 // #rgb + * <li>#ff0000 // #rrggbb + * <li>rgb(255, 0, 0) + * <li>rgb(100%, 0%, 0%) + * <li>hsl(0, 100%, 50%) + * <li>rgba(0, 0, 255, 0.5) + * <li>hsla(120, 100%, 50%, 1) + * + * </ul>The SVG 1.0 color keywords names are also supported, such as "aliceblue" + * and "yellowgreen". The "transparent" keyword is supported for a + * fully-transparent color. + * + * <p>If the <tt>format</tt> argument is already an instance of <tt>Color</tt>, + * the argument is returned with no further processing. + * + * @param {string} format the color specification string, such as "#f00". + * @returns {pv.Color} the corresponding <tt>Color</tt>. + * @see <a href="http://www.w3.org/TR/SVG/types.html#ColorKeywords">SVG color + * keywords</a> + * @see <a href="http://www.w3.org/TR/css3-color/">CSS3 color module</a> + */ +pv.color = function(format) { + if (!format || (format == "transparent")) { + return pv.rgb(0, 0, 0, 0); + } + if (format instanceof pv.Color) { + return format; + } + + /* Handle hsl, rgb. */ + var m1 = /([a-z]+)\((.*)\)/i.exec(format); + if (m1) { + var m2 = m1[2].split(","), a = 1; + switch (m1[1]) { + case "hsla": + case "rgba": { + a = parseFloat(m2[3]); + break; + } + } + switch (m1[1]) { + case "hsla": + case "hsl": { + var h = parseFloat(m2[0]), // degrees + s = parseFloat(m2[1]) / 100, // percentage + l = parseFloat(m2[2]) / 100; // percentage + return (new pv.Color.Hsl(h, s, l, a)).rgb(); + } + case "rgba": + case "rgb": { + function parse(c) { // either integer or percentage + var f = parseFloat(c); + return (c[c.length - 1] == '%') ? Math.round(f * 2.55) : f; + } + var r = parse(m2[0]), g = parse(m2[1]), b = parse(m2[2]); + return pv.rgb(r, g, b, a); + } + } + } + + /* Named colors. */ + format = pv.Color.names[format] || format; + + /* Hexadecimal colors: #rgb and #rrggbb. */ + if (format.charAt(0) == "#") { + var r, g, b; + if (format.length == 4) { + r = format.charAt(1); r += r; + g = format.charAt(2); g += g; + b = format.charAt(3); b += b; + } else if (format.length == 7) { + r = format.substring(1, 3); + g = format.substring(3, 5); + b = format.substring(5, 7); + } + return pv.rgb(parseInt(r, 16), parseInt(g, 16), parseInt(b, 16), 1); + } + + /* Otherwise, assume named colors. TODO allow lazy conversion to RGB. */ + return new pv.Color(format, 1); +}; + +/** + * Constructs a color with the specified color format string and opacity. This + * constructor should not be invoked directly; use {@link pv.color} instead. + * + * @class Represents an abstract (possibly translucent) color. The color is + * divided into two parts: the <tt>color</tt> attribute, an opaque color format + * string, and the <tt>opacity</tt> attribute, a float in [0, 1]. The color + * space is dependent on the implementing class; all colors support the + * {@link #rgb} method to convert to RGB color space for interpolation. + * + * <p>See also the <a href="../../api/Color.html">Color guide</a>. + * + * @param {string} color an opaque color format string, such as "#f00". + * @param {number} opacity the opacity, in [0,1]. + * @see pv.color + */ +pv.Color = function(color, opacity) { + /** + * An opaque color format string, such as "#f00". + * + * @type string + * @see <a href="http://www.w3.org/TR/SVG/types.html#ColorKeywords">SVG color + * keywords</a> + * @see <a href="http://www.w3.org/TR/css3-color/">CSS3 color module</a> + */ + this.color = color; + + /** + * The opacity, a float in [0, 1]. + * + * @type number + */ + this.opacity = opacity; +}; + +/** + * Returns a new color that is a brighter version of this color. The behavior of + * this method may vary slightly depending on the underlying color space. + * Although brighter and darker are inverse operations, the results of a series + * of invocations of these two methods might be inconsistent because of rounding + * errors. + * + * @param [k] {number} an optional scale factor; defaults to 1. + * @see #darker + * @returns {pv.Color} a brighter color. + */ +pv.Color.prototype.brighter = function(k) { + return this.rgb().brighter(k); +}; + +/** + * Returns a new color that is a brighter version of this color. The behavior of + * this method may vary slightly depending on the underlying color space. + * Although brighter and darker are inverse operations, the results of a series + * of invocations of these two methods might be inconsistent because of rounding + * errors. + * + * @param [k] {number} an optional scale factor; defaults to 1. + * @see #brighter + * @returns {pv.Color} a darker color. + */ +pv.Color.prototype.darker = function(k) { + return this.rgb().darker(k); +}; + +/** + * Constructs a new RGB color with the specified channel values. + * + * @param {number} r the red channel, an integer in [0,255]. + * @param {number} g the green channel, an integer in [0,255]. + * @param {number} b the blue channel, an integer in [0,255]. + * @param {number} [a] the alpha channel, a float in [0,1]. + * @returns pv.Color.Rgb + */ +pv.rgb = function(r, g, b, a) { + return new pv.Color.Rgb(r, g, b, (arguments.length == 4) ? a : 1); +}; + +/** + * Constructs a new RGB color with the specified channel values. + * + * @class Represents a color in RGB space. + * + * @param {number} r the red channel, an integer in [0,255]. + * @param {number} g the green channel, an integer in [0,255]. + * @param {number} b the blue channel, an integer in [0,255]. + * @param {number} a the alpha channel, a float in [0,1]. + * @extends pv.Color + */ +pv.Color.Rgb = function(r, g, b, a) { + pv.Color.call(this, a ? ("rgb(" + r + "," + g + "," + b + ")") : "none", a); + + /** + * The red channel, an integer in [0, 255]. + * + * @type number + */ + this.r = r; + + /** + * The green channel, an integer in [0, 255]. + * + * @type number + */ + this.g = g; + + /** + * The blue channel, an integer in [0, 255]. + * + * @type number + */ + this.b = b; + + /** + * The alpha channel, a float in [0, 1]. + * + * @type number + */ + this.a = a; +}; +pv.Color.Rgb.prototype = pv.extend(pv.Color); + +/** + * Constructs a new RGB color with the same green, blue and alpha channels as + * this color, with the specified red channel. + * + * @param {number} r the red channel, an integer in [0,255]. + */ +pv.Color.Rgb.prototype.red = function(r) { + return pv.rgb(r, this.g, this.b, this.a); +}; + +/** + * Constructs a new RGB color with the same red, blue and alpha channels as this + * color, with the specified green channel. + * + * @param {number} g the green channel, an integer in [0,255]. + */ +pv.Color.Rgb.prototype.green = function(g) { + return pv.rgb(this.r, g, this.b, this.a); +}; + +/** + * Constructs a new RGB color with the same red, green and alpha channels as + * this color, with the specified blue channel. + * + * @param {number} b the blue channel, an integer in [0,255]. + */ +pv.Color.Rgb.prototype.blue = function(b) { + return pv.rgb(this.r, this.g, b, this.a); +}; + +/** + * Constructs a new RGB color with the same red, green and blue channels as this + * color, with the specified alpha channel. + * + * @param {number} a the alpha channel, a float in [0,1]. + */ +pv.Color.Rgb.prototype.alpha = function(a) { + return pv.rgb(this.r, this.g, this.b, a); +}; + +/** + * Returns the RGB color equivalent to this color. This method is abstract and + * must be implemented by subclasses. + * + * @returns {pv.Color.Rgb} an RGB color. + * @function + * @name pv.Color.prototype.rgb + */ + +/** + * Returns this. + * + * @returns {pv.Color.Rgb} this. + */ +pv.Color.Rgb.prototype.rgb = function() { return this; }; + +/** + * Returns a new color that is a brighter version of this color. This method + * applies an arbitrary scale factor to each of the three RGB components of this + * color to create a brighter version of this color. Although brighter and + * darker are inverse operations, the results of a series of invocations of + * these two methods might be inconsistent because of rounding errors. + * + * @param [k] {number} an optional scale factor; defaults to 1. + * @see #darker + * @returns {pv.Color.Rgb} a brighter color. + */ +pv.Color.Rgb.prototype.brighter = function(k) { + k = Math.pow(0.7, arguments.length ? k : 1); + var r = this.r, g = this.g, b = this.b, i = 30; + if (!r && !g && !b) return pv.rgb(i, i, i, this.a); + if (r && (r < i)) r = i; + if (g && (g < i)) g = i; + if (b && (b < i)) b = i; + return pv.rgb( + Math.min(255, Math.floor(r / k)), + Math.min(255, Math.floor(g / k)), + Math.min(255, Math.floor(b / k)), + this.a); +}; + +/** + * Returns a new color that is a darker version of this color. This method + * applies an arbitrary scale factor to each of the three RGB components of this + * color to create a darker version of this color. Although brighter and darker + * are inverse operations, the results of a series of invocations of these two + * methods might be inconsistent because of rounding errors. + * + * @param [k] {number} an optional scale factor; defaults to 1. + * @see #brighter + * @returns {pv.Color.Rgb} a darker color. + */ +pv.Color.Rgb.prototype.darker = function(k) { + k = Math.pow(0.7, arguments.length ? k : 1); + return pv.rgb( + Math.max(0, Math.floor(k * this.r)), + Math.max(0, Math.floor(k * this.g)), + Math.max(0, Math.floor(k * this.b)), + this.a); +}; + +/** + * Constructs a new HSL color with the specified values. + * + * @param {number} h the hue, an integer in [0, 360]. + * @param {number} s the saturation, a float in [0, 1]. + * @param {number} l the lightness, a float in [0, 1]. + * @param {number} [a] the opacity, a float in [0, 1]. + * @returns pv.Color.Hsl + */ +pv.hsl = function(h, s, l, a) { + return new pv.Color.Hsl(h, s, l, (arguments.length == 4) ? a : 1); +}; + +/** + * Constructs a new HSL color with the specified values. + * + * @class Represents a color in HSL space. + * + * @param {number} h the hue, an integer in [0, 360]. + * @param {number} s the saturation, a float in [0, 1]. + * @param {number} l the lightness, a float in [0, 1]. + * @param {number} a the opacity, a float in [0, 1]. + * @extends pv.Color + */ +pv.Color.Hsl = function(h, s, l, a) { + pv.Color.call(this, "hsl(" + h + "," + (s * 100) + "%," + (l * 100) + "%)", a); + + /** + * The hue, an integer in [0, 360]. + * + * @type number + */ + this.h = h; + + /** + * The saturation, a float in [0, 1]. + * + * @type number + */ + this.s = s; + + /** + * The lightness, a float in [0, 1]. + * + * @type number + */ + this.l = l; + + /** + * The opacity, a float in [0, 1]. + * + * @type number + */ + this.a = a; +}; +pv.Color.Hsl.prototype = pv.extend(pv.Color); + +/** + * Constructs a new HSL color with the same saturation, lightness and alpha as + * this color, and the specified hue. + * + * @param {number} h the hue, an integer in [0, 360]. + */ +pv.Color.Hsl.prototype.hue = function(h) { + return pv.hsl(h, this.s, this.l, this.a); +}; + +/** + * Constructs a new HSL color with the same hue, lightness and alpha as this + * color, and the specified saturation. + * + * @param {number} s the saturation, a float in [0, 1]. + */ +pv.Color.Hsl.prototype.saturation = function(s) { + return pv.hsl(this.h, s, this.l, this.a); +}; + +/** + * Constructs a new HSL color with the same hue, saturation and alpha as this + * color, and the specified lightness. + * + * @param {number} l the lightness, a float in [0, 1]. + */ +pv.Color.Hsl.prototype.lightness = function(l) { + return pv.hsl(this.h, this.s, l, this.a); +}; + +/** + * Constructs a new HSL color with the same hue, saturation and lightness as + * this color, and the specified alpha. + * + * @param {number} a the opacity, a float in [0, 1]. + */ +pv.Color.Hsl.prototype.alpha = function(a) { + return pv.hsl(this.h, this.s, this.l, a); +}; + +/** + * Returns the RGB color equivalent to this HSL color. + * + * @returns {pv.Color.Rgb} an RGB color. + */ +pv.Color.Hsl.prototype.rgb = function() { + var h = this.h, s = this.s, l = this.l; + + /* Some simple corrections for h, s and l. */ + h = h % 360; if (h < 0) h += 360; + s = Math.max(0, Math.min(s, 1)); + l = Math.max(0, Math.min(l, 1)); + + /* From FvD 13.37, CSS Color Module Level 3 */ + var m2 = (l <= .5) ? (l * (1 + s)) : (l + s - l * s); + var m1 = 2 * l - m2; + function v(h) { + if (h > 360) h -= 360; + else if (h < 0) h += 360; + if (h < 60) return m1 + (m2 - m1) * h / 60; + if (h < 180) return m2; + if (h < 240) return m1 + (m2 - m1) * (240 - h) / 60; + return m1; + } + function vv(h) { + return Math.round(v(h) * 255); + } + + return pv.rgb(vv(h + 120), vv(h), vv(h - 120), this.a); +}; + +/** + * @private SVG color keywords, per CSS Color Module Level 3. + * + * @see <a href="http://www.w3.org/TR/SVG/types.html#ColorKeywords">SVG color + * keywords</a> + */ +pv.Color.names = { + aliceblue: "#f0f8ff", + antiquewhite: "#faebd7", + aqua: "#00ffff", + aquamarine: "#7fffd4", + azure: "#f0ffff", + beige: "#f5f5dc", + bisque: "#ffe4c4", + black: "#000000", + blanchedalmond: "#ffebcd", + blue: "#0000ff", + blueviolet: "#8a2be2", + brown: "#a52a2a", + burlywood: "#deb887", + cadetblue: "#5f9ea0", + chartreuse: "#7fff00", + chocolate: "#d2691e", + coral: "#ff7f50", + cornflowerblue: "#6495ed", + cornsilk: "#fff8dc", + crimson: "#dc143c", + cyan: "#00ffff", + darkblue: "#00008b", + darkcyan: "#008b8b", + darkgoldenrod: "#b8860b", + darkgray: "#a9a9a9", + darkgreen: "#006400", + darkgrey: "#a9a9a9", + darkkhaki: "#bdb76b", + darkmagenta: "#8b008b", + darkolivegreen: "#556b2f", + darkorange: "#ff8c00", + darkorchid: "#9932cc", + darkred: "#8b0000", + darksalmon: "#e9967a", + darkseagreen: "#8fbc8f", + darkslateblue: "#483d8b", + darkslategray: "#2f4f4f", + darkslategrey: "#2f4f4f", + darkturquoise: "#00ced1", + darkviolet: "#9400d3", + deeppink: "#ff1493", + deepskyblue: "#00bfff", + dimgray: "#696969", + dimgrey: "#696969", + dodgerblue: "#1e90ff", + firebrick: "#b22222", + floralwhite: "#fffaf0", + forestgreen: "#228b22", + fuchsia: "#ff00ff", + gainsboro: "#dcdcdc", + ghostwhite: "#f8f8ff", + gold: "#ffd700", + goldenrod: "#daa520", + gray: "#808080", + green: "#008000", + greenyellow: "#adff2f", + grey: "#808080", + honeydew: "#f0fff0", + hotpink: "#ff69b4", + indianred: "#cd5c5c", + indigo: "#4b0082", + ivory: "#fffff0", + khaki: "#f0e68c", + lavender: "#e6e6fa", + lavenderblush: "#fff0f5", + lawngreen: "#7cfc00", + lemonchiffon: "#fffacd", + lightblue: "#add8e6", + lightcoral: "#f08080", + lightcyan: "#e0ffff", + lightgoldenrodyellow: "#fafad2", + lightgray: "#d3d3d3", + lightgreen: "#90ee90", + lightgrey: "#d3d3d3", + lightpink: "#ffb6c1", + lightsalmon: "#ffa07a", + lightseagreen: "#20b2aa", + lightskyblue: "#87cefa", + lightslategray: "#778899", + lightslategrey: "#778899", + lightsteelblue: "#b0c4de", + lightyellow: "#ffffe0", + lime: "#00ff00", + limegreen: "#32cd32", + linen: "#faf0e6", + magenta: "#ff00ff", + maroon: "#800000", + mediumaquamarine: "#66cdaa", + mediumblue: "#0000cd", + mediumorchid: "#ba55d3", + mediumpurple: "#9370db", + mediumseagreen: "#3cb371", + mediumslateblue: "#7b68ee", + mediumspringgreen: "#00fa9a", + mediumturquoise: "#48d1cc", + mediumvioletred: "#c71585", + midnightblue: "#191970", + mintcream: "#f5fffa", + mistyrose: "#ffe4e1", + moccasin: "#ffe4b5", + navajowhite: "#ffdead", + navy: "#000080", + oldlace: "#fdf5e6", + olive: "#808000", + olivedrab: "#6b8e23", + orange: "#ffa500", + orangered: "#ff4500", + orchid: "#da70d6", + palegoldenrod: "#eee8aa", + palegreen: "#98fb98", + paleturquoise: "#afeeee", + palevioletred: "#db7093", + papayawhip: "#ffefd5", + peachpuff: "#ffdab9", + peru: "#cd853f", + pink: "#ffc0cb", + plum: "#dda0dd", + powderblue: "#b0e0e6", + purple: "#800080", + red: "#ff0000", + rosybrown: "#bc8f8f", + royalblue: "#4169e1", + saddlebrown: "#8b4513", + salmon: "#fa8072", + sandybrown: "#f4a460", + seagreen: "#2e8b57", + seashell: "#fff5ee", + sienna: "#a0522d", + silver: "#c0c0c0", + skyblue: "#87ceeb", + slateblue: "#6a5acd", + slategray: "#708090", + slategrey: "#708090", + snow: "#fffafa", + springgreen: "#00ff7f", + steelblue: "#4682b4", + tan: "#d2b48c", + teal: "#008080", + thistle: "#d8bfd8", + tomato: "#ff6347", + turquoise: "#40e0d0", + violet: "#ee82ee", + wheat: "#f5deb3", + white: "#ffffff", + whitesmoke: "#f5f5f5", + yellow: "#ffff00", + yellowgreen: "#9acd32" +}; +/** + * Returns a new categorical color encoding using the specified colors. The + * arguments to this method are an array of colors; see {@link pv.color}. For + * example, to create a categorical color encoding using the <tt>species</tt> + * attribute: + * + * <pre>pv.colors("red", "green", "blue").by(function(d) d.species)</pre> + * + * The result of this expression can be used as a fill- or stroke-style + * property. This assumes that the data's <tt>species</tt> attribute is a + * string. + * + * @param {string} colors... categorical colors. + * @see pv.Scale.ordinal + * @returns {pv.Scale.ordinal} an ordinal color scale. + */ +pv.colors = function() { + var scale = pv.Scale.ordinal(); + scale.range.apply(scale, arguments); + return scale; +}; + +/** + * A collection of standard color palettes for categorical encoding. + * + * @namespace A collection of standard color palettes for categorical encoding. + */ +pv.Colors = {}; + +/** + * Returns a new 10-color scheme. The arguments to this constructor are + * optional, and equivalent to calling {@link pv.Scale.OrdinalScale#domain}. The + * following colors are used: + * + * <div style="background:#1f77b4;">#1f77b4</div> + * <div style="background:#ff7f0e;">#ff7f0e</div> + * <div style="background:#2ca02c;">#2ca02c</div> + * <div style="background:#d62728;">#d62728</div> + * <div style="background:#9467bd;">#9467bd</div> + * <div style="background:#8c564b;">#8c564b</div> + * <div style="background:#e377c2;">#e377c2</div> + * <div style="background:#7f7f7f;">#7f7f7f</div> + * <div style="background:#bcbd22;">#bcbd22</div> + * <div style="background:#17becf;">#17becf</div> + * + * @param {number...} domain... domain values. + * @returns {pv.Scale.ordinal} a new ordinal color scale. + * @see pv.color + */ +pv.Colors.category10 = function() { + var scale = pv.colors( + "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", + "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"); + scale.domain.apply(scale, arguments); + return scale; +}; + +/** + * Returns a new 20-color scheme. The arguments to this constructor are + * optional, and equivalent to calling {@link pv.Scale.OrdinalScale#domain}. The + * following colors are used: + * + * <div style="background:#1f77b4;">#1f77b4</div> + * <div style="background:#aec7e8;">#aec7e8</div> + * <div style="background:#ff7f0e;">#ff7f0e</div> + * <div style="background:#ffbb78;">#ffbb78</div> + * <div style="background:#2ca02c;">#2ca02c</div> + * <div style="background:#98df8a;">#98df8a</div> + * <div style="background:#d62728;">#d62728</div> + * <div style="background:#ff9896;">#ff9896</div> + * <div style="background:#9467bd;">#9467bd</div> + * <div style="background:#c5b0d5;">#c5b0d5</div> + * <div style="background:#8c564b;">#8c564b</div> + * <div style="background:#c49c94;">#c49c94</div> + * <div style="background:#e377c2;">#e377c2</div> + * <div style="background:#f7b6d2;">#f7b6d2</div> + * <div style="background:#7f7f7f;">#7f7f7f</div> + * <div style="background:#c7c7c7;">#c7c7c7</div> + * <div style="background:#bcbd22;">#bcbd22</div> + * <div style="background:#dbdb8d;">#dbdb8d</div> + * <div style="background:#17becf;">#17becf</div> + * <div style="background:#9edae5;">#9edae5</div> + * + * @param {number...} domain... domain values. + * @returns {pv.Scale.ordinal} a new ordinal color scale. + * @see pv.color +*/ +pv.Colors.category20 = function() { + var scale = pv.colors( + "#1f77b4", "#aec7e8", "#ff7f0e", "#ffbb78", "#2ca02c", + "#98df8a", "#d62728", "#ff9896", "#9467bd", "#c5b0d5", + "#8c564b", "#c49c94", "#e377c2", "#f7b6d2", "#7f7f7f", + "#c7c7c7", "#bcbd22", "#dbdb8d", "#17becf", "#9edae5"); + scale.domain.apply(scale, arguments); + return scale; +}; + +/** + * Returns a new alternative 19-color scheme. The arguments to this constructor + * are optional, and equivalent to calling + * {@link pv.Scale.OrdinalScale#domain}. The following colors are used: + * + * <div style="background:#9c9ede;">#9c9ede</div> + * <div style="background:#7375b5;">#7375b5</div> + * <div style="background:#4a5584;">#4a5584</div> + * <div style="background:#cedb9c;">#cedb9c</div> + * <div style="background:#b5cf6b;">#b5cf6b</div> + * <div style="background:#8ca252;">#8ca252</div> + * <div style="background:#637939;">#637939</div> + * <div style="background:#e7cb94;">#e7cb94</div> + * <div style="background:#e7ba52;">#e7ba52</div> + * <div style="background:#bd9e39;">#bd9e39</div> + * <div style="background:#8c6d31;">#8c6d31</div> + * <div style="background:#e7969c;">#e7969c</div> + * <div style="background:#d6616b;">#d6616b</div> + * <div style="background:#ad494a;">#ad494a</div> + * <div style="background:#843c39;">#843c39</div> + * <div style="background:#de9ed6;">#de9ed6</div> + * <div style="background:#ce6dbd;">#ce6dbd</div> + * <div style="background:#a55194;">#a55194</div> + * <div style="background:#7b4173;">#7b4173</div> + * + * @param {number...} domain... domain values. + * @returns {pv.Scale.ordinal} a new ordinal color scale. + * @see pv.color + */ +pv.Colors.category19 = function() { + var scale = pv.colors( + "#9c9ede", "#7375b5", "#4a5584", "#cedb9c", "#b5cf6b", + "#8ca252", "#637939", "#e7cb94", "#e7ba52", "#bd9e39", + "#8c6d31", "#e7969c", "#d6616b", "#ad494a", "#843c39", + "#de9ed6", "#ce6dbd", "#a55194", "#7b4173"); + scale.domain.apply(scale, arguments); + return scale; +}; +/** + * Returns a linear color ramp from the specified <tt>start</tt> color to the + * specified <tt>end</tt> color. The color arguments may be specified either as + * <tt>string</tt>s or as {@link pv.Color}s. + * + * @param {string} start the start color; may be a <tt>pv.Color</tt>. + * @param {string} end the end color; may be a <tt>pv.Color</tt>. + * @returns {Function} a color ramp from <tt>start</tt> to <tt>end</tt>. + * @see pv.Scale.linear + */ +pv.ramp = function(start, end) { + var scale = pv.Scale.linear(); + scale.range.apply(scale, arguments); + return scale; +}; +// TODO don't populate default attributes? + +/** + * @private + * @namespace + */ +pv.Scene = pv.SvgScene = {}; + +/** + * Updates the display for the specified array of scene nodes. + * + * @param scenes {array} an array of scene nodes. + */ +pv.SvgScene.updateAll = function(scenes) { + if (!scenes.length) return; + if ((scenes[0].reverse) + && (scenes.type != "line") + && (scenes.type != "area")) { + var reversed = pv.extend(scenes); + for (var i = 0, j = scenes.length - 1; j >= 0; i++, j--) { + reversed[i] = scenes[j]; + } + scenes = reversed; + } + this.removeSiblings(this[scenes.type](scenes)); +}; + +/** + * Creates a new SVG element of the specified type. + * + * @param type {string} an SVG element type, such as "rect". + * @return a new SVG element. + */ +pv.SvgScene.create = function(type) { + return document.createElementNS(pv.ns.svg, type); +}; + +/** + * Expects the element <i>e</i> to be the specified type. If the element does + * not exist, a new one is created. If the element does exist but is the wrong + * type, it is replaced with the specified element. + * + * @param type {string} an SVG element type, such as "rect". + * @return a new SVG element. + */ +pv.SvgScene.expect = function(type, e) { + if (!e) return this.create(type); + if (e.tagName == "a") e = e.firstChild; + if (e.tagName == type) return e; + var n = this.create(type); + e.parentNode.replaceChild(n, e); + return n; +}; + +/** TODO */ +pv.SvgScene.append = function(e, scenes, index) { + e.$scene = {scenes:scenes, index:index}; + e = this.title(e, scenes[index]); + if (!e.parentNode) scenes.$g.appendChild(e); + return e.nextSibling; +}; + +/** + * Applies a title tooltip to the specified element <tt>e</tt>, using the + * <tt>title</tt> property of the specified scene node <tt>s</tt>. Note that + * this implementation does not create an SVG <tt>title</tt> element as a child + * of <tt>e</tt>; although this is the recommended standard, it is only + * supported in Opera. Instead, an anchor element is created around the element + * <tt>e</tt>, and the <tt>xlink:title</tt> attribute is set accordingly. + * + * @param e an SVG element. + * @param s a scene node. + */ +pv.SvgScene.title = function(e, s) { + var a = e.parentNode, t = String(s.title); + if (a && (a.tagName != "a")) a = null; + if (t) { + if (!a) { + a = this.create("a"); + if (e.parentNode) e.parentNode.replaceChild(a, e); + a.appendChild(e); + } + a.setAttributeNS(pv.ns.xlink, "title", t); + return a; + } + if (a) a.parentNode.replaceChild(e, a); + return e; +}; + +/** TODO */ +pv.SvgScene.dispatch = function(e) { + var t = e.target.$scene; + if (t) { + t.scenes.mark.dispatch(e.type, t.scenes, t.index); + e.preventDefault(); + } +}; + +/** TODO */ +pv.SvgScene.removeSiblings = function(e) { + while (e) { + var n = e.nextSibling; + e.parentNode.removeChild(e); + e = n; + } +}; +// TODO strokeStyle for areaSegment? + +pv.SvgScene.area = function(scenes) { + var e = scenes.$g.firstChild; + if (!scenes.length) return e; + var s = scenes[0]; + + /* segmented */ + if (s.segmented) return this.areaSegment(scenes); + + /* visible */ + if (!s.visible) return e; + var fill = pv.color(s.fillStyle), stroke = pv.color(s.strokeStyle); + if (!fill.opacity && !stroke.opacity) return e; + + /* points */ + var p1 = "", p2 = ""; + for (var i = 0, j = scenes.length - 1; j >= 0; i++, j--) { + var si = scenes[i], sj = scenes[j]; + p1 += si.left + "," + si.top + " "; + p2 += (sj.left + sj.width) + "," + (sj.top + sj.height) + " "; + + /* interpolate (assume linear by default) */ + if (i < scenes.length - 1) { + var sk = scenes[i + 1], sl = scenes[j - 1]; + switch (s.interpolate) { + case "step-before": { + p1 += si.left + "," + sk.top + " "; + p2 += (sl.left + sl.width) + "," + (sj.top + sj.height) + " "; + break; + } + case "step-after": { + p1 += sk.left + "," + si.top + " "; + p2 += (sj.left + sj.width) + "," + (sl.top + sl.height) + " "; + break; + } + } + } + } + + e = this.expect("polygon", e); + e.setAttribute("cursor", s.cursor); + e.setAttribute("points", p1 + p2); + var fill = pv.color(s.fillStyle); + e.setAttribute("fill", fill.color); + e.setAttribute("fill-opacity", fill.opacity); + var stroke = pv.color(s.strokeStyle); + e.setAttribute("stroke", stroke.color); + e.setAttribute("stroke-opacity", stroke.opacity); + e.setAttribute("stroke-width", s.lineWidth); + return this.append(e, scenes, 0); +}; + +pv.SvgScene.areaSegment = function(scenes) { + var e = scenes.$g.firstChild; + for (var i = 0, n = scenes.length - 1; i < n; i++) { + var s1 = scenes[i], s2 = scenes[i + 1]; + + /* visible */ + if (!s1.visible || !s2.visible) continue; + var fill = pv.color(s1.fillStyle), stroke = pv.color(s1.strokeStyle); + if (!fill.opacity && !stroke.opacity) continue; + + /* points */ + var p = s1.left + "," + s1.top + " " + + s2.left + "," + s2.top + " " + + (s2.left + s2.width) + "," + (s2.top + s2.height) + " " + + (s1.left + s1.width) + "," + (s1.top + s1.height); + + e = this.expect("polygon", e); + e.setAttribute("cursor", s1.cursor); + e.setAttribute("points", p); + e.setAttribute("fill", fill.color); + e.setAttribute("fill-opacity", fill.opacity); + e.setAttribute("stroke", stroke.color); + e.setAttribute("stroke-opacity", stroke.opacity); + e.setAttribute("stroke-width", s1.lineWidth); + e = this.append(e, scenes, i); + } + return e; +}; +pv.SvgScene.bar = function(scenes) { + var e = scenes.$g.firstChild; + for (var i = 0; i < scenes.length; i++) { + var s = scenes[i]; + + /* visible */ + if (!s.visible) continue; + var fill = pv.color(s.fillStyle), stroke = pv.color(s.strokeStyle); + if (!fill.opacity && !stroke.opacity) continue; + + e = this.expect("rect", e); + e.setAttribute("cursor", s.cursor); + e.setAttribute("x", s.left); + e.setAttribute("y", s.top); + e.setAttribute("width", Math.max(1E-10, s.width)); + e.setAttribute("height", Math.max(1E-10, s.height)); + e.setAttribute("fill", fill.color); + e.setAttribute("fill-opacity", fill.opacity); + e.setAttribute("stroke", stroke.color); + e.setAttribute("stroke-opacity", stroke.opacity); + e.setAttribute("stroke-width", s.lineWidth); + e = this.append(e, scenes, i); + } + return e; +}; +pv.SvgScene.dot = function(scenes) { + var e = scenes.$g.firstChild; + for (var i = 0; i < scenes.length; i++) { + var s = scenes[i]; + + /* visible */ + if (!s.visible) continue; + var fill = pv.color(s.fillStyle), stroke = pv.color(s.strokeStyle); + if (!fill.opacity && !stroke.opacity) continue; + + /* points */ + var radius = Math.sqrt(s.size), fillPath = "", strokePath = ""; + switch (s.shape) { + case "cross": { + fillPath = "M" + -radius + "," + -radius + + "L" + radius + "," + radius + + "M" + radius + "," + -radius + + "L" + -radius + "," + radius; + break; + } + case "triangle": { + var h = radius, w = radius * 2 / Math.sqrt(3); + fillPath = "M0," + h + + "L" + w +"," + -h + + " " + -w + "," + -h + + "Z"; + break; + } + case "diamond": { + radius *= Math.sqrt(2); + fillPath = "M0," + -radius + + "L" + radius + ",0" + + " 0," + radius + + " " + -radius + ",0" + + "Z"; + break; + } + case "square": { + fillPath = "M" + -radius + "," + -radius + + "L" + radius + "," + -radius + + " " + radius + "," + radius + + " " + -radius + "," + radius + + "Z"; + break; + } + case "tick": { + fillPath = "M0,0L0," + -s.size; + break; + } + default: { + function circle(r) { + return "M0," + r + + "A" + r + "," + r + " 0 1,1 0," + (-r) + + "A" + r + "," + r + " 0 1,1 0," + r + + "Z"; + } + if (s.lineWidth / 2 > radius) strokePath = circle(s.lineWidth); + fillPath = circle(radius); + break; + } + } + + /* transform */ + var transform = "translate(" + s.left + "," + s.top + ")" + + (s.angle ? " rotate(" + 180 * s.angle / Math.PI + ")" : ""); + + /* The normal fill path. */ + e = this.expect("path", e); + e.setAttribute("d", fillPath); + e.setAttribute("transform", transform); + e.setAttribute("fill", fill.color); + e.setAttribute("fill-opacity", fill.opacity); + e.setAttribute("cursor", s.cursor); + if (strokePath) { + e.setAttribute("stroke", "none"); + } else { + e.setAttribute("stroke", stroke.color); + e.setAttribute("stroke-opacity", stroke.opacity); + e.setAttribute("stroke-width", s.lineWidth); + } + e = this.append(e, scenes, i); + + /* The special-case stroke path. */ + if (strokePath) { + e = this.expect("path", e); + e.setAttribute("d", strokePath); + e.setAttribute("transform", transform); + e.setAttribute("fill", stroke.color); + e.setAttribute("fill-opacity", stroke.opacity); + e.setAttribute("cursor", s.cursor); + e = this.append(e, scenes, i); + } + } + return e; +}; +pv.SvgScene.image = function(scenes) { + var e = scenes.$g.firstChild; + for (var i = 0; i < scenes.length; i++) { + var s = scenes[i]; + + /* visible */ + if (!s.visible) continue; + + /* fill */ + e = this.fill(e, scenes, i); + + /* image */ + e = this.expect("image", e); + e.setAttribute("preserveAspectRatio", "none"); + e.setAttribute("x", s.left); + e.setAttribute("y", s.top); + e.setAttribute("width", s.width); + e.setAttribute("height", s.height); + e.setAttribute("cursor", s.cursor); + e.setAttributeNS(pv.ns.xlink, "href", s.url); + e = this.append(e, scenes, i); + + /* stroke */ + e = this.stroke(e, scenes, i); + } + return e; +}; +pv.SvgScene.label = function(scenes) { + var e = scenes.$g.firstChild; + for (var i = 0; i < scenes.length; i++) { + var s = scenes[i]; + + /* visible */ + if (!s.visible) continue; + var fill = pv.color(s.textStyle); + if (!fill.opacity) continue; + + /* text-baseline, text-align */ + var x = 0, y = 0, dy = 0, anchor = "start"; + switch (s.textBaseline) { + case "middle": dy = ".35em"; break; + case "top": dy = ".71em"; y = s.textMargin; break; + case "bottom": y = "-" + s.textMargin; break; + } + switch (s.textAlign) { + case "right": anchor = "end"; x = "-" + s.textMargin; break; + case "center": anchor = "middle"; break; + case "left": x = s.textMargin; break; + } + + e = this.expect("text", e); + e.setAttribute("pointer-events", "none"); + e.setAttribute("x", x); + e.setAttribute("y", y); + e.setAttribute("dy", dy); + e.setAttribute("text-anchor", anchor); + e.setAttribute("transform", + "translate(" + s.left + "," + s.top + ")" + + (s.textAngle ? " rotate(" + 180 * s.textAngle / Math.PI + ")" : "")); + e.setAttribute("fill", fill.color); + e.setAttribute("fill-opacity", fill.opacity); + e.style.font = s.font; + e.style.textShadow = s.textShadow; + if (e.firstChild) e.firstChild.nodeValue = s.text; + else e.appendChild(document.createTextNode(s.text)); + e = this.append(e, scenes, i); + } + return e; +}; +// TODO fillStyle for lineSegment? +// TODO lineOffset for flow maps? + +pv.SvgScene.line = function(scenes) { + var e = scenes.$g.firstChild; + if (scenes.length < 2) return e; + var s = scenes[0]; + + /* segmented */ + if (s.segmented) return this.lineSegment(scenes); + + /* visible */ + if (!s.visible) return e; + var fill = pv.color(s.fillStyle), stroke = pv.color(s.strokeStyle); + if (!fill.opacity && !stroke.opacity) return e; + + /* points */ + var p = ""; + for (var i = 0; i < scenes.length; i++) { + var si = scenes[i]; + p += si.left + "," + si.top + " "; + + /* interpolate (assume linear by default) */ + if (i < scenes.length - 1) { + var sj = scenes[i + 1]; + switch (s.interpolate) { + case "step-before": { + p += si.left + "," + sj.top + " "; + break; + } + case "step-after": { + p += sj.left + "," + si.top + " "; + break; + } + } + } + } + + + e = this.expect("polyline", e); + e.setAttribute("cursor", s.cursor); + e.setAttribute("points", p); + e.setAttribute("fill", fill.color); + e.setAttribute("fill-opacity", fill.opacity); + e.setAttribute("stroke", stroke.color); + e.setAttribute("stroke-opacity", stroke.opacity); + e.setAttribute("stroke-width", s.lineWidth); + return this.append(e, scenes, 0); +}; + +pv.SvgScene.lineSegment = function(scenes) { + var e = scenes.$g.firstChild; + for (var i = 0, n = scenes.length - 1; i < n; i++) { + var s1 = scenes[i], s2 = scenes[i + 1]; + + /* visible */ + if (!s1.visible || !s2.visible) continue; + var stroke = pv.color(s1.strokeStyle); + if (!stroke.opacity) continue; + + /* Line-line intersection, per Akenine-Moller 16.16.1. */ + function intersect(o1, d1, o2, d2) { + return o1.plus(d1.times(o2.minus(o1).dot(d2.perp()) / d1.dot(d2.perp()))); + } + + /* + * P1-P2 is the current line segment. V is a vector that is perpendicular to + * the line segment, and has length lineWidth / 2. ABCD forms the initial + * bounding box of the line segment (i.e., the line segment if we were to do + * no joins). + */ + var p1 = pv.vector(s1.left, s1.top), + p2 = pv.vector(s2.left, s2.top), + p = p2.minus(p1), + v = p.perp().norm(), + w = v.times(s1.lineWidth / 2), + a = p1.plus(w), + b = p2.plus(w), + c = p2.minus(w), + d = p1.minus(w); + + /* + * Start join. P0 is the previous line segment's start point. We define the + * cutting plane as the average of the vector perpendicular to P0-P1, and + * the vector perpendicular to P1-P2. This insures that the cross-section of + * the line on the cutting plane is equal if the line-width is unchanged. + * Note that we don't implement miter limits, so these can get wild. + */ + if (i > 0) { + var s0 = scenes[i - 1]; + if (s0.visible) { + var v1 = p1.minus(s0.left, s0.top).perp().norm().plus(v); + d = intersect(p1, v1, d, p); + a = intersect(p1, v1, a, p); + } + } + + /* Similarly, for end join. */ + if (i < (n - 1)) { + var s3 = scenes[i + 2]; + if (s3.visible) { + var v2 = pv.vector(s3.left, s3.top).minus(p2).perp().norm().plus(v); + c = intersect(p2, v2, c, p); + b = intersect(p2, v2, b, p); + } + } + + /* points */ + var p = a.x + "," + a.y + " " + + b.x + "," + b.y + " " + + c.x + "," + c.y + " " + + d.x + "," + d.y; + + e = this.expect("polygon", e); + e.setAttribute("cursor", s1.cursor); + e.setAttribute("points", p); + e.setAttribute("fill", stroke.color); + e.setAttribute("fill-opacity", stroke.opacity); + e = this.append(e, scenes, i); + } + return e; +}; +var guid = 0; + +pv.SvgScene.panel = function(scenes) { + var g = scenes.$g, e = g && g.firstChild; + for (var i = 0; i < scenes.length; i++) { + var s = scenes[i]; + + /* visible */ + if (!s.visible) continue; + + /* svg */ + if (!scenes.parent) { + s.canvas.style.display = "inline-block"; + g = s.canvas.firstChild; + if (!g) { + g = s.canvas.appendChild(this.create("svg")); + g.onclick + = g.onmousedown + = g.onmouseup + = g.onmousemove + = g.onmouseout + = g.onmouseover + = pv.SvgScene.dispatch; + } + scenes.$g = g; + g.setAttribute("width", s.width + s.left + s.right); + g.setAttribute("height", s.height + s.top + s.bottom); + if (typeof e == "undefined") e = g.firstChild; + } + + /* clip (nest children) */ + if (s.overflow == "hidden") { + var c = this.expect("g", e), id = (guid++).toString(36); + c.setAttribute("clip-path", "url(#" + id + ")"); + if (!c.parentNode) g.appendChild(c); + scenes.$g = g = c; + e = c.firstChild; + + e = this.expect("clipPath", e); + e.setAttribute("id", id); + var r = e.firstChild || e.appendChild(this.create("rect")); + r.setAttribute("x", s.left); + r.setAttribute("y", s.top); + r.setAttribute("width", s.width); + r.setAttribute("height", s.height); + if (!e.parentNode) g.appendChild(e); + e = e.nextSibling; + } + + /* fill */ + e = this.fill(e, scenes, i); + + /* children */ + for (var j = 0; j < s.children.length; j++) { + s.children[j].$g = e = this.expect("g", e); + e.setAttribute("transform", "translate(" + s.left + "," + s.top + ")"); + this.updateAll(s.children[j]); + if (!e.parentNode) g.appendChild(e); + e = e.nextSibling; + } + + /* stroke */ + e = this.stroke(e, scenes, i); + + /* clip (restore group) */ + if (s.overflow == "hidden") { + scenes.$g = g = c.parentNode; + e = c.nextSibling; + } + } + return e; +}; + +pv.SvgScene.fill = function(e, scenes, i) { + var s = scenes[i], fill = pv.color(s.fillStyle); + if (fill.opacity) { + e = this.expect("rect", e); + e.setAttribute("x", s.left); + e.setAttribute("y", s.top); + e.setAttribute("width", s.width); + e.setAttribute("height", s.height); + e.setAttribute("cursor", s.cursor); + e.setAttribute("fill", fill.color); + e.setAttribute("fill-opacity", fill.opacity); + e = this.append(e, scenes, i); + } + return e; +}; + +pv.SvgScene.stroke = function(e, scenes, i) { + var s = scenes[i], stroke = pv.color(s.strokeStyle); + if (stroke.opacity) { + e = this.expect("rect", e); + e.setAttribute("x", s.left); + e.setAttribute("y", s.top); + e.setAttribute("width", Math.max(1E-10, s.width)); + e.setAttribute("height", Math.max(1E-10, s.height)); + e.setAttribute("cursor", s.cursor); + e.setAttribute("fill", "none"); + e.setAttribute("stroke", stroke.color); + e.setAttribute("stroke-opacity", stroke.opacity); + e.setAttribute("stroke-width", s.lineWidth); + e = this.append(e, scenes, i); + } + return e; +}; +pv.SvgScene.rule = function(scenes) { + var e = scenes.$g.firstChild; + for (var i = 0; i < scenes.length; i++) { + var s = scenes[i]; + + /* visible */ + if (!s.visible) continue; + var stroke = pv.color(s.strokeStyle); + if (!stroke.opacity) continue; + + e = this.expect("line", e); + e.setAttribute("cursor", s.cursor); + e.setAttribute("x1", s.left); + e.setAttribute("y1", s.top); + e.setAttribute("x2", s.left + s.width); + e.setAttribute("y2", s.top + s.height); + e.setAttribute("stroke", stroke.color); + e.setAttribute("stroke-opacity", stroke.opacity); + e.setAttribute("stroke-width", s.lineWidth); + e = this.append(e, scenes, i); + } + return e; +}; +pv.SvgScene.wedge = function(scenes) { + var e = scenes.$g.firstChild; + for (var i = 0; i < scenes.length; i++) { + var s = scenes[i]; + + /* visible */ + if (!s.visible) continue; + var fill = pv.color(s.fillStyle), stroke = pv.color(s.strokeStyle); + if (!fill.opacity && !stroke.opacity) continue; + + /* points */ + var r1 = s.innerRadius, r2 = s.outerRadius, a = Math.abs(s.angle), p; + if (a >= 2 * Math.PI) { + if (r1) { + p = "M0," + r2 + + "A" + r2 + "," + r2 + " 0 1,1 0," + (-r2) + + "A" + r2 + "," + r2 + " 0 1,1 0," + r2 + + "M0," + r1 + + "A" + r1 + "," + r1 + " 0 1,1 0," + (-r1) + + "A" + r1 + "," + r1 + " 0 1,1 0," + r1 + + "Z"; + } else { + p = "M0," + r2 + + "A" + r2 + "," + r2 + " 0 1,1 0," + (-r2) + + "A" + r2 + "," + r2 + " 0 1,1 0," + r2 + + "Z"; + } + } else { + var sa = Math.min(s.startAngle, s.endAngle), + ea = Math.max(s.startAngle, s.endAngle), + c1 = Math.cos(sa), c2 = Math.cos(ea), + s1 = Math.sin(sa), s2 = Math.sin(ea); + if (r1) { + p = "M" + r2 * c1 + "," + r2 * s1 + + "A" + r2 + "," + r2 + " 0 " + + ((a < Math.PI) ? "0" : "1") + ",1 " + + r2 * c2 + "," + r2 * s2 + + "L" + r1 * c2 + "," + r1 * s2 + + "A" + r1 + "," + r1 + " 0 " + + ((a < Math.PI) ? "0" : "1") + ",0 " + + r1 * c1 + "," + r1 * s1 + "Z"; + } else { + p = "M" + r2 * c1 + "," + r2 * s1 + + "A" + r2 + "," + r2 + " 0 " + + ((a < Math.PI) ? "0" : "1") + ",1 " + + r2 * c2 + "," + r2 * s2 + "L0,0Z"; + } + } + + e = this.expect("path", e); + e.setAttribute("fill-rule", "evenodd"); + e.setAttribute("cursor", s.cursor); + e.setAttribute("transform", "translate(" + s.left + "," + s.top + ")"); + e.setAttribute("d", p); + e.setAttribute("fill", fill.color); + e.setAttribute("fill-opacity", fill.opacity); + e.setAttribute("stroke", stroke.color); + e.setAttribute("stroke-opacity", stroke.opacity); + e.setAttribute("stroke-width", s.lineWidth); + e = this.append(e, scenes, i); + } + return e; +}; +/** + * Constructs a new mark with default properties. Marks, with the exception of + * the root panel, are not typically constructed directly; instead, they are + * added to a panel or an existing mark via {@link pv.Mark#add}. + * + * @class Represents a data-driven graphical mark. The <tt>Mark</tt> class is + * the base class for all graphical marks in Protovis; it does not provide any + * specific rendering functionality, but together with {@link Panel} establishes + * the core framework. + * + * <p>Concrete mark types include familiar visual elements such as bars, lines + * and labels. Although a bar mark may be used to construct a bar chart, marks + * know nothing about charts; it is only through their specification and + * composition that charts are produced. These building blocks permit many + * combinatorial possibilities. + * + * <p>Marks are associated with <b>data</b>: a mark is generated once per + * associated datum, mapping the datum to visual <b>properties</b> such as + * position and color. Thus, a single mark specification represents a set of + * visual elements that share the same data and visual encoding. The type of + * mark defines the names of properties and their meaning. A property may be + * static, ignoring the associated datum and returning a constant; or, it may be + * dynamic, derived from the associated datum or index. Such dynamic encodings + * can be specified succinctly using anonymous functions. Special properties + * called event handlers can be registered to add interactivity. + * + * <p>Protovis uses <b>inheritance</b> to simplify the specification of related + * marks: a new mark can be derived from an existing mark, inheriting its + * properties. The new mark can then override properties to specify new + * behavior, potentially in terms of the old behavior. In this way, the old mark + * serves as the <b>prototype</b> for the new mark. Most mark types share the + * same basic properties for consistency and to facilitate inheritance. + * + * <p>The prioritization of redundant properties is as follows:<ol> + * + * <li>If the <tt>width</tt> property is not specified (i.e., null), its value + * is the width of the parent panel, minus this mark's left and right margins; + * the left and right margins are zero if not specified. + * + * <li>Otherwise, if the <tt>right</tt> margin is not specified, its value is + * the width of the parent panel, minus this mark's width and left margin; the + * left margin is zero if not specified. + * + * <li>Otherwise, if the <tt>left</tt> property is not specified, its value is + * the width of the parent panel, minus this mark's width and the right margin. + * + * </ol>This prioritization is then duplicated for the <tt>height</tt>, + * <tt>bottom</tt> and <tt>top</tt> properties, respectively. + * + * <p>While most properties are <i>variable</i>, some mark types, such as lines + * and areas, generate a single visual element rather than a distinct visual + * element per datum. With these marks, some properties may be <b>fixed</b>. + * Fixed properties can vary per mark, but not <i>per datum</i>! These + * properties are evaluated solely for the first (0-index) datum, and typically + * are specified as a constant. However, it is valid to use a function if the + * property varies between panels or is dynamically generated. + * + * <p>See also the <a href="../../api/">Protovis guide</a>. + */ +pv.Mark = function() { + /* + * TYPE 0 constant defs + * TYPE 1 function defs + * TYPE 2 constant properties + * TYPE 3 function properties + * in order of evaluation! + */ + this.$properties = []; +}; + +/** @private TOOD */ +pv.Mark.prototype.properties = {}; + +/** + * @private Defines and registers a property method for the property with the + * given name. This method should be called on a mark class prototype to define + * each exposed property. (Note this refers to the JavaScript + * <tt>prototype</tt>, not the Protovis mark prototype, which is the {@link + * #proto} field.) + * + * <p>The created property method supports several modes of invocation: <ol> + * + * <li>If invoked with a <tt>Function</tt> argument, this function is evaluated + * for each associated datum. The return value of the function is used as the + * computed property value. The context of the function (<tt>this</tt>) is this + * mark. The arguments to the function are the associated data of this mark and + * any enclosing panels. For example, a linear encoding of numerical data to + * height is specified as + * + * <pre>m.height(function(d) d * 100);</pre> + * + * The expression <tt>d * 100</tt> will be evaluated for the height property of + * each mark instance. The return value of the property method (e.g., + * <tt>m.height</tt>) is this mark (<tt>m</tt>)).<p> + * + * <li>If invoked with a non-function argument, the property is treated as a + * constant. The return value of the property method (e.g., <tt>m.height</tt>) + * is this mark.<p> + * + * <li>If invoked with no arguments, the computed property value for the current + * mark instance in the scene graph is returned. This facilitates <i>property + * chaining</i>, where one mark's properties are defined in terms of another's. + * For example, to offset a mark's location from its prototype, you might say + * + * <pre>m.top(function() this.proto.top() + 10);</pre> + * + * Note that the index of the mark being evaluated (in the above example, + * <tt>this.proto</tt>) is inherited from the <tt>Mark</tt> class and set by + * this mark. So, if the fifth element's top property is being evaluated, the + * fifth instance of <tt>this.proto</tt> will similarly be queried for the value + * of its top property. If the mark being evaluated has a different number of + * instances, or its data is unrelated, the behavior of this method is + * undefined. In these cases it may be better to index the <tt>scene</tt> + * explicitly to specify the exact instance. + * + * </ol><p>Property names should follow standard JavaScript method naming + * conventions, using lowerCamel-style capitalization. + * + * <p>In addition to creating the property method, every property is registered + * in the {@link #properties} map on the <tt>prototype</tt>. Although this is an + * instance field, it is considered immutable and shared by all instances of a + * given mark type. The <tt>properties</tt> map can be queried to see if a mark + * type defines a particular property, such as width or height. + * + * @param {string} name the property name. + */ +pv.Mark.prototype.property = function(name) { + if (!this.hasOwnProperty("properties")) { + this.properties = pv.extend(this.properties); + } + this.properties[name] = true; + + /* + * Define the setter-getter globally, since the default behavior should be the + * same for all properties, and since the Protovis inheritance chain is + * independent of the JavaScript inheritance chain. For example, anchors + * define a "name" property that is evaluated on derived marks, even though + * those marks don't normally have a name. + */ + pv.Mark.prototype[name] = function(v) { + if (arguments.length) { + this.$properties.push({ + name: name, + type: (typeof v == "function") ? 3 : 2, + value: v + }); + return this; + } + return this.scene[this.index][name]; + }; + + return this; +}; + +/* Define all global properties. */ +pv.Mark.prototype + .property("data") + .property("visible") + .property("left") + .property("right") + .property("top") + .property("bottom") + .property("cursor") + .property("title") + .property("reverse"); + +/** + * The mark type; a lower camelCase name. The type name controls rendering + * behavior, and unless the rendering engine is extended, must be one of the + * built-in concrete mark types: area, bar, dot, image, label, line, panel, + * rule, or wedge. + * + * @type string + * @name pv.Mark.prototype.type + */ + +/** + * The mark prototype, possibly undefined, from which to inherit property + * functions. The mark prototype is not necessarily of the same type as this + * mark. Any properties defined on this mark will override properties inherited + * either from the prototype or from the type-specific defaults. + * + * @type pv.Mark + * @name pv.Mark.prototype.proto + */ + +/** + * The enclosing parent panel. The parent panel is generally undefined only for + * the root panel; however, it is possible to create "offscreen" marks that are + * used only for inheritance purposes. + * + * @type pv.Panel + * @name pv.Mark.prototype.parent + */ + +/** + * The child index. -1 if the enclosing parent panel is null; otherwise, the + * zero-based index of this mark into the parent panel's <tt>children</tt> array. + * + * @type number + */ +pv.Mark.prototype.childIndex = -1; + +/** + * The mark index. The value of this field depends on which instance (i.e., + * which element of the data array) is currently being evaluated. During the + * build phase, the index is incremented over each datum; when handling events, + * the index is set to the instance that triggered the event. + * + * @type number + */ +pv.Mark.prototype.index = -1; + +/** + * The scene graph. The scene graph is an array of objects; each object (or + * "node") corresponds to an instance of this mark and an element in the data + * array. The scene graph can be traversed to lookup previously-evaluated + * properties. + * + * <p>For instance, consider a stacked area chart. The bottom property of the + * area can be defined using the <i>cousin</i> instance, which is the current + * area instance in the previous instantiation of the parent panel. In this + * sample code, + * + * <pre>new pv.Panel() + * .width(150).height(150) + * .add(pv.Panel) + * .data([[1, 1.2, 1.7, 1.5, 1.7], + * [.5, 1, .8, 1.1, 1.3], + * [.2, .5, .8, .9, 1]]) + * .add(pv.Area) + * .data(function(d) d) + * .bottom(function() { + * var c = this.cousin(); + * return c ? (c.bottom + c.height) : 0; + * }) + * .height(function(d) d * 40) + * .left(function() this.index * 35) + * .root.render();</pre> + * + * the bottom property is computed based on the upper edge of the corresponding + * datum in the previous series. The area's parent panel is instantiated once + * per series, so the cousin refers to the previous (below) area mark. (Note + * that the position of the upper edge is not the same as the top property, + * which refers to the top margin: the distance from the top edge of the panel + * to the top edge of the mark.) + * + * @see #first + * @see #last + * @see #sibling + * @see #cousin + * @name pv.Mark.prototype.scene + */ + +/** + * The root parent panel. This may be undefined for "offscreen" marks that are + * created for inheritance purposes only. + * + * @type pv.Panel + * @name pv.Mark.prototype.root + */ + +/** + * The data property; an array of objects. The size of the array determines the + * number of marks that will be instantiated; each element in the array will be + * passed to property functions to compute the property values. Typically, the + * data property is specified as a constant array, such as + * + * <pre>m.data([1, 2, 3, 4, 5]);</pre> + * + * However, it is perfectly acceptable to define the data property as a + * function. This function might compute the data dynamically, allowing + * different data to be used per enclosing panel. For instance, in the stacked + * area graph example (see {@link #scene}), the data function on the area mark + * dereferences each series. + * + * @type array + * @name pv.Mark.prototype.data + */ + +/** + * The visible property; a boolean determining whether or not the mark instance + * is visible. If a mark instance is not visible, its other properties will not + * be evaluated. Similarly, for panels no child marks will be rendered. + * + * @type boolean + * @name pv.Mark.prototype.visible + */ + +/** + * The left margin; the distance, in pixels, between the left edge of the + * enclosing panel and the left edge of this mark. Note that in some cases this + * property may be redundant with the right property, or with the conjunction of + * right and width. + * + * @type number + * @name pv.Mark.prototype.left + */ + +/** + * The right margin; the distance, in pixels, between the right edge of the + * enclosing panel and the right edge of this mark. Note that in some cases this + * property may be redundant with the left property, or with the conjunction of + * left and width. + * + * @type number + * @name pv.Mark.prototype.right + */ + +/** + * The top margin; the distance, in pixels, between the top edge of the + * enclosing panel and the top edge of this mark. Note that in some cases this + * property may be redundant with the bottom property, or with the conjunction + * of bottom and height. + * + * @type number + * @name pv.Mark.prototype.top + */ + +/** + * The bottom margin; the distance, in pixels, between the bottom edge of the + * enclosing panel and the bottom edge of this mark. Note that in some cases + * this property may be redundant with the top property, or with the conjunction + * of top and height. + * + * @type number + * @name pv.Mark.prototype.bottom + */ + +/** + * The cursor property; corresponds to the CSS cursor property. This is + * typically used in conjunction with event handlers to indicate interactivity. + * + * @type string + * @name pv.Mark.prototype.cursor + * @see <a href="http://www.w3.org/TR/CSS2/ui.html#propdef-cursor">CSS2 cursor</a> + */ + +/** + * The title property; corresponds to the HTML/SVG title property, allowing the + * general of simple plain text tooltips. + * + * @type string + * @name pv.Mark.prototype.title + */ + +/** + * The reverse property; a boolean determining whether marks are ordered from + * front-to-back or back-to-front. SVG does not support explicit z-ordering; + * shapes are rendered in the order they appear. Thus, by default, marks are + * rendered in data order. Setting the reverse property to false reverses the + * order in which they are rendered; however, the properties are still evaluated + * (i.e., built) in forward order. + * + * @type boolean + * @name pv.Mark.prototype.reverse + */ + +/** + * Default properties for all mark types. By default, the data array is the + * parent data as a single-element array; if the data property is not specified, + * this causes each mark to be instantiated as a singleton with the parents + * datum. The visible property is true by default, and the reverse property is + * false. + * + * @type pv.Mark + */ +pv.Mark.prototype.defaults = new pv.Mark() + .data(function(d) { return [d]; }) + .visible(true) + .reverse(false) + .cursor("") + .title(""); + +/* Private categorical colors for default fill & stroke styles. */ +var defaultFillStyle = pv.Colors.category20().by(pv.parent), + defaultStrokeStyle = pv.Colors.category10().by(pv.parent); + +/** + * Sets the prototype of this mark to the specified mark. Any properties not + * defined on this mark may be inherited from the specified prototype mark, or + * its prototype, and so on. The prototype mark need not be the same type of + * mark as this mark. (Note that for inheritance to be useful, properties with + * the same name on different mark types should have equivalent meaning.) + * + * @param {pv.Mark} proto the new prototype. + * @return {pv.Mark} this mark. + * @see #add + */ +pv.Mark.prototype.extend = function(proto) { + this.proto = proto; + return this; +}; + +/** + * Adds a new mark of the specified type to the enclosing parent panel, whilst + * simultaneously setting the prototype of the new mark to be this mark. + * + * @param {function} type the type of mark to add; a constructor, such as + * <tt>pv.Bar</tt>. + * @return {pv.Mark} the new mark. + * @see #extend + */ +pv.Mark.prototype.add = function(type) { + return this.parent.add(type).extend(this); +}; + +/** + * Defines a local variable on this mark. Local variables are initialized once + * per mark (i.e., per parent panel instance), and can be used to store local + * state for the mark. Here are a few reasons you might want to use + * <tt>def</tt>: + * + * <p>1. To store local state. For example, say you were visualizing employment + * statistics, and your root panel had an array of occupations. In a child + * panel, you might want to initialize a local scale, and reference it from a + * property function: + * + * <pre>.def("y", function(d) pv.Scale.linear(0, pv.max(d.values)).range(0, h)) + * .height(function(d) this.y()(d))</pre> + * + * In this example, <tt>this.y()</tt> returns the defined local scale. We then + * invoke the scale function, passing in the datum, to compute the height. Note + * that defs are similar to fixed properties: they are only evaluated once per + * parent panel, and <tt>this.y()</tt> returns a function, rather than + * automatically evaluating this function as a property. + * + * <p>2. To store temporary state for interaction. Say you have an array of + * bars, and you want to color the bar differently if the mouse is over it. Use + * <tt>def</tt> to define a local variable, and event handlers to override this + * variable interactively: + * + * <pre>.def("i", -1) + * .event("mouseover", function() this.i(this.index)) + * .event("mouseout", function() this.i(-1)) + * .fillStyle(function() this.i() == this.index ? "red" : "blue")</pre> + * + * Notice that <tt>this.i()</tt> can be used both to set the value of <i>i</i> + * (when an argument is specified), and to get the value of <i>i</i> (when no + * arguments are specified). In this way, it's like other property methods. + * + * <p>3. To specify fixed properties efficiently. Sometimes, the value of a + * property may be locally a constant, but dependent on parent panel data which + * is variable. In this scenario, you can use <tt>def</tt> to define a property; + * it will only get computed once per mark, rather than once per datum. + * + * @param {string} name the name of the local variable. + * @param {function} [value] an optional initializer; may be a constant or a + * function. + */ +pv.Mark.prototype.def = function(name, value) { + this.$properties.push({ + name: name, + type: (typeof value == "function") ? 1 : 0, + value: value + }); + return this; +}; + +/** + * Returns an anchor with the specified name. While anchor names are typically + * constants, the anchor name is a true property, which means you can specify a + * function to compute the anchor name dynamically. See the + * {@link pv.Anchor#name} property for details. + * + * @param {string} name the anchor name; either a string or a property function. + * @returns {pv.Anchor} the new anchor. + */ +pv.Mark.prototype.anchor = function(name) { + var anchor = new pv.Anchor().extend(this).name(name); + anchor.parent = this.parent; + return anchor; +}; + +/** + * Returns the anchor target of this mark, if it is derived from an anchor; + * otherwise returns null. For example, if a label is derived from a bar anchor, + * + * <pre>bar.anchor("top").add(pv.Label);</pre> + * + * then property functions on the label can refer to the bar via the + * <tt>anchorTarget</tt> method. This method is also useful for mark types + * defining properties on custom anchors. + * + * @returns {pv.Mark} the anchor target of this mark; possibly null. + */ +pv.Mark.prototype.anchorTarget = function() { + var target = this; + while (!(target instanceof pv.Anchor)) { + target = target.proto; + if (!target) return null; + } + return target.proto; +}; + +/** + * Returns the first instance of this mark in the scene graph. This method can + * only be called when the mark is bound to the scene graph (for example, from + * an event handler, or within a property function). + * + * @returns a node in the scene graph. + */ +pv.Mark.prototype.first = function() { + return this.scene[0]; +}; + +/** + * Returns the last instance of this mark in the scene graph. This method can + * only be called when the mark is bound to the scene graph (for example, from + * an event handler, or within a property function). In addition, note that mark + * instances are built sequentially, so the last instance of this mark may not + * yet be constructed. + * + * @returns a node in the scene graph. + */ +pv.Mark.prototype.last = function() { + return this.scene[this.scene.length - 1]; +}; + +/** + * Returns the previous instance of this mark in the scene graph, or null if + * this is the first instance. + * + * @returns a node in the scene graph, or null. + */ +pv.Mark.prototype.sibling = function() { + return (this.index == 0) ? null : this.scene[this.index - 1]; +}; + +/** + * Returns the current instance in the scene graph of this mark, in the previous + * instance of the enclosing parent panel. May return null if this instance + * could not be found. See the {@link pv.Layout.stack} function for an example + * property function using cousin. + * + * @see pv.Layout.stack + * @returns a node in the scene graph, or null. + */ +pv.Mark.prototype.cousin = function() { + var p = this.parent, s = p && p.sibling(); + return (s && s.children) ? s.children[this.childIndex][this.index] : null; +}; + +/** + * Renders this mark, including recursively rendering all child marks if this is + * a panel. + */ +pv.Mark.prototype.render = function() { + /* + * Rendering consists of three phases: bind, build and update. The update + * phase is decoupled to allow different rendering engines. + * + * In the bind phase, inherited property definitions are cached so they do not + * need to be queried during build. In the build phase, properties are + * evaluated, and the scene graph is generated. In the update phase, the scene + * is rendered by creating and updating elements and attributes in the SVG + * image. No properties are evaluated during the update phase; instead the + * values computed previously in the build phase are simply translated into + * SVG. + */ + this.bind(); + this.build(); + pv.Scene.updateAll(this.scene); +}; + +/** @private Computes the root data stack for the specified mark. */ +function argv(mark) { + var stack = []; + while (mark) { + stack.push(mark.scene[mark.index].data); + mark = mark.parent; + } + return stack; +} + +/** @private TODO */ +pv.Mark.prototype.bind = function() { + var seen = {}, types = [[], [], [], []], data, visible; + + /** TODO */ + function bind(mark) { + do { + var properties = mark.$properties; + for (var i = properties.length - 1; i >= 0 ; i--) { + var p = properties[i]; + if (!(p.name in seen)) { + seen[p.name] = 1; + switch (p.name) { + case "data": data = p; break; + case "visible": visible = p; break; + default: types[p.type].push(p); break; + } + } + } + } while (mark = mark.proto); + } + + /** TODO */ + function def(name) { + return function(v) { + var defs = this.scene.defs; + if (arguments.length) { + if (v == undefined) { + delete defs.locked[name]; + } else { + defs.locked[name] = true; + } + defs.values[name] = v; + return this; + } else { + return defs.values[name]; + } + }; + } + + /* Scan the proto chain for all defined properties. */ + bind(this); + bind(this.defaults); + types[1].reverse(); + types[3].reverse(); + + /* Any undefined properties are null. */ + var mark = this; + do for (var name in mark.properties) { + if (!(name in seen)) { + seen[name] = 1; + types[2].push({name: name, type: 2, value: null}); + } + } while (mark = mark.proto); + + /* Define setter-getter for inherited defs. */ + var defs = types[0].concat(types[1]); + for (var i = 0; i < defs.length; i++) { + var d = defs[i]; + this[d.name] = def(d.name); + } + + /* Setup binds to evaluate constants before functions. */ + this.binds = { + data: data, + visible: visible, + defs: defs, + properties: pv.blend(types) + }; +}; + +/** + * @private Evaluates properties and computes implied properties. Properties are + * stored in the {@link #scene} array for each instance of this mark. + * + * <p>As marks are built recursively, the {@link #index} property is updated to + * match the current index into the data array for each mark. Note that the + * index property is only set for the mark currently being built and its + * enclosing parent panels. The index property for other marks is unset, but is + * inherited from the global <tt>Mark</tt> class prototype. This allows mark + * properties to refer to properties on other marks <i>in the same panel</i> + * conveniently; however, in general it is better to reference mark instances + * specifically through the scene graph rather than depending on the magical + * behavior of {@link #index}. + * + * <p>The root scene array has a special property, <tt>data</tt>, which stores + * the current data stack. The first element in this stack is the current datum, + * followed by the datum of the enclosing parent panel, and so on. The data + * stack should not be accessed directly; instead, property functions are passed + * the current data stack as arguments. + * + * <p>The evaluation of the <tt>data</tt> and <tt>visible</tt> properties is + * special. The <tt>data</tt> property is evaluated first; unlike the other + * properties, the data stack is from the parent panel, rather than the current + * mark, since the data is not defined until the data property is evaluated. + * The <tt>visisble</tt> property is subsequently evaluated for each instance; + * only if true will the {@link #buildInstance} method be called, evaluating + * other properties and recursively building the scene graph. + * + * <p>If this mark is being re-built, any old instances of this mark that no + * longer exist (because the new data array contains fewer elements) will be + * cleared using {@link #clearInstance}. + * + * @param parent the instance of the parent panel from the scene graph. + */ +pv.Mark.prototype.build = function() { + var scene = this.scene; + if (!scene) { + scene = this.scene = []; + scene.mark = this; + scene.type = this.type; + scene.childIndex = this.childIndex; + if (this.parent) { + scene.parent = this.parent.scene; + scene.parentIndex = this.parent.index; + } + } + + /* Set the data stack. */ + var stack = this.root.scene.data; + if (!stack) this.root.scene.data = stack = argv(this.parent); + + /* Evaluate defs. */ + if (this.binds.defs.length) { + var defs = scene.defs; + if (!defs) scene.defs = defs = {values: {}, locked: {}}; + for (var i = 0; i < this.binds.defs.length; i++) { + var d = this.binds.defs[i]; + if (!(d.name in defs.locked)) { + var v = d.value; + if (d.type == 1) { + property = d.name; + v = v.apply(this, stack); + } + defs.values[d.name] = v; + } + } + } + + /* Evaluate special data property. */ + var data = this.binds.data; + switch (data.type) { + case 0: case 1: data = defs.values.data; break; + case 2: data = data.value; break; + case 3: { + property = "data"; + data = data.value.apply(this, stack); + break; + } + } + + /* Create, update and delete scene nodes. */ + stack.unshift(null); + scene.length = data.length; + for (var i = 0; i < data.length; i++) { + pv.Mark.prototype.index = this.index = i; + var s = scene[i]; + if (!s) scene[i] = s = {}; + s.data = stack[0] = data[i]; + + /* Evaluate special visible property. */ + var visible = this.binds.visible; + switch (visible.type) { + case 0: case 1: visible = defs.values.visible; break; + case 2: visible = visible.value; break; + case 3: { + property = "visible"; + visible = visible.value.apply(this, stack); + break; + } + } + + if (s.visible = visible) this.buildInstance(s); + } + stack.shift(); + delete this.index; + pv.Mark.prototype.index = -1; + if (!this.parent) scene.data = null; + + return this; +}; + +/** + * @private Evaluates the specified array of properties for the specified + * instance <tt>s</tt> in the scene graph. + * + * @param s a node in the scene graph; the instance of the mark to build. + * @param properties an array of properties. + */ +pv.Mark.prototype.buildProperties = function(s, properties) { + for (var i = 0, n = properties.length; i < n; i++) { + var p = properties[i], v = p.value; + switch (p.type) { + case 0: case 1: v = this.scene.defs.values[p.name]; break; + case 3: { + property = p.name; + v = v.apply(this, this.root.scene.data); + break; + } + } + s[p.name] = v; + } +}; + +/** + * @private Evaluates all of the properties for this mark for the specified + * instance <tt>s</tt> in the scene graph. The set of properties to evaluate is + * retrieved from the {@link #properties} array for this mark type (see {@link + * #type}). After these properties are evaluated, any <b>implied</b> properties + * may be computed by the mark and set on the scene graph; see + * {@link #buildImplied}. + * + * <p>For panels, this method recursively builds the scene graph for all child + * marks as well. In general, this method should not need to be overridden by + * concrete mark types. + * + * @param s a node in the scene graph; the instance of the mark to build. + */ +pv.Mark.prototype.buildInstance = function(s) { + this.buildProperties(s, this.binds.properties); + this.buildImplied(s); +}; + +/** + * @private Computes the implied properties for this mark for the specified + * instance <tt>s</tt> in the scene graph. Implied properties are those with + * dependencies on multiple other properties; for example, the width property + * may be implied if the left and right properties are set. This method can be + * overridden by concrete mark types to define new implied properties, if + * necessary. + * + * @param s a node in the scene graph; the instance of the mark to build. + */ +pv.Mark.prototype.buildImplied = function(s) { + var l = s.left; + var r = s.right; + var t = s.top; + var b = s.bottom; + + /* Assume width and height are zero if not supported by this mark type. */ + var p = this.properties; + var w = p.width ? s.width : 0; + var h = p.height ? s.height : 0; + + /* Compute implied width, right and left. */ + var width = this.parent ? this.parent.width() : (w + l + r); + if (w == null) { + w = width - (r = r || 0) - (l = l || 0); + } else if (r == null) { + r = width - w - (l = l || 0); + } else if (l == null) { + l = width - w - (r = r || 0); + } + + /* Compute implied height, bottom and top. */ + var height = this.parent ? this.parent.height() : (h + t + b); + if (h == null) { + h = height - (t = t || 0) - (b = b || 0); + } else if (b == null) { + b = height - h - (t = t || 0); + } else if (t == null) { + t = height - h - (b = b || 0); + } + + s.left = l; + s.right = r; + s.top = t; + s.bottom = b; + + /* Only set width and height if they are supported by this mark type. */ + if (p.width) s.width = w; + if (p.height) s.height = h; +}; + +/** + * @private The name of the property being evaluated, for so-called "smart" + * functions that change behavior depending on which property is being + * evaluated. This functionality is somewhat magical, so for now, this feature + * is not exposed outside the library. + * + * @type string + */ +var property; + +/** @private The current mouse location. */ +var pageX = 0, pageY = 0; +pv.listen(window, "mousemove", function(e) { pageX = e.pageX; pageY = e.pageY; }); + +/** + * Returns the current location of the mouse (cursor) relative to this mark's + * parent. The <i>x</i> coordinate corresponds to the left margin, while the + * <i>y</i> coordinate corresponds to the top margin. + * + * @returns {pv.Vector} the mouse location. + */ +pv.Mark.prototype.mouse = function() { + var x = 0, y = 0, mark = (this instanceof pv.Panel) ? this : this.parent; + do { + x += mark.left(); + y += mark.top(); + } while (mark = mark.parent); + var node = this.root.canvas(); + do { + x += node.offsetLeft; + y += node.offsetTop; + } while (node = node.offsetParent); + return pv.vector(pageX - x, pageY - y); +}; + +/** + * Registers an event handler for the specified event type with this mark. When + * an event of the specified type is triggered, the specified handler will be + * invoked. The handler is invoked in a similar method to property functions: + * the context is <tt>this</tt> mark instance, and the arguments are the full + * data stack. Event handlers can use property methods to manipulate the display + * properties of the mark: + * + * <pre>m.event("click", function() this.fillStyle("red"));</pre> + * + * Alternatively, the external data can be manipulated and the visualization + * redrawn: + * + * <pre>m.event("click", function(d) { + * data = all.filter(function(k) k.name == d); + * vis.render(); + * });</pre> + * + * The return value of the event handler determines which mark gets re-rendered. + * Use defs ({@link #def}) to set temporary state from event handlers. + * + * <p>The complete set of event types is defined by SVG; see the reference + * below. The set of supported event types is:<ul> + * + * <li>click + * <li>mousedown + * <li>mouseup + * <li>mouseover + * <li>mousemove + * <li>mouseout + * + * </ul>Since Protovis does not specify any concept of focus, it does not + * support key events; these should be handled outside the visualization using + * standard JavaScript. In the future, support for interaction may be extended + * to support additional event types, particularly those most relevant to + * interactive visualization, such as selection. + * + * <p>TODO In the current implementation, event handlers are not inherited from + * prototype marks. They must be defined explicitly on each interactive mark. In + * addition, only one event handler for a given event type can be defined; when + * specifying multiple event handlers for the same type, only the last one will + * be used. + * + * @see <a href="http://www.w3.org/TR/SVGTiny12/interact.html#SVGEvents">SVG events</a> + * @param {string} type the event type. + * @param {function} handler the event handler. + * @returns {pv.Mark} this. + */ +pv.Mark.prototype.event = function(type, handler) { + if (!this.$handlers) this.$handlers = {}; + this.$handlers[type] = handler; + return this; +}; + +/** @private TODO */ +pv.Mark.prototype.dispatch = function(type, scenes, index) { + var l = this.$handlers && this.$handlers[type]; + if (!l) { + if (this.parent) { + this.parent.dispatch(type, scenes.parent, scenes.parentIndex); + } + return; + } + try { + + /* Setup the scene stack. */ + var mark = this; + do { + mark.index = index; + mark.scene = scenes; + index = scenes.parentIndex; + scenes = scenes.parent; + } while (mark = mark.parent); + + /* Execute the event listener. */ + try { + mark = l.apply(this, this.root.scene.data = argv(this)); + } finally { + this.root.scene.data = null; + } + + /* Update the display. TODO dirtying. */ + if (mark instanceof pv.Mark) mark.render(); + + } finally { + + /* Restore the scene stack. */ + var mark = this; + do { + if (mark.parent) delete mark.scene; + delete mark.index; + } while (mark = mark.parent); + } +}; +/** + * Constructs a new mark anchor with default properties. + * + * @class Represents an anchor on a given mark. An anchor is itself a mark, but + * without a visual representation. It serves only to provide useful default + * properties that can be inherited by other marks. Each type of mark can define + * any number of named anchors for convenience. If the concrete mark type does + * not define an anchor implementation specifically, one will be inherited from + * the mark's parent class. + * + * <p>For example, the bar mark provides anchors for its four sides: left, + * right, top and bottom. Adding a label to the top anchor of a bar, + * + * <pre>bar.anchor("top").add(pv.Label);</pre> + * + * will render a text label on the top edge of the bar; the top anchor defines + * the appropriate position properties (top and left), as well as text-rendering + * properties for convenience (textAlign and textBaseline). + * + * @extends pv.Mark + */ +pv.Anchor = function() { + pv.Mark.call(this); +}; + +pv.Anchor.prototype = pv.extend(pv.Mark) + .property("name"); + +/** + * The anchor name. The set of supported anchor names is dependent on the + * concrete mark type; see the mark type for details. For example, bars support + * left, right, top and bottom anchors. + * + * <p>While anchor names are typically constants, the anchor name is a true + * property, which means you can specify a function to compute the anchor name + * dynamically. For instance, if you wanted to alternate top and bottom anchors, + * saying + * + * <pre>m.anchor(function() (this.index % 2) ? "top" : "bottom").add(pv.Dot);</pre> + * + * would have the desired effect. + * + * @type string + * @name pv.Anchor.prototype.name + */ +/** + * Constructs a new area mark with default properties. Areas are not typically + * constructed directly, but by adding to a panel or an existing mark via + * {@link pv.Mark#add}. + * + * @class Represents an area mark: the solid area between two series of + * connected line segments. Unsurprisingly, areas are used most frequently for + * area charts. + * + * <p>Just as a line represents a polyline, the <tt>Area</tt> mark type + * represents a <i>polygon</i>. However, an area is not an arbitrary polygon; + * vertices are paired either horizontally or vertically into parallel + * <i>spans</i>, and each span corresponds to an associated datum. Either the + * width or the height must be specified, but not both; this determines whether + * the area is horizontally-oriented or vertically-oriented. Like lines, areas + * can be stroked and filled with arbitrary colors. + * + * <p>See also the <a href="../../api/Area.html">Area guide</a>. + * + * @extends pv.Mark + */ +pv.Area = function() { + pv.Mark.call(this); +}; + +pv.Area.prototype = pv.extend(pv.Mark) + .property("width") + .property("height") + .property("lineWidth") + .property("strokeStyle") + .property("fillStyle") + .property("segmented") + .property("interpolate"); + +pv.Area.prototype.type = "area"; + +/** + * The width of a given span, in pixels; used for horizontal spans. If the width + * is specified, the height property should be 0 (the default). Either the top + * or bottom property should be used to space the spans vertically, typically as + * a multiple of the index. + * + * @type number + * @name pv.Area.prototype.width + */ + +/** + * The height of a given span, in pixels; used for vertical spans. If the height + * is specified, the width property should be 0 (the default). Either the left + * or right property should be used to space the spans horizontally, typically + * as a multiple of the index. + * + * @type number + * @name pv.Area.prototype.height + */ + +/** + * The width of stroked lines, in pixels; used in conjunction with + * <tt>strokeStyle</tt> to stroke the perimeter of the area. Unlike the + * {@link Line} mark type, the entire perimeter is stroked, rather than just one + * edge. The default value of this property is 1.5, but since the default stroke + * style is null, area marks are not stroked by default. + * + * <p>This property is <i>fixed</i> for non-segmented areas. See + * {@link pv.Mark}. + * + * @type number + * @name pv.Area.prototype.lineWidth + */ + +/** + * The style of stroked lines; used in conjunction with <tt>lineWidth</tt> to + * stroke the perimeter of the area. Unlike the {@link Line} mark type, the + * entire perimeter is stroked, rather than just one edge. The default value of + * this property is null, meaning areas are not stroked by default. + * + * <p>This property is <i>fixed</i> for non-segmented areas. See + * {@link pv.Mark}. + * + * @type string + * @name pv.Area.prototype.strokeStyle + * @see pv.color + */ + +/** + * The area fill style; if non-null, the interior of the polygon forming the + * area is filled with the specified color. The default value of this property + * is a categorical color. + * + * <p>This property is <i>fixed</i> for non-segmented areas. See + * {@link pv.Mark}. + * + * @type string + * @name pv.Area.prototype.fillStyle + * @see pv.color + */ + +/** + * Whether the area is segmented; whether variations in fill style, stroke + * style, and the other properties are treated as fixed. Rendering segmented + * areas is noticeably slower than non-segmented areas. + * + * <p>This property is <i>fixed</i>. See {@link pv.Mark}. + * + * @type boolean + * @name pv.Area.prototype.segmented + */ + +/** + * How to interpolate between values. Linear interpolation ("linear") is the + * default, producing a straight line between points. For piecewise constant + * functions (i.e., step functions), either "step-before" or "step-after" can be + * specified. + * + * <p>Note: this property is currently supported only on non-segmented areas. + * + * <p>This property is <i>fixed</i>. See {@link pv.Mark}. + * + * @type string + * @name pv.Area.prototype.interpolate + */ + +/** + * Default properties for areas. By default, there is no stroke and the fill + * style is a categorical color. + * + * @type pv.Area + */ +pv.Area.prototype.defaults = new pv.Area() + .extend(pv.Mark.prototype.defaults) + .lineWidth(1.5) + .fillStyle(defaultFillStyle) + .interpolate("linear"); + +/** + * Constructs a new area anchor with default properties. Areas support five + * different anchors:<ul> + * + * <li>top + * <li>left + * <li>center + * <li>bottom + * <li>right + * + * </ul>In addition to positioning properties (left, right, top bottom), the + * anchors support text rendering properties (text-align, text-baseline). Text is + * rendered to appear inside the area polygon. + * + * <p>To facilitate stacking of areas, the anchors are defined in terms of their + * opposite edge. For example, the top anchor defines the bottom property, such + * that the area grows upwards; the bottom anchor instead defines the top + * property, such that the area grows downwards. Of course, in general it is + * more robust to use panels and the cousin accessor to define stacked area + * marks; see {@link pv.Mark#scene} for an example. + * + * @param {string} name the anchor name; either a string or a property function. + * @returns {pv.Anchor} + */ +pv.Area.prototype.anchor = function(name) { + var area = this; + return pv.Mark.prototype.anchor.call(this, name) + .left(function() { + switch (this.name()) { + case "bottom": + case "top": + case "center": return area.left() + area.width() / 2; + case "right": return area.left() + area.width(); + } + return null; + }) + .right(function() { + switch (this.name()) { + case "bottom": + case "top": + case "center": return area.right() + area.width() / 2; + case "left": return area.right() + area.width(); + } + return null; + }) + .top(function() { + switch (this.name()) { + case "left": + case "right": + case "center": return area.top() + area.height() / 2; + case "bottom": return area.top() + area.height(); + } + return null; + }) + .bottom(function() { + switch (this.name()) { + case "left": + case "right": + case "center": return area.bottom() + area.height() / 2; + case "top": return area.bottom() + area.height(); + } + return null; + }) + .textAlign(function() { + switch (this.name()) { + case "bottom": + case "top": + case "center": return "center"; + case "right": return "right"; + } + return "left"; + }) + .textBaseline(function() { + switch (this.name()) { + case "right": + case "left": + case "center": return "middle"; + case "top": return "top"; + } + return "bottom"; + }); +}; + +/** + * @private Overrides the default behavior of {@link pv.Mark.buildImplied} such + * that the width and height are set to zero if null. + * + * @param s a node in the scene graph; the instance of the mark to build. + */ +pv.Area.prototype.buildImplied = function(s) { + if (s.height == null) s.height = 0; + if (s.width == null) s.width = 0; + pv.Mark.prototype.buildImplied.call(this, s); +}; + +/** @private */ +var pv_Area_specials = {left:1, top:1, right:1, bottom:1, width:1, height:1, name:1}; + +/** @private */ +pv.Area.prototype.bind = function() { + pv.Mark.prototype.bind.call(this); + var binds = this.binds, + properties = binds.properties, + specials = binds.specials = []; + for (var i = 0, n = properties.length; i < n; i++) { + var p = properties[i]; + if (p.name in pv_Area_specials) specials.push(p); + } +}; + +/** @private */ +pv.Area.prototype.buildInstance = function(s) { + if (this.index && !this.scene[0].segmented) { + this.buildProperties(s, this.binds.specials); + this.buildImplied(s); + } else { + pv.Mark.prototype.buildInstance.call(this, s); + } +}; +/** + * Constructs a new bar mark with default properties. Bars are not typically + * constructed directly, but by adding to a panel or an existing mark via + * {@link pv.Mark#add}. + * + * @class Represents a bar: an axis-aligned rectangle that can be stroked and + * filled. Bars are used for many chart types, including bar charts, histograms + * and Gantt charts. Bars can also be used as decorations, for example to draw a + * frame border around a panel; in fact, a panel is a special type (a subclass) + * of bar. + * + * <p>Bars can be positioned in several ways. Most commonly, one of the four + * corners is fixed using two margins, and then the width and height properties + * determine the extent of the bar relative to this fixed location. For example, + * using the bottom and left properties fixes the bottom-left corner; the width + * then extends to the right, while the height extends to the top. As an + * alternative to the four corners, a bar can be positioned exclusively using + * margins; this is convenient as an inset from the containing panel, for + * example. See {@link pv.Mark} for details on the prioritization of redundant + * positioning properties. + * + * <p>See also the <a href="../../api/Bar.html">Bar guide</a>. + * + * @extends pv.Mark + */ +pv.Bar = function() { + pv.Mark.call(this); +}; + +pv.Bar.prototype = pv.extend(pv.Mark) + .property("width") + .property("height") + .property("lineWidth") + .property("strokeStyle") + .property("fillStyle"); + +pv.Bar.prototype.type = "bar"; + +/** + * The width of the bar, in pixels. If the left position is specified, the bar + * extends rightward from the left edge; if the right position is specified, the + * bar extends leftward from the right edge. + * + * @type number + * @name pv.Bar.prototype.width + */ + +/** + * The height of the bar, in pixels. If the bottom position is specified, the + * bar extends upward from the bottom edge; if the top position is specified, + * the bar extends downward from the top edge. + * + * @type number + * @name pv.Bar.prototype.height + */ + +/** + * The width of stroked lines, in pixels; used in conjunction with + * <tt>strokeStyle</tt> to stroke the bar's border. + * + * @type number + * @name pv.Bar.prototype.lineWidth + */ + +/** + * The style of stroked lines; used in conjunction with <tt>lineWidth</tt> to + * stroke the bar's border. The default value of this property is null, meaning + * bars are not stroked by default. + * + * @type string + * @name pv.Bar.prototype.strokeStyle + * @see pv.color + */ + +/** + * The bar fill style; if non-null, the interior of the bar is filled with the + * specified color. The default value of this property is a categorical color. + * + * @type string + * @name pv.Bar.prototype.fillStyle + * @see pv.color + */ + +/** + * Default properties for bars. By default, there is no stroke and the fill + * style is a categorical color. + * + * @type pv.Bar + */ +pv.Bar.prototype.defaults = new pv.Bar() + .extend(pv.Mark.prototype.defaults) + .lineWidth(1.5) + .fillStyle(defaultFillStyle); + +/** + * Constructs a new bar anchor with default properties. Bars support five + * different anchors:<ul> + * + * <li>top + * <li>left + * <li>center + * <li>bottom + * <li>right + * + * </ul>In addition to positioning properties (left, right, top bottom), the + * anchors support text rendering properties (text-align, text-baseline). Text + * is rendered to appear inside the bar. + * + * <p>To facilitate stacking of bars, the anchors are defined in terms of their + * opposite edge. For example, the top anchor defines the bottom property, such + * that the bar grows upwards; the bottom anchor instead defines the top + * property, such that the bar grows downwards. Of course, in general it is more + * robust to use panels and the cousin accessor to define stacked bars; see + * {@link pv.Mark#scene} for an example. + * + * <p>Bar anchors also "smartly" specify position properties based on whether + * the derived mark type supports the width and height properties. If the + * derived mark type does not support these properties (e.g., dots), the + * position will be centered on the corresponding edge. Otherwise (e.g., bars), + * the position will be in the opposite side. + * + * @param {string} name the anchor name; either a string or a property function. + * @returns {pv.Anchor} + */ +pv.Bar.prototype.anchor = function(name) { + var bar = this; + return pv.Mark.prototype.anchor.call(this, name) + .left(function() { + switch (this.name()) { + case "bottom": + case "top": + case "center": return bar.left() + (this.properties.width ? 0 : (bar.width() / 2)); + case "right": return bar.left() + bar.width(); + } + return null; + }) + .right(function() { + switch (this.name()) { + case "bottom": + case "top": + case "center": return bar.right() + (this.properties.width ? 0 : (bar.width() / 2)); + case "left": return bar.right() + bar.width(); + } + return null; + }) + .top(function() { + switch (this.name()) { + case "left": + case "right": + case "center": return bar.top() + (this.properties.height ? 0 : (bar.height() / 2)); + case "bottom": return bar.top() + bar.height(); + } + return null; + }) + .bottom(function() { + switch (this.name()) { + case "left": + case "right": + case "center": return bar.bottom() + (this.properties.height ? 0 : (bar.height() / 2)); + case "top": return bar.bottom() + bar.height(); + } + return null; + }) + .textAlign(function() { + switch (this.name()) { + case "bottom": + case "top": + case "center": return "center"; + case "right": return "right"; + } + return "left"; + }) + .textBaseline(function() { + switch (this.name()) { + case "right": + case "left": + case "center": return "middle"; + case "top": return "top"; + } + return "bottom"; + }); +}; +/** + * Constructs a new dot mark with default properties. Dots are not typically + * constructed directly, but by adding to a panel or an existing mark via + * {@link pv.Mark#add}. + * + * @class Represents a dot; a dot is simply a sized glyph centered at a given + * point that can also be stroked and filled. The <tt>size</tt> property is + * proportional to the area of the rendered glyph to encourage meaningful visual + * encodings. Dots can visually encode up to eight dimensions of data, though + * this may be unwise due to integrality. See {@link pv.Mark} for details on the + * prioritization of redundant positioning properties. + * + * <p>See also the <a href="../../api/Dot.html">Dot guide</a>. + * + * @extends pv.Mark + */ +pv.Dot = function() { + pv.Mark.call(this); +}; + +pv.Dot.prototype = pv.extend(pv.Mark) + .property("size") + .property("shape") + .property("angle") + .property("lineWidth") + .property("strokeStyle") + .property("fillStyle"); + +pv.Dot.prototype.type = "dot"; + +/** + * The size of the dot, in square pixels. Square pixels are used such that the + * area of the dot is linearly proportional to the value of the size property, + * facilitating representative encodings. + * + * @see #radius + * @type number + * @name pv.Dot.prototype.size + */ + +/** + * The shape name. Several shapes are supported:<ul> + * + * <li>cross + * <li>triangle + * <li>diamond + * <li>square + * <li>tick + * <li>circle + * + * </ul>These shapes can be further changed using the {@link #angle} property; + * for instance, a cross can be turned into a plus by rotating. Similarly, the + * tick, which is vertical by default, can be rotated horizontally. Note that + * some shapes (cross and tick) do not have interior areas, and thus do not + * support fill style meaningfully. + * + * <p>Note: it may be more natural to use the {@link pv.Rule} mark for + * horizontal and vertical ticks. The tick shape is only necessary if angled + * ticks are needed. + * + * @type string + * @name pv.Dot.prototype.shape + */ + +/** + * The rotation angle, in radians. Used to rotate shapes, such as to turn a + * cross into a plus. + * + * @type number + * @name pv.Dot.prototype.angle + */ + +/** + * The width of stroked lines, in pixels; used in conjunction with + * <tt>strokeStyle</tt> to stroke the dot's shape. + * + * @type number + * @name pv.Dot.prototype.lineWidth + */ + +/** + * The style of stroked lines; used in conjunction with <tt>lineWidth</tt> to + * stroke the dot's shape. The default value of this property is a categorical + * color. + * + * @type string + * @name pv.Dot.prototype.strokeStyle + * @see pv.color + */ + +/** + * The fill style; if non-null, the interior of the dot is filled with the + * specified color. The default value of this property is null, meaning dots are + * not filled by default. + * + * @type string + * @name pv.Dot.prototype.fillStyle + * @see pv.color + */ + +/** + * Default properties for dots. By default, there is no fill and the stroke + * style is a categorical color. The default shape is "circle" with size 20. + * + * @type pv.Dot + */ +pv.Dot.prototype.defaults = new pv.Dot() + .extend(pv.Mark.prototype.defaults) + .size(20) + .shape("circle") + .lineWidth(1.5) + .strokeStyle(defaultStrokeStyle); + +/** + * Constructs a new dot anchor with default properties. Dots support five + * different anchors:<ul> + * + * <li>top + * <li>left + * <li>center + * <li>bottom + * <li>right + * + * </ul>In addition to positioning properties (left, right, top bottom), the + * anchors support text rendering properties (text-align, text-baseline). Text is + * rendered to appear outside the dot. Note that this behavior is different from + * other mark anchors, which default to rendering text <i>inside</i> the mark. + * + * <p>For consistency with the other mark types, the anchor positions are + * defined in terms of their opposite edge. For example, the top anchor defines + * the bottom property, such that a bar added to the top anchor grows upward. + * + * @param {string} name the anchor name; either a string or a property function. + * @returns {pv.Anchor} + */ +pv.Dot.prototype.anchor = function(name) { + var dot = this; + return pv.Mark.prototype.anchor.call(this, name) + .left(function(d) { + switch (this.name()) { + case "bottom": + case "top": + case "center": return dot.left(); + case "right": return dot.left() + dot.radius(); + } + return null; + }) + .right(function(d) { + switch (this.name()) { + case "bottom": + case "top": + case "center": return dot.right(); + case "left": return dot.right() + dot.radius(); + } + return null; + }) + .top(function(d) { + switch (this.name()) { + case "left": + case "right": + case "center": return dot.top(); + case "bottom": return dot.top() + dot.radius(); + } + return null; + }) + .bottom(function(d) { + switch (this.name()) { + case "left": + case "right": + case "center": return dot.bottom(); + case "top": return dot.bottom() + dot.radius(); + } + return null; + }) + .textAlign(function(d) { + switch (this.name()) { + case "left": return "right"; + case "bottom": + case "top": + case "center": return "center"; + } + return "left"; + }) + .textBaseline(function(d) { + switch (this.name()) { + case "right": + case "left": + case "center": return "middle"; + case "bottom": return "top"; + } + return "bottom"; + }); +}; + +/** + * Returns the radius of the dot, which is defined to be the square root of the + * {@link #size} property. + * + * @returns {number} the radius. + */ +pv.Dot.prototype.radius = function() { + return Math.sqrt(this.size()); +}; +/** + * Constructs a new label mark with default properties. Labels are not typically + * constructed directly, but by adding to a panel or an existing mark via + * {@link pv.Mark#add}. + * + * @class Represents a text label, allowing textual annotation of other marks or + * arbitrary text within the visualization. The character data must be plain + * text (unicode), though the text can be styled using the {@link #font} + * property. If rich text is needed, external HTML elements can be overlaid on + * the canvas by hand. + * + * <p>Labels are positioned using the box model, similarly to {@link Dot}. Thus, + * a label has no width or height, but merely a text anchor location. The text + * is positioned relative to this anchor location based on the + * {@link #textAlign}, {@link #textBaseline} and {@link #textMargin} properties. + * Furthermore, the text may be rotated using {@link #textAngle}. + * + * <p>Labels ignore events, so as to not interfere with event handlers on + * underlying marks, such as bars. In the future, we may support event handlers + * on labels. + * + * <p>See also the <a href="../../api/Label.html">Label guide</a>. + * + * @extends pv.Mark + */ +pv.Label = function() { + pv.Mark.call(this); +}; + +pv.Label.prototype = pv.extend(pv.Mark) + .property("text") + .property("font") + .property("textAngle") + .property("textStyle") + .property("textAlign") + .property("textBaseline") + .property("textMargin") + .property("textShadow"); + +pv.Label.prototype.type = "label"; + +/** + * The character data to render; a string. The default value of the text + * property is the identity function, meaning the label's associated datum will + * be rendered using its <tt>toString</tt>. + * + * @type string + * @name pv.Label.prototype.text + */ + +/** + * The font format, per the CSS Level 2 specification. The default font is "10px + * sans-serif", for consistency with the HTML 5 canvas element specification. + * Note that since text is not wrapped, any line-height property will be + * ignored. The other font-style, font-variant, font-weight, font-size and + * font-family properties are supported. + * + * @see <a href="http://www.w3.org/TR/CSS2/fonts.html#font-shorthand">CSS2 fonts</a> + * @type string + * @name pv.Label.prototype.font + */ + +/** + * The rotation angle, in radians. Text is rotated clockwise relative to the + * anchor location. For example, with the default left alignment, an angle of + * Math.PI / 2 causes text to proceed downwards. The default angle is zero. + * + * @type number + * @name pv.Label.prototype.textAngle + */ + +/** + * The text color. The name "textStyle" is used for consistency with "fillStyle" + * and "strokeStyle", although it might be better to rename this property (and + * perhaps use the same name as "strokeStyle"). The default color is black. + * + * @type string + * @name pv.Label.prototype.textStyle + * @see pv.color + */ + +/** + * The horizontal text alignment. One of:<ul> + * + * <li>left + * <li>center + * <li>right + * + * </ul>The default horizontal alignment is left. + * + * @type string + * @name pv.Label.prototype.textAlign + */ + +/** + * The vertical text alignment. One of:<ul> + * + * <li>top + * <li>middle + * <li>bottom + * + * </ul>The default vertical alignment is bottom. + * + * @type string + * @name pv.Label.prototype.textBaseline + */ + +/** + * The text margin; may be specified in pixels, or in font-dependent units (such + * as ".1ex"). The margin can be used to pad text away from its anchor location, + * in a direction dependent on the horizontal and vertical alignment + * properties. For example, if the text is left- and middle-aligned, the margin + * shifts the text to the right. The default margin is 3 pixels. + * + * @type number + * @name pv.Label.prototype.textMargin + */ + +/** + * A list of shadow effects to be applied to text, per the CSS Text Level 3 + * text-shadow property. An example specification is "0.1em 0.1em 0.1em + * rgba(0,0,0,.5)"; the first length is the horizontal offset, the second the + * vertical offset, and the third the blur radius. + * + * @see <a href="http://www.w3.org/TR/css3-text/#text-shadow">CSS3 text</a> + * @type string + * @name pv.Label.prototype.textShadow + */ + +/** + * Default properties for labels. See the individual properties for the default + * values. + * + * @type pv.Label + */ +pv.Label.prototype.defaults = new pv.Label() + .extend(pv.Mark.prototype.defaults) + .text(pv.identity) + .font("10px sans-serif") + .textAngle(0) + .textStyle("black") + .textAlign("left") + .textBaseline("bottom") + .textMargin(3); +/** + * Constructs a new line mark with default properties. Lines are not typically + * constructed directly, but by adding to a panel or an existing mark via + * {@link pv.Mark#add}. + * + * @class Represents a series of connected line segments, or <i>polyline</i>, + * that can be stroked with a configurable color and thickness. Each + * articulation point in the line corresponds to a datum; for <i>n</i> points, + * <i>n</i>-1 connected line segments are drawn. The point is positioned using + * the box model. Arbitrary paths are also possible, allowing radar plots and + * other custom visualizations. + * + * <p>Like areas, lines can be stroked and filled with arbitrary colors. In most + * cases, lines are only stroked, but the fill style can be used to construct + * arbitrary polygons. + * + * <p>See also the <a href="../../api/Line.html">Line guide</a>. + * + * @extends pv.Mark + */ +pv.Line = function() { + pv.Mark.call(this); +}; + +pv.Line.prototype = pv.extend(pv.Mark) + .property("lineWidth") + .property("strokeStyle") + .property("fillStyle") + .property("segmented") + .property("interpolate"); + +pv.Line.prototype.type = "line"; + +/** + * The width of stroked lines, in pixels; used in conjunction with + * <tt>strokeStyle</tt> to stroke the line. + * + * @type number + * @name pv.Line.prototype.lineWidth + */ + +/** + * The style of stroked lines; used in conjunction with <tt>lineWidth</tt> to + * stroke the line. The default value of this property is a categorical color. + * + * @type string + * @name pv.Line.prototype.strokeStyle + * @see pv.color + */ + +/** + * The line fill style; if non-null, the interior of the line is closed and + * filled with the specified color. The default value of this property is a + * null, meaning that lines are not filled by default. + * + * @type string + * @name pv.Line.prototype.fillStyle + * @see pv.color + */ + +/** + * Whether the line is segmented; whether variations in stroke style, line width + * and the other properties are treated as fixed. Rendering segmented lines is + * noticeably slower than non-segmented lines. + * + * <p>This property is <i>fixed</i>. See {@link pv.Mark}. + * + * @type boolean + * @name pv.Line.prototype.segmented + */ + +/** + * How to interpolate between values. Linear interpolation ("linear") is the + * default, producing a straight line between points. For piecewise constant + * functions (i.e., step functions), either "step-before" or "step-after" can be + * specified. + * + * <p>Note: this property is currently supported only on non-segmented lines. + * + * <p>This property is <i>fixed</i>. See {@link pv.Mark}. + * + * @type string + * @name pv.Line.prototype.interpolate + */ + +/** + * Default properties for lines. By default, there is no fill and the stroke + * style is a categorical color. The default interpolation is linear. + * + * @type pv.Line + */ +pv.Line.prototype.defaults = new pv.Line() + .extend(pv.Mark.prototype.defaults) + .lineWidth(1.5) + .strokeStyle(defaultStrokeStyle) + .interpolate("linear"); + +/** @private */ +var pv_Line_specials = {left:1, top:1, right:1, bottom:1, name:1}; + +/** @private */ +pv.Line.prototype.bind = function() { + pv.Mark.prototype.bind.call(this); + var binds = this.binds, + properties = binds.properties, + specials = binds.specials = []; + for (var i = 0, n = properties.length; i < n; i++) { + var p = properties[i]; + if (p.name in pv_Line_specials) specials.push(p); + } +}; + +/** @private */ +pv.Line.prototype.buildInstance = function(s) { + if (this.index && !this.scene[0].segmented) { + this.buildProperties(s, this.binds.specials); + this.buildImplied(s); + } else { + pv.Mark.prototype.buildInstance.call(this, s); + } +}; +/** + * Constructs a new rule with default properties. Rules are not typically + * constructed directly, but by adding to a panel or an existing mark via + * {@link pv.Mark#add}. + * + * @class Represents a horizontal or vertical rule. Rules are frequently used + * for axes and grid lines. For example, specifying only the bottom property + * draws horizontal rules, while specifying only the left draws vertical + * rules. Rules can also be used as thin bars. The visual style is controlled in + * the same manner as lines. + * + * <p>Rules are positioned exclusively the standard box model properties. The + * following combinations of properties are supported: + * + * <table> + * <thead><th style="width:12em;">Properties</th><th>Orientation</th></thead> + * <tbody> + * <tr><td>left</td><td>vertical</td></tr> + * <tr><td>right</td><td>vertical</td></tr> + * <tr><td>left, bottom, top</td><td>vertical</td></tr> + * <tr><td>right, bottom, top</td><td>vertical</td></tr> + * <tr><td>top</td><td>horizontal</td></tr> + * <tr><td>bottom</td><td>horizontal</td></tr> + * <tr><td>top, left, right</td><td>horizontal</td></tr> + * <tr><td>bottom, left, right</td><td>horizontal</td></tr> + * <tr><td>left, top, height</td><td>vertical</td></tr> + * <tr><td>left, bottom, height</td><td>vertical</td></tr> + * <tr><td>right, top, height</td><td>vertical</td></tr> + * <tr><td>right, bottom, height</td><td>vertical</td></tr> + * <tr><td>left, top, width</td><td>horizontal</td></tr> + * <tr><td>left, bottom, width</td><td>horizontal</td></tr> + * <tr><td>right, top, width</td><td>horizontal</td></tr> + * <tr><td>right, bottom, width</td><td>horizontal</td></tr> + * </tbody> + * </table> + * + * <p>Small rules can be used as tick marks; alternatively, a {@link Dot} with + * the "tick" shape can be used. + * + * <p>See also the <a href="../../api/Rule.html">Rule guide</a>. + * + * @see pv.Line + * @extends pv.Mark + */ +pv.Rule = function() { + pv.Mark.call(this); +}; + +pv.Rule.prototype = pv.extend(pv.Mark) + .property("width") + .property("height") + .property("lineWidth") + .property("strokeStyle"); + +pv.Rule.prototype.type = "rule"; + +/** + * The width of the rule, in pixels. If the left position is specified, the rule + * extends rightward from the left edge; if the right position is specified, the + * rule extends leftward from the right edge. + * + * @type number + * @name pv.Rule.prototype.width + */ + +/** + * The height of the rule, in pixels. If the bottom position is specified, the + * rule extends upward from the bottom edge; if the top position is specified, + * the rule extends downward from the top edge. + * + * @type number + * @name pv.Rule.prototype.height + */ + +/** + * The width of stroked lines, in pixels; used in conjunction with + * <tt>strokeStyle</tt> to stroke the rule. The default value is 1 pixel. + * + * @type number + * @name pv.Rule.prototype.lineWidth + */ + +/** + * The style of stroked lines; used in conjunction with <tt>lineWidth</tt> to + * stroke the rule. The default value of this property is black. + * + * @type string + * @name pv.Rule.prototype.strokeStyle + * @see pv.color + */ + +/** + * Default properties for rules. By default, a single-pixel black line is + * stroked. + * + * @type pv.Rule + */ +pv.Rule.prototype.defaults = new pv.Rule() + .extend(pv.Mark.prototype.defaults) + .lineWidth(1) + .strokeStyle("black"); + +/** + * Constructs a new rule anchor with default properties. Rules support five + * different anchors:<ul> + * + * <li>top + * <li>left + * <li>center + * <li>bottom + * <li>right + * + * </ul>In addition to positioning properties (left, right, top bottom), the + * anchors support text rendering properties (text-align, text-baseline). Text is + * rendered to appear outside the rule. Note that this behavior is different + * from other mark anchors, which default to rendering text <i>inside</i> the + * mark. + * + * <p>For consistency with the other mark types, the anchor positions are + * defined in terms of their opposite edge. For example, the top anchor defines + * the bottom property, such that a bar added to the top anchor grows upward. + * + * @param {string} name the anchor name; either a string or a property function. + * @returns {pv.Anchor} + */ +pv.Rule.prototype.anchor = function(name) { + return pv.Bar.prototype.anchor.call(this, name) + .textAlign(function(d) { + switch (this.name()) { + case "left": return "right"; + case "bottom": + case "top": + case "center": return "center"; + case "right": return "left"; + } + }) + .textBaseline(function(d) { + switch (this.name()) { + case "right": + case "left": + case "center": return "middle"; + case "top": return "bottom"; + case "bottom": return "top"; + } + }); +}; + +/** + * @private Overrides the default behavior of {@link pv.Mark.buildImplied} to + * determine the orientation (vertical or horizontal) of the rule. + * + * @param s a node in the scene graph; the instance of the rule to build. + */ +pv.Rule.prototype.buildImplied = function(s) { + var l = s.left, r = s.right, t = s.top, b = s.bottom; + + /* Determine horizontal or vertical orientation. */ + if ((s.width != null) + || ((l == null) && (r == null)) + || ((r != null) && (l != null))) { + s.height = 0; + } else { + s.width = 0; + } + + pv.Mark.prototype.buildImplied.call(this, s); +}; +/** + * Constructs a new, empty panel with default properties. Panels, with the + * exception of the root panel, are not typically constructed directly; instead, + * they are added to an existing panel or mark via {@link pv.Mark#add}. + * + * @class Represents a container mark. Panels allow repeated or nested + * structures, commonly used in small multiple displays where a small + * visualization is tiled to facilitate comparison across one or more + * dimensions. Other types of visualizations may benefit from repeated and + * possibly overlapping structure as well, such as stacked area charts. Panels + * can also offset the position of marks to provide padding from surrounding + * content. + * + * <p>All Protovis displays have at least one panel; this is the root panel to + * which marks are rendered. The box model properties (four margins, width and + * height) are used to offset the positions of contained marks. The data + * property determines the panel count: a panel is generated once per associated + * datum. When nested panels are used, property functions can declare additional + * arguments to access the data associated with enclosing panels. + * + * <p>Panels can be rendered inline, facilitating the creation of sparklines. + * This allows designers to reuse browser layout features, such as text flow and + * tables; designers can also overlay HTML elements such as rich text and + * images. + * + * <p>All panels have a <tt>children</tt> array (possibly empty) containing the + * child marks in the order they were added. Panels also have a <tt>root</tt> + * field which points to the root (outermost) panel; the root panel's root field + * points to itself. + * + * <p>See also the <a href="../../api/">Protovis guide</a>. + * + * @extends pv.Bar + */ +pv.Panel = function() { + pv.Bar.call(this); + + /** + * The child marks; zero or more {@link pv.Mark}s in the order they were + * added. + * + * @see #add + * @type pv.Mark[] + */ + this.children = []; + this.root = this; + + /** + * The internal $dom field is set by the Protovis loader; see lang/init.js. It + * refers to the script element that contains the Protovis specification, so + * that the panel knows where in the DOM to insert the generated SVG element. + * + * @private + */ + this.$dom = pv.Panel.$dom; +}; + +pv.Panel.prototype = pv.extend(pv.Bar) + .property("canvas") + .property("overflow"); + +pv.Panel.prototype.type = "panel"; + +/** + * The canvas element; either the string ID of the canvas element in the current + * document, or a reference to the canvas element itself. If null, a canvas + * element will be created and inserted into the document at the location of the + * script element containing the current Protovis specification. This property + * only applies to root panels and is ignored on nested panels. + * + * <p>Note: the "canvas" element here refers to a <tt>div</tt> (or other suitable + * HTML container element), <i>not</i> a <tt>canvas</tt> element. The name of + * this property is a historical anachronism from the first implementation that + * used HTML 5 canvas, rather than SVG. + * + * @type string + * @name pv.Panel.prototype.canvas + */ + +/** + * Default properties for panels. By default, the margins are zero, the fill + * style is transparent. + * + * @type pv.Panel + */ +pv.Panel.prototype.defaults = new pv.Panel() + .extend(pv.Bar.prototype.defaults) + .fillStyle(null) + .overflow("visible"); + +/** + * Returns an anchor with the specified name. This method is overridden since + * the behavior of Panel anchors is slightly different from normal anchors: + * adding to an anchor adds to the anchor target's, rather than the anchor + * target's parent. To avoid double margins, we override the anchor's proto so + * that the margins are zero. + * + * @param {string} name the anchor name; either a string or a property function. + * @returns {pv.Anchor} the new anchor. + */ +pv.Panel.prototype.anchor = function(name) { + + /* A "view" of this panel whose margins appear to be zero. */ + function z() { return 0; } + z.prototype = this; + z.prototype.left = z.prototype.right = z.prototype.top = z.prototype.bottom = z; + + var anchor = pv.Bar.prototype.anchor.call(new z(), name) + .data(function(d) { return [d]; }); + anchor.parent = this; + return anchor; +}; + +/** + * Adds a new mark of the specified type to this panel. Unlike the normal + * {@link Mark#add} behavior, adding a mark to a panel does not cause the mark + * to inherit from the panel. Since the contained marks are offset by the panel + * margins already, inheriting properties is generally undesirable; of course, + * it is always possible to change this behavior by calling {@link Mark#extend} + * explicitly. + * + * @param {function} type the type of the new mark to add. + * @returns {pv.Mark} the new mark. + */ +pv.Panel.prototype.add = function(type) { + var child = new type(); + child.parent = this; + child.root = this.root; + child.childIndex = this.children.length; + this.children.push(child); + return child; +}; + +/** @private TODO */ +pv.Panel.prototype.bind = function() { + pv.Mark.prototype.bind.call(this); + for (var i = 0; i < this.children.length; i++) { + this.children[i].bind(); + } +}; + +/** + * @private Evaluates all of the properties for this panel for the specified + * instance <tt>s</tt> in the scene graph, including recursively building the + * scene graph for child marks. + * + * @param s a node in the scene graph; the instance of the panel to build. + * @see Mark#scene + */ +pv.Panel.prototype.buildInstance = function(s) { + pv.Bar.prototype.buildInstance.call(this, s); + if (!s.children) s.children = []; + + /* + * Build each child, passing in the parent (this panel) scene graph node. The + * child mark's scene is initialized from the corresponding entry in the + * existing scene graph, such that properties from the previous build can be + * reused; this is largely to facilitate the recycling of SVG elements. + */ + for (var i = 0; i < this.children.length; i++) { + this.children[i].scene = s.children[i]; // possibly undefined + this.children[i].build(); + } + + /* + * Once the child marks have been built, the new scene graph nodes are removed + * from the child marks and placed into the scene graph. The nodes cannot + * remain on the child nodes because this panel (or a parent panel) may be + * instantiated multiple times! + */ + for (var i = 0; i < this.children.length; i++) { + s.children[i] = this.children[i].scene; + delete this.children[i].scene; + } + + /* Delete any expired child scenes, should child marks have been removed. */ + s.children.length = this.children.length; +}; + +/** + * @private Computes the implied properties for this panel for the specified + * instance <tt>s</tt> in the scene graph. Panels have two implied + * properties:<ul> + * + * <li>The <tt>canvas</tt> property references the DOM element, typically a DIV, + * that contains the SVG element that is used to display the visualization. This + * property may be specified as a string, referring to the unique ID of the + * element in the DOM. The string is converted to a reference to the DOM + * element. The width and height of the SVG element is inferred from this DOM + * element. If no canvas property is specified, a new SVG element is created and + * inserted into the document, using the panel dimensions; see + * {@link #createCanvas}. + * + * <li>The <tt>children</tt> array, while not a property per se, contains the + * scene graph for each child mark. This array is initialized to be empty, and + * is populated above in {@link #buildInstance}. + * + * </ul>The current implementation creates the SVG element, if necessary, during + * the build phase; in the future, it may be preferrable to move this to the + * update phase, although then the canvas property would be undefined. In + * addition, DOM inspection is necessary to define the implied width and height + * properties that may be inferred from the DOM. + * + * @param s a node in the scene graph; the instance of the panel to build. + */ +pv.Panel.prototype.buildImplied = function(s) { + if (!this.parent) { + var c = s.canvas; + if (c) { + if (typeof c == "string") c = document.getElementById(c); + + /* Clear the container if it's not associated with this panel. */ + if (c.$panel != this) { + c.$panel = this; + c.innerHTML = ""; + } + + /* If width and height weren't specified, inspect the container. */ + var w, h; + if (s.width == null) { + w = parseFloat(pv.css(c, "width")); + s.width = w - s.left - s.right; + } + if (s.height == null) { + h = parseFloat(pv.css(c, "height")); + s.height = h - s.top - s.bottom; + } + } else if (s.$canvas) { + + /* + * If the canvas property is null, and we previously created a canvas for + * this scene node, reuse the previous canvas rather than creating a new + * one. + */ + c = s.$canvas; + } else { + + /** + * Returns the last element in the current document's body. The canvas + * element is appended to this last element if another DOM element has not + * already been specified via the <tt>$dom</tt> field. + */ + function lastElement() { + var node = document.body; + while (node.lastChild && node.lastChild.tagName) { + node = node.lastChild; + } + return (node == document.body) ? node : node.parentNode; + } + + /* Insert a new container into the DOM. */ + c = s.$canvas = document.createElement("span"); + this.$dom // script element for text/javascript+protovis + ? this.$dom.parentNode.insertBefore(c, this.$dom) + : lastElement().appendChild(c); + } + s.canvas = c; + } + pv.Bar.prototype.buildImplied.call(this, s); +}; +/** + * Constructs a new dot mark with default properties. Images are not typically + * constructed directly, but by adding to a panel or an existing mark via + * {@link pv.Mark#add}. + * + * @class Represents an image. Images share the same layout and style properties as + * bars, in conjunction with an external image such as PNG or JPEG. The image is + * specified via the {@link #url} property. The fill, if specified, appears + * beneath the image, while the optional stroke appears above the image. + * + * <p>TODO Restore support for dynamic images (such as heatmaps). These were + * supported in the canvas implementation using the pixel buffer API; although + * SVG does not support pixel manipulation, it is possible to embed a canvas + * element in SVG using foreign objects. + * + * <p>TODO Allow different modes of image placement: "scale" -- scale and + * preserve aspect ratio, "tile" -- repeat the image, "center" -- center the + * image, "fill" -- scale without preserving aspect ratio. + * + * <p>See {@link pv.Bar} for details on positioning properties. + * + * @extends pv.Bar + */ +pv.Image = function() { + pv.Bar.call(this); +}; + +pv.Image.prototype = pv.extend(pv.Bar) + .property("url"); + +pv.Image.prototype.type = "image"; + +/** + * The URL of the image to display. The set of supported image types is + * browser-dependent; PNG and JPEG are recommended. + * + * @type string + * @name pv.Image.prototype.url + */ + +/** + * Default properties for images. By default, there is no stroke or fill style. + * + * @type pv.Image + */ +pv.Image.prototype.defaults = new pv.Image() + .extend(pv.Bar.prototype.defaults) + .fillStyle(null); +/** + * Constructs a new wedge with default properties. Wedges are not typically + * constructed directly, but by adding to a panel or an existing mark via + * {@link pv.Mark#add}. + * + * @class Represents a wedge, or pie slice. Specified in terms of start and end + * angle, inner and outer radius, wedges can be used to construct donut charts + * and polar bar charts as well. If the {@link #angle} property is used, the end + * angle is implied by adding this value to start angle. By default, the start + * angle is the previously-generated wedge's end angle. This design allows + * explicit control over the wedge placement if desired, while offering + * convenient defaults for the construction of radial graphs. + * + * <p>The center point of the circle is positioned using the standard box model. + * The wedge can be stroked and filled, similar to {link Bar}. + * + * <p>See also the <a href="../../api/Wedge.html">Wedge guide</a>. + * + * @extends pv.Mark + */ +pv.Wedge = function() { + pv.Mark.call(this); +}; + +pv.Wedge.prototype = pv.extend(pv.Mark) + .property("startAngle") + .property("endAngle") + .property("angle") + .property("innerRadius") + .property("outerRadius") + .property("lineWidth") + .property("strokeStyle") + .property("fillStyle"); + +pv.Wedge.prototype.type = "wedge"; + +/** + * The start angle of the wedge, in radians. The start angle is measured + * clockwise from the 3 o'clock position. The default value of this property is + * the end angle of the previous instance (the {@link Mark#sibling}), or -PI / 2 + * for the first wedge; for pie and donut charts, typically only the + * {@link #angle} property needs to be specified. + * + * @type number + * @name pv.Wedge.prototype.startAngle + */ + +/** + * The end angle of the wedge, in radians. If not specified, the end angle is + * implied as the start angle plus the {@link #angle}. + * + * @type number + * @name pv.Wedge.prototype.endAngle + */ + +/** + * The angular span of the wedge, in radians. This property is used if end angle + * is not specified. + * + * @type number + * @name pv.Wedge.prototype.angle + */ + +/** + * The inner radius of the wedge, in pixels. The default value of this property + * is zero; a positive value will produce a donut slice rather than a pie slice. + * The inner radius can vary per-wedge. + * + * @type number + * @name pv.Wedge.prototype.innerRadius + */ + +/** + * The outer radius of the wedge, in pixels. This property is required. For + * pies, only this radius is required; for donuts, the inner radius must be + * specified as well. The outer radius can vary per-wedge. + * + * @type number + * @name pv.Wedge.prototype.outerRadius + */ + +/** + * The width of stroked lines, in pixels; used in conjunction with + * <tt>strokeStyle</tt> to stroke the wedge's border. + * + * @type number + * @name pv.Wedge.prototype.lineWidth + */ + +/** + * The style of stroked lines; used in conjunction with <tt>lineWidth</tt> to + * stroke the wedge's border. The default value of this property is null, + * meaning wedges are not stroked by default. + * + * @type string + * @name pv.Wedge.prototype.strokeStyle + * @see pv.color + */ + +/** + * The wedge fill style; if non-null, the interior of the wedge is filled with + * the specified color. The default value of this property is a categorical + * color. + * + * @type string + * @name pv.Wedge.prototype.fillStyle + * @see pv.color + */ + +/** + * Default properties for wedges. By default, there is no stroke and the fill + * style is a categorical color. + * + * @type pv.Wedge + */ +pv.Wedge.prototype.defaults = new pv.Wedge() + .extend(pv.Mark.prototype.defaults) + .startAngle(function() { + var s = this.sibling(); + return s ? s.endAngle : -Math.PI / 2; + }) + .innerRadius(0) + .lineWidth(1.5) + .strokeStyle(null) + .fillStyle(defaultFillStyle.by(pv.index)); + +/** + * Returns the mid-radius of the wedge, which is defined as half-way between the + * inner and outer radii. + * + * @see #innerRadius + * @see #outerRadius + * @returns {number} the mid-radius, in pixels. + */ +pv.Wedge.prototype.midRadius = function() { + return (this.innerRadius() + this.outerRadius()) / 2; +}; + +/** + * Returns the mid-angle of the wedge, which is defined as half-way between the + * start and end angles. + * + * @see #startAngle + * @see #endAngle + * @returns {number} the mid-angle, in radians. + */ +pv.Wedge.prototype.midAngle = function() { + return (this.startAngle() + this.endAngle()) / 2; +}; + +/** + * Constructs a new wedge anchor with default properties. Wedges support five + * different anchors:<ul> + * + * <li>outer + * <li>inner + * <li>center + * <li>start + * <li>end + * + * </ul>In addition to positioning properties (left, right, top bottom), the + * anchors support text rendering properties (text-align, text-baseline, + * textAngle). Text is rendered to appear inside the wedge. + * + * @param {string} name the anchor name; either a string or a property function. + * @returns {pv.Anchor} + */ +pv.Wedge.prototype.anchor = function(name) { + var w = this; + return pv.Mark.prototype.anchor.call(this, name) + .left(function() { + switch (this.name()) { + case "outer": return w.left() + w.outerRadius() * Math.cos(w.midAngle()); + case "inner": return w.left() + w.innerRadius() * Math.cos(w.midAngle()); + case "start": return w.left() + w.midRadius() * Math.cos(w.startAngle()); + case "center": return w.left() + w.midRadius() * Math.cos(w.midAngle()); + case "end": return w.left() + w.midRadius() * Math.cos(w.endAngle()); + } + }) + .right(function() { + switch (this.name()) { + case "outer": return w.right() + w.outerRadius() * Math.cos(w.midAngle()); + case "inner": return w.right() + w.innerRadius() * Math.cos(w.midAngle()); + case "start": return w.right() + w.midRadius() * Math.cos(w.startAngle()); + case "center": return w.right() + w.midRadius() * Math.cos(w.midAngle()); + case "end": return w.right() + w.midRadius() * Math.cos(w.endAngle()); + } + }) + .top(function() { + switch (this.name()) { + case "outer": return w.top() + w.outerRadius() * Math.sin(w.midAngle()); + case "inner": return w.top() + w.innerRadius() * Math.sin(w.midAngle()); + case "start": return w.top() + w.midRadius() * Math.sin(w.startAngle()); + case "center": return w.top() + w.midRadius() * Math.sin(w.midAngle()); + case "end": return w.top() + w.midRadius() * Math.sin(w.endAngle()); + } + }) + .bottom(function() { + switch (this.name()) { + case "outer": return w.bottom() + w.outerRadius() * Math.sin(w.midAngle()); + case "inner": return w.bottom() + w.innerRadius() * Math.sin(w.midAngle()); + case "start": return w.bottom() + w.midRadius() * Math.sin(w.startAngle()); + case "center": return w.bottom() + w.midRadius() * Math.sin(w.midAngle()); + case "end": return w.bottom() + w.midRadius() * Math.sin(w.endAngle()); + } + }) + .textAlign(function() { + switch (this.name()) { + case "outer": return pv.Wedge.upright(w.midAngle()) ? "right" : "left"; + case "inner": return pv.Wedge.upright(w.midAngle()) ? "left" : "right"; + } + return "center"; + }) + .textBaseline(function() { + switch (this.name()) { + case "start": return pv.Wedge.upright(w.startAngle()) ? "top" : "bottom"; + case "end": return pv.Wedge.upright(w.endAngle()) ? "bottom" : "top"; + } + return "middle"; + }) + .textAngle(function() { + var a = 0; + switch (this.name()) { + case "center": + case "inner": + case "outer": a = w.midAngle(); break; + case "start": a = w.startAngle(); break; + case "end": a = w.endAngle(); break; + } + return pv.Wedge.upright(a) ? a : (a + Math.PI); + }); +}; + +/** + * Returns true if the specified angle is considered "upright", as in, text + * rendered at that angle would appear upright. If the angle is not upright, + * text is rotated 180 degrees to be upright, and the text alignment properties + * are correspondingly changed. + * + * @param {number} angle an angle, in radius. + * @returns {boolean} true if the specified angle is upright. + */ +pv.Wedge.upright = function(angle) { + angle = angle % (2 * Math.PI); + angle = (angle < 0) ? (2 * Math.PI + angle) : angle; + return (angle < Math.PI / 2) || (angle > 3 * Math.PI / 2); +}; + +/** + * @private Overrides the default behavior of {@link pv.Mark.buildImplied} such + * that the end angle is computed from the start angle and angle (angular span) + * if not specified. + * + * @param s a node in the scene graph; the instance of the wedge to build. + */ +pv.Wedge.prototype.buildImplied = function(s) { + pv.Mark.prototype.buildImplied.call(this, s); + + /* + * TODO If the angle or endAngle is updated by an event handler, the implied + * properties won't recompute correctly, so this will lead to potentially + * buggy redraw. How to re-evaluate implied properties on update? + */ + if (s.endAngle == null) s.endAngle = s.startAngle + s.angle; + if (s.angle == null) s.angle = s.endAngle - s.startAngle; +}; +/** + * @ignore + * @namespace + */ +pv.Layout = {}; +/** + * Returns a new grid layout. + * + * @class A grid layout with regularly-sized rows and columns. <img + * src="../grid.png" width="160" height="160" align="right"> The number of rows + * and columns are determined from the array, which should be in row-major + * order. For example, the 2×3 array: + * + * <pre>1 2 3 + * 4 5 6</pre> + * + * should be represented as: + * + * <pre>[[1, 2, 3], [4, 5, 6]]</pre> + * + * If your data is in column-major order, you can use {@link pv.transpose} to + * transpose it. + * + * <p>This layout defines left, top, width, height and data properties. The data + * property will be the associated element in the array. For example, if the + * array is a two-dimensional array of values in the range [0,1], a simple + * heatmap can be generated as: + * + * <pre>.add(pv.Bar) + * .extend(pv.Layout.grid(array)) + * .fillStyle(pv.ramp("white", "black"))</pre> + * + * By default, the grid fills the full width and height of the parent panel. + * + * @param {array[]} arrays an array of arrays. + * @returns {pv.Layout.grid} a grid layout. + */ +pv.Layout.grid = function(arrays) { + var rows = arrays.length, cols = arrays[0].length; + + /** @private */ + function w() { return this.parent.width() / cols; } + + /** @private */ + function h() { return this.parent.height() / rows; } + + /* A dummy mark, like an anchor, which the caller extends. */ + return new pv.Mark() + .data(pv.blend(arrays)) + .left(function() { return w.call(this) * (this.index % cols); }) + .top(function() { return h.call(this) * Math.floor(this.index / cols); }) + .width(w) + .height(h); +}; +/** + * Returns a new stack layout. + * + * @class A layout for stacking marks vertically or horizontally, using the + * <i>cousin</i> instance. This layout is designed to be used for one of the + * four positional properties in the box model, and changes behavior depending + * on the property being evaluated:<ul> + * + * <li>bottom: cousin.bottom + cousin.height + * <li>top: cousin.top + cousin.height + * <li>left: cousin.left + cousin.width + * <li>right: cousin.right + cousin.width + * + * </ul>If no cousin instance is available (for example, for first instance), + * the specified offset is used. If no offset is specified, zero is used. For + * example, + * + * <pre>new pv.Panel() + * .width(150).height(150) + * .add(pv.Panel) + * .data([[1, 1.2, 1.7, 1.5, 1.7], + * [.5, 1, .8, 1.1, 1.3], + * [.2, .5, .8, .9, 1]]) + * .add(pv.Area) + * .data(function(d) d) + * .bottom(pv.Layout.stack()) + * .height(function(d) d * 40) + * .left(function() this.index * 35) + * .root.render();</pre> + * + * specifies a vertically-stacked area chart. + * + * @returns {pv.Layout.stack} a stack property function. + * @see pv.Mark#cousin + */ +pv.Layout.stack = function() { + /** @private */ + var offset = function() { return 0; }; + + /** @private */ + function layout() { + + /* Find the previous visible parent instance. */ + var i = this.parent.index, p, c; + while ((i-- > 0) && !c) { + p = this.parent.scene[i]; + if (p.visible) c = p.children[this.childIndex][this.index]; + } + + if (c) { + switch (property) { + case "bottom": return c.bottom + c.height; + case "top": return c.top + c.height; + case "left": return c.left + c.width; + case "right": return c.right + c.width; + } + } + + return offset.apply(this, arguments); + } + + /** + * Sets the offset for this stack layout. The offset can either be specified + * as a function or as a constant. If a function, the function is invoked in + * the same context as a normal property function: <tt>this</tt> refers to the + * mark, and the arguments are the full data stack. By default the offset is + * zero. + * + * @function + * @name pv.Layout.stack.prototype.offset + * @param {function} f offset function, or constant value. + * @returns {pv.Layout.stack} this. + */ + layout.offset = function(f) { + offset = (f instanceof Function) ? f : function() { return f; }; + return this; + }; + + return layout; +}; +// TODO share code with Treemap +// TODO vertical / horizontal orientation? + +/** + * Returns a new icicle tree layout. + * + * @class A tree layout in the form of an icicle. <img src="../icicle.png" + * width="160" height="160" align="right"> The first row corresponds to the root + * of the tree; subsequent rows correspond to each tier. Rows are subdivided + * into cells based on the size of nodes, per {@link #size}. Within a row, cells + * are sorted by size. + * + * <p>This tree layout is intended to be extended (see {@link pv.Mark#extend}) + * by a {@link pv.Bar}. The data property returns an array of nodes for use by + * other property functions. The following node attributes are supported: + * + * <ul> + * <li><tt>left</tt> - the cell left position. + * <li><tt>top</tt> - the cell top position. + * <li><tt>width</tt> - the cell width. + * <li><tt>height</tt> - the cell height. + * <li><tt>depth</tt> - the node depth (tier; the root is 0). + * <li><tt>keys</tt> - an array of string keys for the node. + * <li><tt>size</tt> - the aggregate node size. + * <li><tt>children</tt> - child nodes, if any. + * <li><tt>data</tt> - the associated tree element, for leaf nodes. + * </ul> + * + * To produce a default icicle layout, say: + * + * <pre>.add(pv.Bar) + * .extend(pv.Layout.icicle(tree))</pre> + * + * To customize the tree to highlight leaf nodes bigger than 10,000 (1E4), you + * might say: + * + * <pre>.add(pv.Bar) + * .extend(pv.Layout.icicle(tree)) + * .fillStyle(function(n) n.data > 1e4 ? "#ff0" : "#fff")</pre> + * + * The format of the <tt>tree</tt> argument is any hierarchical object whose + * leaf nodes are numbers corresponding to their size. For an example, and + * information on how to convert tabular data into such a tree, see + * {@link pv.Tree}. If the leaf nodes are not numbers, a {@link #size} function + * can be specified to override how the tree is interpreted. This size function + * can also be used to transform the data. + * + * <p>By default, the icicle fills the full width and height of the parent + * panel. An optional root key can be specified using {@link #root} for + * convenience. + * + * @param tree a tree (an object) who leaf attributes have sizes. + * @returns {pv.Layout.icicle} a tree layout. + */ +pv.Layout.icicle = function(tree) { + var keys = [], sizeof = Number; + + /** @private */ + function accumulate(map) { + var node = {size: 0, children: [], keys: keys.slice()}; + for (var key in map) { + var child = map[key], size = sizeof(child); + keys.push(key); + if (isNaN(size)) { + child = accumulate(child); + } else { + child = {size: size, data: child, keys: keys.slice()}; + } + node.children.push(child); + node.size += child.size; + keys.pop(); + } + node.children.sort(function(a, b) { return b.size - a.size; }); + return node; + } + + /** @private */ + function scale(node, k) { + node.size *= k; + if (node.children) { + for (var i = 0; i < node.children.length; i++) { + scale(node.children[i], k); + } + } + } + + /** @private */ + function depth(node, i) { + i = i ? (i + 1) : 1; + return node.children + ? pv.max(node.children, function(n) { return depth(n, i); }) + : i; + } + + /** @private */ + function layout(node) { + if (node.children) { + icify(node); + for (var i = 0; i < node.children.length; i++) { + layout(node.children[i]); + } + } + } + + /** @private */ + function icify(node) { + var left = node.left; + for (var i = 0; i < node.children.length; i++) { + var child = node.children[i], width = (child.size / node.size) * node.width; + child.left = left; + child.top = node.top + node.height; + child.width = width; + child.height = node.height; + child.depth = node.depth + 1; + left += width; + if (child.children) { + icify(child); + } + } + } + + /** @private */ + function flatten(node, array) { + if (node.children) { + for (var i = 0; i < node.children.length; i++) { + flatten(node.children[i], array); + } + } + array.push(node) + return array; + } + + /** @private */ + function data() { + var root = accumulate(tree); + root.top = 0; + root.left = 0; + root.width = this.parent.width(); + root.height = this.parent.height() / depth(root); + root.depth = 0; + layout(root); + return flatten(root, []).reverse(); + } + + /* A dummy mark, like an anchor, which the caller extends. */ + var mark = new pv.Mark() + .data(data) + .left(function(n) { return n.left; }) + .top(function(n) { return n.top; }) + .width(function(n) { return n.width; }) + .height(function(n) { return n.height; }); + + /** + * Specifies the root key; optional. The root key is prepended to the + * <tt>keys</tt> attribute for all generated nodes. This method is provided + * for convenience and does not affect layout. + * + * @param {string} v the root key. + * @function + * @name pv.Layout.icicle.prototype.root + * @returns {pv.Layout.icicle} this. + */ + mark.root = function(v) { + keys = [v]; + return this; + }; + + /** + * Specifies the sizing function. By default, the sizing function is + * <tt>Number</tt>. The sizing function is invoked for each node in the tree + * (passed to the constructor): the sizing function must return + * <tt>undefined</tt> or <tt>NaN</tt> for internal nodes, and a number for + * leaf nodes. The aggregate sizes of internal nodes will be automatically + * computed by the layout. + * + * <p>For example, if the tree data structure represents a file system, with + * files as leaf nodes, and each file has a <tt>bytes</tt> attribute, you can + * specify a size function as: + * + * <pre>.size(function(d) d.bytes)</pre> + * + * This function will return <tt>undefined</tt> for internal nodes (since + * these do not have a <tt>bytes</tt> attribute), and a number for leaf nodes. + * + * <p>Note that the built-in <tt>Math.sqrt</tt> and <tt>Math.log</tt> methods + * can also be used as sizing functions. These function similarly to + * <tt>Number</tt>, except perform a root and log scale, respectively. + * + * @param {function} f the new sizing function. + * @function + * @name pv.Layout.icicle.prototype.size + * @returns {pv.Layout.icicle} this. + */ + mark.size = function(f) { + sizeof = f; + return this; + }; + + return mark; +}; +// TODO share code with Treemap +// TODO inspect parent panel dimensions to set inner and outer radii + +/** + * Returns a new sunburst tree layout. + * + * @class A tree layout in the form of a sunburst. <img + * src="../sunburst.png" width="160" height="160" align="right"> The + * center circle corresponds to the root of the tree; subsequent rings + * correspond to each tier. Rings are subdivided into wedges based on the size + * of nodes, per {@link #size}. Within a ring, wedges are sorted by size. + * + * <p>The tree layout is intended to be extended (see {@link pv.Mark#extend} by + * a {@link pv.Wedge}. The data property returns an array of nodes for use by + * other property functions. The following node attributes are supported: + * + * <ul> + * <li><tt>left</tt> - the wedge left position. + * <li><tt>top</tt> - the wedge top position. + * <li><tt>innerRadius</tt> - the wedge inner radius. + * <li><tt>outerRadius</tt> - the wedge outer radius. + * <li><tt>startAngle</tt> - the wedge start angle. + * <li><tt>endAngle</tt> - the wedge end angle. + * <li><tt>angle</tt> - the wedge angle. + * <li><tt>depth</tt> - the node depth (tier; the root is 0). + * <li><tt>keys</tt> - an array of string keys for the node. + * <li><tt>size</tt> - the aggregate node size. + * <li><tt>children</tt> - child nodes, if any. + * <li><tt>data</tt> - the associated tree element, for leaf nodes. + * </ul> + * + * <p>To produce a default sunburst layout, say: + * + * <pre>.add(pv.Wedge) + * .extend(pv.Layout.sunburst(tree))</pre> + * + * To only show nodes at a depth of two or greater, you might say: + * + * <pre>.add(pv.Wedge) + * .extend(pv.Layout.sunburst(tree)) + * .visible(function(n) n.depth > 1)</pre> + * + * The format of the <tt>tree</tt> argument is a hierarchical object whose leaf + * nodes are numbers corresponding to their size. For an example, and + * information on how to convert tabular data into such a tree, see + * {@link pv.Tree}. If the leaf nodes are not numbers, a {@link #size} function + * can be specified to override how the tree is interpreted. This size function + * can also be used to transform the data. + * + * <p>By default, the sunburst fills the full width and height of the parent + * panel. An optional root key can be specified using {@link #root} for + * convenience. + * + * @param tree a tree (an object) who leaf attributes have sizes. + * @returns {pv.Layout.sunburst} a tree layout. + */ +pv.Layout.sunburst = function(tree) { + var keys = [], sizeof = Number, w, h, r; + + /** @private */ + function accumulate(map) { + var node = {size: 0, children: [], keys: keys.slice()}; + for (var key in map) { + var child = map[key], size = sizeof(child); + keys.push(key); + if (isNaN(size)) { + child = accumulate(child); + } else { + child = {size: size, data: child, keys: keys.slice()}; + } + node.children.push(child); + node.size += child.size; + keys.pop(); + } + node.children.sort(function(a, b) { return b.size - a.size; }); + return node; + } + + /** @private */ + function scale(node, k) { + node.size *= k; + if (node.children) { + for (var i = 0; i < node.children.length; i++) { + scale(node.children[i], k); + } + } + } + + /** @private */ + function depth(node, i) { + i = i ? (i + 1) : 1; + return node.children + ? pv.max(node.children, function(n) { return depth(n, i); }) + : i; + } + + /** @private */ + function layout(node) { + if (node.children) { + wedgify(node); + for (var i = 0; i < node.children.length; i++) { + layout(node.children[i]); + } + } + } + + /** @private */ + function wedgify(node) { + var startAngle = node.startAngle; + for (var i = 0; i < node.children.length; i++) { + var child = node.children[i], angle = (child.size / node.size) * node.angle; + child.startAngle = startAngle; + child.angle = angle; + child.endAngle = startAngle + angle; + child.depth = node.depth + 1; + child.left = w / 2; + child.top = h / 2; + child.innerRadius = Math.max(0, child.depth - .5) * r; + child.outerRadius = (child.depth + .5) * r; + startAngle += angle; + if (child.children) { + wedgify(child); + } + } + } + + /** @private */ + function flatten(node, array) { + if (node.children) { + for (var i = 0; i < node.children.length; i++) { + flatten(node.children[i], array); + } + } + array.push(node) + return array; + } + + /** @private */ + function data() { + var root = accumulate(tree); + w = this.parent.width(); + h = this.parent.height(); + r = Math.min(w, h) / 2 / (depth(root) - .5); + root.left = w / 2; + root.top = h / 2; + root.startAngle = 0; + root.angle = 2 * Math.PI; + root.endAngle = 2 * Math.PI; + root.innerRadius = 0; + root.outerRadius = r; + root.depth = 0; + layout(root); + return flatten(root, []).reverse(); + } + + /* A dummy mark, like an anchor, which the caller extends. */ + var mark = new pv.Mark() + .data(data) + .left(function(n) { return n.left; }) + .top(function(n) { return n.top; }) + .startAngle(function(n) { return n.startAngle; }) + .angle(function(n) { return n.angle; }) + .innerRadius(function(n) { return n.innerRadius; }) + .outerRadius(function(n) { return n.outerRadius; }); + + /** + * Specifies the root key; optional. The root key is prepended to the + * <tt>keys</tt> attribute for all generated nodes. This method is provided + * for convenience and does not affect layout. + * + * @param {string} v the root key. + * @function + * @name pv.Layout.sunburst.prototype.root + * @returns {pv.Layout.sunburst} this. + */ + mark.root = function(v) { + keys = [v]; + return this; + }; + + /** + * Specifies the sizing function. By default, the sizing function is + * <tt>Number</tt>. The sizing function is invoked for each node in the tree + * (passed to the constructor): the sizing function must return + * <tt>undefined</tt> or <tt>NaN</tt> for internal nodes, and a number for + * leaf nodes. The aggregate sizes of internal nodes will be automatically + * computed by the layout. + * + * <p>For example, if the tree data structure represents a file system, with + * files as leaf nodes, and each file has a <tt>bytes</tt> attribute, you can + * specify a size function as: + * + * <pre>.size(function(d) d.bytes)</pre> + * + * This function will return <tt>undefined</tt> for internal nodes (since + * these do not have a <tt>bytes</tt> attribute), and a number for leaf nodes. + * + * <p>Note that the built-in <tt>Math.sqrt</tt> and <tt>Math.log</tt> methods + * can be used as sizing functions. These function similarly to + * <tt>Number</tt>, except perform a root and log scale, respectively. + * + * @param {function} f the new sizing function. + * @function + * @name pv.Layout.sunburst.prototype.size + * @returns {pv.Layout.sunburst} this. + */ + mark.size = function(f) { + sizeof = f; + return this; + }; + + return mark; +}; +// TODO add `by` function for determining size (and children?) + +/** + * Returns a new treemap tree layout. + * + * @class A tree layout in the form of an treemap. <img + * src="../treemap.png" width="160" height="160" align="right"> Treemaps + * are a form of space-filling layout that represents nodes as boxes, with child + * nodes placed within parent boxes. The size of each box is proportional to the + * size of the node in the tree. + * + * <p>This particular algorithm is taken from Bruls, D.M., C. Huizing, and + * J.J. van Wijk, <a href="http://www.win.tue.nl/~vanwijk/stm.pdf">"Squarified + * Treemaps"</a> in <i>Data Visualization 2000, Proceedings of the Joint + * Eurographics and IEEE TCVG Sumposium on Visualization</i>, 2000, + * pp. 33-42. + * + * <p>This tree layout is intended to be extended (see {@link pv.Mark#extend}) + * by a {@link pv.Bar}. The data property returns an array of nodes for use by + * other property functions. The following node attributes are supported: + * + * <ul> + * <li><tt>left</tt> - the cell left position. + * <li><tt>top</tt> - the cell top position. + * <li><tt>width</tt> - the cell width. + * <li><tt>height</tt> - the cell height. + * <li><tt>depth</tt> - the node depth (tier; the root is 0). + * <li><tt>keys</tt> - an array of string keys for the node. + * <li><tt>size</tt> - the aggregate node size. + * <li><tt>children</tt> - child nodes, if any. + * <li><tt>data</tt> - the associated tree element, for leaf nodes. + * </ul> + * + * To produce a default treemap layout, say: + * + * <pre>.add(pv.Bar) + * .extend(pv.Layout.treemap(tree))</pre> + * + * To display internal nodes, and color by depth, say: + * + * <pre>.add(pv.Bar) + * .extend(pv.Layout.treemap(tree).inset(10)) + * .fillStyle(pv.Colors.category19().by(function(n) n.depth))</pre> + * + * The format of the <tt>tree</tt> argument is a hierarchical object whose leaf + * nodes are numbers corresponding to their size. For an example, and + * information on how to convert tabular data into such a tree, see + * {@link pv.Tree}. If the leaf nodes are not numbers, a {@link #size} function + * can be specified to override how the tree is interpreted. This size function + * can also be used to transform the data. + * + * <p>By default, the treemap fills the full width and height of the parent + * panel, and only leaf nodes are rendered. If an {@link #inset} is specified, + * internal nodes will be rendered, each inset from their parent by the + * specified margins. Rounding can be enabled using {@link #round}. Finally, an + * optional root key can be specified using {@link #root} for convenience. + * + * @param tree a tree (an object) who leaf attributes have sizes. + * @returns {pv.Layout.treemap} a tree layout. + */ +pv.Layout.treemap = function(tree) { + var keys = [], round, inset, sizeof = Number; + + /** @private */ + function rnd(i) { + return round ? Math.round(i) : i; + } + + /** @private */ + function accumulate(map) { + var node = {size: 0, children: [], keys: keys.slice()}; + for (var key in map) { + var child = map[key], size = sizeof(child); + keys.push(key); + if (isNaN(size)) { + child = accumulate(child); + } else { + child = {size: size, data: child, keys: keys.slice()}; + } + node.children.push(child); + node.size += child.size; + keys.pop(); + } + node.children.sort(function(a, b) { return a.size - b.size; }); + return node; + } + + /** @private */ + function scale(node, k) { + node.size *= k; + if (node.children) { + for (var i = 0; i < node.children.length; i++) { + scale(node.children[i], k); + } + } + } + + /** @private */ + function ratio(row, l) { + var rmax = -Infinity, rmin = Infinity, s = 0; + for (var i = 0; i < row.length; i++) { + var r = row[i].size; + if (r < rmin) rmin = r; + if (r > rmax) rmax = r; + s += r; + } + s = s * s; + l = l * l; + return Math.max(l * rmax / s, s / (l * rmin)); + } + + /** @private */ + function squarify(node) { + var row = [], mink = Infinity; + var x = node.left + (inset ? inset.left : 0), + y = node.top + (inset ? inset.top : 0), + w = node.width - (inset ? inset.left + inset.right : 0), + h = node.height - (inset ? inset.top + inset.bottom : 0), + l = Math.min(w, h); + + scale(node, w * h / node.size); + + function position(row) { + var s = pv.sum(row, function(node) { return node.size; }), + hh = (l == 0) ? 0 : rnd(s / l); + + for (var i = 0, d = 0; i < row.length; i++) { + var n = row[i], nw = rnd(n.size / hh); + if (w == l) { + n.left = x + d; + n.top = y; + n.width = nw; + n.height = hh; + } else { + n.left = x; + n.top = y + d; + n.width = hh; + n.height = nw; + } + d += nw; + } + + if (w == l) { + if (n) n.width += w - d; // correct rounding error + y += hh; + h -= hh; + } else { + if (n) n.height += h - d; // correct rounding error + x += hh; + w -= hh; + } + l = Math.min(w, h); + } + + var children = node.children.slice(); // copy + while (children.length > 0) { + var child = children[children.length - 1]; + if (child.size <= 0) { + children.pop(); + continue; + } + row.push(child); + + var k = ratio(row, l); + if (k <= mink) { + children.pop(); + mink = k; + } else { + row.pop(); + position(row); + row.length = 0; + mink = Infinity; + } + } + + if (row.length > 0) { + position(row); + } + + /* correct rounding error */ + if (w == l) { + for (var i = 0; i < row.length; i++) { + row[i].width += w; + } + } else { + for (var i = 0; i < row.length; i++) { + row[i].height += h; + } + } + } + + /** @private */ + function layout(node) { + if (node.children) { + squarify(node); + for (var i = 0; i < node.children.length; i++) { + var child = node.children[i]; + child.depth = node.depth + 1; + layout(child); + } + } + } + + /** @private */ + function flatten(node, array) { + if (node.children) { + for (var i = 0; i < node.children.length; i++) { + flatten(node.children[i], array); + } + } + if (inset || !node.children) { + array.push(node) + } + return array; + } + + /** @private */ + function data() { + var root = accumulate(tree); + root.left = 0; + root.top = 0; + root.width = this.parent.width(); + root.height = this.parent.height(); + root.depth = 0; + layout(root); + return flatten(root, []).reverse(); + } + + /* A dummy mark, like an anchor, which the caller extends. */ + var mark = new pv.Mark() + .data(data) + .left(function(n) { return n.left; }) + .top(function(n) { return n.top; }) + .width(function(n) { return n.width; }) + .height(function(n) { return n.height; }); + + /** + * Enables or disables rounding. When rounding is enabled, the left, top, + * width and height properties will be rounded to integer pixel values. The + * rounding algorithm uses error accumulation to ensure an exact fit. + * + * @param {boolean} v whether rounding should be enabled. + * @function + * @name pv.Layout.treemap.prototype.round + * @returns {pv.Layout.treemap} this. + */ + mark.round = function(v) { + round = v; + return this; + }; + + /** + * Specifies the margins to inset child nodes from their parents; as a side + * effect, this also enables the display of internal nodes, which are hidden + * by default. If only a single argument is specified, this value is used to + * inset all four sides. + * + * @param {number} top the top margin. + * @param {number} [right] the right margin. + * @param {number} [bottom] the bottom margin. + * @param {number} [left] the left margin. + * @function + * @name pv.Layout.treemap.prototype.inset + * @returns {pv.Layout.treemap} this. + */ + mark.inset = function(top, right, bottom, left) { + if (arguments.length == 1) right = bottom = left = top; + inset = {top:top, right:right, bottom:bottom, left:left}; + return this; + }; + + /** + * Specifies the root key; optional. The root key is prepended to the + * <tt>keys</tt> attribute for all generated nodes. This method is provided + * for convenience and does not affect layout. + * + * @param {string} v the root key. + * @function + * @name pv.Layout.treemap.prototype.root + * @returns {pv.Layout.treemap} this. + */ + mark.root = function(v) { + keys = [v]; + return this; + }; + + /** + * Specifies the sizing function. By default, the sizing function is + * <tt>Number</tt>. The sizing function is invoked for each node in the tree + * (passed to the constructor): the sizing function must return + * <tt>undefined</tt> or <tt>NaN</tt> for internal nodes, and a number for + * leaf nodes. The aggregate sizes of internal nodes will be automatically + * computed by the layout. + * + * <p>For example, if the tree data structure represents a file system, with + * files as leaf nodes, and each file has a <tt>bytes</tt> attribute, you can + * specify a size function as: + * + * <pre>.size(function(d) d.bytes)</pre> + * + * This function will return <tt>undefined</tt> for internal nodes (since + * these do not have a <tt>bytes</tt> attribute), and a number for leaf nodes. + * + * <p>Note that the built-in <tt>Math.sqrt</tt> and <tt>Math.log</tt> methods + * can be used as sizing functions. These function similarly to + * <tt>Number</tt>, except perform a root and log scale, respectively. + * + * @param {function} f the new sizing function. + * @function + * @name pv.Layout.treemap.prototype.size + * @returns {pv.Layout.treemap} this. + */ + mark.size = function(f) { + sizeof = f; + return this; + }; + + return mark; +}; + return pv;}();/* + * Parses the Protovis specifications on load, allowing the use of JavaScript + * 1.8 function expressions on browsers that only support JavaScript 1.6. + * + * @see pv.parse + */ +pv.listen(window, "load", function() { + var scripts = document.getElementsByTagName("script"); + for (var i = 0; i < scripts.length; i++) { + var s = scripts[i]; + if (s.type == "text/javascript+protovis") { + try { + pv.Panel.$dom = s; + window.eval(pv.parse(s.textContent || s.innerHTML)); // IE + } catch (e) { + pv.error(e); + } + delete pv.Panel.$dom; + } + } + }); diff --git a/hyperkitty/templates/404.html b/hyperkitty/templates/404.html new file mode 100644 index 0000000..dbc98cb --- /dev/null +++ b/hyperkitty/templates/404.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} +{% load i18n %} +{% block content %} + <style> + #contentBox + { + padding:10px; + font-family: 'Times New Roman'; + font-size:20px; + color:#444; + margin-left:300px; + margin-top:100px; + } + .hello + { + font-size:100px; + color:#444; + font-family: 'Times New Roman'; + } + a + { + color:#323232; + } + </style> + + <div style="margin: 60px 40px 0 40px;"> + <div id="contentBox" style="width:50%; "> + <span class="hello"> 404 </span> + <br/><br/> + <span style="font-size:60px; color:#444;font-family: 'Times New Roman';"> Oh! Snap </span> + <br/><br/> + This is not the page you were looking for right! + <a href ="/">Fix This </a> + </div> + </div> + +{% endblock %}
\ No newline at end of file diff --git a/hyperkitty/templates/500.html b/hyperkitty/templates/500.html new file mode 100644 index 0000000..6c156d7 --- /dev/null +++ b/hyperkitty/templates/500.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} +{% load i18n %} +{% block content %} + <style> + #contentBox + { + padding:10px; + font-family: 'Times New Roman'; + font-size:20px; + color:#444; + margin-left:300px; + margin-top:100px; + } + .hello + { + font-size:100px; + color:#444; + font-family: 'Times New Roman'; + } + a + { + color:#323232; + } + </style> + + <div style="margin: 60px 40px 0 40px;"> + <div id="contentBox" style="width:50%; "> + <span class="hello"> 500 </span> + <br/><br/> + <span style="font-size:60px; color:#444;font-family: 'Times New Roman';"> Oh! Snap </span> + <br/><br/> + This is not the page you were looking for right! + <a href ="/">Fix This </a> + </div> + </div> + +{% endblock %}
\ No newline at end of file diff --git a/hyperkitty/templates/api.html b/hyperkitty/templates/api.html new file mode 100644 index 0000000..85f3cf0 --- /dev/null +++ b/hyperkitty/templates/api.html @@ -0,0 +1,77 @@ +{% extends "base.html" %} + +{% block additional_stylesheets %} + <link rel="stylesheet" type="text/css" media="all" href="{{ STATIC_URL }}css/thread.css" /> +{% endblock %} + +{% block content %} + <h2>REST API</h2> + <p> + HyperKitty comes with a small REST API allowing you to programatically retrieve + emails and information. + </p> + + <div class="odd" style="padding-left: 1em"> + <h3>Formats</h3> + <p> + This REST API can return the information into several formats. + The default format is html to allow human readibility.<br /> + To change the format, just add + <span style="font-style:italic">?format=<FORMAT></span> to the url + </p> + <p>The list of available formats is:</p> + <ul> + <li>json <a>(?format=json)</a></li> + <li>json-p <a>(?format=json-p)</a></li> + <li>txt <a>(?format=txt)</a></li> + <li>xml <a>(?format=xml)</a></li> + <li>html <a>(?format=html)</a></li> + <li>xhtml <a>(?format=xhtml)</a></li> + </ul> + </div> + + <div class="even" style="padding-left: 1em"> + <h3>Emails <a>/api/email/<list name>/<Message-ID></a></h3> + <p> +Using the address /api/email/<list name>/<Message-ID> you will be able to +retrieve the information known about a specific email on the specified mailing-list. + </p> + <p> For example: <a href="/api/email/devel@fp.o/<1312985457.28933.34.camel@ankur.pc>/"> + /api/email/devel@fp.o/<1312985457.28933.34.camel@ankur.pc>/ + </a> + </p> + </div> + <div class="odd" style="padding-left: 1em"> + <h3>Threads <a>/api/thread/<list name>/<ThreadID></a></h3> + <p> + </p> + <p> +Using the address /api/thread/<list name>/<Message-ID> you will be able to +retrieve the all the email for a specific thread on the specified mailing-list. + </p> + <p> For example: <a href="/api/thread/devel@fp.o/1/"> + /api/email/devel@fp.o/1/ + </a> + </p> + </div> + <div class="even" style="padding-left: 1em"> + <h3>Search <a>/api/search/<list name>/<field>/<keyword></a></h3> + <p> + </p> + <p> +Using the address /api/search/<list name>/<field>/<keyword> you will be able to +search for all emails of the specified mailing-list containing the provided keyword in the given field. + </p> + <p>The list of available field is:</p> + <ul> + <li>From</li> + <li>Subject</li> + <li>Content</li> + <li>SubjectContent</li> + </ul> + <p> For example: <a href="/api/search/devel@fp.o/From/pingoured"> + /api/search/devel@fp.o/From/pingoured + </a> + </p> + </div> +{% endblock %} diff --git a/hyperkitty/templates/base.html b/hyperkitty/templates/base.html new file mode 100644 index 0000000..0ae6d4c --- /dev/null +++ b/hyperkitty/templates/base.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> + <meta name="ROBOTS" content="INDEX, FOLLOW" /> + <title>{% block title %}{{ app_name|title }}{% endblock %}</title> + <meta name="author" content="" /> + <meta name="dc.language" content="en" /> + <link rel="stylesheet" type="text/css" media="all" href="{{ STATIC_URL }}css/normalize.css" /> + <link rel="stylesheet" type="text/css" media="all" href="{{ STATIC_URL }}css/bootstrap.css" /> + <link rel="stylesheet" type="text/css" media="all" href="{{ STATIC_URL }}css/style.css" /> + {% block additional_stylesheets %} {% endblock %} + </head> + {% load i18n %} + + <body> + <div class="navbar navbar-fixed-top"> + <div class="navbar-inner"> + <div class="container"> + <div class="nav-collapse collapse"> + + {% if user.is_authenticated %} + <li class="user_nav"> + <a class="mm_logout" style="float:right;" href="{% url user_logout %}">Logout</a> + </li> + <li class="user_nav" style="float:right;"> + <a style="float:right;" href="{% url user_profile %}">{{ user.username }}</a> + </li> + {% else %} + <li class="user_nav" style="float:right;"> + <a class="mm_user" style="float:right;" href="{% url user_login %}">Login</a> + </li> + <li class="user_nav" style="float:right;"> + <a style="float:right;" href="{% url user_registration %}"> Sign Up </a> + </li> + {% endif %} + + + <ul class="nav"> + + {% if list_address %} + <li id="list_name"> + <a href="/list/{{list_address}}">{{list_address}}</a> + </li> + {% endif %} + + + </ul> + </div> + </div> + </div> + </div> + + + + + <div class="container"> + {% block content %} {% endblock %} + </div> + {% block footer %} {% endblock %} + </body> + + <script src="{{ STATIC_URL }}js/libs/jquery-1.7.1.min.js"></script> + {% block additionaljs %} {% endblock %} +</html> diff --git a/hyperkitty/templates/index.html b/hyperkitty/templates/index.html new file mode 100644 index 0000000..87ba148 --- /dev/null +++ b/hyperkitty/templates/index.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% load i18n %} +{% block content %} +<h1>{% trans 'Lists' %}</h1> + +<table class="table table-bordered table-striped"> + <tbody> + {% for mlist in lists %} + <tr> + <td>{{ mlist }}</td> + <td><a href="/list/{{mlist}}"> Overview </a></td> + <td><a href="/archives/{{mlist}}"> Archives </a></td> + </tr> + {% endfor %} + </tbody> +</table> + +{% endblock %} diff --git a/hyperkitty/templates/login.html b/hyperkitty/templates/login.html new file mode 100644 index 0000000..a137c97 --- /dev/null +++ b/hyperkitty/templates/login.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} + +<h2>Login with username and password</h2> + +<form action="" method="post" class="login mm_clear"> + {% csrf_token %} + {{ form.as_p }} + <input type=hidden name=next value={{ next }}> + <div class="field"> + <button class="btn btn-primary" type="submit"> + {% trans "Login" %} + </button> + </div> +</form> + + +<ul class="socialLogin"> + <li><a title="Google" class="socialaccount_provider google" href="/login/google"><img src="{{ STATIC_URL }}img/login/google.png" alt="Google"></a></li> + + <li><a title="Yahoo" class="socialaccount_provider yahoo" href="/login/yahoo"><img src="{{ STATIC_URL }}img/login/yahoo.png" alt="Yahoo"></a></li> + + <li> + <form method="post" action="{% url socialauth_complete "browserid" %}"> + {% csrf_token %} + <input type="hidden" name="assertion" value="" /> + <a rel="nofollow" id="browserid" href="#"><img src="{{ STATIC_URL }}img/login/browserid.png" alt="Login using BrowserID" /></a> + </form> + </li> + +</ul> + +{% endblock %} + +{% block additionaljs %} +<!-- Include BrowserID JavaScript --> +<script src="https://browserid.org/include.js" type="text/javascript"></script> +<!-- Setup click handler that receives BrowserID assertion code and sends POST data --> +<script type="text/javascript"> + $(function() { + $('#browserid').click(function(e) { + e.preventDefault(); + var self = $(this); + + navigator.id.get(function(assertion) { + if (assertion) { + self.parent('form').find('input[type=hidden]').attr('value', assertion).end().submit(); + } else { + alert('Some error occurred'); + } + }); + }); + }); +</script> +<!-- end browserid stuff --> +{% endblock additionaljs %} diff --git a/hyperkitty/templates/message.html b/hyperkitty/templates/message.html new file mode 100644 index 0000000..592e85d --- /dev/null +++ b/hyperkitty/templates/message.html @@ -0,0 +1,71 @@ +{% extends "base.html" %} +{% load gravatar %} + +{% block additional_stylesheets %} +<link rel="stylesheet" type="text/css" media="all" href="{{ STATIC_URL }}css/thread.css" /> +{% endblock %} + +{% block content %} + +<section id="thread_content"> + {% include 'messages/first_email.html' with first_mail=message %} +</section> + +{% endblock %} + +{% block additionaljs %} +<script type="text/javascript"> + $(document).ready(function() { + $(".voteup").click(function() { + // @TODO: Extract the message id from the HTML DOM element instead of hard coding it in Javascript. + message_id = this.parentElement.getAttribute('messageid'); + {% if user.is_authenticated %} + $.ajax({ + type : "POST", + url : '/vote/{{list_address}}/', + data : { + vote : 1, + messageid : message_id, + list : "{{list_address}}", + csrfmiddlewaretoken : '{{ csrf_token }}' + }, + success : function(response) { + console.log(response); + location.reload(); + } + }); + return false; + {% else %} + alert('You need to login in order to vote'); + {% endif %} + }); + + $(".votedown").click(function() { + message_id = this.parentElement.getAttribute('messageid'); + {% if user.is_authenticated %} + $.ajax({ + type : "POST", + url : '/vote/{{list_address}}/', + data : { + vote : -1, + messageid : message_id, + list : "{{list_address}}", + csrfmiddlewaretoken : '{{ csrf_token }}' + }, + success : function(response) { + console.log(response); + // @TODO : Remove this reload and update count using AJAX + location.reload(); + } + }); + return false; + {% else %} + alert('You need to login in order to vote'); + {% endif %} + + }); + + }); +</script> + +{% endblock %} diff --git a/hyperkitty/templates/messages/first_email.html b/hyperkitty/templates/messages/first_email.html new file mode 100644 index 0000000..36c8468 --- /dev/null +++ b/hyperkitty/templates/messages/first_email.html @@ -0,0 +1,5 @@ +{% load gravatar %} + +<div class="first_email"> + {% include 'messages/message.html' with email=first_mail unfolded='True' %} +</div> diff --git a/hyperkitty/templates/messages/message.html b/hyperkitty/templates/messages/message.html new file mode 100644 index 0000000..feffcb9 --- /dev/null +++ b/hyperkitty/templates/messages/message.html @@ -0,0 +1,34 @@ +{% load gravatar %} + +<div class="email_header"> + {% gravatar_img_for_email email.email 40 %} + <div class="email_author inline-block"> + <span class="name"> <a href="/message/{{list_address}}/{{email.message_id}}">{{email.sender}}</a> </span> + <br /> + <span class="rank">Rank 8</span> + </div> + <div class="email_date inline-block right"> + <span class="date">{{email.date}}</span> + <br /> + </div> +</div> +{% if unfolded %} + <div class="first_email_body"> +{% else %} + <div class="email_body"> +{% endif %} + + {{email.content}} +</div> +<ul class="email_info inline" messageid="{{email.message_id}}"> + <li class="neutral"> + +{{email.likes}}/-{{email.dislikes}} + </li> + <li class="voteup"> + <a href="#like"> Like</a> + </li> + <li class="votedown"> + <a href="#dislike"> Dislike</a> + </li> +</ul> + diff --git a/hyperkitty/templates/month_view.html b/hyperkitty/templates/month_view.html new file mode 100644 index 0000000..5ba2445 --- /dev/null +++ b/hyperkitty/templates/month_view.html @@ -0,0 +1,111 @@ +{% extends "base.html" %} +{% load poll_extras %} +{% load gravatar %} + +{% block content %} + +<div id="recent_activities"> + {% for email in threads %} + <!-- New thread --> + <div class="thread"> + <div class="notsaved"> + <span class="thread_title"> <a name="{{email.thread_id}}" + href="/thread/{{list_address}}/{{email.thread_id}}"> {{email.subject}} </a> </span> + <span class="thread_date"> {{email.date}}</span> + </div> + <div class="thread_content"> + {% if email.category_tag %} + <div class="inline-block type type_{{email.category_tag}}"> + <a href="/tag/{{list_address}}/{{email.category_tag}}"> {{email.category}} </a> + </div> + {% else %} + <div class="inline-block type type_{{email.category|lower}}"> + <a href="/tag/{{list_address}}/{{email.category|lower}}"> {{email.category}} </a> + </div> + {% endif %} + <div class="inline-block gravatar"> + {% if email.email %} + {% gravatar_img_for_email email.email 40 %} + <br /> + {% endif %} + {{email.sender}} + </div> + <div class="inline-block thread_email"> + <span class="expander"> {{email.content}} </span> + </div> + </div> + <div class="thread_info"> + <ul class="tags inline"> + <li> + Tags: + </li> + {% for tag in email.tags %} + <li> + <a href="/tag/{{list_address}}/{{tag}}">{{tag}}</a> + </li> + {% endfor %} + </ul> + <ul class="inline-block"> + <li class="participant"> + {{email.participants|length}} participants + </li> + <li class="discussion"> + {{email.answers}} comments + </li> + </ul> + <ul class="inline-block"> + <li class="like"> + +{{email.liked}} + </li> + <li class="youlike"> + <a href="#like"> Like</a> + </li> + <li class="youdislike"> + <a href="#dislike"> Dislike</a> + </li> + {% if email.answers %} + <li class="showdiscussion"> + <a href="#show"> Show discussion</a> + </li> + {% else %} + <li class="showdiscussion invisible"> + <a href="#show"> Show discussion</a> + </li> + {% endif %} + </ul> + </div> + </div> + <!-- End of thread --> + {% empty %} + Sorry no emails could be found for your search. + {% endfor %} +</div> +<div id="archives"> + {% for key, value in archives_length|sort %} + <h3>{{ key }}</h3> + <div> + <ul> + {% for ar_month in value %} + <li> + <a href="/archives/{{list_address}}/{{key}}/{{ar_month}}"> {{ ar_month|tomonth }} </a> + </li> + {% endfor %} + </ul> + </div> + {% endfor %} +</div> + +{% endblock %} + +{% block additionaljs %} +<script src="{{ STATIC_URL }}jquery.expander.js"></script> +<script> + $(document).ready(function() { + $('span.expander').expander({ + userCollapseText : 'View Less', + expandText : 'View More' + }); + }); +</script> +{% endblock %} + diff --git a/hyperkitty/templates/recent_activities.html b/hyperkitty/templates/recent_activities.html new file mode 100644 index 0000000..d6f4516 --- /dev/null +++ b/hyperkitty/templates/recent_activities.html @@ -0,0 +1,212 @@ +{% extends "base.html" %} +{% load poll_extras %} +{% load gravatar %} + +{% block additional_stylesheets %} +<link rel="stylesheet" type="text/css" media="all" href="{{ STATIC_URL }}css/stats.css" /> +{% endblock %} + +{% block content %} + +<section id="recent_activities"> + <section id="graph"> + <div id="fig"> + <script type="text/javascript+protovis" > + + var dates = ["{{dates_string|join:'","'}}"]; + + var data = {{evolution}}, + w = 500, + h = 300, + x = pv.Scale.ordinal(pv.range(32)).splitBanded(0, w, 4/5), + y = pv.Scale.linear(0, {{evolution|length}}).range(0, h); + + var vis = new pv.Panel() + .width(w) + .height(250) + .bottom(60) + .left(30) + .right(5) + .top(5); + + var bar = vis.add(pv.Bar) + .data(data) + .event("click", function(n) self.location = "/archives/{{list_address}}/" + dates[this.index]) + .left(function() x(this.index)) + .width(x.range().band) + .bottom(0) + .height(y); + + bar.anchor("bottom").add(pv.Label) + .textMargin(5) + .textAlign("right") + .textBaseline("middle") + .textAngle(-Math.PI / 3) + .text(function() xlabel(this.index)); + + function xlabel(ind){ + if (!dates[ind -1]){ + return dates[ind]; + } + prev = dates[ind - 1]; + cur = dates[ind]; + if (prev.substring(0,7) == cur.substring(0,7)){ + cur = cur.substring(8); + } + return cur; + } + + var title = vis.add(pv.Label) + .left(250) + .top(16) + .textAlign("center") + .text("Activities on the list over the last 30 days"); + + vis.add(pv.Rule) + .data(y.ticks()) + .bottom(function(d) Math.round(y(d)) - .5) + .strokeStyle(function(d) d ? "rgba(255,255,255,.3)" : "#000") + .add(pv.Rule) + .left(0) + .width(5) + .strokeStyle("#000") + .anchor("left").add(pv.Label) + .text(function(d) d.toFixed(1)); + + vis.render(); + + </script> + </div> + </section> + <section id="most_active"> + <h2>Recently active discussions</h2> + {% for email in most_active_threads %} + <!-- Start thread --> + <div class="thread"> + <span class="thread_id">#{{forloop.counter}}</span> + <span class="thread_title"> <a name="{{email.thread_id}}" + href="/thread/{{list_address}}/{{email.thread_id}}"> {{email.subject}} </a> </span> + <div class="thread_stats"> + <ul class="inline-block"> + {% if email.category_tag %} + <li class="type type_{{email.category_tag}}"> + <a href="/tag/{{list_address}}/{{email.category_tag}}"> {{email.category}} </a> + </li> + {% else %} + <li class="type type_{{email.category|lower}}"> + <a href="/tag/{{list_address}}/{{email.category|lower}}"> {{email.category}} </a> + </li> + {% endif %} + <li class="neutral"> + 0 + </li> + <li class="participant"> + {{email.participants|length}} + </li> + <li class="discussion"> + {{email.answers}} + </li> + </ul> + </div> + </div> + <!-- End thread --> + {% endfor %} + </section> + + <section id="top_discussion"> + <h2>Top discussions the last 30 days</h2> + {% for email in top_threads %} + <!-- Start thread --> + <div class="thread"> + <span class="thread_id">#{{forloop.counter}}</span> + <span class="thread_title"> <a name="{{email.thread_id}}" + href="/thread/{{list_address}}/{{email.thread_id}}"> {{email.subject}} </a> </span> + <div class="thread_stats"> + <ul class="inline-block"> + {% if email.category_tag %} + <li class="type type_{{email.category_tag}}"> + <a href="/tag/{{list_address}}/{{email.category_tag}}"> {{email.category}} </a> + </li> + {% else %} + <li class="type type_{{email.category|lower}}"> + <a href="/tag/{{list_address}}/{{email.category|lower}}"> {{email.category}} </a> + </li> + {% endif %} + <li class="neutral"> + 0 + </li> + <li class="participant"> + {{email.participants|length}} + </li> + <li class="discussion"> + {{email.answers}} + </li> + </ul> + </div> + </div> + <!-- End thread --> + {% endfor %} + </section> + + <section id="discussion_marker"> + <h2>Prominent discussion maker</h2> + {% for author in top_author %} + <!-- Start discussion maker --> + <div class="maker"> + <div class="inline-block maker_id"> + #{{forloop.counter}} + </div> + <div class="inline-block gravatar"> + {% if author.email %} + {% gravatar_img_for_email author.email 40 %} + <br /> + {% endif %} + </div> + <div class="inline-block"> + <span class="marker_name">{{author.name}}</span> + <br /> + <span class="score">+{{author.kudos}}</span> kudos + </div> + </div> + <!-- End discussion maker --> + {% endfor %} + + <h2>Tag cloud</h2> + </section> + + <section id="discussion_by_topic"> + <h2>Discussion by topic the last 30 days</h2> + {% for category, thread in threads_per_category.items %} + <div> + <h2 class="category type_{{category}}"> {{category}} </h2> + <ul class="category_entry"> + {% for email in thread %} + <li> + {{email.title}} + </li> + {% endfor %} + </ul> + </div> + {% endfor %} + </section> +</section> +<section id="archives"> + {% for key, value in archives_length|sort %} + <h3>{{ key }}</h3> + <div> + <ul> + {% for ar_month in value %} + <li> + <a href="/archives/{{list_address}}/{{key}}/{{ar_month}}"> {{ ar_month|tomonth }} </a> + </li> + {% endfor %} + </ul> + </div> + {% endfor %} +</section> +{% endblock %} + +{% block additionaljs %} + <script type="text/javascript" src="{{ STATIC_URL }}protovis-d3.1.js"></script> +{% endblock %} + diff --git a/hyperkitty/templates/register.html b/hyperkitty/templates/register.html new file mode 100644 index 0000000..1e96907 --- /dev/null +++ b/hyperkitty/templates/register.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} + +<form action="" method="post" class="login mm_clear"> + {% csrf_token %} + {{ form.as_p }} + <input type=hidden name=next value={{ next }}> + <div class="field"> + <button class="btn btn-primary" type="submit"> + {% trans "Register" %} + </button> + </div> +</form> + +{% endblock %} diff --git a/hyperkitty/templates/search.html b/hyperkitty/templates/search.html new file mode 100644 index 0000000..e0e4f8c --- /dev/null +++ b/hyperkitty/templates/search.html @@ -0,0 +1,106 @@ +{% extends "base.html" %} +{% load poll_extras %} +{% load gravatar %} + +{% block content %} + +{% if threads.object_list %} +<div class="pagination"> + <span class="step-links"> {% if threads.has_previous %} <a href="{{full_path|strip_page}}/{{ threads.previous_page_number }}">previous</a> {% endif %} <span class="current"> Page {{ threads.number }} of {{ threads.paginator.num_pages }}. </span> {% if threads.has_next %} <a href="{{full_path|strip_page}}/{{ threads.next_page_number }}">next</a> {% endif %} </span> +</div> +{% endif %} + +{% for email in threads.object_list %} +<!-- New thread --> +<div class="thread"> + <div class="notsaved"> + <a href="/thread/{{list_address}}/{{email.thread_id}}"> <span class="thread_title">{{email.subject}}</span> </a> + <span class="thread_date">{{email.date}}</span> + </div> + <div class="thread_content"> + {% if email.category_tag %} + <div class="inline-block type type_{{email.category_tag}}"> + <a href="/tag/{{list_address}}/{{email.category_tag}}"> {{email.category}} </a> + </div> + {% else %} + <div class="inline-block type type_{{email.category|lower}}"> + <a href="/tag/{{list_address}}/{{email.category|lower}}"> {{email.category}} </a> + </div> + {% endif %} + <div class="inline-block gravatar"> + {% if email.email %} + {% gravatar_img_for_email email.email 40 %} + <br /> + {% endif %} + {{email.sender}} + </div> + <div class="inline-block thread_email"> + <span class="expander"> {{email.content}} </span> + </div> + </div> + <div class="thread_info"> + <ul class="tags inline"> + <li> + Tags: + </li> + {% for tag in email.tags %} + <li> + <a href="/tag/{{list_address}}/{{tag}}">{{tag}}</a> + </li> + {% endfor %} + </ul> + <ul class="inline-block"> + <li class="participant"> + {{email.participants|length}} participants + </li> + <li class="discussion"> + {{email.answers}} comments + </li> + </ul> + <ul class="inline-block"> + <li class="like"> + +{{email.liked}} + </li> + <li class="youlike"> + <a href="#like"> Like</a> + </li> + <li class="youdislike"> + <a href="#dislike"> Dislike</a> + </li> + {% if email.answers %} + <li class="showdiscussion"> + <a href="#show"> Show discussion</a> + </li> + {% else %} + <li class="showdiscussion invisible"> + <a href="#show"> Show discussion</a> + </li> + {% endif %} + </ul> + </div> +</div> +<!-- End of thread --> +{% empty %} +Sorry no emails could be found for your search. +{% endfor %} + +{% if threads.object_list %} +<div class="pagination"> + <span class="step-links"> {% if threads.has_previous %} <a href="{{ threads.previous_page_number }}">previous</a> {% endif %} <span class="current"> Page {{ threads.number }} of {{ threads.paginator.num_pages }}. </span> {% if threads.has_next %} <a href="{{ threads.next_page_number }}">next</a> {% endif %} </span> +</div> +{% endif %} + +{% endblock %} + +{% block additionaljs %} +<script src="{{ STATIC_URL }}jquery.expander.js"></script> +<script> + $(document).ready(function() { + $('span.expander').expander({ + userCollapseText : 'View Less', + expandText : 'View More' + }); + }); +</script> +{% endblock %} + diff --git a/hyperkitty/templates/thread.html b/hyperkitty/templates/thread.html new file mode 100644 index 0000000..abf6747 --- /dev/null +++ b/hyperkitty/templates/thread.html @@ -0,0 +1,99 @@ +{% extends "base.html" %} + +{% load gravatar %} + +{% block additional_stylesheets %} +<link rel="stylesheet" type="text/css" media="all" href="{{ STATIC_URL }}css/thread.css" /> +{% endblock %} + +{% block content %} + + {% include 'threads/right_col.html' %} + + <!-- main section, the email thread --> + <section id="thread_content"> + + <!-- Start first email --> + {% include 'messages/first_email.html' %} + <!-- End first email --> + + {% for email in threads %} + <div class="{% cycle 'even' 'odd' %}"> + <!-- Start email --> + {% include 'messages/message.html' %} + <!-- End of email --> + </div> + {% endfor %} + + </section> + +<!-- end of content --> +{% endblock %} + +{% block additionaljs %} + +<script src="{{ STATIC_URL }}jquery.expander.js"></script> +<script type="text/javascript"> + $(document).ready(function() { + $('div.email_body').expander({ + userCollapseText : 'View Less', + expandText : 'View More' + }); + }); +</script> + +<script type="text/javascript"> + $(document).ready(function() { + $(".voteup").click(function() { + // @TODO: Extract the message id from the HTML DOM element instead of hard coding it in Javascript. + message_id = this.parentElement.getAttribute('messageid'); + {% if user.is_authenticated %} + $.ajax({ + type : "POST", + url : '/vote/{{list_address}}/', + data : { + vote : 1, + messageid : message_id, + list : "{{list_address}}", + csrfmiddlewaretoken : '{{ csrf_token }}' + }, + success : function(response) { + console.log(response); + location.reload(); + } + }); + return false; + {% else %} + alert('You need to login in order to vote'); + {% endif %} + }); + + $(".votedown").click(function() { + message_id = this.parentElement.getAttribute('messageid'); + {% if user.is_authenticated %} + $.ajax({ + type : "POST", + url : '/vote/{{list_address}}/', + data : { + vote : -1, + messageid : message_id, + list : "{{list_address}}", + csrfmiddlewaretoken : '{{ csrf_token }}' + }, + success : function(response) { + console.log(response); + // @TODO : Remove this reload and update count using AJAX + location.reload(); + } + }); + return false; + {% else %} + alert('You need to login in order to vote'); + {% endif %} + + }); + + }); +</script> + +{% endblock %} diff --git a/hyperkitty/templates/threads/add_tag_form.html b/hyperkitty/templates/threads/add_tag_form.html new file mode 100644 index 0000000..65f6577 --- /dev/null +++ b/hyperkitty/templates/threads/add_tag_form.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block header %} {% endblock %} + +{% block content %} +<form action="/addtag/{{list_address}}/{{email_id}}/" method="post"> + {% csrf_token %} + {{ addtag_form }} + <button type="submit"> + Add + </button> +</form> +{% endblock %} diff --git a/hyperkitty/templates/threads/right_col.html b/hyperkitty/templates/threads/right_col.html new file mode 100644 index 0000000..9a1e448 --- /dev/null +++ b/hyperkitty/templates/threads/right_col.html @@ -0,0 +1,60 @@ +{% load gravatar %} + +<!-- right column --> +<section id="thread_overview_info"> + <!-- Start dates --> + <div id="thread_date_info"> + <div class="days_num inline-block"> + 21 + </div> + <div class="days_text inline-block"> + days + <br /> + inactive + </div> + <div id="days_old" class="days_num inline-block"> + 24 + </div> + <div class="days_text inline-block"> + days + <br /> + old + </div> + </div> + <p id="add_to_fav"> + <a href="#AddFav" class="notsaved">Add to favorite discussions</a> + </p> + <!-- End dates --> + <hr id="grey"/> + <div id="tags"> + <span id="tag_title">tags </span>({{tags|length}}) + <ul class="inline"> + {% for tag in tags %} + <li> + {{ tag }} | + </li> + {% endfor %} + </ul> + </div> + <div id="add_tag"> + <form action="/addtag/{{list_address}}/{{first_mail.message_id}}/" method="post"> + {% csrf_token %} + {{ addtag_form.as_p }} + <button type="submit"> + Add a tag + </button> + </form> + </div> + <div id="participants"> + <span id="participants_title"> participants </span>({{participants|length}}) + <ul> + {% for key,value in participants.items %} + <li> + {% gravatar_img_for_email value.email 20%} + {{key}} + </li> + {% endfor %} + </ul> + </div> +</section> + diff --git a/hyperkitty/templates/user_profile.html b/hyperkitty/templates/user_profile.html new file mode 100644 index 0000000..f0aa27e --- /dev/null +++ b/hyperkitty/templates/user_profile.html @@ -0,0 +1,69 @@ +{% extends "base.html" %} +{% load i18n %} +{% load poll_extras %} + +{% block content %} + <h1>User Profile <span>- {{ user }}</span></h1> + + <table class="table table-bordered table-striped mm_userData"> + <tbody> + <tr> + <th>{% trans 'User name' %}</th> + <td>{{ user.username}}</td> + </tr> + <tr> + <th>{% trans 'Firstname' %}</th> + <td>{{ user.first_name }}</td> + </tr> + <tr> + <th>{% trans 'Lastname' %}</th> + <td>{{ user.last_name }}</td> + </tr> + <tr> + <th>{% trans 'Email' %}</th> + <td>{{ user.email }}</td> + </tr> + <tr> + <th>{% trans 'Karma' %}</th> + <td>{{ user_profile.karma }}</td> + </tr> + <tr> + <th>{% trans 'Date Joined' %}</th> + <td>{{ user.date_joined }}</td> + </tr> + </tbody> + </table> + <h3> Up Votes : </h3> + + <ul> + {% for vote in user_profile.votes %} + {% if vote.vote == 1 %} + <li> + {% if vote.message.content|trimString|length > 0 %} + <a href="/message/{{vote.list_address}}/{{vote.messageid}}">{{ vote.message.content|truncatechars:20 }}</a> + {% else %} + <a href="/message/{{vote.list_address}}/{{vote.messageid}}">Message is empty</a> + {% endif %} + </li> + {% endif %} + {% endfor %} + </ul> + + <h3> Down Votes : </h3> + + <ul> + {% for vote in user_profile.votes %} + {% if vote.vote == -1 %} + <li> + {% if vote.message.content|trimString|length > 0 %} + <a href="/message/{{vote.list_address}}/{{vote.messageid}}">{{ vote.message.content|truncatechars:20 }}</a> + {% else %} + <a href="/message/{{vote.list_address}}/{{vote.messageid}}">Message is empty</a> + {% endif %} + </li> + {% endif %} + {% endfor %} + </ul> + + +{% endblock %} diff --git a/hyperkitty/templatetags/__init__.py b/hyperkitty/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/hyperkitty/templatetags/__init__.py diff --git a/hyperkitty/templatetags/poll_extras.py b/hyperkitty/templatetags/poll_extras.py new file mode 100644 index 0000000..a5f1032 --- /dev/null +++ b/hyperkitty/templatetags/poll_extras.py @@ -0,0 +1,55 @@ +from django import template +from django.http import HttpRequest +from django.utils.datastructures import SortedDict +import re + +register = template.Library() + +@register.filter(name="trimString") +def trimString(str): + return re.sub('\s+', ' ', str) + +@register.filter(name='sort') +def listsort(value): + if isinstance(value, dict): + new_dict = SortedDict() + key_list = value.keys() + key_list.sort() + key_list.reverse() + for key in key_list: + values = value[key] + values.sort() + values.reverse() + new_dict[key] = values + return new_dict.items() + elif isinstance(value, list): + new_list = list(value) + new_list.sort() + return new_list + else: + return value + listsort.is_safe = True + +@register.filter(name="tomonth") +def to_month(value): + months = ('January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December') + return months[value -1] + +@register.filter(name="strip_page") +def strip_page(value): + print repr(value), repr(value)[-2] + if not value: + return value + if value.endswith('/') and value[-3] == '/': + end_with_number = False + try: + if int(value[-2]) in range(0,10): + end_with_number = True + if end_with_number: + output = value.rsplit('/', 2) + except ValueError: + output = value.rsplit('/', 1) + else: + output = value.rsplit('/', 1) + return output[0] diff --git a/hyperkitty/tests/__init__.py b/hyperkitty/tests/__init__.py new file mode 100644 index 0000000..4cf4941 --- /dev/null +++ b/hyperkitty/tests/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. +# +# This file is part of HyperKitty. +# +# HyperKitty is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# HyperKitty is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# HyperKitty. If not, see <http://www.gnu.org/licenses/>. +# +# Author: Aamir Khan <syst3m.w0rm@gmail.com> +# + +from gsoc.tests.test_views import * +from gsoc.tests.test_models import * +from gsoc.tests.test_forms import * diff --git a/hyperkitty/tests/test_forms.py b/hyperkitty/tests/test_forms.py new file mode 100644 index 0000000..9a056cd --- /dev/null +++ b/hyperkitty/tests/test_forms.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. +# +# This file is part of HyperKitty. +# +# HyperKitty is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# HyperKitty is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# HyperKitty. If not, see <http://www.gnu.org/licenses/>. +# +# Author: Aamir Khan <syst3m.w0rm@gmail.com> +# diff --git a/hyperkitty/tests/test_models.py b/hyperkitty/tests/test_models.py new file mode 100644 index 0000000..9a056cd --- /dev/null +++ b/hyperkitty/tests/test_models.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. +# +# This file is part of HyperKitty. +# +# HyperKitty is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# HyperKitty is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# HyperKitty. If not, see <http://www.gnu.org/licenses/>. +# +# Author: Aamir Khan <syst3m.w0rm@gmail.com> +# diff --git a/hyperkitty/tests/test_views.py b/hyperkitty/tests/test_views.py new file mode 100644 index 0000000..b94ef43 --- /dev/null +++ b/hyperkitty/tests/test_views.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. +# +# This file is part of HyperKitty. +# +# HyperKitty is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# HyperKitty is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# HyperKitty. If not, see <http://www.gnu.org/licenses/>. +# +# Author: Aamir Khan <syst3m.w0rm@gmail.com> +# + +from django.test import TestCase +from django.test.client import Client +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse + +class AccountViewsTestCase(TestCase): + + def setUp(self): + self.client = Client() + + def test_login(self): + # Try to access user profile (private data) without logging in + response = self.client.get(reverse('user_profile')) + self.assertRedirects(response, "%s?next=%s" % (reverse('user_login'),reverse('user_profile'))) + + def test_profile(self): + User.objects.create_user('testuser', 'syst3m.w0rm+test@gmail.com', 'testPass') + user = self.client.login(username='testuser', password='testPass') + + response = self.client.get(reverse('user_profile')) + self.assertEqual(response.status_code, 200) + + # Verify that user_profile is present in request context + self.assertTrue('user_profile' in response.context) + + # Verify karma for newly created user is 1 + self.assertEqual(response.context['user_profile'].karma, 1) + + + def test_registration(self): + + User.objects.create_user('testuser', 'syst3m.w0rm+test@gmail.com', 'testPass') + user = self.client.login(username='testuser', password='testPass') + + # If the user if already logged in, redirect to index page..don't let him register again + response = self.client.get(reverse('user_registration')) + self.assertRedirects(response, reverse('index')) + self.client.logout() + + # Access the user registration page after logging out and try to register now + response = self.client.get(reverse('user_registration')) + self.assertEqual(response.status_code, 200) + + # @TODO: Try to register a user and verify its working + + + + + + +
\ No newline at end of file diff --git a/hyperkitty/todo b/hyperkitty/todo new file mode 100644 index 0000000..ee1603e --- /dev/null +++ b/hyperkitty/todo @@ -0,0 +1,5 @@ +1. Better error handling -> log everything and handle exception gracefully. +2. write test cases +3. look at social-authentication application from detailed point of view. +4. make user profile editable. +5. setup.py to install this as a python application - code refactoring diff --git a/hyperkitty/urls.py b/hyperkitty/urls.py new file mode 100644 index 0000000..29edb06 --- /dev/null +++ b/hyperkitty/urls.py @@ -0,0 +1,106 @@ +from django.conf.urls.defaults import patterns, include, url +from django.conf import settings +from django.views.generic.simple import direct_to_template +from api import EmailResource, ThreadResource, SearchResource + +from django.contrib.staticfiles.urls import staticfiles_urlpatterns + +# Uncomment the next two lines to enable the admin: +from django.contrib import admin +admin.autodiscover() + +urlpatterns = patterns('', + # Account + url(r'^accounts/login/$', 'views.accounts.user_login', name='user_login'), + url(r'^accounts/logout/$', 'views.accounts.user_logout', name='user_logout'), + url(r'^accounts/profile/$', 'views.accounts.user_profile', name='user_profile'), + url(r'^accounts/register/$', 'views.accounts.user_registration', name='user_registration'), + + + # Index + url(r'^/$', 'views.pages.index', name='index'), + url(r'^$', 'views.pages.index', name='index'), + + # Archives + url(r'^archives/(?P<mlist_fqdn>.*@.*)/(?P<year>\d{4})/(?P<month>\d\d?)/(?P<day>\d\d?)/$', + 'views.list.archives'), + url(r'^archives/(?P<mlist_fqdn>.*@.*)/(?P<year>\d{4})/(?P<month>\d\d?)/$', + 'views.list.archives'), + url(r'^archives/(?P<mlist_fqdn>.*@.*)/$', + 'views.list.archives'), + + # Threads + url(r'^thread/(?P<mlist_fqdn>.*@.*)/(?P<threadid>.+)/$', + 'views.thread.thread_index'), + + + # Lists + url(r'^list/$', 'views.pages.index'), # Can I remove this URL? + url(r'^list/(?P<mlist_fqdn>.*@.*)/$', + 'views.list.list'), + + # Search Tag + url(r'^tag/(?P<mlist_fqdn>.*@.*)\/(?P<tag>.*)\/(?P<page>\d+)/$', + 'views.list.search_tag'), + url(r'^tag/(?P<mlist_fqdn>.*@.*)\/(?P<tag>.*)/$', + 'views.list.search_tag'), + + # Search + # If page number is present in URL + url(r'^search/(?P<mlist_fqdn>.*@.*)\/(?P<target>.*)\/(?P<keyword>.*)\/(?P<page>\d+)/$', + 'views.list.search_keyword'), + # Show the first page as default when no page number is present in URL + url(r'^search/(?P<mlist_fqdn>.*@.*)\/(?P<target>.*)\/(?P<keyword>.*)/$', + 'views.list.search_keyword'), + url(r'^search/(?P<mlist_fqdn>.*@.*)/$', + 'views.list.search'), + + + ### MESSAGE LEVEL VIEWS ### + # Vote a message + url(r'^message/(?P<mlist_fqdn>.*@.*)/(?P<messageid>.+)/$', + 'views.message.index'), + + url(r'^vote/(?P<mlist_fqdn>.*@.*)/$', + 'views.message.vote'), + ### MESSAGE LEVEL VIEW ENDS ### + + + + ### THREAD LEVEL VIEWS ### + # Thread view page + url(r'^thread/(?P<mlist_fqdn>.*@.*)/(?P<threadid>.+)/$', + 'views.thread.thread_index'), + + # Add Tag to a thread + url(r'^addtag/(?P<mlist_fqdn>.*@.*)\/(?P<email_id>.*)/$', + 'views.thread.add_tag'), + ### THREAD LEVEL VIEW ENDS ### + + + # REST API + url(r'^api/$', 'views.api.api'), + url(r'^api/email\/(?P<mlist_fqdn>.*@.*)\/(?P<messageid>.*)/', + EmailResource.as_view()), + url(r'^api/thread\/(?P<mlist_fqdn>.*@.*)\/(?P<threadid>.*)/', + ThreadResource.as_view()), + url(r'^api/search\/(?P<mlist_fqdn>.*@.*)\/(?P<field>.*)\/(?P<keyword>.*)/', + SearchResource.as_view()), + + # Uncomment the admin/doc line below to enable admin documentation: + # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), + + # Admin + url(r'^admin/', include(admin.site.urls)), + + # Robots.txt + url(r'^robots\.txt$', direct_to_template, + {'template': 'robots.txt', 'mimetype': 'text/plain'}), + + # Social Auth + url(r'', include('social_auth.urls')), + +) +#) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) +urlpatterns += staticfiles_urlpatterns() + diff --git a/hyperkitty/utils.py b/hyperkitty/utils.py new file mode 100644 index 0000000..ae55826 --- /dev/null +++ b/hyperkitty/utils.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. +# +# This file is part of HyperKitty. +# +# HyperKitty is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# HyperKitty is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# HyperKitty. If not, see <http://www.gnu.org/licenses/>. +# +# Author: Aamir Khan <syst3m.w0rm@gmail.com> +# + +import logging +import traceback + +from django.conf import settings +from django.core import mail +from django.views.debug import ExceptionReporter, get_exception_reporter_filter + +LOG_FILE = 'hk.log' + +def log(level, *args, **kwargs): + """Small wrapper around logger functions.""" + { + 'debug': logger.debug, + 'error': logger.error, + 'exception': logger.exception, + 'warn': logger.warn + }[level](*args, **kwargs) + + +class HyperKittyLogHandler(logging.Handler): + """A custom HyperKitty log handler. + + If the request is passed as the first argument to the log record, + request data will be provided in the email report. + """ + + def __init__(self, log_to_file=True, email_admins=True): + logging.Handler.__init__(self) + self.log_to_file = log_to_file + self.email_admins = email_admins + + def emit(self, record): + try: + request = record.request + subject = '%s (%s IP): %s' % ( + record.levelname, + (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS + and 'internal' or 'EXTERNAL'), + record.getMessage() + ) + filter = get_exception_reporter_filter(request) + request_repr = filter.get_request_repr(request) + except Exception: + subject = '%s: %s' % ( + record.levelname, + record.getMessage() + ) + request = None + request_repr = "Request repr() unavailable." + subject = self.format_subject(subject) + + if record.exc_info: + exc_info = record.exc_info + stack_trace = '\n'.join(traceback.format_exception(*record.exc_info)) + else: + exc_info = (None, record.getMessage(), None) + stack_trace = 'No stack trace available' + + message = "%s\n\n%s" % (stack_trace, request_repr) + reporter = ExceptionReporter(request, is_email=True, *exc_info) + html_message = reporter.get_traceback_html() + + if self.email_admins: + mail.mail_admins(subject, message, fail_silently=True, html_message=html_message) + + if self.log_to_file: + log_file = open(LOG_FILE, 'a') + log_file.write(message) + log_file.close() + + def format_subject(self, subject): + """ + Escape CR and LF characters, and limit length. + RFC 2822's hard limit is 998 characters per line. So, minus "Subject: " + the actual subject must be no longer than 989 characters. + """ + formatted_subject = subject.replace('\n', '\\n').replace('\r', '\\r') + return formatted_subject[:989] + + +logger = None +if not logger: + logger = logging.getLogger('HyperKitty') + +if not logger.handlers: + logger.addHandler(HyperKittyLogHandler(True, True)) diff --git a/hyperkitty/views/__init__.py b/hyperkitty/views/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/hyperkitty/views/__init__.py diff --git a/hyperkitty/views/accounts.py b/hyperkitty/views/accounts.py new file mode 100644 index 0000000..3154563 --- /dev/null +++ b/hyperkitty/views/accounts.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. +# +# This file is part of HyperKitty. +# +# HyperKitty is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# HyperKitty is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# HyperKitty. If not, see <http://www.gnu.org/licenses/>. +# +# Author: Aamir Khan <syst3m.w0rm@gmail.com> +# + +import re +import sys + +from django.conf import settings +from django.contrib import messages +from django.contrib.auth import logout, authenticate, login +from django.contrib.auth.decorators import (login_required, + permission_required, + user_passes_test) +from django.contrib.auth.forms import AuthenticationForm +from gsoc.models import UserProfile +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import render_to_response, redirect +from django.template import Context, loader, RequestContext +from django.utils.translation import gettext as _ +from urllib2 import HTTPError +from urlparse import urlparse + +from forms import RegistrationForm +from gsoc.utils import log + +def user_logout(request): + logout(request) + return redirect('user_login') + +def user_login(request,template = 'login.html'): + + parse_r = urlparse(request.META.get('HTTP_REFERER', 'index')) + previous = '%s%s' % (parse_r.path, parse_r.query) + + next_var = request.POST.get('next', request.GET.get('next', previous)) + + if request.method == 'POST': + form = AuthenticationForm(request.POST) + user = authenticate(username=request.POST.get('username'), + password=request.POST.get('password')) + + if user is not None: + log('debug', user) + if user.is_active: + login(request,user) + return redirect(next_var) + + else: + form = AuthenticationForm() + return render_to_response(template, {'form': form, 'next' : next_var}, + context_instance=RequestContext(request)) + +@login_required +def user_profile(request, user_email = None): + if not request.user.is_authenticated(): + return redirect('user_login') + # try to render the user profile. + try: + user_profile = request.user.get_profile() + # @TODO: Include the error name e.g, ProfileDoesNotExist? + except: + user_profile = UserProfile.objects.create(user=request.user) + + t = loader.get_template('user_profile.html') + + c = RequestContext(request, { + 'user_profile' : user_profile, + }) + + return HttpResponse(t.render(c)) + + +def user_registration(request): + if request.user.is_authenticated(): + # Already registered, redirect back to index page + return redirect('index') + + if request.POST: + form = RegistrationForm(request.POST) + if form.is_valid(): + # Save the user data. + form.save(form.cleaned_data) + user = authenticate(username=form.cleaned_data['username'], + password=form.cleaned_data['password1']) + + if user is not None: + log('debug', user) + if user.is_active: + login(request,user) + return redirect('index') + else: + form = RegistrationForm() + + return render_to_response('register.html', {'form': form}, context_instance=RequestContext(request)) + diff --git a/hyperkitty/views/api.py b/hyperkitty/views/api.py new file mode 100644 index 0000000..3a764db --- /dev/null +++ b/hyperkitty/views/api.py @@ -0,0 +1,27 @@ +import re +import os +import json +import urllib +import django.utils.simplejson as simplejson + +from calendar import timegm +from datetime import datetime, timedelta + +from urlparse import urljoin +from django.http import HttpResponse, HttpResponseRedirect +from django.template import RequestContext, loader +from django.conf import settings +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger, InvalidPage +from django.contrib.auth.decorators import (login_required, + permission_required, + user_passes_test) +from gsoc.utils import log + + +def api(request): + t = loader.get_template('api.html') + c = RequestContext(request, { + }) + return HttpResponse(t.render(c)) + + diff --git a/hyperkitty/views/forms.py b/hyperkitty/views/forms.py new file mode 100644 index 0000000..97e5550 --- /dev/null +++ b/hyperkitty/views/forms.py @@ -0,0 +1,59 @@ +from django import forms +from django.core import validators +from django.contrib.auth.models import User + +def isValidUsername(username): + try: + User.objects.get(username=username) + except User.DoesNotExist: + return + raise validators.ValidationError('The username "%s" is already taken.' % username) + +class RegistrationForm(forms.Form): + + username = forms.CharField(label='username', help_text=None, + widget=forms.TextInput( + attrs={'placeholder': 'username...'} + ), required = True, validators=[isValidUsername] + ) + + email = forms.EmailField(required=True) + + password1 = forms.CharField(widget=forms.PasswordInput) + + password2 = forms.CharField(widget=forms.PasswordInput) + + def save(self, new_user_data): + u = User.objects.create_user(new_user_data['username'], + new_user_data['email'], + new_user_data['password1']) + u.is_active = True + u.save() + return u + + +class AddTagForm(forms.Form): + tag = forms.CharField(label='', help_text=None, + widget=forms.TextInput( + attrs={'placeholder': 'Add a tag...'} + ) + ) + from_url = forms.CharField(widget=forms.HiddenInput, required=False) + +class SearchForm(forms.Form): + target = forms.CharField(label='', help_text=None, + widget=forms.Select( + choices=(('Subject', 'Subject'), + ('Content', 'Content'), + ('SubjectContent', 'Subject & Content'), + ('From', 'From')) + ) + ) + + keyword = forms.CharField(max_length=100,label='', help_text=None, + widget=forms.TextInput( + attrs={'placeholder': 'Search this list.'} + ) + ) + + diff --git a/hyperkitty/views/list.py b/hyperkitty/views/list.py new file mode 100644 index 0000000..17da2c6 --- /dev/null +++ b/hyperkitty/views/list.py @@ -0,0 +1,291 @@ +import re +import os +import json +import urllib +import django.utils.simplejson as simplejson + +from calendar import timegm +from datetime import datetime, timedelta + +from urlparse import urljoin +from django.http import HttpResponse, HttpResponseRedirect +from django.template import RequestContext, loader +from django.conf import settings +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger, InvalidPage +from django.contrib.auth.decorators import (login_required, + permission_required, + user_passes_test) +from kittystore.kittysastore import KittySAStore + +from gsoc.models import Rating +from lib.mockup import * +from forms import * +from gsoc.utils import log + + +STORE = KittySAStore(settings.KITTYSTORE_URL) + + +# @TODO : Move this into settings.py +MONTH_PARTICIPANTS = 284 +MONTH_DISCUSSIONS = 82 + + + +def archives(request, mlist_fqdn, year=None, month=None, day=None): + # No year/month: past 32 days + # year and month: find the 32 days for that month + # @TODO : modify url.py to account for page number + + end_date = None + if year or month or day: + try: + start_day = 1 + end_day = 1 + start_month = int(month) + end_month = int(month) + 1 + start_year = int(year) + end_year = int(year) + if day: + start_day = int(day) + end_day = start_day + 1 + end_month = start_month + if start_month == 12: + end_month = 1 + end_year = start_year + 1 + + begin_date = datetime(start_year, start_month, start_day) + end_date = datetime(end_year, end_month, end_day) + month_string = begin_date.strftime('%B %Y') + except ValueError, err: + print err + logger.error('Wrong format given for the date') + + if not end_date: + today = datetime.utcnow() + begin_date = datetime(today.year, today.month, 1) + end_date = datetime(today.year, today.month+1, 1) + month_string = 'Past thirty days' + list_name = mlist_fqdn.split('@')[0] + + search_form = SearchForm(auto_id=False) + t = loader.get_template('month_view.html') + threads = STORE.get_archives(list_name, start=begin_date, + end=end_date) + + participants = set() + cnt = 0 + for msg in threads: + # Statistics on how many participants and threads this month + participants.add(msg.sender) + msg.participants = STORE.get_thread_participants(list_name, + msg.thread_id) + msg.answers = STORE.get_thread_length(list_name, + msg.thread_id) + threads[cnt] = msg + cnt = cnt + 1 + #print msg + + paginator = Paginator(threads, 10) + pageNo = request.GET.get('page') + + try: + threads = paginator.page(pageNo) + except PageNotAnInteger: + # If page is not an integer, deliver first page. + threads = paginator.page(1) + except EmptyPage: + # If page is out of range (e.g. 9999), deliver last page of results. + threads = paginator.page(paginator.num_pages) + + + archives_length = STORE.get_archives_length(list_name) + + c = RequestContext(request, { + 'list_name' : list_name, + 'list_address': mlist_fqdn, + 'search_form': search_form, + 'month': month_string, + 'month_participants': len(participants), + 'month_discussions': len(threads), + 'threads': threads, + 'archives_length': archives_length, + }) + return HttpResponse(t.render(c)) + +def list(request, mlist_fqdn=None): + if not mlist_fqdn: + return HttpResponseRedirect('/') + t = loader.get_template('recent_activities.html') + search_form = SearchForm(auto_id=False) + list_name = mlist_fqdn.split('@')[0] + + # Get stats for last 30 days + today = datetime.utcnow() + end_date = datetime(today.year, today.month, today.day) + begin_date = end_date - timedelta(days=32) + + threads = STORE.get_archives(list_name=list_name,start=begin_date, + end=end_date) + + participants = set() + dates = {} + cnt = 0 + for msg in threads: + month = msg.date.month + if month < 10: + month = '0%s' % month + day = msg.date.day + if day < 10: + day = '0%s' % day + key = '%s%s%s' % (msg.date.year, month, day) + if key in dates: + dates[key] = dates[key] + 1 + else: + dates[key] = 1 + # Statistics on how many participants and threads this month + participants.add(msg.sender) + msg.participants = STORE.get_thread_participants(list_name, + msg.thread_id) + msg.answers = STORE.get_thread_length(list_name, + msg.thread_id) + threads[cnt] = msg + cnt = cnt + 1 + + # top threads are the one with the most answers + top_threads = sorted(threads, key=lambda entry: entry.answers, reverse=True) + + # active threads are the ones that have the most recent posting + active_threads = sorted(threads, key=lambda entry: entry.date, reverse=True) + + archives_length = STORE.get_archives_length(list_name) + + # top authors are the ones that have the most kudos. How do we determine + # that? Most likes for their post? + authors = generate_top_author() + authors = sorted(authors, key=lambda author: author.kudos) + authors.reverse() + + # Get the list activity per day + days = dates.keys() + days.sort() + dates_string = ["%s/%s/%s" % (key[0:4], key[4:6], key[6:8]) for key in days] + #print days + #print dates_string + evolution = [dates[key] for key in days] + if not evolution: + evolution.append(0) + + # threads per category is the top thread titles in each category + threads_per_category = generate_thread_per_category() + c = RequestContext(request, { + 'list_name' : list_name, + 'list_address': mlist_fqdn, + 'search_form': search_form, + 'month': 'Recent activity', + 'month_participants': len(participants), + 'month_discussions': len(threads), + 'top_threads': top_threads[:5], + 'most_active_threads': active_threads[:5], + 'top_author': authors, + 'threads_per_category': threads_per_category, + 'archives_length': archives_length, + 'evolution': evolution, + 'dates_string': dates_string, + }) + return HttpResponse(t.render(c)) + + +def _search_results_page(request, mlist_fqdn, threads, search_type, + page=1, num_threads=25, limit=None): + search_form = SearchForm(auto_id=False) + t = loader.get_template('search.html') + list_name = mlist_fqdn.split('@')[0] + res_num = len(threads) + + participants = set() + for msg in threads: + participants.add(msg.sender) + + paginator = Paginator(threads, num_threads) + + #If page request is out of range, deliver last page of results. + try: + threads = paginator.page(page) + except (EmptyPage, InvalidPage): + threads = paginator.page(paginator.num_pages) + + cnt = 0 + for msg in threads.object_list: + msg.email = msg.email.strip() + # Statistics on how many participants and threads this month + participants.add(msg.sender) + if msg.thread_id: + msg.participants = STORE.get_thread_participants(list_name, + msg.thread_id) + msg.answers = STORE.get_thread_length(list_name, + msg.thread_id) + else: + msg.participants = 0 + msg.answers = 0 + threads.object_list[cnt] = msg + cnt = cnt + 1 + + c = RequestContext(request, { + 'list_name' : list_name, + 'list_address': mlist_fqdn, + 'search_form': search_form, + 'month': search_type, + 'month_participants': len(participants), + 'month_discussions': res_num, + 'threads': threads, + 'full_path': request.get_full_path(), + }) + return HttpResponse(t.render(c)) + + +def search(request, mlist_fqdn): + keyword = request.GET.get('keyword') + target = request.GET.get('target') + page = request.GET.get('page') + if keyword and target: + url = '/search/%s/%s/%s/' % (mlist_fqdn, target, keyword) + if page: + url += '%s/' % page + else: + url = '/search/%s' % (mlist_fqdn) + return HttpResponseRedirect(url) + + +def search_keyword(request, mlist_fqdn, target, keyword, page=1): + ## Should we remove the code below? + ## If urls.py does it job we should never need it + if not keyword: + keyword = request.GET.get('keyword') + if not target: + target = request.GET.get('target') + if not target: + target = 'Subject' + regex = '%%%s%%' % keyword + list_name = mlist_fqdn.split('@')[0] + if target.lower() == 'subjectcontent': + threads = STORE.search_content_subject(list_name, keyword) + elif target.lower() == 'subject': + threads = STORE.search_subject(list_name, keyword) + elif target.lower() == 'content': + threads = STORE.search_content(list_name, keyword) + elif target.lower() == 'from': + threads = STORE.search_sender(list_name, keyword) + + return _search_results_page(request, mlist_fqdn, threads, 'Search', page) + + +def search_tag(request, mlist_fqdn, tag=None, page=1): + '''Searches both tag and topic''' + if tag: + query_string = {'Category': tag.capitalize()} + else: + query_string = None + return _search_results_page(request, mlist_fqdn, query_string, + 'Tag search', page, limit=50) + diff --git a/hyperkitty/views/message.py b/hyperkitty/views/message.py new file mode 100644 index 0000000..0b70969 --- /dev/null +++ b/hyperkitty/views/message.py @@ -0,0 +1,82 @@ +import re +import os +import django.utils.simplejson as simplejson + +from django.http import HttpResponse, HttpResponseRedirect +from django.template import RequestContext, loader +from django.conf import settings +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger, InvalidPage +from django.contrib.auth.decorators import (login_required, + permission_required, + user_passes_test) + +from kittystore.kittysastore import KittySAStore + +from gsoc.models import Rating +from lib.mockup import * +from forms import * +from gsoc.utils import log + +STORE = KittySAStore(settings.KITTYSTORE_URL) + + +def index (request, mlist_fqdn, messageid): + ''' Displays a single message identified by its messageid ''' + list_name = mlist_fqdn.split('@')[0] + + search_form = SearchForm(auto_id=False) + t = loader.get_template('message.html') + message = STORE.get_email(list_name, messageid) + message.email = message.email.strip() + # Extract all the votes for this message + try: + votes = Rating.objects.filter(messageid = messageid) + except Rating.DoesNotExist: + votes = {} + + likes = 0 + dislikes = 0 + + for vote in votes: + if vote.vote == 1: + likes = likes + 1 + elif vote.vote == -1: + dislikes = dislikes + 1 + else: + pass + + message.votes = votes + message.likes = likes + message.dislikes = dislikes + + c = RequestContext(request, { + 'list_name' : list_name, + 'list_address': mlist_fqdn, + 'message': message, + 'messageid' : messageid, + }) + return HttpResponse(t.render(c)) + + + +@login_required +def vote (request, mlist_fqdn): + """ Add a rating to a given message identified by messageid. """ + if not request.user.is_authenticated(): + return redirect('user_login') + + value = request.POST['vote'] + messageid = request.POST['messageid'] + + # Checks if the user has already voted for a this message. If yes modify db entry else create a new one. + try: + v = Rating.objects.get(user = request.user, messageid = messageid, list_address = mlist_fqdn) + except Rating.DoesNotExist: + v = Rating(list_address=mlist_fqdn, messageid = messageid, vote = value) + + v.user = request.user + v.vote = value + v.save() + response_dict = { } + + return HttpResponse(simplejson.dumps(response_dict), mimetype='application/javascript') diff --git a/hyperkitty/views/pages.py b/hyperkitty/views/pages.py new file mode 100644 index 0000000..189d574 --- /dev/null +++ b/hyperkitty/views/pages.py @@ -0,0 +1,38 @@ +#-*- coding: utf-8 -*- + +import re +import os +import json +import urllib +import django.utils.simplejson as simplejson + +from calendar import timegm +from datetime import datetime, timedelta + +from urlparse import urljoin +from django.http import HttpResponse, HttpResponseRedirect +from django.template import RequestContext, loader +from django.conf import settings +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger, InvalidPage +from django.contrib.auth.decorators import (login_required, + permission_required, + user_passes_test) +from gsoc.models import Rating +from lib.mockup import * +from forms import * +from gsoc.utils import log + +def index(request): + t = loader.get_template('index.html') + search_form = SearchForm(auto_id=False) + + base_url = settings.MAILMAN_API_URL % { + 'username': settings.MAILMAN_USER, 'password': settings.MAILMAN_PASS} + + list_data = ['devel@fp.o', 'packaging@fp.o', 'fr-users@fp.o'] + + c = RequestContext(request, { + 'lists': list_data, + 'search_form': search_form, + }) + return HttpResponse(t.render(c)) diff --git a/hyperkitty/views/thread.py b/hyperkitty/views/thread.py new file mode 100644 index 0000000..06a1bda --- /dev/null +++ b/hyperkitty/views/thread.py @@ -0,0 +1,113 @@ +import django.utils.simplejson as simplejson + +from django.http import HttpResponse, HttpResponseRedirect +from django.template import RequestContext, loader +from django.conf import settings +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger, InvalidPage +from django.contrib.auth.decorators import (login_required, + permission_required, + user_passes_test) +from kittystore.kittysastore import KittySAStore + +from gsoc.models import Rating +from lib.mockup import * +from forms import * +from gsoc.utils import log + +STORE = KittySAStore(settings.KITTYSTORE_URL) + + + +def thread_index (request, mlist_fqdn, threadid): + ''' Displays all the email for a given thread identifier ''' + list_name = mlist_fqdn.split('@')[0] + + search_form = SearchForm(auto_id=False) + t = loader.get_template('thread.html') + threads = STORE.get_thread(list_name, threadid) + #prev_thread = mongo.get_thread_name(list_name, int(threadid) - 1) + prev_thread = [] + if len(prev_thread) > 30: + prev_thread = '%s...' % prev_thread[:31] + #next_thread = mongo.get_thread_name(list_name, int(threadid) + 1) + next_thread = [] + if len(next_thread) > 30: + next_thread = '%s...' % next_thread[:31] + + participants = {} + cnt = 0 + + for message in threads: + # @TODO: Move this logic inside KittyStore? + message.email = message.email.strip() + + # Extract all the votes for this message + try: + votes = Rating.objects.filter(messageid = message.message_id) + except Rating.DoesNotExist: + votes = {} + + likes = 0 + dislikes = 0 + + for vote in votes: + if vote.vote == 1: + likes = likes + 1 + elif vote.vote == -1: + dislikes = dislikes + 1 + else: + pass + + message.votes = votes + message.likes = likes + message.dislikes = dislikes + + # Statistics on how many participants and threads this month + participants[message.sender] = {'email': message.email} + cnt = cnt + 1 + + archives_length = STORE.get_archives_length(list_name) + from_url = '/thread/%s/%s/' %(mlist_fqdn, threadid) + tag_form = AddTagForm(initial={'from_url' : from_url}) + + c = RequestContext(request, { + 'list_name' : list_name, + 'list_address': mlist_fqdn, + 'search_form': search_form, + 'addtag_form': tag_form, + 'month': 'Thread', + 'participants': participants, + 'answers': cnt, + 'first_mail': threads[0], + 'threads': threads[1:], + 'next_thread': next_thread, + 'next_thread_id': 0, + 'prev_thread': prev_thread, + 'prev_thread_id': 0, + 'archives_length': archives_length, + }) + return HttpResponse(t.render(c)) + + +@login_required +def add_tag(request, mlist_fqdn, email_id): + """ Add a tag to a given thread. """ + t = loader.get_template('threads/add_tag_form.html') + if request.method == 'POST': + form = AddTagForm(request.POST) + if form.is_valid(): + print "THERE WE ARE" + # TODO: Add the logic to add the tag + if form.data['from_url']: + return HttpResponseRedirect(form.data['from_url']) + else: + return HttpResponseRedirect('/') + else: + form = AddTagForm() + c = RequestContext(request, { + 'list_address': mlist_fqdn, + 'email_id': email_id, + 'addtag_form': form, + }) + return HttpResponse(t.render(c)) + |