diff --git a/.gitignore b/.gitignore index 700ce0ca3..4d61b9286 100644 --- a/.gitignore +++ b/.gitignore @@ -4,15 +4,15 @@ babel .coverage _build _static -panoramix/bin/panoramixc +dashed/bin/dashedc build *.db tmp -panoramix_config.py +dashed_config.py local_config.py env dist -panoramix.egg-info/ +dashed.egg-info/ app.db *.bak diff --git a/.landscape.yml b/.landscape.yml index 139b3db6c..7a9ab6210 100644 --- a/.landscape.yml +++ b/.landscape.yml @@ -15,8 +15,8 @@ pep8: full: true ignore-paths: - docs - - panoramix/migrations/env.py - - panoramix/ascii_art.py + - dashed/migrations/env.py + - dashed/ascii_art.py ignore-patterns: - ^example/doc_.*\.py$ - (^|/)docs(/|$) diff --git a/.travis.yml b/.travis.yml index c216ff4a0..e623eb0c8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,12 +9,12 @@ install: - pip wheel -w $HOME/.wheelhouse -f $HOME/.wheelhouse -r requirements.txt - pip install --find-links=$HOME/.wheelhouse --no-index -rrequirements.txt - python setup.py install - - cd panoramix/assets + - cd dashed/assets - npm install - npm run prod - cd $TRAVIS_BUILD_DIR script: bash run_tests.sh after_success: - coveralls - - cd panoramix/assets + - cd dashed/assets - npm run lint diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3e5493e54..9410d800c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,8 +30,8 @@ Look through the GitHub issues for features. Anything tagged with ### Documentation -Panoramix could always use better documentation, -whether as part of the official Panoramix docs, +Dashed could always use better documentation, +whether as part of the official Dashed docs, in docstrings, `docs/*.rst` or even on the web as blog posts or articles. @@ -49,14 +49,14 @@ If you are proposing a feature: ## Latest Documentation -[API Documentation](http://pythonhosted.com/panoramix) +[API Documentation](http://pythonhosted.com/dashed) ## Setting up a Python development environment # fork the repo on github and then clone it # alternatively you may want to clone the main repo but that won't work # so well if you are planning on sending PRs - # git clone git@github.com:mistercrunch/panoramix.git + # git clone git@github.com:mistercrunch/dashed.git # [optional] setup a virtual env and activate it virtualenv env @@ -66,24 +66,24 @@ If you are proposing a feature: python setup.py develop # Create an admin user - fabmanager create-admin --app panoramix + fabmanager create-admin --app dashed # Initialize the database - panoramix db upgrade + dashed db upgrade # Create default roles and permissions - panoramix init + dashed init # Load some data to play with - panoramix load_examples + dashed load_examples # start a dev web server - panoramix runserver -d + dashed runserver -d ## Setting up the node / npm javascript environment -`panoramix/assets` contains all npm-managed, front end assets. +`dashed/assets` contains all npm-managed, front end assets. Flask-Appbuilder itself comes bundled with jQuery and bootstrap. While these may be phased out over time, these packages are currently not managed with npm. @@ -116,7 +116,7 @@ new `node_modules/` folder within `assets/`. npm install ``` -To parse and generate bundled files for panoramix, run either of the +To parse and generate bundled files for dashed, run either of the following commands. The `dev` flag will keep the npm script running and re-run it upon any changes within the assets directory. @@ -132,7 +132,7 @@ For every development session you will have to start a flask dev server as well as an npm watcher ``` -panoramix runserver -d -p 8081 +dashed runserver -d -p 8081 npm run dev ``` @@ -157,12 +157,12 @@ Generate the documentation with: cd docs && ./build.sh ## CSS Themes -As part of the npm build process, CSS for Panoramix is compiled from ```Less```, a dynamic stylesheet language. +As part of the npm build process, CSS for Dashed is compiled from ```Less```, a dynamic stylesheet language. -It's possible to customize or add your own theme to Panoramix, either by overriding CSS rules or preferably +It's possible to customize or add your own theme to Dashed, either by overriding CSS rules or preferably by modifying the Less variables or files in ```assets/stylesheets/less/```. -The ```variables.less``` and ```bootswatch.less``` files that ship with Panoramix are derived from +The ```variables.less``` and ```bootswatch.less``` files that ship with Dashed are derived from [Bootswatch](https://bootswatch.com) and thus extend Bootstrap. Modify variables in these files directly, or swap them out entirely with the equivalent files from other Bootswatch (themes)[https://github.com/thomaspark/bootswatch.git] diff --git a/MANIFEST.in b/MANIFEST.in index 9480b5027..ce0e121d0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,8 @@ -recursive-include panoramix/templates * -recursive-include panoramix/static * -recursive-exclude panoramix/static/assets/node_modules * -recursive-include panoramix/static/assets/node_modules/font-awesome * -recursive-exclude panoramix/static/docs * +recursive-include dashed/templates * +recursive-include dashed/static * +recursive-exclude dashed/static/assets/node_modules * +recursive-include dashed/static/assets/node_modules/font-awesome * +recursive-exclude dashed/static/docs * recursive-exclude tests * -recursive-include panoramix/data * -recursive-include panoramix/migrations * +recursive-include dashed/data * +recursive-include dashed/migrations * diff --git a/README.md b/README.md index 37966c1ca..426a23ebc 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,30 @@ -Panoramix +Dashed ========= -[![Join the chat at https://gitter.im/mistercrunch/panoramix](https://badges.gitter.im/mistercrunch/panoramix.svg)](https://gitter.im/mistercrunch/panoramix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -![img](https://travis-ci.org/mistercrunch/panoramix.svg?branch=master) -[![Coverage Status](https://coveralls.io/repos/mistercrunch/panoramix/badge.svg?branch=master&service=github)](https://coveralls.io/github/mistercrunch/panoramix?branch=master) -[![Code Health](https://landscape.io/github/mistercrunch/panoramix/immune_to_filter/landscape.svg?style=flat)](https://landscape.io/github/mistercrunch/panoramix/master) +[![Join the chat at https://gitter.im/mistercrunch/dashed](https://badges.gitter.im/mistercrunch/dashed.svg)](https://gitter.im/mistercrunch/dashed?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +![img](https://travis-ci.org/mistercrunch/dashed.svg?branch=master) +[![Coverage Status](https://coveralls.io/repos/mistercrunch/dashed/badge.svg?branch=master&service=github)](https://coveralls.io/github/mistercrunch/dashed?branch=master) +[![Code Health](https://landscape.io/github/mistercrunch/dashed/immune_to_filter/landscape.svg?style=flat)](https://landscape.io/github/mistercrunch/dashed/master) -Panoramix is a data exploration platform designed to be visual, intuitive +Dashed is a data exploration platform designed to be visual, intuitive and interactive. -Video - Introduction to Panoramix +Video - Introduction to Dashed --------------------------------- -[![Panoramix - ](http://img.youtube.com/vi/3Txm_nj_R7M/0.jpg)](http://www.youtube.com/watch?v=3Txm_nj_R7M) +[![Dashed - ](http://img.youtube.com/vi/3Txm_nj_R7M/0.jpg)](http://www.youtube.com/watch?v=3Txm_nj_R7M) Screenshots ------------ ![img](http://i.imgur.com/bi09J9X.png) ![img](http://i.imgur.com/aOaH0ty.png) -Panoramix +Dashed --------- -Panoramix's main goal is to make it easy to slice, dice and visualize data. +Dashed's main goal is to make it easy to slice, dice and visualize data. It empowers its user to perform **analytics at the speed of thought**. -Panoramix provides: +Dashed provides: * A quick way to intuitively visualize datasets * Create and share interactive dashboards * A rich set of visualizations to analyze your data, as well as a flexible @@ -37,7 +37,7 @@ Panoramix provides: displayed in the UI, by defining which fields should show up in which dropdown and which aggregation and function (metrics) are made available to the user -* Deep integration with Druid allows for Panoramix to stay blazing fast while +* Deep integration with Druid allows for Dashed to stay blazing fast while slicing and dicing large, realtime datasets @@ -52,7 +52,7 @@ Buzz Phrases Database Support ---------------- -Panoramix was originally designed on to of Druid.io, but quickly broadened +Dashed was originally designed on to of Druid.io, but quickly broadened its scope to support other databases through the use of SqlAlchemy, a Python ORM that is compatible with [most common databases](http://docs.sqlalchemy.org/en/rel_1_0/core/engines.html). @@ -73,53 +73,53 @@ power analytic dashboards and applications.* Installation ------------ -Panoramix is currently only tested using Python 2.7.*. Python 3 support is +Dashed is currently only tested using Python 2.7.*. Python 3 support is on the roadmap, Python 2.6 won't be supported. -Follow these few simple steps to install Panoramix. +Follow these few simple steps to install Dashed. ``` -# Install panoramix -pip install panoramix +# Install dashed +pip install dashed # Create an admin user -fabmanager create-admin --app panoramix +fabmanager create-admin --app dashed # Initialize the database -panoramix db upgrade +dashed db upgrade # Create default roles and permissions -panoramix init +dashed init # Load some data to play with -panoramix load_examples +dashed load_examples # Start the development web server -panoramix runserver -d +dashed runserver -d ``` After installation, you should be able to point your browser to the right hostname:port [http://localhost:8088](http://localhost:8088), login using the credential you entered while creating the admin account, and navigate to `Menu -> Admin -> Refresh Metadata`. This action should bring in all of -your datasources for Panoramix to be aware of, and they should show up in +your datasources for Dashed to be aware of, and they should show up in `Menu -> Datasources`, from where you can start playing with your data! Configuration ------------- To configure your application, you need to create a file (module) -`panoramix_config.py` and make sure it is in your PYTHONPATH. Here are some +`dashed_config.py` and make sure it is in your PYTHONPATH. Here are some of the parameters you can copy / paste in that configuration module: ``` #--------------------------------------------------------- -# Panoramix specifix config +# Dashed specifix config #--------------------------------------------------------- ROW_LIMIT = 5000 WEBSERVER_THREADS = 8 -PANORAMIX_WEBSERVER_PORT = 8088 +DASHED_WEBSERVER_PORT = 8088 #--------------------------------------------------------- #--------------------------------------------------------- @@ -129,7 +129,7 @@ PANORAMIX_WEBSERVER_PORT = 8088 SECRET_KEY = '\2\1thisismyscretkey\1\2\e\y\y\h' # The SQLAlchemy connection string. -SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/panoramix.db' +SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/dashed.db' # Flask-WTF flag for CSRF CSRF_ENABLED = True @@ -139,8 +139,8 @@ DEBUG = True ``` This file also allows you to define configuration parameters used by -Flask App Builder, the web framework used by Panoramix. Please consult -the [Flask App Builder Documentation](http://flask-appbuilder.readthedocs.org/en/latest/config.html) for more information on how to configure Panoramix. +Flask App Builder, the web framework used by Dashed. Please consult +the [Flask App Builder Documentation](http://flask-appbuilder.readthedocs.org/en/latest/config.html) for more information on how to configure Dashed. * From the UI, enter the information about your clusters in the @@ -161,14 +161,14 @@ More screenshots Related Links ------------- -* [Panoramix Google Group] (https://groups.google.com/forum/#!forum/airbnb_panoramix) -* [Gitter (live chat) Channel](https://gitter.im/mistercrunch/panoramix) +* [Dashed Google Group] (https://groups.google.com/forum/#!forum/airbnb_dashed) +* [Gitter (live chat) Channel](https://gitter.im/mistercrunch/dashed) Tip of the Hat -------------- -Panoramix would not be possible without these great frameworks / libs +Dashed would not be possible without these great frameworks / libs * Flask App Builder - Allowing us to focus on building the app quickly while getting the foundation for free @@ -180,4 +180,4 @@ getting the foundation for free Contributing ------------ -Interested in contributing? Casual hacking? Check out [Contributing.MD](https://github.com/mistercrunch/panoramix/blob/master/CONTRIBUTING.md) +Interested in contributing? Casual hacking? Check out [Contributing.MD](https://github.com/mistercrunch/dashed/blob/master/CONTRIBUTING.md) diff --git a/TODO.md b/TODO.md index c32067d3d..089db8488 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,5 @@ # TODO -List of TODO items for Panoramix +List of TODO items for Dashed ## Important * **Caching:** integrate with flask-cache @@ -8,7 +8,7 @@ List of TODO items for Panoramix testing all the ajax-type calls * **Viz Plugins:** Allow people to define and share visualization plugins. ideally one would only need to drop in a set of files in a folder and - Panoramix would discover and expose the plugins + Dashed would discover and expose the plugins ## Features * **Stars:** set dashboards, slices and datasets as favorites diff --git a/alembic.ini b/alembic.ini index 24ddf3adb..6d12b48b5 100644 --- a/alembic.ini +++ b/alembic.ini @@ -29,7 +29,7 @@ script_location = migrations # are written from script.py.mako # output_encoding = utf-8 -sqlalchemy.url = scheme://localhost/panoramix +sqlalchemy.url = scheme://localhost/dashed # Logging configuration [loggers] diff --git a/dashed/__init__.py b/dashed/__init__.py new file mode 100644 index 000000000..808ed9ce6 --- /dev/null +++ b/dashed/__init__.py @@ -0,0 +1,38 @@ +"""Package's main module!""" + +import logging +import os +from flask import Flask, redirect +from flask.ext.appbuilder import SQLA, AppBuilder, IndexView +from flask.ext.appbuilder.baseviews import expose +from flask.ext.migrate import Migrate + + +APP_DIR = os.path.dirname(__file__) +CONFIG_MODULE = os.environ.get('DASHED_CONFIG', 'dashed.config') + +# Logging configuration +logging.basicConfig(format='%(asctime)s:%(levelname)s:%(name)s:%(message)s') +logging.getLogger().setLevel(logging.DEBUG) + +app = Flask(__name__) +app.config.from_object(CONFIG_MODULE) +db = SQLA(app) +migrate = Migrate(app, db, directory=APP_DIR + "/migrations") + + +class MyIndexView(IndexView): + @expose('/') + def index(self): + return redirect('/dashed/featured') + +appbuilder = AppBuilder( + app, db.session, + base_template='dashed/base.html', + indexview=MyIndexView, + security_manager_class=app.config.get("CUSTOM_SECURITY_MANAGER")) + +sm = appbuilder.sm + +get_session = appbuilder.get_session +from dashed import config, views # noqa diff --git a/dashed/ascii_art.py b/dashed/ascii_art.py new file mode 100644 index 000000000..480446948 --- /dev/null +++ b/dashed/ascii_art.py @@ -0,0 +1,68 @@ +error = ( +"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+ +"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+ +"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMM8OI++=~~~~~~=+?IODMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+ +"MMMMMMMMMMMMMMMMMMMMMMMMMD$~~~~~~~~~~~~~~~~~~~~~~~=$MMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+ +"MMMMMMMMMMMMMMMMMMMMMMN8?:~~~~~~~~~~~~~~~~~~~~~~~~~~=+8NMMMMMMMMMMMMMMMMMMMMMMMM\n"+ +"MMMMMMMMMMMMMMMMMMMMO=~~~~~~~~~~~~~~~~~+I??~~~~~~~~~~~~~+DMMMMMMMMMMMMMMMMMMMMMM\n"+ +"MMMMMMMMMMMMMMMMMMNI~~~~~~~~~~~~~~~~~~IIIII=~~~~~~~~~~~~~~=NMMMMMMMMMMMMMMMMMMMM\n"+ +"MMMMMMMMMMMMMMMMM+=~~~~~~~~~~~~~~~~~~~=III+~~~~~~~~~~~~~~~~~?8MMMMMMMMMMMMMMMMMM\n"+ +"MMMMMMMMMMMMMMMM?~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+++=~~~~8MMMMMMMMMMMMMMMMM\n"+ +"MMMMMMMMMMMMMMI=~~~~~~~~~~~~~~~~~~~~~~~~~III?I~~~~~~~~,:++++++~~8MMMMMMMMMMMMMMM\n"+ +"MMMMMMMMMMMMN7~~~~~~~~~~~~~~~~==+=~~~~~~=IIIII~~~~~~:. ..:=++=~=MMMMMMMMMMMMMMM\n"+ +"MMMMMMMMMMMO=~~~~~~~~~~~~~~~~+++=~~~~~~~~??I?I~~~~~~. ...,~~~~IMMMMMMMMMMMMM\n"+ +"MMMMMMMMMMM~~~~~~~~~~~~~~~~~+++:,~~~~~~~~~~~?=~~~~~:. ..~~~~~OMMMMMMMMMMMM\n"+ +"MMMMMMMMM$=~~~~~~~~~~~~~~~=++:.. ..~~~~~~~~~~~~~~~~,. . . :~~~~~OMMMMMMMMMMM\n"+ +"MMMMMMMMM~~~~~~~~~~~~~~~~+++,. .~~~~~~~~~~~~~~~.. .. . .~~~~~=OMMMMMMMMMM\n"+ +"MMMMMMMM?~~~~~~~~~~~~~~~=+~. .~~~~~~~~~~~~~~. ,MMMMM,=~~~~~~NMMMMMMMMM\n"+ +"MMMMMMMN~~~~~~~~~~~~~~~~~,. .,~~~~~~~~~~~~~.. ZMMM,+Z:~~~~~~$MMMMMMMMM\n"+ +"MMMMMM8?~~~~~~~~~~~~~~~~~.. ..~~~~~~~~~~~~~:. DMMM,+D~~~~~~~~IMMMMMMMM\n"+ +"MMMMMMI~~~~~~~~~~~~~~~~~~.. :MMMO~~~~~~~~~~~~~~~,.. ?MMMMMI~~~~~~~~~MMMMMMMM\n"+ +"MMMMMM=~~~~~~~~~~~~~~~~~~.. MMM+=M:~~~~~~~~~~~~~:. .:IM$~~~~~~~~~~~8MMMMMMM\n"+ +"MMMMMD~~~~~~~~~~~~~~~~~~~:. MMM:,M:~~~~~~~~~~~~~~~.......:~~~~~~~~~~$MMMMMMM\n"+ +"MMMMMI~~~~~~~~~~~~~~~~~~~~, MMMMMM~~~~~~~~~~~~~~~~~~,..:~~~~~~~~~~~~+MMMMMMM\n"+ +"MMMMD+~~~~~~~~~~~~~~~~~~~~~. $MMMM$~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=MMMMMMM\n"+ +"MMMM8~~~~~~~~~~~~~~~~~~~~~~:. . .:~~~~~~,..:. .=~~~~~~~~~~~~~~~~~~~~MMMMMMM\n"+ +"MMMMO~~~~~~~~~~~~~~~~~~~~~~~:, .:~~~~~=8.. .+ . =8ZI~~~~~~~~~~~~~~~~=MMMMMMM\n"+ +"MMMMZ=~~~~~~~~~~~~~~~~~~~~~~~~:,,,:~~~~~~IZ8:. .O....888?~~~~~~~~~~~~~~~+MMMMMMM\n"+ +"MMMMO=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~?888=...I~I88888O?~~~~~~~~~~~~~~7MMMMMMM\n"+ +"MMMMO~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Z888OO88888888888O?~~~~~~~~~~~~~OMMMMMMM\n"+ +"MMMMD+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=8888888888888888888~~~~~~~~~~~~+MMMMMMMM\n"+ +"MMMMM7~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~?8888888888888888888?~~~~~~~~~~=$MMMMMMMM\n"+ +"MMMMMD~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=$8888888888888888888O~~~~~~~~~~8MMMMMMMMM\n"+ +"MMMMMN=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+Z88888888888888888ZZ7=~~~~~~~~?MMMMMMMMMM\n"+ +"MMMMMMZ=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+Z88888888Z7I===~~~~~~~~~~~~~=OMMMMMMMMMMM\n"+ +"MMMMMMN$~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=$88888O7?=~~~~~~~~~~~~~~~~~~OMMMMMMMMMMMM\n"+ +"MMMMMMMM?~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~I8OZ+~~~~~~~~~~~~~~~~~~~~=DMMMMMMMMMMMMMM\n"+ +"MMMMMMMM8=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+$+=~~~~~~~~~~~~~~~~~~~~+MMMMMMMMMMMMMMMM\n"+ +"MMMMMMMMMD7~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=$DMMMMMMMMMMMMMMMMMM\n"+ +"MMMMMMMMMMM?~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=$OMMMMMMMMMMMMMMMMMMMMM\n"+ +"MMMMMMMMMMMMD7=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ZMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+ +"MMMMMMMMMMMMMMZ7=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~78MMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+ +"MMMMMMMMMMMMMMMMM8OI=~~~~~~~~~~~~~~~~~~~=+?ZDNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+ +"MMMMMMMMMMMMMMMMMMMMNDZ7?++~=~==~+?IONMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+ +"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+ +"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+ +"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+ +"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM") + +stacktrace=""" +------------------------------------------------------------------------------------------------------- +======================================================================================================= +------------------------------------------------------------------------------------------------------- + ___ ___ ___ + ( ) ( ) ( ) + .--. | |_ .---. .--. | | ___ | |_ ___ .-. .---. .--. .--. + / _ \ ( __) / .-, \ / \ | | ( ) ( __) ( ) \ / .-, \ / \ / \\ + . .' `. ; | | (__) ; | | .-. ; | | ' / | | | ' .-. ; (__) ; | | .-. ; | .-. ; + | ' | | | | ___ .'` | | |(___) | |,' / | | ___ | / (___) .'` | | |(___) | | | | + _\_`.(___) | |( ) / .'| | | | | . '. | |( ) | | / .'| | | | | |/ | +( ). '. | | | | | / | | | | ___ | | `. \ | | | | | | | / | | | | ___ | ' _.' + | | `\ | | ' | | ; | ; | | '( ) | | \ \ | ' | | | | ; | ; | | '( ) | .'.-. + ; '._,' ' ' `-' ; ' `-' | ' `-' | | | \ . ' `-' ; | | ' `-' | ' `-' | ' `-' / + '.___.' `.__. `.__.'_. `.__,' (___ ) (___) `.__. (___) `.__.'_. `.__,' `.__.' + +------------------------------------------------------------------------------------------------------- +======================================================================================================= +------------------------------------------------------------------------------------------------------- +""" diff --git a/dashed/assets/.babelrc b/dashed/assets/.babelrc new file mode 100644 index 000000000..ad8d1fe60 --- /dev/null +++ b/dashed/assets/.babelrc @@ -0,0 +1,3 @@ +{ + "presets" : ["es2015", "react"] +} diff --git a/dashed/assets/.eslintignore b/dashed/assets/.eslintignore new file mode 100644 index 000000000..926b5a451 --- /dev/null +++ b/dashed/assets/.eslintignore @@ -0,0 +1,3 @@ +node_modules/* +vendor/* +javascripts/dist/* diff --git a/dashed/assets/.eslintrc b/dashed/assets/.eslintrc new file mode 100644 index 000000000..882ad973a --- /dev/null +++ b/dashed/assets/.eslintrc @@ -0,0 +1,234 @@ +{ + "root": true, + + "globals": { + "Symbol": false, + "Map": false, + "Set": false, + "Reflect": false, + }, + + "env": { + "es6": false, + "browser": true, + "node": true, + }, + + "parserOptions": { + "ecmaVersion": 5, + "sourceType": "module" + }, + + "rules": { + "array-bracket-spacing": [2, "never", { + "singleValue": false, + "objectsInArrays": false, + "arraysInArrays": false + }], + "array-callback-return": [2], + "block-spacing": [2, "always"], + "brace-style": [2, "1tbs", { "allowSingleLine": true }], + "callback-return": [2, ["callback"]], + "camelcase": [0], + "comma-dangle": [2, "never"], + "comma-spacing": [2], + "comma-style": [2, "last"], + "curly": [2, "all"], + "eqeqeq": 2, + "func-names": [0], + "id-length": [2, { "min": 1, "max": 25, "properties": "never" }], + "key-spacing": [2, { "beforeColon": false, "afterColon": true }], + "keyword-spacing": [2, { + "before": true, + "after": true, + "overrides": { + "return": { "after": true }, + "throw": { "after": true }, + "case": { "after": true } + } + }], + "linebreak-style": [2, "unix"], + "lines-around-comment": [2, { + "beforeBlockComment": false, + "afterBlockComment": false, + "beforeLineComment": false, + "allowBlockStart": true, + "allowBlockEnd": true + }], + "max-depth": [2, 5], + "max-len": [0, 80, 4], + "max-nested-callbacks": [1, 2], + "max-params": [1, 4], + "new-parens": [2], + "newline-after-var": [0], + "no-bitwise": [0], + "no-cond-assign": [2], + "no-console": [2], + "no-const-assign": [2], + "no-constant-condition": [2], + "no-control-regex": [2], + "no-debugger": [2], + "no-delete-var": [2], + "no-dupe-args": [2], + "no-dupe-class-members": [2], + "no-dupe-keys": [2], + "no-duplicate-case": [2], + "no-else-return": [0], + "no-empty": [2], + "no-eq-null": [0], + "no-eval": [2], + "no-ex-assign": [2], + "no-extend-native": [2], + "no-extra-bind": [2], + "no-extra-boolean-cast": [2], + "no-extra-label": [2], + "no-extra-parens": [0], // needed for clearer #math eg (a - b) / c + "no-extra-semi": [2], + "no-fallthrough": [2], + "no-floating-decimal": [2], + "no-func-assign": [2], + "no-implied-eval": [2], + "no-implicit-coercion": [2, { + "boolean": false, + "number": true, + "string": true + }], + "no-implicit-globals": [2], + "no-inline-comments": [0], + "no-invalid-regexp": [2], + "no-irregular-whitespace": [2], + "no-iterator": [2], + "no-label-var": [2], + "no-labels": [2, { "allowLoop": false, "allowSwitch": false }], + "no-lone-blocks": [2], + "no-lonely-if": [2], + "no-loop-func": [2], + "no-magic-numbers": [0], // doesn't work well with vis cosmetic constant + "no-mixed-requires": [1, false], + "no-mixed-spaces-and-tabs": [2, false], + "no-multi-spaces": [2, { + "exceptions": { + "ImportDeclaration": true, + "Property": true, + "VariableDeclarator": true + } + }], + "no-multi-str": [2], + "no-multiple-empty-lines": [2, { "max": 1, "maxEOF": 1 }], + "no-native-reassign": [2], + "no-negated-condition": [2], + "no-negated-in-lhs": [2], + "no-nested-ternary": [0], + "no-new": [2], + "no-new-func": [2], + "no-new-object": [2], + "no-new-require": [0], + "no-new-symbol": [2], + "no-new-wrappers": [2], + "no-obj-calls": [2], + "no-octal": [2], + "no-octal-escape": [2], + "no-path-concat": [0], + "no-process-env": [0], + "no-process-exit": [2], + "no-proto": [2], + "no-redeclare": [2], + "no-regex-spaces": [2], + "no-restricted-modules": [0], + "no-restricted-imports": [0], + "no-restricted-syntax": [2, + "DebuggerStatement", + "LabeledStatement", + "WithStatement" + ], + "no-return-assign": [2, "always"], + "no-script-url": [2], + "no-self-assign": [2], + "no-self-compare": [0], + "no-sequences": [2], + "no-shadow-restricted-names": [2], + "no-spaced-func": [2], + "no-sparse-arrays": [2], + "no-sync": [0], + "no-ternary": [0], + "no-this-before-super": [2], + "no-throw-literal": [2], + "no-trailing-spaces": [2, { "skipBlankLines": false }], + "no-undef": [2, { "typeof": true }], + "no-undef-init": [2], + "no-undefined": [0], + "no-underscore-dangle": [0], // __data__ sometimes + "no-unexpected-multiline": [2], + "no-unmodified-loop-condition": [2], + "no-unneeded-ternary": [2], + "no-unreachable": [2], + "no-unused-expressions": [2], + "no-unused-labels": [2], + "no-unused-vars": [2, { + "vars": "all", + "args": "none", // (d, i) pattern d3 func makes difficult to enforce + "varsIgnorePattern": "jQuery" + }], + "no-use-before-define": [0], + "no-useless-call": [2], + "no-useless-concat": [2], + "no-useless-constructor": [2], + "no-void": [0], + "no-warning-comments": [0, { "terms": ["todo", "fixme", "xxx"], "location": "start" }], + "no-with": [2], + "no-whitespace-before-property": [2], + "object-curly-spacing": [2, "always"], + "object-shorthand": [2, "never"], + "one-var": [0], + "one-var-declaration-per-line": [2, "initializations"], + "operator-assignment": [0, "always"], + "padded-blocks": [0], + "prefer-arrow-callback": [0], + "prefer-const": [0], + "prefer-reflect": [0], + "prefer-rest-params": [0], + "prefer-spread": [0], + "prefer-template": [0], + "quote-props": [2, "as-needed", { "keywords": true }], + "radix": [2], + "require-yield": [2], + "semi": [2], + "semi-spacing": [2, { "before": false, "after": true }], + "sort-vars": [0], + "sort-imports": [0], + "space-before-function-paren": [2, { "anonymous": "always", "named": "never" }], + "space-before-blocks": [2, { "functions": "always", "keywords": "always" }], + "space-in-brackets": [0, "never", { + "singleValue": true, + "arraysInArrays": false, + "arraysInObjects": false, + "objectsInArrays": true, + "objectsInObjects": true, + "propertyName": false + }], + }, + // Temporarily not enforced + "new-cap": [2], // @TODO more tricky for the moment + "newline-per-chained-call": [2, { "ignoreChainWithDepth": 6 }], + "no-param-reassign": [0], // turn on once default args supported + "no-shadow": [2, { // @TODO more tricky for the moment with eg 'data' + "builtinGlobals": false, + "hoist": "functions", + "allow": ["i", "d"] + }], + "space-in-parens": [2, "never"], + "space-infix-ops": [2], + "space-unary-ops": [2, { "words": true, "nonwords": false }], + "spaced-comment": [2, "always", { "markers": ["!"] }], + "spaced-line-comment": [0, "always"], + "strict": [2, "global"], + "template-curly-spacing": [2, "never"], + "use-isnan": [2], + "valid-jsdoc": [0], + "valid-typeof": [2], + "vars-on-top": [0], + "wrap-iife": [2], + "wrap-regex": [2], + "yield-star-spacing": [2, { "before": false, "after": true }], + "yoda": [2, "never", { "exceptRange": true, "onlyEquality": false }] +} diff --git a/dashed/assets/images/favicon.png b/dashed/assets/images/favicon.png new file mode 100644 index 000000000..77fd477de Binary files /dev/null and b/dashed/assets/images/favicon.png differ diff --git a/dashed/assets/javascripts/css-theme.js b/dashed/assets/javascripts/css-theme.js new file mode 100644 index 000000000..93722b914 --- /dev/null +++ b/dashed/assets/javascripts/css-theme.js @@ -0,0 +1 @@ +require('../stylesheets/less/index.less'); diff --git a/dashed/assets/javascripts/dashboard.js b/dashed/assets/javascripts/dashboard.js new file mode 100644 index 000000000..1ea68ee9c --- /dev/null +++ b/dashed/assets/javascripts/dashboard.js @@ -0,0 +1,229 @@ +var $ = window.$ = require('jquery'); +var jQuery = window.jQuery = $; +var px = require('./modules/dashed.js'); +var d3 = require('d3'); +require('bootstrap'); + +var ace = require('brace'); +require('brace/mode/css'); +require('brace/theme/crimson_editor'); + +require('./dashed-select2.js'); +require('../node_modules/gridster/dist/jquery.gridster.min.css'); +require('../node_modules/gridster/dist/jquery.gridster.min.js'); + +var Dashboard = function (dashboardData) { + var dashboard = $.extend(dashboardData, { + filters: {}, + init: function () { + this.initDashboardView(); + var sliceObjects = [], + dash = this; + dashboard.slices.forEach(function (data) { + var slice = px.Slice(data, dash); + $("#slice_" + data.slice_id).find('a.refresh').click(function () { + slice.render(); + }); + sliceObjects.push(slice); + slice.render(); + }); + this.slices = sliceObjects; + }, + setFilter: function (slice_id, col, vals) { + this.addFilter(slice_id, col, vals, false); + }, + addFilter: function (slice_id, col, vals, merge) { + if (merge === undefined) { + merge = true; + } + if (!(slice_id in this.filters)) { + this.filters[slice_id] = {}; + } + if (!(col in this.filters[slice_id]) || !merge) { + this.filters[slice_id][col] = vals; + } else { + this.filters[slice_id][col] = d3.merge([this.filters[slice_id][col], vals]); + } + this.refreshExcept(slice_id); + }, + readFilters: function () { + // Returns a list of human readable active filters + return JSON.stringify(this.filters, null, 4); + }, + refreshExcept: function (slice_id) { + var immune = this.metadata.filter_immune_slices; + if (immune) { + this.slices.forEach(function (slice) { + if (slice.data.slice_id !== slice_id && immune.indexOf(slice.data.slice_id) === -1) { + slice.render(); + } + }); + } + }, + clearFilters: function (slice_id) { + delete this.filters[slice_id]; + this.refreshExcept(slice_id); + }, + removeFilter: function (slice_id, col, vals) { + if (slice_id in this.filters) { + if (col in this.filters[slice_id]) { + var a = []; + this.filters[slice_id][col].forEach(function (v) { + if (vals.indexOf(v) < 0) { + a.push(v); + } + }); + this.filters[slice_id][col] = a; + } + } + this.refreshExcept(slice_id); + }, + getSlice: function (slice_id) { + this.slices.forEach(function (slice, i) { + if (slice.slice_id === slice_id) { + return slice; + } + }); + }, + initDashboardView: function () { + dashboard = this; + var gridster = $(".gridster ul").gridster({ + autogrow_cols: true, + widget_margins: [10, 10], + widget_base_dimensions: [100, 100], + draggable: { + handle: '.drag' + }, + resize: { + enabled: true, + stop: function (e, ui, element) { + var slice_data = $(element).data('slice'); + if (slice_data) { + dashboard.getSlice(slice_data.slice_id).resize(); + } + } + }, + serialize_params: function (_w, wgd) { + return { + slice_id: $(_w).attr('slice_id'), + col: wgd.col, + row: wgd.row, + size_x: wgd.size_x, + size_y: wgd.size_y + }; + } + }).data('gridster'); + $("div.gridster").css('visibility', 'visible'); + $("#savedash").click(function () { + var expanded_slices = {}; + $.each($(".slice_info"), function (i, d) { + var widget = $(this).parents('.widget'); + var slice_description = widget.find('.slice_description'); + if (slice_description.is(":visible")) { + expanded_slices[$(d).attr('slice_id')] = true; + } + }); + var data = { + positions: gridster.serialize(), + css: editor.getValue(), + expanded_slices: expanded_slices + }; + $.ajax({ + type: "POST", + url: '/dashed/save_dash/' + dashboard.id + '/', + data: { + data: JSON.stringify(data) + }, + success: function () { + alert("Saved!"); + }, + error: function () { + alert("Error :("); + } + }); + }); + + var editor = ace.edit("dash_css"); + editor.$blockScrolling = Infinity; + + editor.setTheme("ace/theme/crimson_editor"); + editor.setOptions({ + minLines: 16, + maxLines: Infinity, + useWorker: false + }); + editor.getSession().setMode("ace/mode/css"); + + $(".select2").select2({ + dropdownAutoWidth: true + }); + $("#css_template").on("change", function () { + var css = $(this).find('option:selected').data('css'); + editor.setValue(css); + + $('#dash_css').val(css); + injectCss("dashboard-template", css); + + }); + $('#filters').click(function () { + alert(dashboard.readFilters()); + }); + $("a.remove-chart").click(function () { + var li = $(this).parents("li"); + gridster.remove_widget(li); + }); + + $("li.widget").click(function (e) { + var $this = $(this); + var $target = $(e.target); + + if ($target.hasClass("slice_info")) { + $this.find(".slice_description").slideToggle(0, function () { + $this.find('.refresh').click(); + }); + } else if ($target.hasClass("controls-toggle")) { + $this.find(".chart-controls").toggle(); + } + }); + + editor.on("change", function () { + var css = editor.getValue(); + $('#dash_css').val(css); + injectCss("dashboard-template", css); + }); + + var css = $('.dashboard').data('css'); + injectCss("dashboard-template", css); + + // Injects the passed css string into a style sheet with the specified className + // If a stylesheet doesn't exist with the passed className, one will be injected into + function injectCss(className, css) { + + var head = document.head || document.getElementsByTagName('head')[0]; + var style = document.querySelector('.' + className); + + if (!style) { + if (className.split(' ').length > 1) { + throw new Error("This method only supports selections with a single class name."); + } + style = document.createElement('style'); + style.className = className; + style.type = 'text/css'; + head.appendChild(style); + } + + if (style.styleSheet) { + style.styleSheet.cssText = css; + } else { + style.innerHTML = css; + } + } + } + }); + dashboard.init(); + return dashboard; +}; + +$(document).ready(function () { + Dashboard($('.dashboard').data('dashboard')); +}); diff --git a/dashed/assets/javascripts/dashed-select2.js b/dashed/assets/javascripts/dashed-select2.js new file mode 100644 index 000000000..2b1543512 --- /dev/null +++ b/dashed/assets/javascripts/dashed-select2.js @@ -0,0 +1,5 @@ +require('../node_modules/select2/select2.css'); +require('../node_modules/select2-bootstrap-css/select2-bootstrap.min.css'); +require('../node_modules/jquery-ui/themes/base/jquery-ui.css'); +require('select2'); +require('../vendor/select2.sortable.js'); diff --git a/dashed/assets/javascripts/explore.js b/dashed/assets/javascripts/explore.js new file mode 100644 index 000000000..8abe4dc90 --- /dev/null +++ b/dashed/assets/javascripts/explore.js @@ -0,0 +1,334 @@ +// Javascript for the explorer page +// Init explorer view -> load vis dependencies -> read data (from dynamic html) -> render slice +// nb: to add a new vis, you must also add a Python fn in viz.py +// +// js +var $ = window.$ = require('jquery'); +var jQuery = window.jQuery = $; +var px = require('./modules/dashed.js'); + +require('jquery-ui'); +$.widget.bridge('uitooltip', $.ui.tooltip); // Shutting down jq-ui tooltips +require('bootstrap'); + +require('./dashed-select2.js'); + +require('../node_modules/bootstrap-toggle/js/bootstrap-toggle.min.js'); + +// css +require('../vendor/pygments.css'); +require('../node_modules/bootstrap-toggle/css/bootstrap-toggle.min.css'); + +var slice; + +function prepForm() { + var i = 1; + // Assigning the right id to form elements in filters + $("#filters > div").each(function () { + $(this).attr("id", function () { + return "flt_" + i; + }); + $(this).find("#flt_col_0") + .attr("id", function () { + return "flt_col_" + i; + }) + .attr("name", function () { + return "flt_col_" + i; + }); + $(this).find("#flt_op_0") + .attr("id", function () { + return "flt_op_" + i; + }) + .attr("name", function () { + return "flt_op_" + i; + }); + $(this).find("#flt_eq_0") + .attr("id", function () { + return "flt_eq_" + i; + }) + .attr("name", function () { + return "flt_eq_" + i; + }); + i++; + }); +} + +function renderSlice() { + prepForm(); + slice.render(); +} + +function initExploreView() { + + function druidify() { + $('div.alert').remove(); + history.pushState({}, document.title, slice.querystring()); + renderSlice(); + } + + function get_collapsed_fieldsets() { + var collapsed_fieldsets = $("#collapsed_fieldsets").val(); + + if (collapsed_fieldsets !== undefined && collapsed_fieldsets !== "") { + collapsed_fieldsets = collapsed_fieldsets.split('||'); + } else { + collapsed_fieldsets = []; + } + return collapsed_fieldsets; + } + + function toggle_fieldset(legend, animation) { + var parent = legend.parent(); + var fieldset = parent.find(".legend_label").text(); + var collapsed_fieldsets = get_collapsed_fieldsets(); + var index; + + if (parent.hasClass("collapsed")) { + if (animation) { + parent.find(".fieldset_content").slideDown(); + } else { + parent.find(".fieldset_content").show(); + } + parent.removeClass("collapsed"); + parent.find("span.collapser").text("[-]"); + + // removing from array, js is overcomplicated + index = collapsed_fieldsets.indexOf(fieldset); + if (index !== -1) { + collapsed_fieldsets.splice(index, 1); + } + } else { // not collapsed + if (animation) { + parent.find(".fieldset_content").slideUp(); + } else { + parent.find(".fieldset_content").hide(); + } + + parent.addClass("collapsed"); + parent.find("span.collapser").text("[+]"); + index = collapsed_fieldsets.indexOf(fieldset); + if (index === -1 && fieldset !== "" && fieldset !== undefined) { + collapsed_fieldsets.push(fieldset); + } + } + + $("#collapsed_fieldsets").val(collapsed_fieldsets.join("||")); + } + + $('legend').click(function () { + toggle_fieldset($(this), true); + }); + + function copyURLToClipboard(url) { + var textArea = document.createElement("textarea"); + textArea.style.position = 'fixed'; + textArea.style.left = '-1000px'; + textArea.value = url; + + document.body.appendChild(textArea); + textArea.select(); + + try { + var successful = document.execCommand('copy'); + if (!successful) { + throw new Error("Not successful"); + } + } catch (err) { + window.alert("Sorry, your browser does not support copying. Use Ctrl / Cmd + C!"); + } + document.body.removeChild(textArea); + return successful; + } + + $('#shortner').click(function () { + $.ajax({ + type: "POST", + url: '/r/shortner/', + data: { + data: '/' + window.location.pathname + slice.querystring() + }, + success: function (data) { + var close = ''; + var copy = ''; + var spaces = '   '; + var popover = data + spaces + copy + spaces + close; + + var $shortner = $('#shortner') + .popover({ + content: popover, + placement: 'left', + html: true, + trigger: 'manual' + }) + .popover('show'); + + $('#copy_url').tooltip().click(function () { + var success = copyURLToClipboard(data); + if (success) { + $(this).attr("data-original-title", "Copied!").tooltip('fixTitle').tooltip('show'); + window.setTimeout(destroyPopover, 1200); + } + }); + $('#close_shortner').click(destroyPopover); + + function destroyPopover() { + $shortner.popover('destroy'); + } + }, + error: function () { + alert("Error :("); + } + }); + }); + $("#viz_type").change(function () { + $("#query").submit(); + }); + + var collapsed_fieldsets = get_collapsed_fieldsets(); + for (var i = 0; i < collapsed_fieldsets.length; i++) { + toggle_fieldset($('legend:contains("' + collapsed_fieldsets[i] + '")'), false); + } + + $(".select2").select2({ + dropdownAutoWidth: true + }); + $(".select2Sortable").select2({ + dropdownAutoWidth: true + }); + $(".select2Sortable").select2Sortable({ + bindOrder: 'sortableStop' + }); + $("form").show(); + $('[data-toggle="tooltip"]').tooltip({ + container: 'body' + }); + $(".ui-helper-hidden-accessible").remove(); // jQuery-ui 1.11+ creates a div for every tooltip + + function set_filters() { + for (var i = 1; i < 10; i++) { + var eq = px.getParam("flt_eq_" + i); + if (eq !== '') { + add_filter(i); + } + } + } + set_filters(); + + function add_filter(i) { + var cp = $("#flt0").clone(); + $(cp).appendTo("#filters"); + $(cp).show(); + if (i !== undefined) { + $(cp).find("#flt_eq_0").val(px.getParam("flt_eq_" + i)); + $(cp).find("#flt_op_0").val(px.getParam("flt_op_" + i)); + $(cp).find("#flt_col_0").val(px.getParam("flt_col_" + i)); + } + $(cp).find('select').select2(); + $(cp).find('.remove').click(function () { + $(this).parent().parent().remove(); + }); + } + + $(window).bind("popstate", function (event) { + // Browser back button + var returnLocation = history.location || document.location; + // Could do something more lightweight here, but we're not optimizing + // for the use of the back button anyways + returnLocation.reload(); + }); + + $("#plus").click(add_filter); + $("#btn_save").click(function () { + var slice_name = prompt("Name your slice!"); + if (slice_name !== "" && slice_name !== null) { + $("#slice_name").val(slice_name); + prepForm(); + $("#action").val("save"); + $("#query").submit(); + } + }); + $("#btn_overwrite").click(function () { + var flag = confirm("Overwrite slice [" + $("#slice_name").val() + "] !?"); + if (flag) { + $("#action").val("overwrite"); + prepForm(); + $("#query").submit(); + } + }); + + $(".druidify").click(druidify); + + function create_choices(term, data) { + var filtered = $(data).filter(function () { + return this.text.localeCompare(term) === 0; + }); + if (filtered.length === 0) { + return { + id: term, + text: term + }; + } + } + + function initSelectionToValue(element, callback) { + callback({ + id: element.val(), + text: element.val() + }); + } + + $(".select2_freeform").each(function () { + var parent = $(this).parent(); + var name = $(this).attr('name'); + var l = []; + var selected = ''; + for (var i = 0; i < this.options.length; i++) { + l.push({ + id: this.options[i].value, + text: this.options[i].text + }); + if (this.options[i].selected) { + selected = this.options[i].value; + } + } + parent.append( + '' + ); + $("input[name='" + name + "']").select2({ + createSearchChoice: create_choices, + initSelection: initSelectionToValue, + dropdownAutoWidth: true, + multiple: false, + data: l + }); + $(this).remove(); + }); +} + +$(document).ready(function () { + initExploreView(); + + // Dynamically register this visualization + var visType = window.viz_type.value; + px.registerViz(visType); + + var data = $('.slice').data('slice'); + slice = px.Slice(data); + + // + $('.slice').data('slice', slice); + + // call vis render method, which issues ajax + renderSlice(); + + // make checkbox inputs display as toggles + $(':checkbox') + .addClass('pull-right') + .attr("data-onstyle", "default") + .bootstrapToggle({ + size: 'mini' + }); + + $('div.toggle').addClass('pull-right'); + slice.bindResizeToWindowResize(); +}); diff --git a/dashed/assets/javascripts/featured.js b/dashed/assets/javascripts/featured.js new file mode 100644 index 000000000..07a4b9207 --- /dev/null +++ b/dashed/assets/javascripts/featured.js @@ -0,0 +1,19 @@ +var $ = window.$ = require('jquery'); +var jQuery = window.jQuery = $; +var px = require('./modules/dashed.js'); + +require('bootstrap'); +require('datatables'); +require('../node_modules/datatables-bootstrap3-plugin/media/css/datatables-bootstrap3.css'); + +$(document).ready(function () { + $('#dataset-table').DataTable({ + bPaginate: false, + order: [ + [1, "asc"] + ] + }); + $('#dataset-table_info').remove(); + //$('input[type=search]').addClass('form-control'); # TODO get search box to look nice + $('#dataset-table').show(); +}); diff --git a/dashed/assets/javascripts/index.jsx b/dashed/assets/javascripts/index.jsx new file mode 100644 index 000000000..d12ac3d59 --- /dev/null +++ b/dashed/assets/javascripts/index.jsx @@ -0,0 +1,18 @@ +var $ = require('jquery'); +var jQuery = $; +import React from 'react'; +import { render } from 'react-dom'; +import { Jumbotron } from 'react-bootstrap'; + +class App extends React.Component { + render () { + return ( + +

Dashed

+

Extensible visualization tool for exploring data from any database.

+
+ ); + } +} + +render(, document.getElementById('app')); diff --git a/dashed/assets/javascripts/modules/dashed.js b/dashed/assets/javascripts/modules/dashed.js new file mode 100644 index 000000000..d0389362d --- /dev/null +++ b/dashed/assets/javascripts/modules/dashed.js @@ -0,0 +1,300 @@ +var $ = require('jquery'); +var jQuery = $; +var d3 = require('d3'); + +require('../../stylesheets/dashed.css'); + +// vis sources +var sourceMap = { + area: 'nvd3_vis.js', + bar: 'nvd3_vis.js', + bubble: 'nvd3_vis.js', + big_number: 'big_number.js', + compare: 'nvd3_vis.js', + dist_bar: 'nvd3_vis.js', + directed_force: 'directed_force.js', + filter_box: 'filter_box.js', + heatmap: 'heatmap.js', + iframe: 'iframe.js', + line: 'nvd3_vis.js', + markup: 'markup.js', + para: 'parallel_coordinates.js', + pie: 'nvd3_vis.js', + pivot_table: 'pivot_table.js', + sankey: 'sankey.js', + sunburst: 'sunburst.js', + table: 'table.js', + word_cloud: 'word_cloud.js', + world_map: 'world_map.js' +}; + +var color = function () { + // Color related utility functions go in this object + var bnbColors = [ + //rausch hackb kazan babu lima beach barol + '#ff5a5f', '#7b0051', '#007A87', '#00d1c1', '#8ce071', '#ffb400', '#b4a76c', + '#ff8083', '#cc0086', '#00a1b3', '#00ffeb', '#bbedab', '#ffd266', '#cbc29a', + '#ff3339', '#ff1ab1', '#005c66', '#00b3a5', '#55d12e', '#b37e00', '#988b4e' + ]; + var spectrums = { + blue_white_yellow: ['#00d1c1', 'white', '#ffb400'], + fire: ['white', 'yellow', 'red', 'black'], + white_black: ['white', 'black'], + black_white: ['black', 'white'] + }; + var colorBnb = function () { + // Color factory + var seen = {}; + return function (s) { + // next line is for dashed series that should have the same color + s = s.replace('---', ''); + if (seen[s] === undefined) { + seen[s] = Object.keys(seen).length; + } + return this.bnbColors[seen[s] % this.bnbColors.length]; + }; + }; + var colorScalerFactory = function (colors, data, accessor) { + // Returns a linear scaler our of an array of color + if (!Array.isArray(colors)) { + colors = spectrums[colors]; + } + + var ext = [0, 1]; + if (data !== undefined) { + ext = d3.extent(data, accessor); + } + + var points = []; + var chunkSize = (ext[1] - ext[0]) / colors.length; + $.each(colors, function (i, c) { + points.push(i * chunkSize); + }); + return d3.scale.linear().domain(points).range(colors); + }; + return { + bnbColors: bnbColors, + category21: colorBnb(), + colorScalerFactory: colorScalerFactory + }; +}; + +var px = (function () { + + var visualizations = {}; + var slice; + + function getParam(name) { + name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); + var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"), + results = regex.exec(location.search); + return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); + } + + function UTC(dttm) { + return new Date(dttm.getUTCFullYear(), dttm.getUTCMonth(), dttm.getUTCDate(), dttm.getUTCHours(), dttm.getUTCMinutes(), dttm.getUTCSeconds()); + } + var tickMultiFormat = d3.time.format.multi([ + [".%L", function (d) { + return d.getMilliseconds(); + }], // If there are millisections, show only them + [":%S", function (d) { + return d.getSeconds(); + }], // If there are seconds, show only them + ["%a %b %d, %I:%M %p", function (d) { + return d.getMinutes() !== 0; + }], // If there are non-zero minutes, show Date, Hour:Minute [AM/PM] + ["%a %b %d, %I %p", function (d) { + return d.getHours() !== 0; + }], // If there are hours that are multiples of 3, show date and AM/PM + ["%a %b %d, %Y", function (d) { + return d.getDate() !== 1; + }], // If not the first of the month, do "month day, year." + ["%B %Y", function (d) { + return d.getMonth() !== 0 && d.getDate() === 1; + }], // If the first of the month, do "month day, year." + ["%Y", function (d) { + return true; + }] // fall back on month, year + ]); + + function formatDate(dttm) { + var d = UTC(new Date(dttm)); + //d = new Date(d.getTime() - 1 * 60 * 60 * 1000); + return tickMultiFormat(d); + } + + function timeFormatFactory(d3timeFormat) { + var f = d3.time.format(d3timeFormat); + return function (dttm) { + var d = UTC(new Date(dttm)); + return f(d); + }; + } + + var Slice = function (data, dashboard) { + var timer; + var token = $('#' + data.token); + var container_id = data.token + '_con'; + var selector = '#' + container_id; + var container = $(selector); + var slice_id = data.slice_id; + var dttm = 0; + var stopwatch = function () { + dttm += 10; + var num = dttm / 1000; + $('#timer').text(num.toFixed(2) + " sec"); + }; + var qrystr = ''; + var always = function (data) { + //Private f, runs after done and error + clearInterval(timer); + $('#timer').removeClass('btn-warning'); + }; + slice = { + data: data, + container: container, + container_id: container_id, + selector: selector, + querystring: function () { + var parser = document.createElement('a'); + parser.href = data.json_endpoint; + if (dashboard !== undefined) { + var flts = encodeURIComponent(JSON.stringify(dashboard.filters)); + qrystr = parser.search + "&extra_filters=" + flts; + } else if ($('#query').length === 0) { + qrystr = parser.search; + } else { + qrystr = '?' + $('#query').serialize(); + } + return qrystr; + }, + jsonEndpoint: function () { + var parser = document.createElement('a'); + parser.href = data.json_endpoint; + var endpoint = parser.pathname + this.querystring() + "&json=true"; + return endpoint; + }, + done: function (data) { + clearInterval(timer); + token.find("img.loading").hide(); + container.show(); + if (data !== undefined) { + $("#query_container").html(data.query); + } + $('#timer').removeClass('btn-warning'); + $('#timer').addClass('btn-success'); + $('span.query').removeClass('disabled'); + $('#json').click(function () { + window.location = data.json_endpoint; + }); + $('#standalone').click(function () { + window.location = data.standalone_endpoint; + }); + $('#csv').click(function () { + window.location = data.csv_endpoint; + }); + $('.btn-group.results span').removeAttr('disabled'); + always(data); + }, + error: function (msg) { + token.find("img.loading").hide(); + var err = '
' + msg + '
'; + container.html(err); + container.show(); + $('span.query').removeClass('disabled'); + $('#timer').addClass('btn-danger'); + always(data); + }, + width: function () { + return token.width(); + }, + height: function () { + var others = 0; + var widget = container.parents('.widget'); + var slice_description = widget.find('.slice_description'); + if (slice_description.is(":visible")) { + others += widget.find('.slice_description').height() + 25; + } + others += widget.find('.chart-header').height(); + return widget.height() - others - 10; + }, + bindResizeToWindowResize: function () { + var resizeTimer; + $(window).on('resize', function (e) { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(function () { + slice.resize(); + }, 500); + }); + }, + render: function () { + $('.btn-group.results span').attr('disabled', 'disabled'); + token.find("img.loading").show(); + container.hide(); + container.html(''); + dttm = 0; + timer = setInterval(stopwatch, 10); + $('#timer').removeClass('btn-danger btn-success'); + $('#timer').addClass('btn-warning'); + this.viz.render(); + }, + resize: function () { + token.find("img.loading").show(); + container.hide(); + container.html(''); + this.viz.render(); + this.viz.resize(); + }, + addFilter: function (col, vals) { + if (dashboard !== undefined) { + dashboard.addFilter(slice_id, col, vals); + } + }, + setFilter: function (col, vals) { + if (dashboard !== undefined) { + dashboard.setFilter(slice_id, col, vals); + } + }, + clearFilter: function () { + if (dashboard !== undefined) { + delete dashboard.clearFilter(slice_id); + } + }, + removeFilter: function (col, vals) { + if (dashboard !== undefined) { + delete dashboard.removeFilter(slice_id, col, vals); + } + } + }; + var visType = data.form_data.viz_type; + px.registerViz(visType); + slice.viz = visualizations[data.form_data.viz_type](slice); + return slice; + }; + + function registerViz(name) { + var visSource = sourceMap[name]; + + if (visSource) { + var visFactory = require('../../visualizations/' + visSource); + if (typeof visFactory === 'function') { + visualizations[name] = visFactory; + } + } else { + throw new Error("require(" + name + ") failed."); + } + } + + // Export public functions + return { + registerViz: registerViz, + Slice: Slice, + formatDate: formatDate, + timeFormatFactory: timeFormatFactory, + color: color(), + getParam: getParam + }; +})(); + +module.exports = px; diff --git a/dashed/assets/javascripts/modules/utils.js b/dashed/assets/javascripts/modules/utils.js new file mode 100644 index 000000000..e582ab65b --- /dev/null +++ b/dashed/assets/javascripts/modules/utils.js @@ -0,0 +1,55 @@ +var d3 = require('d3'); + +/* + Utility function that takes a d3 svg:text selection and a max width, and splits the + text's text across multiple tspan lines such that any given line does not exceed max width + + If text does not span multiple lines AND adjustedY is passed, will set the text to the passed val + */ +function wrapSvgText(text, width, adjustedY) { + var lineHeight = 1; // ems + + text.each(function () { + var text = d3.select(this), + words = text.text().split(/\s+/), + word, + line = [], + lineNumber = 0, + x = text.attr("x"), + y = text.attr("y"), + dy = parseFloat(text.attr("dy")), + tspan = text.text(null) + .append("tspan") + .attr("x", x) + .attr("y", y) + .attr("dy", dy + "em"); + + var didWrap = false; + + for (var i = 0; i < words.length; i++) { + word = words[i]; + line.push(word); + tspan.text(line.join(" ")); + + if (tspan.node().getComputedTextLength() > width) { + line.pop(); // remove word that pushes over the limit + tspan.text(line.join(" ")); + line = [word]; + tspan = text.append("tspan") + .attr("x", x) + .attr("y", y) + .attr("dy", ++lineNumber * lineHeight + dy + "em") + .text(word); + + didWrap = true; + } + } + if (!didWrap && typeof adjustedY !== "undefined") { + tspan.attr("y", adjustedY); + } + }); +} + +module.exports = { + wrapSvgText: wrapSvgText +}; diff --git a/dashed/assets/javascripts/sql.js b/dashed/assets/javascripts/sql.js new file mode 100644 index 000000000..7b2fb27c5 --- /dev/null +++ b/dashed/assets/javascripts/sql.js @@ -0,0 +1,97 @@ +var $ = window.$ = require('jquery'); +var jQuery = window.jQuery = $; +require('select2'); +require('datatables'); +require('bootstrap'); + +var ace = require('brace'); +require('brace/mode/sql'); +require('brace/theme/crimson_editor'); + +$(document).ready(function () { + function getParam(name) { + name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); + var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"), + results = regex.exec(location.search); + return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); + } + + function initSqlEditorView() { + var database_id = $('#database_id').val(); + var editor = ace.edit("sql"); + editor.$blockScrolling = Infinity; + editor.getSession().setUseWrapMode(true); + + $('#sql').hide(); + editor.setTheme("ace/theme/crimson_editor"); + editor.setOptions({ + minLines: 16, + maxLines: Infinity + }); + editor.getSession().setMode("ace/mode/sql"); + editor.focus(); + $("select").select2({ + dropdownAutoWidth: true + }); + + function showTableMetadata() { + $(".metadata").load( + '/dashed/table/' + database_id + '/' + $("#dbtable").val() + '/'); + } + $("#dbtable").on("change", showTableMetadata); + showTableMetadata(); + $("#create_view").click(function () { + alert("Not implemented"); + }); + $(".sqlcontent").show(); + + function selectStarOnClick() { + $.ajax('/dashed/select_star/' + database_id + '/' + $("#dbtable").val() + '/') + .done(function (msg) { + editor.setValue(msg); + }); + } + + $("#select_star").click(selectStarOnClick); + + editor.setValue(getParam('sql')); + $(window).bind("popstate", function (event) { + // Could do something more lightweight here, but we're not optimizing + // for the use of the back button anyways + editor.setValue(getParam('sql')); + $("#run").click(); + }); + $("#run").click(function () { + $('#results').hide(0); + $('#loading').show(0); + history.pushState({}, document.title, '?sql=' + encodeURIComponent(editor.getValue())); + $.ajax({ + type: "POST", + url: '/dashed/runsql/', + data: { + data: JSON.stringify({ + database_id: $('#database_id').val(), + sql: editor.getSession().getValue() + }) + }, + success: function (data) { + $('#loading').hide(0); + $('#results').show(0); + $('#results').html(data); + + $('table.sql_results').DataTable({ + paging: false, + searching: true, + aaSorting: [] + }); + }, + error: function (err, err2) { + $('#loading').hide(0); + $('#results').show(0); + $('#results').html(err.responseText); + } + }); + }); + } + initSqlEditorView(); +}); diff --git a/dashed/assets/javascripts/standalone.js b/dashed/assets/javascripts/standalone.js new file mode 100644 index 000000000..9110581f4 --- /dev/null +++ b/dashed/assets/javascripts/standalone.js @@ -0,0 +1,13 @@ +var $ = window.$ = require('jquery'); +var jQuery = window.jQuery = $; +var px = require('./modules/dashed.js'); + +require('bootstrap'); + +$(document).ready(function () { + var slice; + var data = $('.slice').data('slice'); + slice = px.Slice(data); + slice.render(); + slice.bindResizeToWindowResize(); +}); diff --git a/dashed/assets/package.json b/dashed/assets/package.json new file mode 100644 index 000000000..cb461778a --- /dev/null +++ b/dashed/assets/package.json @@ -0,0 +1,77 @@ +{ + "name": "dashed", + "version": "0.1.0", + "description": "Any database to any visualization", + "directories": { + "doc": "docs", + "test": "tests" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "webpack -d --watch --colors", + "prod": "webpack -p --colors", + "lint": "npm run --silent lint:js", + "lint:js": "eslint --ignore-path=.eslintignore --ext .js .; exit 0;" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/mistercrunch/dashed.git" + }, + "keywords": [ + "big", + "data", + "exploratory", + "analysis", + "react", + "d3", + "airbnb", + "nerds", + "database", + "flask" + ], + "author": "Airbnb", + "bugs": { + "url": "https://github.com/mistercrunch/dashed/issues" + }, + "homepage": "https://github.com/mistercrunch/dashed#readme", + "dependencies": { + "babel-loader": "^6.2.1", + "babel-polyfill": "^6.3.14", + "babel-preset-es2015": "^6.3.13", + "babel-preset-react": "^6.3.13", + "bootstrap": "^3.3.6", + "bootstrap-datepicker": "^1.6.0", + "bootstrap-toggle": "^2.2.1", + "brace": "^0.7.0", + "css-loader": "^0.23.1", + "d3": "^3.5.14", + "d3-cloud": "^1.2.1", + "d3-sankey": "^0.2.1", + "d3-tip": "^0.6.7", + "d3.layout.cloud": "^1.2.0", + "datamaps": "^0.4.4", + "datatables": "^1.10.9", + "datatables-bootstrap3-plugin": "^0.4.0", + "exports-loader": "^0.6.3", + "font-awesome": "^4.5.0", + "gridster": "^0.5.6", + "imports-loader": "^0.6.5", + "jquery": "^2.2.1", + "jquery-ui": "^1.10.5", + "less-loader": "^2.2.2", + "nvd3": "1.8.2", + "react": "^0.14.7", + "react-bootstrap": "^0.28.3", + "react-dom": "^0.14.7", + "select2": "3.5", + "select2-bootstrap-css": "^1.4.6", + "style-loader": "^0.13.0", + "topojson": "^1.6.22", + "webpack": "^1.12.12" + }, + "devDependencies": { + "eslint": "^2.2.0", + "file-loader": "^0.8.5", + "url-loader": "^0.5.7" + } +} diff --git a/dashed/assets/stylesheets/dashed.css b/dashed/assets/stylesheets/dashed.css new file mode 100644 index 000000000..6e5a82256 --- /dev/null +++ b/dashed/assets/stylesheets/dashed.css @@ -0,0 +1,244 @@ +body { + margin: 0px !important; +} + +.modal-dialog { + z-index: 1100; +} + +input.form-control { + background-color: white; +} + +.col-left-fixed { + width:350px; + position: absolute; + float: left; +} +.col-offset { + margin-left: 365px; +} + +.slice_description{ + padding: 8px; + margin: 5px; + border: 1px solid #DDD; + background-color: #F8F8F8; + border-radius: 5px; + font-size: 12px; +} + +.slice_info{ + cursor: pointer; +} + +.padded { + padding: 10px; +} + +.intable-longtext{ + max-height: 200px; + overflow: auto; +} + +.container-fluid { + text-align: left; +} +input[type="checkbox"] { + display: inline-block; + width: 16px; + height: 16px; + float: right; +} +form div { + padding-top: 1px; +} +.navbar-brand a { + color: white; +} + +.header span{ + margin-left: 3px; +} + +#timer { + width: 80px; + text-align: right; +} + +.notbtn { + cursor: default; +} +hr { + margin-top: 15px; + margin-bottom: 15px; +} + +span.title-block { + background-color: #EEE; + border-radius: 4px; + padding: 6px 12px; + margin: 0px 10px; + font-size: 20px; +} + +fieldset.fs-style { + font-family: Verdana, Arial, sans-serif; + font-size: small; + font-weight: normal; + border: 1px solid #CCC; + background-color: #F4F4F4; + border-radius: 6px; + padding: 10px; + margin: 0px 0px 10px 0px; +} +legend.legend-style { + font-size: 14px; + padding: 0px 6px; + cursor: pointer; + margin: 0px; + color: #444; + background-color: transparent; + font-weight: bold; +} +.nvtooltip { + //position: relative !important; + z-index: 888; +} +.nvtooltip table td{ + font-size: 11px !important; +} +legend { + width: auto; + border-bottom: 0px; +} +.navbar { + -webkit-box-shadow: 0px 3px 3px #AAA; + -moz-box-shadow: 0px 3px 3px #AAA; + box-shadow: 0px 3px 3px #AAA; + z-index: 999; +} +.panel.panel-primary { + margin: 10px; +} + +.index .carousel img { + max-height: 500px; +} +.index .carousel { + overflow: hidden; + height: 500px; +} +.index .carousel-caption h1 { + font-size: 80px; +} +.index .carousel-caption p { + font-size: 20px; +} +.index div.carousel-caption{ + background: rgba(0,0,0,0.5); + border-radius: 20px; + top: 150px; + bottom: auto !important; +} +.index .carousel-inner > .item > img { + margin: 0 auto; +} +.index { + margin: -20px; +} +.index .carousel-indicators li { + background-color: #AAA; + border: 1px solid black; +} + +.index .carousel-indicators .active { + background-color: #000; + border: 5px solid black; +} + +.datasource form div.form-control { + margin-bottom: 5px !important; +} +.datasource form input.form-control { + margin-bottom: 5px !important; +} +.datasource .tooltip-inner { + max-width: 350px; +} +img.loading { + width: 40px; +} + +.dashboard a i { + cursor: pointer; +} +.dashboard i.drag { + cursor: move !important; +} +.dashboard .gridster .preview-holder { + z-index: 1; + position: absolute; + background-color: #AAA; + border-color: #AAA; + opacity: 0.3; +} +.gridster li.widget{ + list-style-type: none; + border-radius: 0; + margin: 5px; + border: 1px solid #ccc; + box-shadow: 2px 1px 5px -2px #aaa; + overflow: hidden; + background-color: #fff; +} +.dashboard .gridster .dragging, +.dashboard .gridster .resizing { + opacity: 0.5; +} +.dashboard img.loading { + width: 20px; + margin: 5px; +} +.dashboard .title { + text-align: center; +} +.dashboard .slice_title { + text-align: center; + font-weight: bold; + font-size: 14px; + padding: 5px; +} +.dashboard div.slice_content { + width: 100%; + height: 100%; +} + +.dashboard div.nvtooltip { + z-index: 888; /* this lets tool tips go on top of other slices */ +} + +div.header { + font-weight: bold; +} +li.widget:hover { + z-index: 1000; +} + +li.widget .chart-header { + padding: 5px; + background-color: #f1f1f1; +} + +li.widget .chart-header a { + margin-left: 5px; +} + +li.widget .chart-controls { + display: none; + background-color: #f1f1f1; +} + +li.widget .slice_container { + overflow: auto; +} diff --git a/dashed/assets/stylesheets/fonts/glyphicons-halflings-regular.ttf b/dashed/assets/stylesheets/fonts/glyphicons-halflings-regular.ttf new file mode 100644 index 000000000..1413fc609 Binary files /dev/null and b/dashed/assets/stylesheets/fonts/glyphicons-halflings-regular.ttf differ diff --git a/dashed/assets/stylesheets/fonts/glyphicons-halflings-regular.woff b/dashed/assets/stylesheets/fonts/glyphicons-halflings-regular.woff new file mode 100644 index 000000000..9e612858f Binary files /dev/null and b/dashed/assets/stylesheets/fonts/glyphicons-halflings-regular.woff differ diff --git a/dashed/assets/stylesheets/fonts/glyphicons-halflings-regular.woff2 b/dashed/assets/stylesheets/fonts/glyphicons-halflings-regular.woff2 new file mode 100644 index 000000000..64539b54c Binary files /dev/null and b/dashed/assets/stylesheets/fonts/glyphicons-halflings-regular.woff2 differ diff --git a/dashed/assets/stylesheets/less/bootswatch.less b/dashed/assets/stylesheets/less/bootswatch.less new file mode 100644 index 000000000..e5edac2eb --- /dev/null +++ b/dashed/assets/stylesheets/less/bootswatch.less @@ -0,0 +1,616 @@ +// Paper 3.3.5 +// Bootswatch +// ----------------------------------------------------- + +@web-font-path: "https://fonts.googleapis.com/css?family=Roboto:300,400,500,700"; + +.web-font(@path) { + @import url("@{path}"); +} +.web-font(@web-font-path); + +// Navbar ===================================================================== + +.navbar { + border: none; + .box-shadow(0 1px 2px rgba(0,0,0,.3)); + + &-brand { + font-size: 24px; + } + + &-inverse { + .navbar-form { + + input[type=text], + input[type=password] { + color: #fff; + .box-shadow(inset 0 -1px 0 @navbar-inverse-link-color); + .placeholder(@navbar-inverse-link-color); + + &:focus { + .box-shadow(inset 0 -2px 0 #fff); + } + } + } + } +} + +// Buttons ==================================================================== + +#btn(@class,@bg) { + .btn-@{class} { + background-size: 200%; + background-position: 50%; + + &:focus { + background-color: @bg; + } + + &:hover, + &:active:hover { + background-color: darken(@bg, 6%); + } + + &:active { + background-color: darken(@bg, 12%); + #gradient > .radial(darken(@bg, 12%) 10%, @bg 11%); + background-size: 1000%; + .box-shadow(2px 2px 4px rgba(0,0,0,.4)); + } + } +} + +#btn(default,@btn-default-bg); +#btn(primary,@btn-primary-bg); +#btn(success,@btn-success-bg); +#btn(info,@btn-info-bg); +#btn(warning,@btn-warning-bg); +#btn(danger,@btn-danger-bg); +#btn(link,#fff); + +.btn { + text-transform: uppercase; + border: none; + .box-shadow(1px 1px 4px rgba(0,0,0,.4)); + .transition(all 0.4s); + + &-link { + border-radius: @btn-border-radius-base; + .box-shadow(none); + color: @btn-default-color; + + &:hover, + &:focus { + .box-shadow(none); + color: @btn-default-color; + text-decoration: none; + } + } + + &-default { + + &.disabled { + background-color: rgba(0, 0, 0, 0.1); + color: rgba(0, 0, 0, 0.4); + opacity: 1; + } + } +} + +.btn-group { + .btn + .btn, + .btn + .btn-group, + .btn-group + .btn, + .btn-group + .btn-group { + margin-left: 0; + } + + &-vertical { + > .btn + .btn, + > .btn + .btn-group, + > .btn-group + .btn, + > .btn-group + .btn-group { + margin-top: 0; + } + } +} + +// Typography ================================================================= + +body { + -webkit-font-smoothing: antialiased; + letter-spacing: .1px; +} + +p { + margin: 0 0 1em; +} + +input, +button { + -webkit-font-smoothing: antialiased; + letter-spacing: .1px; +} + +a { + .transition(all 0.2s); +} + +// Tables ===================================================================== + +.table-hover { + > tbody > tr, + > tbody > tr > th, + > tbody > tr > td { + .transition(all 0.2s); + } +} + +// Forms ====================================================================== + +label { + font-weight: normal; +} + +textarea, +textarea.form-control, +input.form-control, +input[type=text], +input[type=password], +input[type=email], +input[type=number], +[type=text].form-control, +[type=password].form-control, +[type=email].form-control, +[type=tel].form-control, +[contenteditable].form-control { + padding: 0; + border: none; + border-radius: 0; + -webkit-appearance: none; + .box-shadow(inset 0 -1px 0 #ddd); + font-size: 16px; + + &:focus { + .box-shadow(inset 0 -2px 0 @brand-primary); + } + + &[disabled], + &[readonly] { + .box-shadow(none); + border-bottom: 1px dotted #ddd; + } + + &.input { + &-sm { + font-size: @font-size-small; + } + + &-lg { + font-size: @font-size-large; + } + } +} + +select, +select.form-control { + border: 0; + border-radius: 0; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + padding-left: 0; + padding-right: 0\9; // remove padding for < ie9 since default arrow can't be removed + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAMAAACelLz8AAAAJ1BMVEVmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmaP/QSjAAAADHRSTlMAAgMJC0uWpKa6wMxMdjkoAAAANUlEQVR4AeXJyQEAERAAsNl7Hf3X6xt0QL6JpZWq30pdvdadme+0PMdzvHm8YThHcT1H7K0BtOMDniZhWOgAAAAASUVORK5CYII=); + background-size: 13px; + background-repeat: no-repeat; + background-position: right center; + .box-shadow(inset 0 -1px 0 #ddd); + font-size: 16px; + line-height: 1.5; + + &::-ms-expand { + display: none; + } + + &.input { + &-sm { + font-size: @font-size-small; + } + + &-lg { + font-size: @font-size-large; + } + } + + &:focus { + .box-shadow(inset 0 -2px 0 @brand-primary); + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAMAAACelLz8AAAAJ1BMVEUhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISF8S9ewAAAADHRSTlMAAgMJC0uWpKa6wMxMdjkoAAAANUlEQVR4AeXJyQEAERAAsNl7Hf3X6xt0QL6JpZWq30pdvdadme+0PMdzvHm8YThHcT1H7K0BtOMDniZhWOgAAAAASUVORK5CYII=); + } + + &[multiple] { + background: none; + } +} + +.radio, +.radio-inline, +.checkbox, +.checkbox-inline { + label { + padding-left: 25px; + } + + input[type="radio"], + input[type="checkbox"] { + margin-left: -25px; + } +} + +input[type="radio"], +.radio input[type="radio"], +.radio-inline input[type="radio"] { + position: relative; + margin-top: 6px; + margin-right: 4px; + vertical-align: top; + border: none; + background-color: transparent; + -webkit-appearance: none; + appearance: none; + cursor: pointer; + + &:focus { + outline: none; + } + + &:before, + &:after { + content: ""; + display: block; + width: 18px; + height: 18px; + border-radius: 50%; + .transition(240ms); + } + + &:before { + position: absolute; + left: 0; + top: -3px; + background-color: @brand-primary; + .scale(0); + } + + &:after { + position: relative; + top: -3px; + border: 2px solid @gray; + } + + &:checked:before { + .scale(0.5); + } + + &:disabled:checked:before { + background-color: @gray-light; + } + + &:checked:after { + border-color: @brand-primary; + } + + &:disabled:after, + &:disabled:checked:after { + border-color: @gray-light; + } +} + +input[type="checkbox"], +.checkbox input[type="checkbox"], +.checkbox-inline input[type="checkbox"] { + position: relative; + border: none; + margin-bottom: -4px; + -webkit-appearance: none; + appearance: none; + cursor: pointer; + + &:focus { + outline: none; + } + + &:focus:after { + border-color: @brand-primary; + } + + &:after { + content: ""; + display: block; + width: 18px; + height: 18px; + margin-top: -2px; + margin-right: 5px; + border: 2px solid @gray; + border-radius: 2px; + .transition(240ms); + } + + &:checked:before { + content: ""; + position: absolute; + top: 0; + left: 6px; + display: table; + width: 6px; + height: 12px; + border: 2px solid #fff; + border-top-width: 0; + border-left-width: 0; + .rotate(45deg); + } + + &:checked:after { + background-color: @brand-primary; + border-color: @brand-primary; + } + + &:disabled:after { + border-color: @gray-light; + } + + &:disabled:checked:after { + background-color: @gray-light; + border-color: transparent; + } +} + +.has-warning { + input:not([type=checkbox]), + .form-control, + input.form-control[readonly], + input[type=text][readonly], + [type=text].form-control[readonly], + input:not([type=checkbox]):focus, + .form-control:focus { + border-bottom: none; + .box-shadow(inset 0 -2px 0 @brand-warning); + } +} + +.has-error { + input:not([type=checkbox]), + .form-control, + input.form-control[readonly], + input[type=text][readonly], + [type=text].form-control[readonly], + input:not([type=checkbox]):focus, + .form-control:focus { + border-bottom: none; + .box-shadow(inset 0 -2px 0 @brand-danger); + } +} + +.has-success { + input:not([type=checkbox]), + .form-control, + input.form-control[readonly], + input[type=text][readonly], + [type=text].form-control[readonly], + input:not([type=checkbox]):focus, + .form-control:focus { + border-bottom: none; + .box-shadow(inset 0 -2px 0 @brand-success); + } +} + +// Remove the Bootstrap feedback styles for input addons +.input-group-addon { + .has-warning &, .has-error &, .has-success & { + color: @input-color; + border-color: @input-group-addon-border-color; + background-color: @input-group-addon-bg; + } +} + +// Navs ======================================================================= + +.nav-tabs { + > li > a, + > li > a:focus { + margin-right: 0; + background-color: transparent; + border: none; + color: @navbar-default-link-color; + .box-shadow(inset 0 -1px 0 #ddd); + .transition(all 0.2s); + + &:hover { + background-color: transparent; + .box-shadow(inset 0 -2px 0 @brand-primary); + color: @brand-primary; + } + } + + & > li.active > a, + & > li.active > a:focus { + border: none; + .box-shadow(inset 0 -2px 0 @brand-primary); + color: @brand-primary; + + &:hover { + border: none; + color: @brand-primary; + } + } + + & > li.disabled > a { + .box-shadow(inset 0 -1px 0 #ddd); + } + + &.nav-justified { + + & > li > a, + & > li > a:hover, + & > li > a:focus, + & > .active > a, + & > .active > a:hover, + & > .active > a:focus { + border: none; + } + } + + .dropdown-menu { + margin-top: 0; + } +} + +.dropdown-menu { + margin-top: 0; + border: none; + .box-shadow(0 1px 4px rgba(0,0,0,.3)); +} + +// Indicators ================================================================= + +.alert { + border: none; + color: #fff; + + &-success { + background-color: @brand-success; + } + + &-info { + background-color: @brand-info; + } + + &-warning { + background-color: @brand-warning; + } + + &-danger { + background-color: @brand-danger; + } + + a:not(.close), + .alert-link { + color: #fff; + font-weight: bold; + } + + .close { + color: #fff; + } +} + +.badge { + padding: 4px 6px 4px; +} + +.progress { + position: relative; + z-index: 1; + height: 6px; + border-radius: 0; + + .box-shadow(none); + + &-bar { + .box-shadow(none); + + &:last-child { + border-radius: 0 3px 3px 0; + } + + &:last-child { + &:before { + display: block; + content: ""; + position: absolute; + width: 100%; + height: 100%; + left: 0; + right: 0; + z-index: -1; + background-color: lighten(@progress-bar-bg, 35%); + } + } + + &-success:last-child.progress-bar:before { + background-color: lighten(@brand-success, 35%); + } + + &-info:last-child.progress-bar:before { + background-color: lighten(@brand-info, 45%); + } + &-warning:last-child.progress-bar:before { + background-color: lighten(@brand-warning, 35%); + } + + &-danger:last-child.progress-bar:before { + background-color: lighten(@brand-danger, 25%); + } + } +} + +// Progress bars ============================================================== + +// Containers ================================================================= + +.close { + font-size: 34px; + font-weight: 300; + line-height: 24px; + opacity: 0.6; + .transition(all 0.2s); + + &:hover { + opacity: 1; + } +} + +.list-group { + + &-item { + padding: 15px; + } + + &-item-text { + color: @gray-light; + } +} + +.well { + border-radius: 0; + .box-shadow(none); +} + +.panel { + border: none; + border-radius: 2px; + .box-shadow(0 1px 4px rgba(0,0,0,.3)); + + &-heading { + border-bottom: none; + } + + &-footer { + border-top: none; + } +} + +.popover { + border: none; + .box-shadow(0 1px 4px rgba(0,0,0,.3)); +} + +.carousel { + &-caption { + h1, h2, h3, h4, h5, h6 { + color: inherit; + } + } +} + diff --git a/dashed/assets/stylesheets/less/index.less b/dashed/assets/stylesheets/less/index.less new file mode 100644 index 000000000..c779c3ebe --- /dev/null +++ b/dashed/assets/stylesheets/less/index.less @@ -0,0 +1,5 @@ +// Index .less, any imports here will be included in the final css build + +@import "~bootstrap/less/bootstrap.less"; +@import "./variables.less"; +@import "./bootswatch.less"; diff --git a/dashed/assets/stylesheets/less/variables.less b/dashed/assets/stylesheets/less/variables.less new file mode 100644 index 000000000..305e9926f --- /dev/null +++ b/dashed/assets/stylesheets/less/variables.less @@ -0,0 +1,881 @@ +// Modified from Bootswatch Paper 3.3.6 +// Variables +// -------------------------------------------------- + + +//== Colors +// +//## Airbnb colors +@rausch: #ff5a5f; // coral +@kazan: #007a87; // dark teal +@hackberry: #7b0051; // purple +@babu: #00d1c1; // light teal +@lima: #8ce071; // bright green +@beach: #ffb400; // yellow +@ebisu: #ffaa91; // peach +@tirol: #b4a76c; // khaki +@foggy: #9CA299; // dark grey +@hof: #565A5C; // light grey + +//## Gray and brand colors for use across Bootstrap. + +@gray-base: #000; +@gray-darker: lighten(@gray-base, 13.5%); // #222 +@gray-dark: #212121; +@gray: #666; +@gray-light: #bbb; +@gray-lighter: lighten(@gray-base, 93.5%); // #eee + +@brand-primary: darken(@babu, 5%); +@brand-success: darken(@lima, 15%); +@brand-info: @beach; +@brand-warning: @hackberry; +@brand-danger: darken(@rausch, 5%); + + +//== Scaffolding +// +//## Settings for some of the most global styles. + +//** Background color for ``. +@body-bg: #fff; +//** Global text color on ``. +@text-color: @gray; + +//** Global textual link color. +@link-color: @brand-primary; +//** Link hover color set via `darken()` function. +@link-hover-color: darken(@link-color, 15%); +//** Link hover decoration. +@link-hover-decoration: underline; + + +//== Typography +// +//## Font, line-height, and color for body text, headings, and more. + +@font-family-sans-serif: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif; +@font-family-serif: Georgia, "Times New Roman", Times, serif; +//** Default monospace fonts for ``, ``, and `
`.
+@font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace;
+@font-family-base:        @font-family-sans-serif;
+
+@font-size-base:          13px;
+@font-size-large:         ceil((@font-size-base * 1.25)); // ~18px
+@font-size-small:         ceil((@font-size-base * 0.85)); // ~12px
+
+@font-size-h1:            56px;
+@font-size-h2:            45px;
+@font-size-h3:            34px;
+@font-size-h4:            24px;
+@font-size-h5:            20px;
+@font-size-h6:            14px;
+
+//** Unit-less `line-height` for use in components like buttons.
+@line-height-base:        1.846; // 20/14
+//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
+@line-height-computed:    floor((@font-size-base * @line-height-base)); // ~20px
+
+//** By default, this inherits from the ``.
+@headings-font-family:    inherit;
+@headings-font-weight:    400;
+@headings-line-height:    1.1;
+@headings-color:          #444;
+
+
+//== Iconography
+//
+//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
+
+//** Load fonts from this directory.
+@icon-font-path:          "../fonts/";
+//** File name for all font files.
+@icon-font-name:          "glyphicons-halflings-regular";
+//** Element ID within SVG icon file.
+@icon-font-svg-id:        "glyphicons_halflingsregular";
+
+
+//== Components
+//
+//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
+
+@padding-base-vertical:     6px;
+@padding-base-horizontal:   16px;
+
+@padding-large-vertical:    10px;
+@padding-large-horizontal:  16px;
+
+@padding-small-vertical:    5px;
+@padding-small-horizontal:  10px;
+
+@padding-xs-vertical:       1px;
+@padding-xs-horizontal:     5px;
+
+@line-height-large:         1.3333333; // extra decimals for Win 8.1 Chrome
+@line-height-small:         1.5;
+
+@border-radius-base:        3px;
+@border-radius-large:       3px;
+@border-radius-small:       3px;
+
+//** Global color for active items (e.g., navs or dropdowns).
+@component-active-color:    #fff;
+//** Global background color for active items (e.g., navs or dropdowns).
+@component-active-bg:       @brand-primary;
+
+//** Width of the `border` for generating carets that indicator dropdowns.
+@caret-width-base:          4px;
+//** Carets increase slightly in size for larger components.
+@caret-width-large:         5px;
+
+
+//== Tables
+//
+//## Customizes the `.table` component with basic values, each used across all table variations.
+
+//** Padding for ``s and ``s.
+@table-cell-padding:            8px;
+//** Padding for cells in `.table-condensed`.
+@table-condensed-cell-padding:  5px;
+
+//** Default background color used for all tables.
+@table-bg:                      transparent;
+//** Background color used for `.table-striped`.
+@table-bg-accent:               #f9f9f9;
+//** Background color used for `.table-hover`.
+@table-bg-hover:                @gray-lighter;
+@table-bg-active:               @table-bg-hover;
+
+//** Border color for table and cell borders.
+@table-border-color:            #ddd;
+
+
+//== Buttons
+//
+//## For each of Bootstrap's buttons, define text, background and border color.
+
+@btn-font-weight:                normal;
+
+@btn-default-color:              #444;
+@btn-default-bg:                 #fff;
+@btn-default-border:             transparent;
+
+@btn-primary-color:              #fff;
+@btn-primary-bg:                 @brand-primary;
+@btn-primary-border:             transparent;
+
+@btn-success-color:              #fff;
+@btn-success-bg:                 @brand-success;
+@btn-success-border:             transparent;
+
+@btn-info-color:                 #fff;
+@btn-info-bg:                    @brand-info;
+@btn-info-border:                transparent;
+
+@btn-warning-color:              #fff;
+@btn-warning-bg:                 @brand-warning;
+@btn-warning-border:             transparent;
+
+@btn-danger-color:               #fff;
+@btn-danger-bg:                  @brand-danger;
+@btn-danger-border:              transparent;
+
+@btn-link-disabled-color:        @gray-light;
+
+// Allows for customizing button radius independently from global border radius
+@btn-border-radius-base:         @border-radius-base;
+@btn-border-radius-large:        @border-radius-large;
+@btn-border-radius-small:        @border-radius-small;
+
+
+//== Forms
+//
+//##
+
+//** `` background color
+@input-bg:                       transparent;
+//** `` background color
+@input-bg-disabled:              transparent;
+
+//** Text color for ``s
+@input-color:                    @gray;
+//** `` border color
+@input-border:                   transparent;
+
+// TODO: Rename `@input-border-radius` to `@input-border-radius-base` in v4
+//** Default `.form-control` border radius
+// This has no effect on ``s in CSS.
+@input-border-radius:            @border-radius-base;
+//** Large `.form-control` border radius
+@input-border-radius-large:      @border-radius-large;
+//** Small `.form-control` border radius
+@input-border-radius-small:      @border-radius-small;
+
+//** Border color for inputs on focus
+@input-border-focus:             #66afe9;
+
+//** Placeholder text color
+@input-color-placeholder:        @gray-light;
+
+//** Default `.form-control` height
+@input-height-base:              (@line-height-computed + (@padding-base-vertical * 2) + 2);
+//** Large `.form-control` height
+@input-height-large:             (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2);
+//** Small `.form-control` height
+@input-height-small:             (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2);
+
+//** `.form-group` margin
+@form-group-margin-bottom:       15px;
+
+@legend-color:                   @gray-dark;
+@legend-border-color:            #e5e5e5;
+
+//** Background color for textual input addons
+@input-group-addon-bg:           transparent;
+//** Border color for textual input addons
+@input-group-addon-border-color: @input-border;
+
+//** Disabled cursor for form controls and buttons.
+@cursor-disabled:                not-allowed;
+
+
+//== Dropdowns
+//
+//## Dropdown menu container and contents.
+
+//** Background for the dropdown menu.
+@dropdown-bg:                    #fff;
+//** Dropdown menu `border-color`.
+@dropdown-border:                rgba(0,0,0,.15);
+//** Dropdown menu `border-color` **for IE8**.
+@dropdown-fallback-border:       #ccc;
+//** Divider color for between dropdown items.
+@dropdown-divider-bg:            #e5e5e5;
+
+//** Dropdown link text color.
+@dropdown-link-color:            @text-color;
+//** Hover color for dropdown links.
+@dropdown-link-hover-color:      darken(@gray-dark, 5%);
+//** Hover background for dropdown links.
+@dropdown-link-hover-bg:         @gray-lighter;
+
+//** Active dropdown menu item text color.
+@dropdown-link-active-color:     @component-active-color;
+//** Active dropdown menu item background color.
+@dropdown-link-active-bg:        @component-active-bg;
+
+//** Disabled dropdown menu item background color.
+@dropdown-link-disabled-color:   @gray-light;
+
+//** Text color for headers within dropdown menus.
+@dropdown-header-color:          @gray-light;
+
+//** Deprecated `@dropdown-caret-color` as of v3.1.0
+@dropdown-caret-color:           @gray-light;
+
+
+//-- Z-index master list
+//
+// Warning: Avoid customizing these values. They're used for a bird's eye view
+// of components dependent on the z-axis and are designed to all work together.
+//
+// Note: These variables are not generated into the Customizer.
+
+@zindex-navbar:            1000;
+@zindex-dropdown:          1000;
+@zindex-popover:           1060;
+@zindex-tooltip:           1070;
+@zindex-navbar-fixed:      1030;
+@zindex-modal-background:  1040;
+@zindex-modal:             1050;
+
+
+//== Media queries breakpoints
+//
+//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
+
+// Extra small screen / phone
+//** Deprecated `@screen-xs` as of v3.0.1
+@screen-xs:                  480px;
+//** Deprecated `@screen-xs-min` as of v3.2.0
+@screen-xs-min:              @screen-xs;
+//** Deprecated `@screen-phone` as of v3.0.1
+@screen-phone:               @screen-xs-min;
+
+// Small screen / tablet
+//** Deprecated `@screen-sm` as of v3.0.1
+@screen-sm:                  768px;
+@screen-sm-min:              @screen-sm;
+//** Deprecated `@screen-tablet` as of v3.0.1
+@screen-tablet:              @screen-sm-min;
+
+// Medium screen / desktop
+//** Deprecated `@screen-md` as of v3.0.1
+@screen-md:                  992px;
+@screen-md-min:              @screen-md;
+//** Deprecated `@screen-desktop` as of v3.0.1
+@screen-desktop:             @screen-md-min;
+
+// Large screen / wide desktop
+//** Deprecated `@screen-lg` as of v3.0.1
+@screen-lg:                  1200px;
+@screen-lg-min:              @screen-lg;
+//** Deprecated `@screen-lg-desktop` as of v3.0.1
+@screen-lg-desktop:          @screen-lg-min;
+
+// So media queries don't overlap when required, provide a maximum
+@screen-xs-max:              (@screen-sm-min - 1);
+@screen-sm-max:              (@screen-md-min - 1);
+@screen-md-max:              (@screen-lg-min - 1);
+
+
+//== Grid system
+//
+//## Define your custom responsive grid.
+
+//** Number of columns in the grid.
+@grid-columns:              12;
+//** Padding between columns. Gets divided in half for the left and right.
+@grid-gutter-width:         30px;
+// Navbar collapse
+//** Point at which the navbar becomes uncollapsed.
+@grid-float-breakpoint:     @screen-sm-min;
+//** Point at which the navbar begins collapsing.
+@grid-float-breakpoint-max: (@grid-float-breakpoint - 1);
+
+
+//== Container sizes
+//
+//## Define the maximum width of `.container` for different screen sizes.
+
+// Small screen / tablet
+@container-tablet:             (720px + @grid-gutter-width);
+//** For `@screen-sm-min` and up.
+@container-sm:                 @container-tablet;
+
+// Medium screen / desktop
+@container-desktop:            (940px + @grid-gutter-width);
+//** For `@screen-md-min` and up.
+@container-md:                 @container-desktop;
+
+// Large screen / wide desktop
+@container-large-desktop:      (1140px + @grid-gutter-width);
+//** For `@screen-lg-min` and up.
+@container-lg:                 @container-large-desktop;
+
+
+//== Navbar
+//
+//##
+
+// Basics of a navbar
+@navbar-height:                    64px;
+@navbar-margin-bottom:             @line-height-computed;
+@navbar-border-radius:             @border-radius-base;
+@navbar-padding-horizontal:        floor((@grid-gutter-width / 2));
+@navbar-padding-vertical:          ((@navbar-height - @line-height-computed) / 2);
+@navbar-collapse-max-height:       340px;
+
+@navbar-default-color:             @gray-light;
+@navbar-default-bg:                #fff;
+@navbar-default-border:            transparent;
+
+// Navbar links
+@navbar-default-link-color:                @gray;
+@navbar-default-link-hover-color:          @gray-dark;
+@navbar-default-link-hover-bg:             transparent;
+@navbar-default-link-active-color:         @gray-dark;
+@navbar-default-link-active-bg:            darken(@navbar-default-bg, 6.5%);
+@navbar-default-link-disabled-color:       #ccc;
+@navbar-default-link-disabled-bg:          transparent;
+
+// Navbar brand label
+@navbar-default-brand-color:               @navbar-default-link-color;
+@navbar-default-brand-hover-color:         @navbar-default-link-hover-color;
+@navbar-default-brand-hover-bg:            transparent;
+
+// Navbar toggle
+@navbar-default-toggle-hover-bg:           transparent;
+@navbar-default-toggle-icon-bar-bg:        rgba(0,0,0,0.5);
+@navbar-default-toggle-border-color:       transparent;
+
+
+//=== Inverted navbar
+// Reset inverted navbar basics
+@navbar-inverse-color:                      @gray-light;
+@navbar-inverse-bg:                         @brand-primary;
+@navbar-inverse-border:                     transparent;
+
+// Inverted navbar links
+@navbar-inverse-link-color:                 lighten(@brand-primary, 30%);
+@navbar-inverse-link-hover-color:           #fff;
+@navbar-inverse-link-hover-bg:              transparent;
+@navbar-inverse-link-active-color:          @navbar-inverse-link-hover-color;
+@navbar-inverse-link-active-bg:             darken(@navbar-inverse-bg, 10%);
+@navbar-inverse-link-disabled-color:        #444;
+@navbar-inverse-link-disabled-bg:           transparent;
+
+// Inverted navbar brand label
+@navbar-inverse-brand-color:                @navbar-inverse-link-color;
+@navbar-inverse-brand-hover-color:          #fff;
+@navbar-inverse-brand-hover-bg:             transparent;
+
+// Inverted navbar toggle\
+@navbar-inverse-toggle-hover-bg:            transparent;
+@navbar-inverse-toggle-icon-bar-bg:         rgba(0,0,0,0.5);
+@navbar-inverse-toggle-border-color:        transparent;
+
+
+//== Navs
+//
+//##
+
+//=== Shared nav styles
+@nav-link-padding:                          10px 15px;
+@nav-link-hover-bg:                         @gray-lighter;
+
+@nav-disabled-link-color:                   @gray-light;
+@nav-disabled-link-hover-color:             @gray-light;
+
+//== Tabs
+@nav-tabs-border-color:                     transparent;
+
+@nav-tabs-link-hover-border-color:          @gray-lighter;
+
+@nav-tabs-active-link-hover-bg:             transparent;
+@nav-tabs-active-link-hover-color:          @gray;
+@nav-tabs-active-link-hover-border-color:   transparent;
+
+@nav-tabs-justified-link-border-color:            @nav-tabs-border-color;
+@nav-tabs-justified-active-link-border-color:     @body-bg;
+
+//== Pills
+@nav-pills-border-radius:                   @border-radius-base;
+@nav-pills-active-link-hover-bg:            @component-active-bg;
+@nav-pills-active-link-hover-color:         @component-active-color;
+
+
+//== Pagination
+//
+//##
+
+@pagination-color:                     @link-color;
+@pagination-bg:                        #fff;
+@pagination-border:                    #ddd;
+
+@pagination-hover-color:               @link-hover-color;
+@pagination-hover-bg:                  @gray-lighter;
+@pagination-hover-border:              #ddd;
+
+@pagination-active-color:              #fff;
+@pagination-active-bg:                 @brand-primary;
+@pagination-active-border:             @brand-primary;
+
+@pagination-disabled-color:            @gray-light;
+@pagination-disabled-bg:               #fff;
+@pagination-disabled-border:           #ddd;
+
+
+//== Pager
+//
+//##
+
+@pager-bg:                             @pagination-bg;
+@pager-border:                         @pagination-border;
+@pager-border-radius:                  15px;
+
+@pager-hover-bg:                       @pagination-hover-bg;
+
+@pager-active-bg:                      @pagination-active-bg;
+@pager-active-color:                   @pagination-active-color;
+
+@pager-disabled-color:                 @pagination-disabled-color;
+
+
+//== Jumbotron
+//
+//##
+
+@jumbotron-padding:              30px;
+@jumbotron-color:                inherit;
+@jumbotron-bg:                   #f9f9f9;
+@jumbotron-heading-color:        @headings-color;
+@jumbotron-font-size:            ceil((@font-size-base * 1.5));
+@jumbotron-heading-font-size:    ceil((@font-size-base * 4.5));
+
+
+//== Form states and alerts
+//
+//## Define colors for form feedback states and, by default, alerts.
+
+@state-success-text:             @brand-success;
+@state-success-bg:               #dff0d8;
+@state-success-border:           darken(spin(@state-success-bg, -10), 5%);
+
+@state-info-text:                @brand-info;
+@state-info-bg:                  #e1bee7;
+@state-info-border:              darken(spin(@state-info-bg, -10), 7%);
+
+@state-warning-text:             @brand-warning;
+@state-warning-bg:               #ffe0b2;
+@state-warning-border:           darken(spin(@state-warning-bg, -10), 5%);
+
+@state-danger-text:              @brand-danger;
+@state-danger-bg:                #f9bdbb;
+@state-danger-border:            darken(spin(@state-danger-bg, -10), 5%);
+
+
+//== Tooltips
+//
+//##
+
+//** Tooltip max width
+@tooltip-max-width:           200px;
+//** Tooltip text color
+@tooltip-color:               #fff;
+//** Tooltip background color
+@tooltip-bg:                  #727272;
+@tooltip-opacity:             .9;
+
+//** Tooltip arrow width
+@tooltip-arrow-width:         5px;
+//** Tooltip arrow color
+@tooltip-arrow-color:         @tooltip-bg;
+
+
+//== Popovers
+//
+//##
+
+//** Popover body background color
+@popover-bg:                          #fff;
+//** Popover maximum width
+@popover-max-width:                   276px;
+//** Popover border color
+@popover-border-color:                transparent;
+//** Popover fallback border color
+@popover-fallback-border-color:       transparent;
+
+//** Popover title background color
+@popover-title-bg:                    darken(@popover-bg, 3%);
+
+//** Popover arrow width
+@popover-arrow-width:                 10px;
+//** Popover arrow color
+@popover-arrow-color:                 @popover-bg;
+
+//** Popover outer arrow width
+@popover-arrow-outer-width:           (@popover-arrow-width + 1);
+//** Popover outer arrow color
+@popover-arrow-outer-color:           fadein(@popover-border-color, 7.5%);
+//** Popover outer arrow fallback color
+@popover-arrow-outer-fallback-color:  darken(@popover-fallback-border-color, 20%);
+
+
+//== Labels
+//
+//##
+
+//** Default label background color
+@label-default-bg:            @gray-light;
+//** Primary label background color
+@label-primary-bg:            @brand-primary;
+//** Success label background color
+@label-success-bg:            @brand-success;
+//** Info label background color
+@label-info-bg:               @brand-info;
+//** Warning label background color
+@label-warning-bg:            @brand-warning;
+//** Danger label background color
+@label-danger-bg:             @brand-danger;
+
+//** Default label text color
+@label-color:                 #fff;
+//** Default text color of a linked label
+@label-link-hover-color:      #fff;
+
+
+//== Modals
+//
+//##
+
+//** Padding applied to the modal body
+@modal-inner-padding:         15px;
+
+//** Padding applied to the modal title
+@modal-title-padding:         15px;
+//** Modal title line-height
+@modal-title-line-height:     @line-height-base;
+
+//** Background color of modal content area
+@modal-content-bg:                             #fff;
+//** Modal content border color
+@modal-content-border-color:                   transparent;
+//** Modal content border color **for IE8**
+@modal-content-fallback-border-color:          #999;
+
+//** Modal backdrop background color
+@modal-backdrop-bg:           #000;
+//** Modal backdrop opacity
+@modal-backdrop-opacity:      .5;
+//** Modal header border color
+@modal-header-border-color:   transparent;
+//** Modal footer border color
+@modal-footer-border-color:   @modal-header-border-color;
+
+@modal-lg:                    900px;
+@modal-md:                    600px;
+@modal-sm:                    300px;
+
+
+//== Alerts
+//
+//## Define alert colors, border radius, and padding.
+
+@alert-padding:               15px;
+@alert-border-radius:         @border-radius-base;
+@alert-link-font-weight:      bold;
+
+@alert-success-bg:            @state-success-bg;
+@alert-success-text:          @state-success-text;
+@alert-success-border:        @state-success-border;
+
+@alert-info-bg:               @state-info-bg;
+@alert-info-text:             @state-info-text;
+@alert-info-border:           @state-info-border;
+
+@alert-warning-bg:            @state-warning-bg;
+@alert-warning-text:          @state-warning-text;
+@alert-warning-border:        @state-warning-border;
+
+@alert-danger-bg:             @state-danger-bg;
+@alert-danger-text:           @state-danger-text;
+@alert-danger-border:         @state-danger-border;
+
+
+//== Progress bars
+//
+//##
+
+//** Background color of the whole progress component
+@progress-bg:                 #f5f5f5;
+//** Progress bar text color
+@progress-bar-color:          #fff;
+//** Variable for setting rounded corners on progress bar.
+@progress-border-radius:      @border-radius-base;
+
+//** Default progress bar color
+@progress-bar-bg:             @brand-primary;
+//** Success progress bar color
+@progress-bar-success-bg:     @brand-success;
+//** Warning progress bar color
+@progress-bar-warning-bg:     @brand-warning;
+//** Danger progress bar color
+@progress-bar-danger-bg:      @brand-danger;
+//** Info progress bar color
+@progress-bar-info-bg:        @brand-info;
+
+
+//== List group
+//
+//##
+
+//** Background color on `.list-group-item`
+@list-group-bg:                 #fff;
+//** `.list-group-item` border color
+@list-group-border:             #ddd;
+//** List group border radius
+@list-group-border-radius:      @border-radius-base;
+
+//** Background color of single list items on hover
+@list-group-hover-bg:           #f5f5f5;
+//** Text color of active list items
+@list-group-active-color:       @component-active-color;
+//** Background color of active list items
+@list-group-active-bg:          @component-active-bg;
+//** Border color of active list elements
+@list-group-active-border:      @list-group-active-bg;
+//** Text color for content within active list items
+@list-group-active-text-color:  lighten(@list-group-active-bg, 40%);
+
+//** Text color of disabled list items
+@list-group-disabled-color:      @gray-light;
+//** Background color of disabled list items
+@list-group-disabled-bg:         @gray-lighter;
+//** Text color for content within disabled list items
+@list-group-disabled-text-color: @list-group-disabled-color;
+
+@list-group-link-color:         #555;
+@list-group-link-hover-color:   @list-group-link-color;
+@list-group-link-heading-color: #333;
+
+
+//== Panels
+//
+//##
+
+@panel-bg:                    #fff;
+@panel-body-padding:          15px;
+@panel-heading-padding:       10px 15px;
+@panel-footer-padding:        @panel-heading-padding;
+@panel-border-radius:         @border-radius-base;
+
+//** Border color for elements within panels
+@panel-inner-border:          #ddd;
+@panel-footer-bg:             #f5f5f5;
+
+@panel-default-text:          @gray-dark;
+@panel-default-border:        #ddd;
+@panel-default-heading-bg:    #f5f5f5;
+
+@panel-primary-text:          #fff;
+@panel-primary-border:        @brand-primary;
+@panel-primary-heading-bg:    @brand-primary;
+
+@panel-success-text:          #fff;
+@panel-success-border:        @state-success-border;
+@panel-success-heading-bg:    @brand-success;
+
+@panel-info-text:             #fff;
+@panel-info-border:           @state-info-border;
+@panel-info-heading-bg:       @brand-info;
+
+@panel-warning-text:          #fff;
+@panel-warning-border:        @state-warning-border;
+@panel-warning-heading-bg:    @brand-warning;
+
+@panel-danger-text:           #fff;
+@panel-danger-border:         @state-danger-border;
+@panel-danger-heading-bg:     @brand-danger;
+
+
+//== Thumbnails
+//
+//##
+
+//** Padding around the thumbnail image
+@thumbnail-padding:           4px;
+//** Thumbnail background color
+@thumbnail-bg:                @body-bg;
+//** Thumbnail border color
+@thumbnail-border:            #ddd;
+//** Thumbnail border radius
+@thumbnail-border-radius:     @border-radius-base;
+
+//** Custom text color for thumbnail captions
+@thumbnail-caption-color:     @text-color;
+//** Padding around the thumbnail caption
+@thumbnail-caption-padding:   9px;
+
+
+//== Wells
+//
+//##
+
+@well-bg:                     #f9f9f9;
+@well-border:                 transparent;
+
+
+//== Badges
+//
+//##
+
+@badge-color:                 #fff;
+//** Linked badge text color on hover
+@badge-link-hover-color:      #fff;
+@badge-bg:                    @gray-light;
+
+//** Badge text color in active nav link
+@badge-active-color:          @link-color;
+//** Badge background color in active nav link
+@badge-active-bg:             #fff;
+
+@badge-font-weight:           normal;
+@badge-line-height:           1;
+@badge-border-radius:         10px;
+
+
+//== Breadcrumbs
+//
+//##
+
+@breadcrumb-padding-vertical:   8px;
+@breadcrumb-padding-horizontal: 15px;
+//** Breadcrumb background color
+@breadcrumb-bg:                 #f5f5f5;
+//** Breadcrumb text color
+@breadcrumb-color:              #ccc;
+//** Text color of current page in the breadcrumb
+@breadcrumb-active-color:       @gray-light;
+//** Textual separator for between breadcrumb elements
+@breadcrumb-separator:          "/";
+
+
+//== Carousel
+//
+//##
+
+@carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6);
+
+@carousel-control-color:                      #fff;
+@carousel-control-width:                      15%;
+@carousel-control-opacity:                    .5;
+@carousel-control-font-size:                  20px;
+
+@carousel-indicator-active-bg:                #fff;
+@carousel-indicator-border-color:             #fff;
+
+@carousel-caption-color:                      #fff;
+
+
+//== Close
+//
+//##
+
+@close-font-weight:           normal;
+@close-color:                 #000;
+@close-text-shadow:           none;
+
+
+//== Code
+//
+//##
+
+@code-color:                  #c7254e;
+@code-bg:                     #f9f2f4;
+
+@kbd-color:                   #fff;
+@kbd-bg:                      #333;
+
+@pre-bg:                      #f5f5f5;
+@pre-color:                   @gray-dark;
+@pre-border-color:            #ccc;
+@pre-scrollable-max-height:   340px;
+
+
+//== Type
+//
+//##
+
+//** Horizontal offset for forms and lists.
+@component-offset-horizontal: 180px;
+//** Text muted color
+@text-muted:                  @gray-light;
+//** Abbreviations and acronyms border color
+@abbr-border-color:           @gray-light;
+//** Headings small color
+@headings-small-color:        @gray-light;
+//** Blockquote small color
+@blockquote-small-color:      @gray-light;
+//** Blockquote font size
+@blockquote-font-size:        (@font-size-base * 1.25);
+//** Blockquote border color
+@blockquote-border-color:     @gray-lighter;
+//** Page header border color
+@page-header-border-color:    @gray-lighter;
+//** Width of horizontal description list titles
+@dl-horizontal-offset:        @component-offset-horizontal;
+//** Point at which .dl-horizontal becomes horizontal
+@dl-horizontal-breakpoint:    @grid-float-breakpoint;
+//** Horizontal line color.
+@hr-border:                   @gray-lighter;
diff --git a/dashed/assets/vendor/parallel_coordinates/d3.parcoords.css b/dashed/assets/vendor/parallel_coordinates/d3.parcoords.css
new file mode 100644
index 000000000..b53849c36
--- /dev/null
+++ b/dashed/assets/vendor/parallel_coordinates/d3.parcoords.css
@@ -0,0 +1,71 @@
+.parcoords svg, .parcoords canvas {
+  font-size: 12px;
+  position: absolute;
+}
+.parcoords > canvas {
+  pointer-events: none;
+}
+
+.parcoords text.label {
+  font: 100%;
+  font-size: 12px;
+  cursor: drag;
+}
+
+.parcoords rect.background {
+  fill: transparent;
+}
+.parcoords rect.background:hover {
+  fill: rgba(120,120,120,0.2);
+}
+.parcoords .resize rect {
+  fill: rgba(0,0,0,0.1);
+}
+.parcoords rect.extent {
+  fill: rgba(255,255,255,0.25);
+  stroke: rgba(0,0,0,0.6);
+}
+.parcoords .axis line, .parcoords .axis path {
+  fill: none;
+  stroke: #222;
+  shape-rendering: crispEdges;
+}
+.parcoords canvas {
+  opacity: 1;
+  -moz-transition: opacity 0.3s;
+  -webkit-transition: opacity 0.3s;
+  -o-transition: opacity 0.3s;
+}
+.parcoords canvas.faded {
+  opacity: 0.25;
+}
+.parcoords {
+	-webkit-touch-callout: none;
+	-webkit-user-select: none;
+	-khtml-user-select: none;
+	-moz-user-select: none;
+	-ms-user-select: none;
+	user-select: none;
+    background-color: white;
+}
+
+/* data table styles */
+.parcoords .row, .parcoords .header {
+    clear: left; font-size: 12px; line-height: 18px; height: 18px;
+    margin: 0px;
+}
+.parcoords .row:nth-child(odd) {
+  background: rgba(0,0,0,0.05);
+}
+.parcoords .header {
+  font-weight: bold;
+}
+.parcoords .cell {
+  float: left;
+  overflow: hidden;
+  white-space: nowrap;
+  width: 100px; height: 18px;
+}
+.parcoords .col-0 {
+  width: 180px;
+}
diff --git a/dashed/assets/vendor/parallel_coordinates/d3.parcoords.js b/dashed/assets/vendor/parallel_coordinates/d3.parcoords.js
new file mode 100644
index 000000000..04095f106
--- /dev/null
+++ b/dashed/assets/vendor/parallel_coordinates/d3.parcoords.js
@@ -0,0 +1,2224 @@
+module.exports = function(config) {
+  var __ = {
+    data: [],
+    highlighted: [],
+    dimensions: [],
+    dimensionTitles: {},
+    dimensionTitleRotation: 0,
+    types: {},
+    brushed: false,
+    brushedColor: null,
+    alphaOnBrushed: 0.0,
+    mode: "default",
+    rate: 20,
+    width: 600,
+    height: 300,
+    margin: { top: 24, right: 0, bottom: 12, left: 0 },
+    nullValueSeparator: "undefined", // set to "top" or "bottom"
+    nullValueSeparatorPadding: { top: 8, right: 0, bottom: 8, left: 0 },
+    color: "#069",
+    composite: "source-over",
+    alpha: 0.7,
+    bundlingStrength: 0.5,
+    bundleDimension: null,
+    smoothness: 0.0,
+    showControlPoints: false,
+    hideAxis : []
+  };
+
+  extend(__, config);
+
+  var pc = function(selection) {
+    selection = pc.selection = d3.select(selection);
+
+    __.width = selection[0][0].clientWidth;
+    __.height = selection[0][0].clientHeight;
+
+    // canvas data layers
+    ["marks", "foreground", "brushed", "highlight"].forEach(function(layer) {
+      canvas[layer] = selection
+        .append("canvas")
+        .attr("class", layer)[0][0];
+      ctx[layer] = canvas[layer].getContext("2d");
+    });
+
+    // svg tick and brush layers
+    pc.svg = selection
+      .append("svg")
+        .attr("width", __.width)
+        .attr("height", __.height)
+      .append("svg:g")
+        .attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")");
+
+    return pc;
+  };
+  var events = d3.dispatch.apply(this,["render", "resize", "highlight", "brush", "brushend", "axesreorder"].concat(d3.keys(__))),
+      w = function() { return __.width - __.margin.right - __.margin.left; },
+      h = function() { return __.height - __.margin.top - __.margin.bottom; },
+      flags = {
+        brushable: false,
+        reorderable: false,
+        axes: false,
+        interactive: false,
+        debug: false
+      },
+      xscale = d3.scale.ordinal(),
+      yscale = {},
+      dragging = {},
+      line = d3.svg.line(),
+      axis = d3.svg.axis().orient("left").ticks(5),
+      g, // groups for axes, brushes
+      ctx = {},
+      canvas = {},
+      clusterCentroids = [];
+
+  // side effects for setters
+  var side_effects = d3.dispatch.apply(this,d3.keys(__))
+    .on("composite", function(d) {
+      ctx.foreground.globalCompositeOperation = d.value;
+      ctx.brushed.globalCompositeOperation = d.value;
+    })
+    .on("alpha", function(d) {
+      ctx.foreground.globalAlpha = d.value;
+      ctx.brushed.globalAlpha = d.value;
+    })
+    .on("brushedColor", function (d) {
+      ctx.brushed.strokeStyle = d.value;
+    })
+    .on("width", function(d) { pc.resize(); })
+    .on("height", function(d) { pc.resize(); })
+    .on("margin", function(d) { pc.resize(); })
+    .on("rate", function(d) {
+      brushedQueue.rate(d.value);
+      foregroundQueue.rate(d.value);
+    })
+    .on("dimensions", function(d) {
+      xscale.domain(__.dimensions);
+      if (flags.interactive){pc.render().updateAxes();}
+    })
+    .on("bundleDimension", function(d) {
+      if (!__.dimensions.length) pc.detectDimensions();
+      if (!(__.dimensions[0] in yscale)) pc.autoscale();
+      if (typeof d.value === "number") {
+        if (d.value < __.dimensions.length) {
+          __.bundleDimension = __.dimensions[d.value];
+        } else if (d.value < __.hideAxis.length) {
+          __.bundleDimension = __.hideAxis[d.value];
+        }
+      } else {
+        __.bundleDimension = d.value;
+      }
+
+      __.clusterCentroids = compute_cluster_centroids(__.bundleDimension);
+    })
+    .on("hideAxis", function(d) {
+      if (!__.dimensions.length) pc.detectDimensions();
+      pc.dimensions(without(__.dimensions, d.value));
+    });
+
+  // expose the state of the chart
+  pc.state = __;
+  pc.flags = flags;
+
+  // create getter/setters
+  getset(pc, __, events);
+
+  // expose events
+  d3.rebind(pc, events, "on");
+
+  // getter/setter with event firing
+  function getset(obj,state,events)  {
+    d3.keys(state).forEach(function(key) {
+      obj[key] = function(x) {
+        if (!arguments.length) {
+      return state[key];
+    }
+        var old = state[key];
+        state[key] = x;
+        side_effects[key].call(pc,{"value": x, "previous": old});
+        events[key].call(pc,{"value": x, "previous": old});
+        return obj;
+      };
+    });
+  };
+
+  function extend(target, source) {
+    for (var key in source) {
+      target[key] = source[key];
+    }
+    return target;
+  };
+
+  function without(arr, item) {
+    return arr.filter(function(elem) { return item.indexOf(elem) === -1; })
+  };
+  /** adjusts an axis' default range [h()+1, 1] if a NullValueSeparator is set */
+  function getRange() {
+    if (__.nullValueSeparator=="bottom") {
+      return [h()+1-__.nullValueSeparatorPadding.bottom-__.nullValueSeparatorPadding.top, 1];
+    } else if (__.nullValueSeparator=="top") {
+      return [h()+1, 1+__.nullValueSeparatorPadding.bottom+__.nullValueSeparatorPadding.top];
+    }
+    return [h()+1, 1];
+  };
+
+  pc.autoscale = function() {
+    // yscale
+    var defaultScales = {
+      "date": function(k) {
+        var extent = d3.extent(__.data, function(d) {
+          return d[k] ? d[k].getTime() : null;
+        });
+
+        // special case if single value
+        if (extent[0] === extent[1]) {
+          return d3.scale.ordinal()
+            .domain([extent[0]])
+            .rangePoints(getRange());
+        }
+
+        return d3.time.scale()
+          .domain(extent)
+          .range(getRange());
+      },
+      "number": function(k) {
+        var extent = d3.extent(__.data, function(d) { return +d[k]; });
+
+        // special case if single value
+        if (extent[0] === extent[1]) {
+          return d3.scale.ordinal()
+            .domain([extent[0]])
+            .rangePoints(getRange());
+        }
+
+        return d3.scale.linear()
+          .domain(extent)
+          .range(getRange());
+      },
+      "string": function(k) {
+        var counts = {},
+            domain = [];
+
+        // Let's get the count for each value so that we can sort the domain based
+        // on the number of items for each value.
+        __.data.map(function(p) {
+          if (p[k] === undefined && __.nullValueSeparator!== "undefined"){
+            return; // null values will be drawn beyond the horizontal null value separator!
+          }
+          if (counts[p[k]] === undefined) {
+            counts[p[k]] = 1;
+          } else {
+            counts[p[k]] = counts[p[k]] + 1;
+          }
+        });
+
+        domain = Object.getOwnPropertyNames(counts).sort(function(a, b) {
+          return counts[a] - counts[b];
+        });
+
+        return d3.scale.ordinal()
+          .domain(domain)
+          .rangePoints(getRange());
+      }
+    };
+
+    __.dimensions.forEach(function(k) {
+      yscale[k] = defaultScales[__.types[k]](k);
+    });
+
+    __.hideAxis.forEach(function(k) {
+      yscale[k] = defaultScales[__.types[k]](k);
+    });
+
+    // xscale
+    xscale.rangePoints([0, w()], 1);
+
+    // canvas sizes
+    pc.selection.selectAll("canvas")
+        .style("margin-top", __.margin.top + "px")
+        .style("margin-left", __.margin.left + "px")
+        .attr("width", w()+2)
+        .attr("height", h()+2);
+
+    // default styles, needs to be set when canvas width changes
+    ctx.foreground.strokeStyle = __.color;
+    ctx.foreground.lineWidth = 1.4;
+    ctx.foreground.globalCompositeOperation = __.composite;
+    ctx.foreground.globalAlpha = __.alpha;
+    ctx.brushed.strokeStyle = __.brushedColor;
+    ctx.brushed.lineWidth = 1.4;
+    ctx.brushed.globalCompositeOperation = __.composite;
+    ctx.brushed.globalAlpha = __.alpha;
+    ctx.highlight.lineWidth = 3;
+
+    return this;
+  };
+
+  pc.scale = function(d, domain) {
+    yscale[d].domain(domain);
+
+    return this;
+  };
+
+  pc.flip = function(d) {
+    //yscale[d].domain().reverse();         // does not work
+    yscale[d].domain(yscale[d].domain().reverse()); // works
+
+    return this;
+  };
+
+  pc.commonScale = function(global, type) {
+    var t = type || "number";
+    if (typeof global === 'undefined') {
+      global = true;
+    }
+
+    // scales of the same type
+    var scales = __.dimensions.concat(__.hideAxis).filter(function(p) {
+      return __.types[p] == t;
+    });
+
+    if (global) {
+      var extent = d3.extent(scales.map(function(p,i) {
+          return yscale[p].domain();
+        }).reduce(function(a,b) {
+          return a.concat(b);
+        }));
+
+      scales.forEach(function(d) {
+        yscale[d].domain(extent);
+      });
+
+    } else {
+      scales.forEach(function(k) {
+        yscale[k].domain(d3.extent(__.data, function(d) { return +d[k]; }));
+      });
+    }
+
+    // update centroids
+    if (__.bundleDimension !== null) {
+      pc.bundleDimension(__.bundleDimension);
+    }
+
+    return this;
+  };
+  pc.detectDimensions = function() {
+    pc.types(pc.detectDimensionTypes(__.data));
+    pc.dimensions(d3.keys(pc.types()));
+    return this;
+  };
+
+  // a better "typeof" from this post: http://stackoverflow.com/questions/7390426/better-way-to-get-type-of-a-javascript-variable
+  pc.toType = function(v) {
+    return ({}).toString.call(v).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
+  };
+
+  // try to coerce to number before returning type
+  pc.toTypeCoerceNumbers = function(v) {
+    if ((parseFloat(v) == v) && (v != null)) {
+    return "number";
+  }
+    return pc.toType(v);
+  };
+
+  // attempt to determine types of each dimension based on first row of data
+  pc.detectDimensionTypes = function(data) {
+    var types = {};
+    d3.keys(data[0])
+      .forEach(function(col) {
+        types[col] = pc.toTypeCoerceNumbers(data[0][col]);
+      });
+    return types;
+  };
+  pc.render = function() {
+    // try to autodetect dimensions and create scales
+    if (!__.dimensions.length) pc.detectDimensions();
+    if (!(__.dimensions[0] in yscale)) pc.autoscale();
+
+    pc.render[__.mode]();
+
+    events.render.call(this);
+    return this;
+  };
+
+  pc.renderBrushed = function() {
+    if (!__.dimensions.length) pc.detectDimensions();
+    if (!(__.dimensions[0] in yscale)) pc.autoscale();
+
+    pc.renderBrushed[__.mode]();
+
+    events.render.call(this);
+    return this;
+  };
+
+  function isBrushed() {
+    if (__.brushed && __.brushed.length !== __.data.length)
+      return true;
+
+    var object = brush.currentMode().brushState();
+
+    for (var key in object) {
+      if (object.hasOwnProperty(key)) {
+        return true;
+      }
+    }
+    return false;
+  };
+
+  pc.render.default = function() {
+    pc.clear('foreground');
+    pc.clear('highlight');
+
+    pc.renderBrushed.default();
+
+    __.data.forEach(path_foreground);
+  };
+
+  var foregroundQueue = d3.renderQueue(path_foreground)
+    .rate(50)
+    .clear(function() {
+      pc.clear('foreground');
+      pc.clear('highlight');
+    });
+
+  pc.render.queue = function() {
+    pc.renderBrushed.queue();
+
+    foregroundQueue(__.data);
+  };
+
+  pc.renderBrushed.default = function() {
+    pc.clear('brushed');
+
+    if (isBrushed()) {
+      __.brushed.forEach(path_brushed);
+    }
+  };
+
+  var brushedQueue = d3.renderQueue(path_brushed)
+    .rate(50)
+    .clear(function() {
+      pc.clear('brushed');
+    });
+
+  pc.renderBrushed.queue = function() {
+    if (isBrushed()) {
+      brushedQueue(__.brushed);
+    } else {
+      brushedQueue([]); // This is needed to clear the currently brushed items
+    }
+  };
+  function compute_cluster_centroids(d) {
+
+    var clusterCentroids = d3.map();
+    var clusterCounts = d3.map();
+    // determine clusterCounts
+    __.data.forEach(function(row) {
+      var scaled = yscale[d](row[d]);
+      if (!clusterCounts.has(scaled)) {
+        clusterCounts.set(scaled, 0);
+      }
+      var count = clusterCounts.get(scaled);
+      clusterCounts.set(scaled, count + 1);
+    });
+
+    __.data.forEach(function(row) {
+      __.dimensions.map(function(p, i) {
+        var scaled = yscale[d](row[d]);
+        if (!clusterCentroids.has(scaled)) {
+          var map = d3.map();
+          clusterCentroids.set(scaled, map);
+        }
+        if (!clusterCentroids.get(scaled).has(p)) {
+          clusterCentroids.get(scaled).set(p, 0);
+        }
+        var value = clusterCentroids.get(scaled).get(p);
+        value += yscale[p](row[p]) / clusterCounts.get(scaled);
+        clusterCentroids.get(scaled).set(p, value);
+      });
+    });
+
+    return clusterCentroids;
+
+  }
+
+  function compute_centroids(row) {
+    var centroids = [];
+
+    var p = __.dimensions;
+    var cols = p.length;
+    var a = 0.5;      // center between axes
+    for (var i = 0; i < cols; ++i) {
+      // centroids on 'real' axes
+      var x = position(p[i]);
+      var y = yscale[p[i]](row[p[i]]);
+      centroids.push($V([x, y]));
+
+      // centroids on 'virtual' axes
+      if (i < cols - 1) {
+        var cx = x + a * (position(p[i+1]) - x);
+        var cy = y + a * (yscale[p[i+1]](row[p[i+1]]) - y);
+        if (__.bundleDimension !== null) {
+          var leftCentroid = __.clusterCentroids.get(yscale[__.bundleDimension](row[__.bundleDimension])).get(p[i]);
+          var rightCentroid = __.clusterCentroids.get(yscale[__.bundleDimension](row[__.bundleDimension])).get(p[i+1]);
+          var centroid = 0.5 * (leftCentroid + rightCentroid);
+          cy = centroid + (1 - __.bundlingStrength) * (cy - centroid);
+        }
+        centroids.push($V([cx, cy]));
+      }
+    }
+
+    return centroids;
+  }
+
+  function compute_control_points(centroids) {
+
+    var cols = centroids.length;
+    var a = __.smoothness;
+    var cps = [];
+
+    cps.push(centroids[0]);
+    cps.push($V([centroids[0].e(1) + a*2*(centroids[1].e(1)-centroids[0].e(1)), centroids[0].e(2)]));
+    for (var col = 1; col < cols - 1; ++col) {
+      var mid = centroids[col];
+      var left = centroids[col - 1];
+      var right = centroids[col + 1];
+
+      var diff = left.subtract(right);
+      cps.push(mid.add(diff.x(a)));
+      cps.push(mid);
+      cps.push(mid.subtract(diff.x(a)));
+    }
+    cps.push($V([centroids[cols-1].e(1) + a*2*(centroids[cols-2].e(1)-centroids[cols-1].e(1)), centroids[cols-1].e(2)]));
+    cps.push(centroids[cols - 1]);
+
+    return cps;
+
+  };
+
+  pc.shadows = function() {
+    flags.shadows = true;
+    pc.alphaOnBrushed(0.1);
+    pc.render();
+    return this;
+  };
+
+  // draw dots with radius r on the axis line where data intersects
+  pc.axisDots = function(r) {
+    var r = r || 0.1;
+    var ctx = pc.ctx.marks;
+    var startAngle = 0;
+    var endAngle = 2 * Math.PI;
+    ctx.globalAlpha = d3.min([ 1 / Math.pow(__.data.length, 1 / 2), 1 ]);
+    __.data.forEach(function(d) {
+      __.dimensions.map(function(p, i) {
+        ctx.beginPath();
+        ctx.arc(position(p), yscale[p](d[p]), r, startAngle, endAngle);
+        ctx.stroke();
+        ctx.fill();
+      });
+    });
+    return this;
+  };
+
+  // draw single cubic bezier curve
+  function single_curve(d, ctx) {
+
+    var centroids = compute_centroids(d);
+    var cps = compute_control_points(centroids);
+
+    ctx.moveTo(cps[0].e(1), cps[0].e(2));
+    for (var i = 1; i < cps.length; i += 3) {
+      if (__.showControlPoints) {
+        for (var j = 0; j < 3; j++) {
+          ctx.fillRect(cps[i+j].e(1), cps[i+j].e(2), 2, 2);
+        }
+      }
+      ctx.bezierCurveTo(cps[i].e(1), cps[i].e(2), cps[i+1].e(1), cps[i+1].e(2), cps[i+2].e(1), cps[i+2].e(2));
+    }
+  };
+
+  // draw single polyline
+  function color_path(d, ctx) {
+    ctx.beginPath();
+    if ((__.bundleDimension !== null && __.bundlingStrength > 0) || __.smoothness > 0) {
+      single_curve(d, ctx);
+    } else {
+      single_path(d, ctx);
+    }
+    ctx.stroke();
+  };
+
+  // draw many polylines of the same color
+  function paths(data, ctx) {
+    ctx.clearRect(-1, -1, w() + 2, h() + 2);
+    ctx.beginPath();
+    data.forEach(function(d) {
+      if ((__.bundleDimension !== null && __.bundlingStrength > 0) || __.smoothness > 0) {
+        single_curve(d, ctx);
+      } else {
+        single_path(d, ctx);
+      }
+    });
+    ctx.stroke();
+  };
+
+  // returns the y-position just beyond the separating null value line
+  function getNullPosition() {
+    if (__.nullValueSeparator=="bottom") {
+      return h()+1;
+    } else if (__.nullValueSeparator=="top") {
+      return 1;
+    } else {
+      console.log("A value is NULL, but nullValueSeparator is not set; set it to 'bottom' or 'top'.");
+    }
+    return h()+1;
+  };
+
+  function single_path(d, ctx) {
+    __.dimensions.map(function(p, i) {
+      if (i == 0) {
+        ctx.moveTo(position(p), typeof d[p] =='undefined' ? getNullPosition() : yscale[p](d[p]));
+      } else {
+        ctx.lineTo(position(p), typeof d[p] =='undefined' ? getNullPosition() : yscale[p](d[p]));
+      }
+    });
+  };
+
+  function path_brushed(d, i) {
+    if (__.brushedColor !== null) {
+      ctx.brushed.strokeStyle = d3.functor(__.brushedColor)(d, i);
+    } else {
+      ctx.brushed.strokeStyle = d3.functor(__.color)(d, i);
+    }
+    return color_path(d, ctx.brushed)
+  };
+
+  function path_foreground(d, i) {
+    ctx.foreground.strokeStyle = d3.functor(__.color)(d, i);
+    return color_path(d, ctx.foreground);
+  };
+
+  function path_highlight(d, i) {
+    ctx.highlight.strokeStyle = d3.functor(__.color)(d, i);
+    return color_path(d, ctx.highlight);
+  };
+  pc.clear = function(layer) {
+    ctx[layer].clearRect(0, 0, w() + 2, h() + 2);
+
+    // This will make sure that the foreground items are transparent
+    // without the need for changing the opacity style of the foreground canvas
+    // as this would stop the css styling from working
+    if(layer === "brushed" && isBrushed()) {
+      ctx.brushed.fillStyle = pc.selection.style("background-color");
+      ctx.brushed.globalAlpha = 1 - __.alphaOnBrushed;
+      ctx.brushed.fillRect(0, 0, w() + 2, h() + 2);
+      ctx.brushed.globalAlpha = __.alpha;
+    }
+    return this;
+  };
+
+  d3.rebind(pc, axis, "ticks", "orient", "tickValues", "tickSubdivide", "tickSize", "tickPadding", "tickFormat");
+
+  function flipAxisAndUpdatePCP(dimension) {
+    var g = pc.svg.selectAll(".dimension");
+
+    pc.flip(dimension);
+
+    d3.select(this.parentElement)
+      .transition()
+        .duration(1100)
+        .call(axis.scale(yscale[dimension]));
+
+    pc.render();
+  }
+
+  function rotateLabels() {
+    var delta = d3.event.deltaY;
+    delta = delta < 0 ? -5 : delta;
+    delta = delta > 0 ? 5 : delta;
+
+    __.dimensionTitleRotation += delta;
+    pc.svg.selectAll("text.label")
+      .attr("transform", "translate(0,-5) rotate(" + __.dimensionTitleRotation + ")");
+    d3.event.preventDefault();
+  }
+
+  function dimensionLabels(d) {
+    return d in __.dimensionTitles ? __.dimensionTitles[d] : d;  // dimension display names
+  }
+
+  pc.createAxes = function() {
+    if (g) pc.removeAxes();
+
+    // Add a group element for each dimension.
+    g = pc.svg.selectAll(".dimension")
+        .data(__.dimensions, function(d) { return d; })
+      .enter().append("svg:g")
+        .attr("class", "dimension")
+        .attr("transform", function(d) { return "translate(" + xscale(d) + ")"; });
+
+    // Add an axis and title.
+    g.append("svg:g")
+        .attr("class", "axis")
+        .attr("transform", "translate(0,0)")
+        .each(function(d) { d3.select(this).call(axis.scale(yscale[d])); })
+      .append("svg:text")
+        .attr({
+          "text-anchor": "middle",
+          "y": 0,
+          "transform": "translate(0,-5) rotate(" + __.dimensionTitleRotation + ")",
+          "x": 0,
+          "class": "label"
+        })
+        .text(dimensionLabels)
+        .on("dblclick", flipAxisAndUpdatePCP)
+        .on("wheel", rotateLabels);
+
+    if (__.nullValueSeparator=="top") {
+      pc.svg.append("line")
+        .attr("x1", 0)
+        .attr("y1", 1+__.nullValueSeparatorPadding.top)
+        .attr("x2", w())
+        .attr("y2", 1+__.nullValueSeparatorPadding.top)
+        .attr("stroke-width", 1)
+        .attr("stroke", "#777")
+        .attr("fill", "none")
+        .attr("shape-rendering", "crispEdges");
+    } else if (__.nullValueSeparator=="bottom") {
+      pc.svg.append("line")
+        .attr("x1", 0)
+        .attr("y1", h()+1-__.nullValueSeparatorPadding.bottom)
+        .attr("x2", w())
+        .attr("y2", h()+1-__.nullValueSeparatorPadding.bottom)
+        .attr("stroke-width", 1)
+        .attr("stroke", "#777")
+        .attr("fill", "none")
+        .attr("shape-rendering", "crispEdges");
+    }
+
+    flags.axes= true;
+    return this;
+  };
+
+  pc.removeAxes = function() {
+    g.remove();
+    return this;
+  };
+
+  pc.updateAxes = function() {
+    var g_data = pc.svg.selectAll(".dimension").data(__.dimensions);
+
+    // Enter
+    g_data.enter().append("svg:g")
+        .attr("class", "dimension")
+        .attr("transform", function(p) { return "translate(" + position(p) + ")"; })
+        .style("opacity", 0)
+      .append("svg:g")
+        .attr("class", "axis")
+        .attr("transform", "translate(0,0)")
+        .each(function(d) { d3.select(this).call(axis.scale(yscale[d])); })
+      .append("svg:text")
+        .attr({
+          "text-anchor": "middle",
+          "y": 0,
+          "transform": "translate(0,-5) rotate(" + __.dimensionTitleRotation + ")",
+          "x": 0,
+          "class": "label"
+        })
+        .text(dimensionLabels)
+        .on("dblclick", flipAxisAndUpdatePCP)
+        .on("wheel", rotateLabels);
+
+    // Update
+    g_data.attr("opacity", 0);
+    g_data.select(".axis")
+      .transition()
+        .duration(1100)
+        .each(function(d) {
+          d3.select(this).call(axis.scale(yscale[d]));
+        });
+    g_data.select(".label")
+      .transition()
+        .duration(1100)
+        .text(dimensionLabels)
+        .attr("transform", "translate(0,-5) rotate(" + __.dimensionTitleRotation + ")");
+
+    // Exit
+    g_data.exit().remove();
+
+    g = pc.svg.selectAll(".dimension");
+    g.transition().duration(1100)
+      .attr("transform", function(p) { return "translate(" + position(p) + ")"; })
+      .style("opacity", 1);
+
+    pc.svg.selectAll(".axis")
+      .transition()
+        .duration(1100)
+        .each(function(d) { d3.select(this).call(axis.scale(yscale[d])); });
+
+    if (flags.brushable) pc.brushable();
+    if (flags.reorderable) pc.reorderable();
+    if (pc.brushMode() !== "None") {
+      var mode = pc.brushMode();
+      pc.brushMode("None");
+      pc.brushMode(mode);
+    }
+    return this;
+  };
+
+  // Jason Davies, http://bl.ocks.org/1341281
+  pc.reorderable = function() {
+    if (!g) pc.createAxes();
+
+    g.style("cursor", "move")
+      .call(d3.behavior.drag()
+        .on("dragstart", function(d) {
+          dragging[d] = this.__origin__ = xscale(d);
+        })
+        .on("drag", function(d) {
+          dragging[d] = Math.min(w(), Math.max(0, this.__origin__ += d3.event.dx));
+          __.dimensions.sort(function(a, b) { return position(a) - position(b); });
+          xscale.domain(__.dimensions);
+          pc.render();
+          g.attr("transform", function(d) { return "translate(" + position(d) + ")"; });
+        })
+        .on("dragend", function(d) {
+          // Let's see if the order has changed and send out an event if so.
+          var i = 0,
+              j = __.dimensions.indexOf(d),
+              elem = this,
+              parent = this.parentElement;
+
+          while((elem = elem.previousElementSibling) != null) ++i;
+          if (i !== j) {
+            events.axesreorder.call(pc, __.dimensions);
+            // We now also want to reorder the actual dom elements that represent
+            // the axes. That is, the g.dimension elements. If we don't do this,
+            // we get a weird and confusing transition when updateAxes is called.
+            // This is due to the fact that, initially the nth g.dimension element
+            // represents the nth axis. However, after a manual reordering,
+            // without reordering the dom elements, the nth dom elements no longer
+            // necessarily represents the nth axis.
+            //
+            // i is the original index of the dom element
+            // j is the new index of the dom element
+            if (i > j) { // Element moved left
+              parent.insertBefore(this, parent.children[j - 1]);
+            } else {     // Element moved right
+              if ((j + 1) < parent.children.length) {
+                parent.insertBefore(this, parent.children[j + 1]);
+              } else {
+                parent.appendChild(this);
+              }
+            }
+          }
+
+          delete this.__origin__;
+          delete dragging[d];
+          d3.select(this).transition().attr("transform", "translate(" + xscale(d) + ")");
+          pc.render();
+        }));
+    flags.reorderable = true;
+    return this;
+  };
+
+  // Reorder dimensions, such that the highest value (visually) is on the left and
+  // the lowest on the right. Visual values are determined by the data values in
+  // the given row.
+  pc.reorder = function(rowdata) {
+    var dims = __.dimensions.slice(0);
+    __.dimensions.sort(function(a, b) {
+      var pixelDifference = yscale[a](rowdata[a]) - yscale[b](rowdata[b]);
+
+      // Array.sort is not necessarily stable, this means that if pixelDifference is zero
+      // the ordering of dimensions might change unexpectedly. This is solved by sorting on
+      // variable name in that case.
+      if (pixelDifference === 0) {
+        return a.localeCompare(b);
+      } // else
+      return pixelDifference;
+    });
+
+    // NOTE: this is relatively cheap given that:
+    // number of dimensions < number of data items
+    // Thus we check equality of order to prevent rerendering when this is the case.
+    var reordered = false;
+    dims.some(function(val, index) {
+      reordered = val !== __.dimensions[index];
+      return reordered;
+    });
+
+    if (reordered) {
+      xscale.domain(__.dimensions);
+      var highlighted = __.highlighted.slice(0);
+      pc.unhighlight();
+
+      g.transition()
+        .duration(1500)
+        .attr("transform", function(d) {
+          return "translate(" + xscale(d) + ")";
+        });
+      pc.render();
+
+      // pc.highlight() does not check whether highlighted is length zero, so we do that here.
+      if (highlighted.length !== 0) {
+        pc.highlight(highlighted);
+      }
+    }
+  }
+
+  // pairs of adjacent dimensions
+  pc.adjacent_pairs = function(arr) {
+    var ret = [];
+    for (var i = 0; i < arr.length-1; i++) {
+      ret.push([arr[i],arr[i+1]]);
+    };
+    return ret;
+  };
+
+  var brush = {
+    modes: {
+      "None": {
+        install: function(pc) {},            // Nothing to be done.
+        uninstall: function(pc) {},          // Nothing to be done.
+        selected: function() { return []; }, // Nothing to return
+        brushState: function() { return {}; }
+      }
+    },
+    mode: "None",
+    predicate: "AND",
+    currentMode: function() {
+      return this.modes[this.mode];
+    }
+  };
+
+  // This function can be used for 'live' updates of brushes. That is, during the
+  // specification of a brush, this method can be called to update the view.
+  //
+  // @param newSelection - The new set of data items that is currently contained
+  //                       by the brushes
+  function brushUpdated(newSelection) {
+    __.brushed = newSelection;
+    events.brush.call(pc,__.brushed);
+    pc.renderBrushed();
+  }
+
+  function brushPredicate(predicate) {
+    if (!arguments.length) { return brush.predicate; }
+
+    predicate = String(predicate).toUpperCase();
+    if (predicate !== "AND" && predicate !== "OR") {
+      throw "Invalid predicate " + predicate;
+    }
+
+    brush.predicate = predicate;
+    __.brushed = brush.currentMode().selected();
+    pc.renderBrushed();
+    return pc;
+  }
+
+  pc.brushModes = function() {
+    return Object.getOwnPropertyNames(brush.modes);
+  };
+
+  pc.brushMode = function(mode) {
+    if (arguments.length === 0) {
+      return brush.mode;
+    }
+
+    if (pc.brushModes().indexOf(mode) === -1) {
+      throw "pc.brushmode: Unsupported brush mode: " + mode;
+    }
+
+    // Make sure that we don't trigger unnecessary events by checking if the mode
+    // actually changes.
+    if (mode !== brush.mode) {
+      // When changing brush modes, the first thing we need to do is clearing any
+      // brushes from the current mode, if any.
+      if (brush.mode !== "None") {
+        pc.brushReset();
+      }
+
+      // Next, we need to 'uninstall' the current brushMode.
+      brush.modes[brush.mode].uninstall(pc);
+      // Finally, we can install the requested one.
+      brush.mode = mode;
+      brush.modes[brush.mode].install();
+      if (mode === "None") {
+        delete pc.brushPredicate;
+      } else {
+        pc.brushPredicate = brushPredicate;
+      }
+    }
+
+    return pc;
+  };
+
+  // brush mode: 1D-Axes
+
+  (function() {
+    var brushes = {};
+
+    function is_brushed(p) {
+      return !brushes[p].empty();
+    }
+
+    // data within extents
+    function selected() {
+      var actives = __.dimensions.filter(is_brushed),
+          extents = actives.map(function(p) { return brushes[p].extent(); });
+
+      // We don't want to return the full data set when there are no axes brushed.
+      // Actually, when there are no axes brushed, by definition, no items are
+      // selected. So, let's avoid the filtering and just return false.
+      //if (actives.length === 0) return false;
+
+      // Resolves broken examples for now. They expect to get the full dataset back from empty brushes
+      if (actives.length === 0) return __.data;
+
+      // test if within range
+      var within = {
+        "date": function(d,p,dimension) {
+    if (typeof yscale[p].rangePoints === "function") { // if it is ordinal
+            return extents[dimension][0] <= yscale[p](d[p]) && yscale[p](d[p]) <= extents[dimension][1]
+          } else {
+            return extents[dimension][0] <= d[p] && d[p] <= extents[dimension][1]
+          }
+        },
+        "number": function(d,p,dimension) {
+          if (typeof yscale[p].rangePoints === "function") { // if it is ordinal
+            return extents[dimension][0] <= yscale[p](d[p]) && yscale[p](d[p]) <= extents[dimension][1]
+          } else {
+            return extents[dimension][0] <= d[p] && d[p] <= extents[dimension][1]
+          }
+        },
+        "string": function(d,p,dimension) {
+          return extents[dimension][0] <= yscale[p](d[p]) && yscale[p](d[p]) <= extents[dimension][1]
+        }
+      };
+
+      return __.data
+        .filter(function(d) {
+          switch(brush.predicate) {
+          case "AND":
+            return actives.every(function(p, dimension) {
+              return within[__.types[p]](d,p,dimension);
+            });
+          case "OR":
+            return actives.some(function(p, dimension) {
+              return within[__.types[p]](d,p,dimension);
+            });
+          default:
+            throw "Unknown brush predicate " + __.brushPredicate;
+          }
+        });
+    };
+
+    function brushExtents(extents) {
+      if(typeof(extents) === 'undefined')
+      {
+        var extents = {};
+        __.dimensions.forEach(function(d) {
+          var brush = brushes[d];
+          if (brush !== undefined && !brush.empty()) {
+            var extent = brush.extent();
+            extent.sort(d3.ascending);
+            extents[d] = extent;
+          }
+        });
+        return extents;
+      }
+      else
+      {
+        //first get all the brush selections
+        var brushSelections = {};
+        g.selectAll('.brush')
+          .each(function(d) {
+            brushSelections[d] = d3.select(this);
+
+        });
+
+        // loop over each dimension and update appropriately (if it was passed in through extents)
+        __.dimensions.forEach(function(d) {
+          if (extents[d] === undefined){
+            return;
+          }
+
+          var brush = brushes[d];
+          if (brush !== undefined) {
+            //update the extent
+            brush.extent(extents[d]);
+
+            //redraw the brush
+            brush(brushSelections[d]);
+
+            //fire some events
+            brush.event(brushSelections[d]);
+          }
+        });
+
+        //redraw the chart
+        pc.renderBrushed();
+      }
+    }
+    function brushFor(axis) {
+      var brush = d3.svg.brush();
+
+      brush
+        .y(yscale[axis])
+        .on("brushstart", function() {
+          if(d3.event.sourceEvent !== null) {
+            d3.event.sourceEvent.stopPropagation();
+          }
+        })
+        .on("brush", function() {
+          brushUpdated(selected());
+        })
+        .on("brushend", function() {
+          events.brushend.call(pc, __.brushed);
+        });
+
+      brushes[axis] = brush;
+      return brush;
+    };
+    function brushReset(dimension) {
+      __.brushed = false;
+      if (g) {
+        g.selectAll('.brush')
+          .each(function(d) {
+            d3.select(this).call(
+              brushes[d].clear()
+            );
+          });
+        pc.renderBrushed();
+      }
+      return this;
+    };
+
+    function install() {
+      if (!g) pc.createAxes();
+
+      // Add and store a brush for each axis.
+      g.append("svg:g")
+        .attr("class", "brush")
+        .each(function(d) {
+          d3.select(this).call(brushFor(d));
+        })
+        .selectAll("rect")
+          .style("visibility", null)
+          .attr("x", -15)
+          .attr("width", 30);
+
+      pc.brushExtents = brushExtents;
+      pc.brushReset = brushReset;
+      return pc;
+    };
+
+    brush.modes["1D-axes"] = {
+      install: install,
+      uninstall: function() {
+        g.selectAll(".brush").remove();
+        brushes = {};
+        delete pc.brushExtents;
+        delete pc.brushReset;
+      },
+      selected: selected,
+      brushState: brushExtents
+    }
+  })();
+  // brush mode: 2D-strums
+  // bl.ocks.org/syntagmatic/5441022
+
+  (function() {
+    var strums = {},
+        strumRect;
+
+    function drawStrum(strum, activePoint) {
+      var svg = pc.selection.select("svg").select("g#strums"),
+          id = strum.dims.i,
+          points = [strum.p1, strum.p2],
+          line = svg.selectAll("line#strum-" + id).data([strum]),
+          circles = svg.selectAll("circle#strum-" + id).data(points),
+          drag = d3.behavior.drag();
+
+      line.enter()
+        .append("line")
+        .attr("id", "strum-" + id)
+        .attr("class", "strum");
+
+      line
+        .attr("x1", function(d) { return d.p1[0]; })
+        .attr("y1", function(d) { return d.p1[1]; })
+        .attr("x2", function(d) { return d.p2[0]; })
+        .attr("y2", function(d) { return d.p2[1]; })
+        .attr("stroke", "black")
+        .attr("stroke-width", 2);
+
+      drag
+        .on("drag", function(d, i) {
+          var ev = d3.event;
+          i = i + 1;
+          strum["p" + i][0] = Math.min(Math.max(strum.minX + 1, ev.x), strum.maxX);
+          strum["p" + i][1] = Math.min(Math.max(strum.minY, ev.y), strum.maxY);
+          drawStrum(strum, i - 1);
+        })
+        .on("dragend", onDragEnd());
+
+      circles.enter()
+        .append("circle")
+        .attr("id", "strum-" + id)
+        .attr("class", "strum");
+
+      circles
+        .attr("cx", function(d) { return d[0]; })
+        .attr("cy", function(d) { return d[1]; })
+        .attr("r", 5)
+        .style("opacity", function(d, i) {
+          return (activePoint !== undefined && i === activePoint) ? 0.8 : 0;
+        })
+        .on("mouseover", function() {
+          d3.select(this).style("opacity", 0.8);
+        })
+        .on("mouseout", function() {
+          d3.select(this).style("opacity", 0);
+        })
+        .call(drag);
+    }
+
+    function dimensionsForPoint(p) {
+      var dims = { i: -1, left: undefined, right: undefined };
+      __.dimensions.some(function(dim, i) {
+        if (xscale(dim) < p[0]) {
+          var next = __.dimensions[i + 1];
+          dims.i = i;
+          dims.left = dim;
+          dims.right = next;
+          return false;
+        }
+        return true;
+      });
+
+      if (dims.left === undefined) {
+        // Event on the left side of the first axis.
+        dims.i = 0;
+        dims.left = __.dimensions[0];
+        dims.right = __.dimensions[1];
+      } else if (dims.right === undefined) {
+        // Event on the right side of the last axis
+        dims.i = __.dimensions.length - 1;
+        dims.right = dims.left;
+        dims.left = __.dimensions[__.dimensions.length - 2];
+      }
+
+      return dims;
+    }
+
+    function onDragStart() {
+      // First we need to determine between which two axes the sturm was started.
+      // This will determine the freedom of movement, because a strum can
+      // logically only happen between two axes, so no movement outside these axes
+      // should be allowed.
+      return function() {
+        var p = d3.mouse(strumRect[0][0]),
+            dims,
+            strum;
+
+        p[0] = p[0] - __.margin.left;
+        p[1] = p[1] - __.margin.top;
+
+        dims = dimensionsForPoint(p),
+        strum = {
+          p1: p,
+          dims: dims,
+          minX: xscale(dims.left),
+          maxX: xscale(dims.right),
+          minY: 0,
+          maxY: h()
+        };
+
+        strums[dims.i] = strum;
+        strums.active = dims.i;
+
+        // Make sure that the point is within the bounds
+        strum.p1[0] = Math.min(Math.max(strum.minX, p[0]), strum.maxX);
+        strum.p2 = strum.p1.slice();
+      };
+    }
+
+    function onDrag() {
+      return function() {
+        var ev = d3.event,
+            strum = strums[strums.active];
+
+        // Make sure that the point is within the bounds
+        strum.p2[0] = Math.min(Math.max(strum.minX + 1, ev.x - __.margin.left), strum.maxX);
+        strum.p2[1] = Math.min(Math.max(strum.minY, ev.y - __.margin.top), strum.maxY);
+        drawStrum(strum, 1);
+      };
+    }
+
+    function containmentTest(strum, width) {
+      var p1 = [strum.p1[0] - strum.minX, strum.p1[1] - strum.minX],
+          p2 = [strum.p2[0] - strum.minX, strum.p2[1] - strum.minX],
+          m1 = 1 - width / p1[0],
+          b1 = p1[1] * (1 - m1),
+          m2 = 1 - width / p2[0],
+          b2 = p2[1] * (1 - m2);
+
+      // test if point falls between lines
+      return function(p) {
+        var x = p[0],
+            y = p[1],
+            y1 = m1 * x + b1,
+            y2 = m2 * x + b2;
+
+        if (y > Math.min(y1, y2) && y < Math.max(y1, y2)) {
+          return true;
+        }
+
+        return false;
+      };
+    }
+
+    function selected() {
+      var ids = Object.getOwnPropertyNames(strums),
+          brushed = __.data;
+
+      // Get the ids of the currently active strums.
+      ids = ids.filter(function(d) {
+        return !isNaN(d);
+      });
+
+      function crossesStrum(d, id) {
+        var strum = strums[id],
+            test = containmentTest(strum, strums.width(id)),
+            d1 = strum.dims.left,
+            d2 = strum.dims.right,
+            y1 = yscale[d1],
+            y2 = yscale[d2],
+            point = [y1(d[d1]) - strum.minX, y2(d[d2]) - strum.minX];
+        return test(point);
+      }
+
+      if (ids.length === 0) { return brushed; }
+
+      return brushed.filter(function(d) {
+        switch(brush.predicate) {
+        case "AND":
+          return ids.every(function(id) { return crossesStrum(d, id); });
+        case "OR":
+          return ids.some(function(id) { return crossesStrum(d, id); });
+        default:
+          throw "Unknown brush predicate " + __.brushPredicate;
+        }
+      });
+    }
+
+    function removeStrum() {
+      var strum = strums[strums.active],
+          svg = pc.selection.select("svg").select("g#strums");
+
+      delete strums[strums.active];
+      strums.active = undefined;
+      svg.selectAll("line#strum-" + strum.dims.i).remove();
+      svg.selectAll("circle#strum-" + strum.dims.i).remove();
+    }
+
+    function onDragEnd() {
+      return function() {
+        var brushed = __.data,
+            strum = strums[strums.active];
+
+        // Okay, somewhat unexpected, but not totally unsurprising, a mousclick is
+        // considered a drag without move. So we have to deal with that case
+        if (strum && strum.p1[0] === strum.p2[0] && strum.p1[1] === strum.p2[1]) {
+          removeStrum(strums);
+        }
+
+        brushed = selected(strums);
+        strums.active = undefined;
+        __.brushed = brushed;
+        pc.renderBrushed();
+        events.brushend.call(pc, __.brushed);
+      };
+    }
+
+    function brushReset(strums) {
+      return function() {
+        var ids = Object.getOwnPropertyNames(strums).filter(function(d) {
+          return !isNaN(d);
+        });
+
+        ids.forEach(function(d) {
+          strums.active = d;
+          removeStrum(strums);
+        });
+        onDragEnd(strums)();
+      };
+    }
+
+    function install() {
+      var drag = d3.behavior.drag();
+
+      // Map of current strums. Strums are stored per segment of the PC. A segment,
+      // being the area between two axes. The left most area is indexed at 0.
+      strums.active = undefined;
+      // Returns the width of the PC segment where currently a strum is being
+      // placed. NOTE: even though they are evenly spaced in our current
+      // implementation, we keep for when non-even spaced segments are supported as
+      // well.
+      strums.width = function(id) {
+        var strum = strums[id];
+
+        if (strum === undefined) {
+          return undefined;
+        }
+
+        return strum.maxX - strum.minX;
+      };
+
+      pc.on("axesreorder.strums", function() {
+        var ids = Object.getOwnPropertyNames(strums).filter(function(d) {
+          return !isNaN(d);
+        });
+
+        // Checks if the first dimension is directly left of the second dimension.
+        function consecutive(first, second) {
+          var length = __.dimensions.length;
+          return __.dimensions.some(function(d, i) {
+            return (d === first)
+              ? i + i < length && __.dimensions[i + 1] === second
+              : false;
+          });
+        }
+
+        if (ids.length > 0) { // We have some strums, which might need to be removed.
+          ids.forEach(function(d) {
+            var dims = strums[d].dims;
+            strums.active = d;
+            // If the two dimensions of the current strum are not next to each other
+            // any more, than we'll need to remove the strum. Otherwise we keep it.
+            if (!consecutive(dims.left, dims.right)) {
+              removeStrum(strums);
+            }
+          });
+          onDragEnd(strums)();
+        }
+      });
+
+      // Add a new svg group in which we draw the strums.
+      pc.selection.select("svg").append("g")
+        .attr("id", "strums")
+        .attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")");
+
+      // Install the required brushReset function
+      pc.brushReset = brushReset(strums);
+
+      drag
+        .on("dragstart", onDragStart(strums))
+        .on("drag", onDrag(strums))
+        .on("dragend", onDragEnd(strums));
+
+      // NOTE: The styling needs to be done here and not in the css. This is because
+      //       for 1D brushing, the canvas layers should not listen to
+      //       pointer-events.
+      strumRect = pc.selection.select("svg").insert("rect", "g#strums")
+        .attr("id", "strum-events")
+        .attr("x", __.margin.left)
+        .attr("y", __.margin.top)
+        .attr("width", w())
+        .attr("height", h() + 2)
+        .style("opacity", 0)
+        .call(drag);
+    }
+
+    brush.modes["2D-strums"] = {
+      install: install,
+      uninstall: function() {
+        pc.selection.select("svg").select("g#strums").remove();
+        pc.selection.select("svg").select("rect#strum-events").remove();
+        pc.on("axesreorder.strums", undefined);
+        delete pc.brushReset;
+
+        strumRect = undefined;
+      },
+      selected: selected,
+      brushState: function () { return strums; }
+    };
+
+  }());
+
+  // brush mode: 1D-Axes with multiple extents
+  // requires d3.svg.multibrush
+
+  (function() {
+    if (typeof d3.svg.multibrush !== 'function') {
+      return;
+    }
+    var brushes = {};
+
+    function is_brushed(p) {
+      return !brushes[p].empty();
+    }
+
+    // data within extents
+    function selected() {
+      var actives = __.dimensions.filter(is_brushed),
+          extents = actives.map(function(p) { return brushes[p].extent(); });
+
+      // We don't want to return the full data set when there are no axes brushed.
+      // Actually, when there are no axes brushed, by definition, no items are
+      // selected. So, let's avoid the filtering and just return false.
+      //if (actives.length === 0) return false;
+
+      // Resolves broken examples for now. They expect to get the full dataset back from empty brushes
+      if (actives.length === 0) return __.data;
+
+      // test if within range
+      var within = {
+        "date": function(d,p,dimension,b) {
+          if (typeof yscale[p].rangePoints === "function") { // if it is ordinal
+            return b[0] <= yscale[p](d[p]) && yscale[p](d[p]) <= b[1]
+          } else {
+              return b[0] <= d[p] && d[p] <= b[1]
+          }
+        },
+        "number": function(d,p,dimension,b) {
+          if (typeof yscale[p].rangePoints === "function") { // if it is ordinal
+            return b[0] <= yscale[p](d[p]) && yscale[p](d[p]) <= b[1]
+          } else {
+              return b[0] <= d[p] && d[p] <= b[1]
+          }
+        },
+        "string": function(d,p,dimension,b) {
+          return b[0] <= yscale[p](d[p]) && yscale[p](d[p]) <= b[1]
+        }
+      };
+
+      return __.data
+      .filter(function(d) {
+        switch(brush.predicate) {
+        case "AND":
+          return actives.every(function(p, dimension) {
+            return extents[dimension].some(function(b) {
+              return within[__.types[p]](d,p,dimension,b);
+            });
+          });
+        case "OR":
+          return actives.some(function(p, dimension) {
+            return extents[dimension].some(function(b) {
+                return within[__.types[p]](d,p,dimension,b);
+              });
+          });
+        default:
+          throw "Unknown brush predicate " + __.brushPredicate;
+        }
+      });
+    };
+
+    function brushExtents() {
+      var extents = {};
+      __.dimensions.forEach(function(d) {
+        var brush = brushes[d];
+        if (brush !== undefined && !brush.empty()) {
+          var extent = brush.extent();
+          extents[d] = extent;
+        }
+      });
+      return extents;
+    }
+
+    function brushFor(axis) {
+      var brush = d3.svg.multibrush();
+
+      brush
+        .y(yscale[axis])
+        .on("brushstart", function() {
+          if(d3.event.sourceEvent !== null) {
+            d3.event.sourceEvent.stopPropagation();
+          }
+        })
+        .on("brush", function() {
+          brushUpdated(selected());
+        })
+        .on("brushend", function() {
+        // d3.svg.multibrush clears extents just before calling 'brushend'
+        // so we have to update here again.
+        // This fixes issue #103 for now, but should be changed in d3.svg.multibrush
+        // to avoid unnecessary computation.
+        brushUpdated(selected());
+          events.brushend.call(pc, __.brushed);
+        })
+        .extentAdaption(function(selection) {
+          selection
+          .style("visibility", null)
+            .attr("x", -15)
+            .attr("width", 30);
+        })
+        .resizeAdaption(function(selection) {
+         selection
+           .selectAll("rect")
+           .attr("x", -15)
+           .attr("width", 30);
+        });
+
+      brushes[axis] = brush;
+      return brush;
+    }
+
+    function brushReset(dimension) {
+      __.brushed = false;
+      if (g) {
+        g.selectAll('.brush')
+          .each(function(d) {
+            d3.select(this).call(
+              brushes[d].clear()
+            );
+          });
+        pc.renderBrushed();
+      }
+      return this;
+    };
+
+    function install() {
+      if (!g) pc.createAxes();
+
+      // Add and store a brush for each axis.
+      g.append("svg:g")
+        .attr("class", "brush")
+        .each(function(d) {
+          d3.select(this).call(brushFor(d));
+        })
+        .selectAll("rect")
+          .style("visibility", null)
+          .attr("x", -15)
+          .attr("width", 30);
+
+      pc.brushExtents = brushExtents;
+      pc.brushReset = brushReset;
+      return pc;
+    }
+
+    brush.modes["1D-axes-multi"] = {
+      install: install,
+      uninstall: function() {
+        g.selectAll(".brush").remove();
+        brushes = {};
+        delete pc.brushExtents;
+        delete pc.brushReset;
+      },
+      selected: selected,
+      brushState: brushExtents
+    }
+  })();
+  // brush mode: angular
+  // code based on 2D.strums.js
+
+  (function() {
+    var arcs = {},
+        strumRect;
+
+    function drawStrum(arc, activePoint) {
+      var svg = pc.selection.select("svg").select("g#arcs"),
+          id = arc.dims.i,
+          points = [arc.p2, arc.p3],
+          line = svg.selectAll("line#arc-" + id).data([{p1:arc.p1,p2:arc.p2},{p1:arc.p1,p2:arc.p3}]),
+          circles = svg.selectAll("circle#arc-" + id).data(points),
+          drag = d3.behavior.drag(),
+          path = svg.selectAll("path#arc-" + id).data([arc]);
+
+      path.enter()
+        .append("path")
+        .attr("id", "arc-" + id)
+        .attr("class", "arc")
+        .style("fill", "orange")
+        .style("opacity", 0.5);
+
+      path
+        .attr("d", arc.arc)
+        .attr("transform", "translate(" + arc.p1[0] + "," + arc.p1[1] + ")");
+
+      line.enter()
+        .append("line")
+        .attr("id", "arc-" + id)
+        .attr("class", "arc");
+
+      line
+        .attr("x1", function(d) { return d.p1[0]; })
+        .attr("y1", function(d) { return d.p1[1]; })
+        .attr("x2", function(d) { return d.p2[0]; })
+        .attr("y2", function(d) { return d.p2[1]; })
+        .attr("stroke", "black")
+        .attr("stroke-width", 2);
+
+      drag
+        .on("drag", function(d, i) {
+          var ev = d3.event,
+            angle = 0;
+
+          i = i + 2;
+
+          arc["p" + i][0] = Math.min(Math.max(arc.minX + 1, ev.x), arc.maxX);
+          arc["p" + i][1] = Math.min(Math.max(arc.minY, ev.y), arc.maxY);
+
+          angle = i === 3 ? arcs.startAngle(id) : arcs.endAngle(id);
+
+          if ((arc.startAngle < Math.PI && arc.endAngle < Math.PI && angle < Math.PI) ||
+              (arc.startAngle >= Math.PI && arc.endAngle >= Math.PI && angle >= Math.PI)) {
+
+            if (i === 2) {
+              arc.endAngle = angle;
+              arc.arc.endAngle(angle);
+            } else if (i === 3) {
+              arc.startAngle = angle;
+              arc.arc.startAngle(angle);
+            }
+
+          }
+
+          drawStrum(arc, i - 2);
+        })
+        .on("dragend", onDragEnd());
+
+      circles.enter()
+        .append("circle")
+        .attr("id", "arc-" + id)
+        .attr("class", "arc");
+
+      circles
+        .attr("cx", function(d) { return d[0]; })
+        .attr("cy", function(d) { return d[1]; })
+        .attr("r", 5)
+        .style("opacity", function(d, i) {
+          return (activePoint !== undefined && i === activePoint) ? 0.8 : 0;
+        })
+        .on("mouseover", function() {
+          d3.select(this).style("opacity", 0.8);
+        })
+        .on("mouseout", function() {
+          d3.select(this).style("opacity", 0);
+        })
+        .call(drag);
+    }
+
+    function dimensionsForPoint(p) {
+      var dims = { i: -1, left: undefined, right: undefined };
+      __.dimensions.some(function(dim, i) {
+        if (xscale(dim) < p[0]) {
+          var next = __.dimensions[i + 1];
+          dims.i = i;
+          dims.left = dim;
+          dims.right = next;
+          return false;
+        }
+        return true;
+      });
+
+      if (dims.left === undefined) {
+        // Event on the left side of the first axis.
+        dims.i = 0;
+        dims.left = __.dimensions[0];
+        dims.right = __.dimensions[1];
+      } else if (dims.right === undefined) {
+        // Event on the right side of the last axis
+        dims.i = __.dimensions.length - 1;
+        dims.right = dims.left;
+        dims.left = __.dimensions[__.dimensions.length - 2];
+      }
+
+      return dims;
+    }
+
+    function onDragStart() {
+      // First we need to determine between which two axes the arc was started.
+      // This will determine the freedom of movement, because a arc can
+      // logically only happen between two axes, so no movement outside these axes
+      // should be allowed.
+      return function() {
+        var p = d3.mouse(strumRect[0][0]),
+            dims,
+            arc;
+
+        p[0] = p[0] - __.margin.left;
+        p[1] = p[1] - __.margin.top;
+
+        dims = dimensionsForPoint(p),
+        arc = {
+          p1: p,
+          dims: dims,
+          minX: xscale(dims.left),
+          maxX: xscale(dims.right),
+          minY: 0,
+          maxY: h(),
+          startAngle: undefined,
+          endAngle: undefined,
+          arc: d3.svg.arc().innerRadius(0)
+        };
+
+        arcs[dims.i] = arc;
+        arcs.active = dims.i;
+
+        // Make sure that the point is within the bounds
+        arc.p1[0] = Math.min(Math.max(arc.minX, p[0]), arc.maxX);
+        arc.p2 = arc.p1.slice();
+        arc.p3 = arc.p1.slice();
+      };
+    }
+
+    function onDrag() {
+      return function() {
+        var ev = d3.event,
+            arc = arcs[arcs.active];
+
+        // Make sure that the point is within the bounds
+        arc.p2[0] = Math.min(Math.max(arc.minX + 1, ev.x - __.margin.left), arc.maxX);
+        arc.p2[1] = Math.min(Math.max(arc.minY, ev.y - __.margin.top), arc.maxY);
+        arc.p3 = arc.p2.slice();
+  //      console.log(arcs.angle(arcs.active));
+  //      console.log(signedAngle(arcs.unsignedAngle(arcs.active)));
+        drawStrum(arc, 1);
+      };
+    }
+
+    // some helper functions
+    function hypothenuse(a, b) {
+      return Math.sqrt(a*a + b*b);
+    }
+
+    var rad = (function() {
+      var c = Math.PI / 180;
+      return function(angle) {
+        return angle * c;
+      };
+    })();
+
+    var deg = (function() {
+      var c = 180 / Math.PI;
+      return function(angle) {
+        return angle * c;
+      };
+    })();
+
+    // [0, 2*PI] -> [-PI/2, PI/2]
+    var signedAngle = function(angle) {
+      var ret = angle;
+      if (angle > Math.PI) {
+      ret = angle - 1.5 * Math.PI;
+      ret = angle - 1.5 * Math.PI;
+      } else {
+        ret = angle - 0.5 * Math.PI;
+        ret = angle - 0.5 * Math.PI;
+      }
+      return -ret;
+    }
+
+    /**
+     * angles are stored in radians from in [0, 2*PI], where 0 in 12 o'clock.
+     * However, one can only select lines from 0 to PI, so we compute the
+     * 'signed' angle, where 0 is the horizontal line (3 o'clock), and +/- PI/2
+     * are 12 and 6 o'clock respectively.
+     */
+    function containmentTest(arc) {
+      var startAngle = signedAngle(arc.startAngle);
+      var endAngle = signedAngle(arc.endAngle);
+
+      if (startAngle > endAngle) {
+        var tmp = startAngle;
+        startAngle = endAngle;
+        endAngle = tmp;
+      }
+
+      // test if segment angle is contained in angle interval
+      return function(a) {
+
+        if (a >= startAngle && a <= endAngle) {
+          return true;
+        }
+
+        return false;
+      };
+    }
+
+    function selected() {
+      var ids = Object.getOwnPropertyNames(arcs),
+          brushed = __.data;
+
+      // Get the ids of the currently active arcs.
+      ids = ids.filter(function(d) {
+        return !isNaN(d);
+      });
+
+      function crossesStrum(d, id) {
+        var arc = arcs[id],
+            test = containmentTest(arc),
+            d1 = arc.dims.left,
+            d2 = arc.dims.right,
+            y1 = yscale[d1],
+            y2 = yscale[d2],
+            a = arcs.width(id),
+            b = y1(d[d1]) - y2(d[d2]),
+            c = hypothenuse(a, b),
+            angle = Math.asin(b/c); // rad in [-PI/2, PI/2]
+        return test(angle);
+      }
+
+      if (ids.length === 0) { return brushed; }
+
+      return brushed.filter(function(d) {
+        switch(brush.predicate) {
+        case "AND":
+          return ids.every(function(id) { return crossesStrum(d, id); });
+        case "OR":
+          return ids.some(function(id) { return crossesStrum(d, id); });
+        default:
+          throw "Unknown brush predicate " + __.brushPredicate;
+        }
+      });
+    }
+
+    function removeStrum() {
+      var arc = arcs[arcs.active],
+          svg = pc.selection.select("svg").select("g#arcs");
+
+      delete arcs[arcs.active];
+      arcs.active = undefined;
+      svg.selectAll("line#arc-" + arc.dims.i).remove();
+      svg.selectAll("circle#arc-" + arc.dims.i).remove();
+      svg.selectAll("path#arc-" + arc.dims.i).remove();
+    }
+
+    function onDragEnd() {
+      return function() {
+        var brushed = __.data,
+            arc = arcs[arcs.active];
+
+        // Okay, somewhat unexpected, but not totally unsurprising, a mousclick is
+        // considered a drag without move. So we have to deal with that case
+        if (arc && arc.p1[0] === arc.p2[0] && arc.p1[1] === arc.p2[1]) {
+          removeStrum(arcs);
+        }
+
+        if (arc) {
+          var angle = arcs.startAngle(arcs.active);
+
+          arc.startAngle = angle;
+            arc.endAngle = angle;
+            arc.arc
+              .outerRadius(arcs.length(arcs.active))
+              .startAngle(angle)
+              .endAngle(angle);
+        }
+
+
+        brushed = selected(arcs);
+        arcs.active = undefined;
+        __.brushed = brushed;
+        pc.renderBrushed();
+        events.brushend.call(pc, __.brushed);
+      };
+    }
+
+    function brushReset(arcs) {
+      return function() {
+        var ids = Object.getOwnPropertyNames(arcs).filter(function(d) {
+          return !isNaN(d);
+        });
+
+        ids.forEach(function(d) {
+          arcs.active = d;
+          removeStrum(arcs);
+        });
+        onDragEnd(arcs)();
+      };
+    }
+
+    function install() {
+      var drag = d3.behavior.drag();
+
+      // Map of current arcs. arcs are stored per segment of the PC. A segment,
+      // being the area between two axes. The left most area is indexed at 0.
+      arcs.active = undefined;
+      // Returns the width of the PC segment where currently a arc is being
+      // placed. NOTE: even though they are evenly spaced in our current
+      // implementation, we keep for when non-even spaced segments are supported as
+      // well.
+      arcs.width = function(id) {
+        var arc = arcs[id];
+
+        if (arc === undefined) {
+          return undefined;
+        }
+
+        return arc.maxX - arc.minX;
+      };
+
+      // returns angles in [-PI/2, PI/2]
+      angle = function(p1, p2) {
+          var a = p1[0] - p2[0],
+            b = p1[1] - p2[1],
+            c = hypothenuse(a, b);
+
+          return Math.asin(b/c);
+      }
+
+      // returns angles in [0, 2 * PI]
+      arcs.endAngle = function(id) {
+        var arc = arcs[id];
+        if (arc === undefined) {
+              return undefined;
+          }
+        var sAngle = angle(arc.p1, arc.p2),
+          uAngle = -sAngle + Math.PI / 2;
+
+        if (arc.p1[0] > arc.p2[0]) {
+          uAngle = 2 * Math.PI - uAngle;
+        }
+
+        return uAngle;
+      }
+
+      arcs.startAngle = function(id) {
+        var arc = arcs[id];
+        if (arc === undefined) {
+              return undefined;
+          }
+
+        var sAngle = angle(arc.p1, arc.p3),
+          uAngle = -sAngle + Math.PI / 2;
+
+        if (arc.p1[0] > arc.p3[0]) {
+          uAngle = 2 * Math.PI - uAngle;
+        }
+
+        return uAngle;
+      }
+
+      arcs.length = function(id) {
+        var arc = arcs[id];
+
+          if (arc === undefined) {
+            return undefined;
+          }
+
+          var a = arc.p1[0] - arc.p2[0],
+            b = arc.p1[1] - arc.p2[1],
+            c = hypothenuse(a, b);
+
+          return(c);
+      }
+
+      pc.on("axesreorder.arcs", function() {
+        var ids = Object.getOwnPropertyNames(arcs).filter(function(d) {
+          return !isNaN(d);
+        });
+
+        // Checks if the first dimension is directly left of the second dimension.
+        function consecutive(first, second) {
+          var length = __.dimensions.length;
+          return __.dimensions.some(function(d, i) {
+            return (d === first)
+              ? i + i < length && __.dimensions[i + 1] === second
+              : false;
+          });
+        }
+
+        if (ids.length > 0) { // We have some arcs, which might need to be removed.
+          ids.forEach(function(d) {
+            var dims = arcs[d].dims;
+            arcs.active = d;
+            // If the two dimensions of the current arc are not next to each other
+            // any more, than we'll need to remove the arc. Otherwise we keep it.
+            if (!consecutive(dims.left, dims.right)) {
+              removeStrum(arcs);
+            }
+          });
+          onDragEnd(arcs)();
+        }
+      });
+
+      // Add a new svg group in which we draw the arcs.
+      pc.selection.select("svg").append("g")
+        .attr("id", "arcs")
+        .attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")");
+
+      // Install the required brushReset function
+      pc.brushReset = brushReset(arcs);
+
+      drag
+        .on("dragstart", onDragStart(arcs))
+        .on("drag", onDrag(arcs))
+        .on("dragend", onDragEnd(arcs));
+
+      // NOTE: The styling needs to be done here and not in the css. This is because
+      //       for 1D brushing, the canvas layers should not listen to
+      //       pointer-events.
+      strumRect = pc.selection.select("svg").insert("rect", "g#arcs")
+        .attr("id", "arc-events")
+        .attr("x", __.margin.left)
+        .attr("y", __.margin.top)
+        .attr("width", w())
+        .attr("height", h() + 2)
+        .style("opacity", 0)
+        .call(drag);
+    }
+
+    brush.modes["angular"] = {
+      install: install,
+      uninstall: function() {
+        pc.selection.select("svg").select("g#arcs").remove();
+        pc.selection.select("svg").select("rect#arc-events").remove();
+        pc.on("axesreorder.arcs", undefined);
+        delete pc.brushReset;
+
+        strumRect = undefined;
+      },
+      selected: selected,
+      brushState: function () { return arcs; }
+    };
+
+  }());
+
+  pc.interactive = function() {
+    flags.interactive = true;
+    return this;
+  };
+
+  // expose a few objects
+  pc.xscale = xscale;
+  pc.yscale = yscale;
+  pc.ctx = ctx;
+  pc.canvas = canvas;
+  pc.g = function() { return g; };
+
+  // rescale for height, width and margins
+  // TODO currently assumes chart is brushable, and destroys old brushes
+  pc.resize = function() {
+    // selection size
+    pc.selection.select("svg")
+      .attr("width", __.width)
+      .attr("height", __.height)
+    pc.svg.attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")");
+
+    // FIXME: the current brush state should pass through
+    if (flags.brushable) pc.brushReset();
+
+    // scales
+    pc.autoscale();
+
+    // axes, destroys old brushes.
+    if (g) pc.createAxes();
+    if (flags.brushable) pc.brushable();
+    if (flags.reorderable) pc.reorderable();
+
+    events.resize.call(this, {width: __.width, height: __.height, margin: __.margin});
+    return this;
+  };
+
+  // highlight an array of data
+  pc.highlight = function(data) {
+    if (arguments.length === 0) {
+      return __.highlighted;
+    }
+
+    __.highlighted = data;
+    pc.clear("highlight");
+    d3.selectAll([canvas.foreground, canvas.brushed]).classed("faded", true);
+    data.forEach(path_highlight);
+    events.highlight.call(this, data);
+    return this;
+  };
+
+  // clear highlighting
+  pc.unhighlight = function() {
+    __.highlighted = [];
+    pc.clear("highlight");
+    d3.selectAll([canvas.foreground, canvas.brushed]).classed("faded", false);
+    return this;
+  };
+
+  // calculate 2d intersection of line a->b with line c->d
+  // points are objects with x and y properties
+  pc.intersection =  function(a, b, c, d) {
+    return {
+      x: ((a.x * b.y - a.y * b.x) * (c.x - d.x) - (a.x - b.x) * (c.x * d.y - c.y * d.x)) / ((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x)),
+      y: ((a.x * b.y - a.y * b.x) * (c.y - d.y) - (a.y - b.y) * (c.x * d.y - c.y * d.x)) / ((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x))
+    };
+  };
+
+  function position(d) {
+    var v = dragging[d];
+    return v == null ? xscale(d) : v;
+  }
+  pc.version = "0.7.0";
+    // this descriptive text should live with other introspective methods
+    pc.toString = function() { return "Parallel Coordinates: " + __.dimensions.length + " dimensions (" + d3.keys(__.data[0]).length + " total) , " + __.data.length + " rows"; };
+
+    return pc;
+  };
+
+  d3.renderQueue = (function(func) {
+    var _queue = [],                  // data to be rendered
+        _rate = 10,                   // number of calls per frame
+        _clear = function() {},       // clearing function
+        _i = 0;                       // current iteration
+
+    var rq = function(data) {
+      if (data) rq.data(data);
+      rq.invalidate();
+      _clear();
+      rq.render();
+    };
+
+    rq.render = function() {
+      _i = 0;
+      var valid = true;
+      rq.invalidate = function() { valid = false; };
+
+      function doFrame() {
+        if (!valid) return true;
+        if (_i > _queue.length) return true;
+
+        // Typical d3 behavior is to pass a data item *and* its index. As the
+        // render queue splits the original data set, we'll have to be slightly
+        // more carefull about passing the correct index with the data item.
+        var end = Math.min(_i + _rate, _queue.length);
+        for (var i = _i; i < end; i++) {
+          func(_queue[i], i);
+        }
+        _i += _rate;
+      }
+
+      d3.timer(doFrame);
+    };
+
+    rq.data = function(data) {
+      rq.invalidate();
+      _queue = data.slice(0);
+      return rq;
+    };
+
+    rq.rate = function(value) {
+      if (!arguments.length) return _rate;
+      _rate = value;
+      return rq;
+    };
+
+    rq.remaining = function() {
+      return _queue.length - _i;
+    };
+
+    // clear the canvas
+    rq.clear = function(func) {
+      if (!arguments.length) {
+        _clear();
+        return rq;
+      }
+      _clear = func;
+      return rq;
+    };
+
+    rq.invalidate = function() {};
+
+    return rq;
+  });
diff --git a/dashed/assets/vendor/parallel_coordinates/divgrid.js b/dashed/assets/vendor/parallel_coordinates/divgrid.js
new file mode 100644
index 000000000..e4086e8ba
--- /dev/null
+++ b/dashed/assets/vendor/parallel_coordinates/divgrid.js
@@ -0,0 +1,59 @@
+// from http://bl.ocks.org/3687826
+module.exports = function(config) {
+  var columns = [];
+
+  var dg = function(selection) {
+    if (columns.length == 0) columns = d3.keys(selection.data()[0][0]);
+
+    // header
+    selection.selectAll(".header")
+        .data([true])
+      .enter().append("div")
+        .attr("class", "header")
+
+    var header = selection.select(".header")
+      .selectAll(".cell")
+      .data(columns);
+
+    header.enter().append("div")
+      .attr("class", function(d,i) { return "col-" + i; })
+      .classed("cell", true)
+
+    selection.selectAll(".header .cell")
+      .text(function(d) { return d; });
+
+    header.exit().remove();
+
+    // rows
+    var rows = selection.selectAll(".row")
+        .data(function(d) { return d; })
+
+    rows.enter().append("div")
+        .attr("class", "row")
+
+    rows.exit().remove();
+
+    var cells = selection.selectAll(".row").selectAll(".cell")
+        .data(function(d) { return columns.map(function(col){return d[col];}) })
+
+    // cells
+    cells.enter().append("div")
+      .attr("class", function(d,i) { return "col-" + i; })
+      .classed("cell", true)
+
+    cells.exit().remove();
+
+    selection.selectAll(".cell")
+      .text(function(d) { return d; });
+
+    return dg;
+  };
+
+  dg.columns = function(_) {
+    if (!arguments.length) return columns;
+    columns = _;
+    return this;
+  };
+
+  return dg;
+};
diff --git a/dashed/assets/vendor/pygments.css b/dashed/assets/vendor/pygments.css
new file mode 100644
index 000000000..ef95359ff
--- /dev/null
+++ b/dashed/assets/vendor/pygments.css
@@ -0,0 +1,62 @@
+.codehilite .hll { background-color: #ffffcc }
+.codehilite  { background: #f8f8f8; }
+.codehilite .c { color: #408080; font-style: italic } /* Comment */
+.codehilite .err { border: 1px solid #FF0000 } /* Error */
+.codehilite .k { color: #008000; font-weight: bold } /* Keyword */
+.codehilite .o { color: #666666 } /* Operator */
+.codehilite .cm { color: #408080; font-style: italic } /* Comment.Multiline */
+.codehilite .cp { color: #BC7A00 } /* Comment.Preproc */
+.codehilite .c1 { color: #408080; font-style: italic } /* Comment.Single */
+.codehilite .cs { color: #408080; font-style: italic } /* Comment.Special */
+.codehilite .gd { color: #A00000 } /* Generic.Deleted */
+.codehilite .ge { font-style: italic } /* Generic.Emph */
+.codehilite .gr { color: #FF0000 } /* Generic.Error */
+.codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */
+.codehilite .gi { color: #00A000 } /* Generic.Inserted */
+.codehilite .go { color: #808080 } /* Generic.Output */
+.codehilite .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
+.codehilite .gs { font-weight: bold } /* Generic.Strong */
+.codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
+.codehilite .gt { color: #0040D0 } /* Generic.Traceback */
+.codehilite .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
+.codehilite .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
+.codehilite .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
+.codehilite .kp { color: #008000 } /* Keyword.Pseudo */
+.codehilite .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
+.codehilite .kt { color: #B00040 } /* Keyword.Type */
+.codehilite .m { color: #666666 } /* Literal.Number */
+.codehilite .s { color: #BA2121 } /* Literal.String */
+.codehilite .na { color: #7D9029 } /* Name.Attribute */
+.codehilite .nb { color: #008000 } /* Name.Builtin */
+.codehilite .nc { color: #0000FF; font-weight: bold } /* Name.Class */
+.codehilite .no { color: #880000 } /* Name.Constant */
+.codehilite .nd { color: #AA22FF } /* Name.Decorator */
+.codehilite .ni { color: #999999; font-weight: bold } /* Name.Entity */
+.codehilite .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
+.codehilite .nf { color: #0000FF } /* Name.Function */
+.codehilite .nl { color: #A0A000 } /* Name.Label */
+.codehilite .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
+.codehilite .nt { color: #008000; font-weight: bold } /* Name.Tag */
+.codehilite .nv { color: #19177C } /* Name.Variable */
+.codehilite .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
+.codehilite .w { color: #bbbbbb } /* Text.Whitespace */
+.codehilite .mf { color: #666666 } /* Literal.Number.Float */
+.codehilite .mh { color: #666666 } /* Literal.Number.Hex */
+.codehilite .mi { color: #666666 } /* Literal.Number.Integer */
+.codehilite .mo { color: #666666 } /* Literal.Number.Oct */
+.codehilite .sb { color: #BA2121 } /* Literal.String.Backtick */
+.codehilite .sc { color: #BA2121 } /* Literal.String.Char */
+.codehilite .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
+.codehilite .s2 { color: #BA2121 } /* Literal.String.Double */
+.codehilite .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
+.codehilite .sh { color: #BA2121 } /* Literal.String.Heredoc */
+.codehilite .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
+.codehilite .sx { color: #008000 } /* Literal.String.Other */
+.codehilite .sr { color: #BB6688 } /* Literal.String.Regex */
+.codehilite .s1 { color: #BA2121 } /* Literal.String.Single */
+.codehilite .ss { color: #19177C } /* Literal.String.Symbol */
+.codehilite .bp { color: #008000 } /* Name.Builtin.Pseudo */
+.codehilite .vc { color: #19177C } /* Name.Variable.Class */
+.codehilite .vg { color: #19177C } /* Name.Variable.Global */
+.codehilite .vi { color: #19177C } /* Name.Variable.Instance */
+.codehilite .il { color: #666666 } /* Literal.Number.Integer.Long */
diff --git a/dashed/assets/vendor/select2.sortable.js b/dashed/assets/vendor/select2.sortable.js
new file mode 100644
index 000000000..18fac5104
--- /dev/null
+++ b/dashed/assets/vendor/select2.sortable.js
@@ -0,0 +1,146 @@
+/**
+ * jQuery Select2 Sortable
+ * - enable select2 to be sortable via normal select element
+ *
+ * author      : Vafour
+ * modified    : Kevin Provance (kprovance)
+ * inspired by : jQuery Chosen Sortable (https://github.com/mrhenry/jquery-chosen-sortable)
+ * License     : GPL
+ */
+
+(function ($) {
+    $.fn.extend({
+        select2SortableOrder: function () {
+            var $this = this.filter('[multiple]');
+
+            $this.each(function () {
+                var $select = $(this);
+
+                // skip elements not select2-ed
+                if (typeof ($select.data('select2')) !== 'object') {
+                    return false;
+                }
+
+                var $select2 = $select.siblings('.select2-container');
+                var sorted;
+
+                // Opt group names
+                var optArr = [];
+
+                $select.find('optgroup').each(function(idx, val) {
+                    optArr.push (val);
+                });
+
+                $select.find('option').each(function(idx, val) {
+                    var groupName = $(this).parent('optgroup').prop('label');
+                    var optVal = this;
+
+                    if (groupName === undefined) {
+                        if (this.value !== '' && !this.selected) {
+                            optArr.push (optVal);
+                        }
+                    }
+                });
+
+                sorted = $($select2.find('.select2-choices li[class!="select2-search-field"]').map(function () {
+                    if (!this) {
+                        return undefined;
+                    }
+
+                    var id = $(this).data('select2Data').id;
+
+                    return $select.find('option[value="' + id + '"]')[0];
+                }));
+
+                 sorted.push.apply(sorted, optArr);
+
+                $select.children().remove();
+                $select.append(sorted);
+              });
+
+            return $this;
+        },
+
+        select2Sortable: function () {
+            var args = Array.prototype.slice.call(arguments, 0);
+            var $this = this.filter('[multiple]'),
+                    validMethods = ['destroy'];
+
+            if (args.length === 0 || typeof (args[0]) === 'object') {
+                var defaultOptions = {
+                    bindOrder: 'formSubmit', // or sortableStop
+                    sortableOptions: {
+                        placeholder: 'ui-state-highlight',
+                        items: 'li:not(.select2-search-field)',
+                        tolerance: 'pointer'
+                    }
+                };
+
+                var options = $.extend(defaultOptions, args[0]);
+
+                // Init select2 only if not already initialized to prevent select2 configuration loss
+                if (typeof ($this.data('select2')) !== 'object') {
+                    $this.select2();
+                }
+
+                $this.each(function () {
+                    var $select = $(this)
+                    var $select2choices = $select.siblings('.select2-container').find('.select2-choices');
+
+                    // Init jQuery UI Sortable
+                    $select2choices.sortable(options.sortableOptions);
+
+                    switch (options.bindOrder) {
+                        case 'sortableStop':
+                            // apply options ordering in sortstop event
+                            $select2choices.on("sortstop.select2sortable", function (event, ui) {
+                                $select.select2SortableOrder();
+                            });
+
+                            $select.on('change', function (e) {
+                                $(this).select2SortableOrder();
+                            });
+                        break;
+
+                        default:
+                            // apply options ordering in form submit
+                            $select.closest('form').unbind('submit.select2sortable').on('submit.select2sortable', function () {
+                                $select.select2SortableOrder();
+                            });
+                        break;
+                    }
+                });
+            }
+            else if (typeof (args[0] === 'string')) {
+                if ($.inArray(args[0], validMethods) == -1) {
+                    throw "Unknown method: " + args[0];
+                }
+
+                if (args[0] === 'destroy') {
+                    $this.select2SortableDestroy();
+                }
+            }
+
+            return $this;
+        },
+
+        select2SortableDestroy: function () {
+            var $this = this.filter('[multiple]');
+            $this.each(function () {
+                var $select = $(this)
+                var $select2choices = $select.parent().find('.select2-choices');
+
+                // unbind form submit event
+                $select.closest('form').unbind('submit.select2sortable');
+
+                // unbind sortstop event
+                $select2choices.unbind("sortstop.select2sortable");
+
+                // destroy select2Sortable
+                $select2choices.sortable('destroy');
+            });
+
+            return $this;
+        }
+    });
+}(jQuery));
diff --git a/dashed/assets/visualizations/big_number.css b/dashed/assets/visualizations/big_number.css
new file mode 100644
index 000000000..872a5715a
--- /dev/null
+++ b/dashed/assets/visualizations/big_number.css
@@ -0,0 +1,26 @@
+.big_number g.axis text {
+  font-size: 10px;
+  font-weight: normal;
+  color: gray;
+  fill: gray;
+  text-anchor: middle;
+  alignment-baseline: middle;
+  font-weight: none;
+}
+
+.big_number text.big {
+  stroke: black;
+  text-anchor: middle;
+  fill: black;
+}
+
+.big_number g.tick line {
+  stroke-width: 1px;
+  stroke: grey;
+}
+
+.big_number .domain {
+  fill: none;
+  stroke: black;
+  stroke-width: 1;
+}
diff --git a/dashed/assets/visualizations/big_number.js b/dashed/assets/visualizations/big_number.js
new file mode 100644
index 000000000..7f6bfcd4b
--- /dev/null
+++ b/dashed/assets/visualizations/big_number.js
@@ -0,0 +1,162 @@
+// JS
+var d3 = window.d3 || require('d3');
+
+// CSS
+require('./big_number.css');
+
+var px = require('../javascripts/modules/dashed.js');
+
+function bigNumberVis(slice) {
+  var div = d3.select(slice.selector);
+
+  function render() {
+    d3.json(slice.jsonEndpoint(), function (error, payload) {
+      //Define the percentage bounds that define color from red to green
+      if (error !== null) {
+        slice.error(error.responseText);
+        return '';
+      }
+      var fd = payload.form_data;
+      var json = payload.data;
+      var color_range = [-1, 1];
+
+      var f = d3.format(fd.y_axis_format);
+      var fp = d3.format('+.1%');
+      var width = slice.width();
+      var height = slice.height();
+      var svg = div.append('svg');
+      svg.attr("width", width);
+      svg.attr("height", height);
+      var data = json.data;
+      var compare_suffix = ' ' + json.compare_suffix;
+      var v_compare = null;
+      var v = data[data.length - 1][1];
+      if (json.compare_lag > 0) {
+        var pos = data.length - (json.compare_lag + 1);
+        if (pos >= 0) {
+          v_compare = (v / data[pos][1]) - 1;
+        }
+      }
+      var date_ext = d3.extent(data, function (d) {
+        return d[0];
+      });
+      var value_ext = d3.extent(data, function (d) {
+        return d[1];
+      });
+
+      var margin = 20;
+      var scale_x = d3.time.scale.utc().domain(date_ext).range([margin, width - margin]);
+      var scale_y = d3.scale.linear().domain(value_ext).range([height - (margin), margin]);
+      var colorRange = [d3.hsl(0, 1, 0.3), d3.hsl(120, 1, 0.3)];
+      var scale_color = d3.scale
+        .linear().domain(color_range)
+        .interpolate(d3.interpolateHsl)
+        .range(colorRange).clamp(true);
+      var line = d3.svg.line()
+        .x(function (d) {
+          return scale_x(d[0]);
+        })
+        .y(function (d) {
+          return scale_y(d[1]);
+        })
+        .interpolate("basis");
+
+      //Drawing trend line
+      var g = svg.append('g');
+
+      g.append('path')
+        .attr('d', function (d) {
+          return line(data);
+        })
+        .attr('stroke-width', 5)
+        .attr('opacity', 0.5)
+        .attr('fill', "none")
+        .attr('stroke-linecap', "round")
+        .attr('stroke', "grey");
+
+      g = svg.append('g')
+        .attr('class', 'digits')
+        .attr('opacity', 1);
+
+      var y = height / 2;
+      if (v_compare !== null) {
+        y = (height / 8) * 3;
+      }
+
+      //Printing big number
+      g.append('text')
+        .attr('x', width / 2)
+        .attr('y', y)
+        .attr('class', 'big')
+        .attr('alignment-baseline', 'middle')
+        .attr('id', 'bigNumber')
+        .style('font-weight', 'bold')
+        .style('cursor', 'pointer')
+        .text(f(v))
+        .style('font-size', d3.min([height, width]) / 3.5)
+        .attr('fill', 'white');
+
+      var c = scale_color(v_compare);
+
+      //Printing compare %
+      if (v_compare !== null) {
+        g.append('text')
+          .attr('x', width / 2)
+          .attr('y', (height / 16) * 12)
+          .text(fp(v_compare) + compare_suffix)
+          .style('font-size', d3.min([height, width]) / 8)
+          .style('text-anchor', 'middle')
+          .attr('fill', c)
+          .attr('stroke', c);
+      }
+
+      var g_axis = svg.append('g').attr('class', 'axis').attr('opacity', 0);
+      g = g_axis.append('g');
+      var x_axis = d3.svg.axis()
+        .scale(scale_x)
+        .orient('bottom')
+        .ticks(4)
+        .tickFormat(px.formatDate);
+      g.call(x_axis);
+      g.attr('transform', 'translate(0,' + (height - margin) + ')');
+
+      g = g_axis.append('g').attr('transform', 'translate(' + (width - margin) + ',0)');
+      var y_axis = d3.svg.axis()
+        .scale(scale_y)
+        .orient('left')
+        .tickFormat(d3.format(fd.y_axis_format))
+        .tickValues(value_ext);
+      g.call(y_axis);
+      g.selectAll('text')
+        .style('text-anchor', 'end')
+        .attr('y', '-7')
+        .attr('x', '-4');
+
+      g.selectAll("text")
+        .style('font-size', '10px');
+
+      div.on('mouseover', function (d) {
+          var div = d3.select(this);
+          div.select('path').transition().duration(500).attr('opacity', 1)
+            .style('stroke-width', '2px');
+          div.select('g.digits').transition().duration(500).attr('opacity', 0.1);
+          div.select('g.axis').transition().duration(500).attr('opacity', 1);
+        })
+        .on('mouseout', function (d) {
+          var div = d3.select(this);
+          div.select('path').transition().duration(500).attr('opacity', 0.5)
+            .style('stroke-width', '5px');
+          div.select('g.digits').transition().duration(500).attr('opacity', 1);
+          div.select('g.axis').transition().duration(500).attr('opacity', 0);
+        });
+      slice.done(payload);
+    });
+  }
+
+  return {
+    render: render,
+    resize: render
+  };
+}
+
+module.exports = bigNumberVis;
diff --git a/dashed/assets/visualizations/directed_force.css b/dashed/assets/visualizations/directed_force.css
new file mode 100644
index 000000000..170eccba1
--- /dev/null
+++ b/dashed/assets/visualizations/directed_force.css
@@ -0,0 +1,19 @@
+.directed_force path.link {
+  fill: none;
+  stroke: #000;
+  stroke-width: 1.5px;
+}
+
+.directed_force circle {
+  fill: #ccc;
+  stroke: #000;
+  stroke-width: 1.5px;
+  stroke-opacity: 1;
+  opacity: 0.75;
+}
+
+.directed_force text {
+  fill: #000;
+  font: 10px sans-serif;
+  pointer-events: none;
+}
diff --git a/dashed/assets/visualizations/directed_force.js b/dashed/assets/visualizations/directed_force.js
new file mode 100644
index 000000000..a0252067e
--- /dev/null
+++ b/dashed/assets/visualizations/directed_force.js
@@ -0,0 +1,175 @@
+// JS
+var d3 = window.d3 || require('d3');
+
+// CSS
+require('./directed_force.css');
+
+/* Modified from http://bl.ocks.org/d3noob/5141278 */
+function directedForceVis(slice) {
+  var div = d3.select(slice.selector);
+  var link_length = slice.data.form_data.link_length || 200;
+  var charge = slice.data.form_data.charge || -500;
+
+  var render = function () {
+    var width = slice.width();
+    var height = slice.height() - 25;
+    d3.json(slice.jsonEndpoint(), function (error, json) {
+
+      if (error !== null) {
+        slice.error(error.responseText);
+        return '';
+      }
+      var links = json.data;
+      var nodes = {};
+      // Compute the distinct nodes from the links.
+      links.forEach(function (link) {
+        link.source = nodes[link.source] || (nodes[link.source] = {
+          name: link.source
+        });
+        link.target = nodes[link.target] || (nodes[link.target] = {
+          name: link.target
+        });
+        link.value = Number(link.value);
+
+        var target_name = link.target.name;
+        var source_name = link.source.name;
+
+        if (nodes[target_name].total === undefined) {
+          nodes[target_name].total = link.value;
+        }
+        if (nodes[source_name].total === undefined) {
+          nodes[source_name].total = 0;
+        }
+        if (nodes[target_name].max === undefined) {
+          nodes[target_name].max = 0;
+        }
+        if (link.value > nodes[target_name].max) {
+          nodes[target_name].max = link.value;
+        }
+        if (nodes[target_name].min === undefined) {
+          nodes[target_name].min = 0;
+        }
+        if (link.value > nodes[target_name].min) {
+          nodes[target_name].min = link.value;
+        }
+
+        nodes[target_name].total += link.value;
+      });
+
+      var force = d3.layout.force()
+        .nodes(d3.values(nodes))
+        .links(links)
+        .size([width, height])
+        .linkDistance(link_length)
+        .charge(charge)
+        .on("tick", tick)
+        .start();
+
+      var svg = div.append("svg")
+        .attr("width", width)
+        .attr("height", height);
+
+      // build the arrow.
+      svg.append("svg:defs").selectAll("marker")
+        .data(["end"]) // Different link/path types can be defined here
+        .enter().append("svg:marker") // This section adds in the arrows
+        .attr("id", String)
+        .attr("viewBox", "0 -5 10 10")
+        .attr("refX", 15)
+        .attr("refY", -1.5)
+        .attr("markerWidth", 6)
+        .attr("markerHeight", 6)
+        .attr("orient", "auto")
+        .append("svg:path")
+        .attr("d", "M0,-5L10,0L0,5");
+
+      var edgeScale = d3.scale.linear()
+        .range([0.1, 0.5]);
+      // add the links and the arrows
+      var path = svg.append("svg:g").selectAll("path")
+        .data(force.links())
+        .enter().append("svg:path")
+        .attr("class", "link")
+        .style("opacity", function (d) {
+          return edgeScale(d.value / d.target.max);
+        })
+        .attr("marker-end", "url(#end)");
+
+      // define the nodes
+      var node = svg.selectAll(".node")
+        .data(force.nodes())
+        .enter().append("g")
+        .attr("class", "node")
+        .on("mouseenter", function (d) {
+          d3.select(this)
+            .select("circle")
+            .transition()
+            .style('stroke-width', 5);
+
+          d3.select(this)
+            .select("text")
+            .transition()
+            .style('font-size', 25);
+        })
+        .on("mouseleave", function (d) {
+          d3.select(this)
+            .select("circle")
+            .transition()
+            .style('stroke-width', 1.5);
+          d3.select(this)
+            .select("text")
+            .transition()
+            .style('font-size', 12);
+        })
+        .call(force.drag);
+
+      // add the nodes
+      var ext = d3.extent(d3.values(nodes), function (d) {
+        return Math.sqrt(d.total);
+      });
+      var circleScale = d3.scale.linear()
+        .domain(ext)
+        .range([3, 30]);
+
+      node.append("circle")
+        .attr("r", function (d) {
+          return circleScale(Math.sqrt(d.total));
+        });
+
+      // add the text
+      node.append("text")
+        .attr("x", 6)
+        .attr("dy", ".35em")
+        .text(function (d) {
+          return d.name;
+        });
+
+      // add the curvy lines
+      function tick() {
+        path.attr("d", function (d) {
+          var dx = d.target.x - d.source.x,
+            dy = d.target.y - d.source.y,
+            dr = Math.sqrt(dx * dx + dy * dy);
+          return "M" +
+            d.source.x + "," +
+            d.source.y + "A" +
+            dr + "," + dr + " 0 0,1 " +
+            d.target.x + "," +
+            d.target.y;
+        });
+
+        node.attr("transform", function (d) {
+          return "translate(" + d.x + "," + d.y + ")";
+        });
+      }
+
+      slice.done(json);
+    });
+  };
+  return {
+    render: render,
+    resize: render
+  };
+}
+
+module.exports = directedForceVis;
diff --git a/dashed/assets/visualizations/filter_box.css b/dashed/assets/visualizations/filter_box.css
new file mode 100644
index 000000000..156978361
--- /dev/null
+++ b/dashed/assets/visualizations/filter_box.css
@@ -0,0 +1,8 @@
+.select2-highlighted > .filter_box {
+    background-color: transparent;
+    border: 1px dashed black;
+}
+
+.dashboard .filter_box .slice_container > div {
+  padding-top: 0;
+}
diff --git a/dashed/assets/visualizations/filter_box.js b/dashed/assets/visualizations/filter_box.js
new file mode 100644
index 000000000..64959a4b2
--- /dev/null
+++ b/dashed/assets/visualizations/filter_box.js
@@ -0,0 +1,82 @@
+// JS
+var $ = window.$ = require('jquery');
+var jQuery = window.jQuery = $;
+var d3 = window.d3 || require('d3');
+
+// CSS
+require('./filter_box.css');
+require('../javascripts/dashed-select2.js');
+
+function filterBox(slice) {
+  var filtersObj = {};
+  var d3token = d3.select(slice.selector);
+
+  var fltChanged = function () {
+    var val = $(this).val();
+    var vals = [];
+    if (val !== '') {
+      vals = val.split(',');
+    }
+    slice.setFilter($(this).attr('name'), vals);
+  };
+
+  var refresh = function () {
+    d3token.selectAll("*").remove();
+    var container = d3token
+      .append('div')
+      .classed('padded', true);
+
+    $.getJSON(slice.jsonEndpoint(), function (payload) {
+        var maxes = {};
+
+        for (var filter in payload.data) {
+          var data = payload.data[filter];
+          maxes[filter] = d3.max(data, function (d) {
+            return d.metric;
+          });
+          var id = 'fltbox__' + filter;
+
+          var div = container.append('div');
+
+          div.append("label").text(filter);
+
+          div.append('div')
+            .attr('name', filter)
+            .classed('form-control', true)
+            .attr('multiple', '')
+            .attr('id', id);
+
+          filtersObj[filter] = $('#' + id).select2({
+              placeholder: "Select [" + filter + ']',
+              containment: 'parent',
+              dropdownAutoWidth: true,
+              data: data,
+              multiple: true,
+              formatResult: select2Formatter
+            })
+            .on('change', fltChanged);
+        }
+        slice.done();
+
+        function select2Formatter(result, container /*, query, escapeMarkup*/) {
+          var perc = Math.round((result.metric / maxes[result.filter]) * 100);
+          var style = 'padding: 2px 5px;';
+          style += "background-image: ";
+          style += "linear-gradient(to right, lightgrey, lightgrey " + perc + "%, rgba(0,0,0,0) " + perc + "%";
+
+          $(container).attr('style', 'padding: 0px; background: white;');
+          $(container).addClass('filter_box');
+          return '
' + result.text + '
'; + } + }) + .fail(function (xhr) { + slice.error(xhr.responseText); + }); + }; + return { + render: refresh, + resize: refresh + }; +} + +module.exports = filterBox; diff --git a/dashed/assets/visualizations/heatmap.css b/dashed/assets/visualizations/heatmap.css new file mode 100644 index 000000000..bce124821 --- /dev/null +++ b/dashed/assets/visualizations/heatmap.css @@ -0,0 +1,79 @@ +.heatmap .axis text { + font: 10px sans-serif; +} + +.heatmap .axis path, +.heatmap .axis line { + fill: none; + stroke: #000; + shape-rendering: crispEdges; +} + +.heatmap svg { +} + +.heatmap canvas, .heatmap img { + image-rendering: optimizeSpeed; /* Older versions of FF */ + image-rendering: -moz-crisp-edges; /* FF 6.0+ */ + image-rendering: -webkit-optimize-contrast; /* Safari */ + image-rendering: -o-crisp-edges; /* OS X & Windows Opera (12.02+) */ + image-rendering: pixelated; /* Awesome future-browsers */ + -ms-interpolation-mode: nearest-neighbor; /* IE */ +} + +/* from d3-tip */ +.d3-tip { + line-height: 1; + font-weight: bold; + padding: 12px; + background: rgba(0, 0, 0, 0.8); + color: #fff; + border-radius: 2px; + pointer-events: none; +} + +/* Creates a small triangle extender for the tooltip */ +.d3-tip:after { + box-sizing: border-box; + display: inline; + font-size: 10px; + width: 100%; + line-height: 1; + color: rgba(0, 0, 0, 0.8); + position: absolute; + pointer-events: none; +} + +/* Northward tooltips */ +.d3-tip.n:after { + content: "\25BC"; + margin: -1px 0 0 0; + top: 100%; + left: 0; + text-align: center; +} + +/* Eastward tooltips */ +.d3-tip.e:after { + content: "\25C0"; + margin: -4px 0 0 0; + top: 50%; + left: -8px; +} + +/* Southward tooltips */ +.d3-tip.s:after { + content: "\25B2"; + margin: 0 0 1px 0; + top: -8px; + left: 0; + text-align: center; +} + +/* Westward tooltips */ +.d3-tip.w:after { + content: "\25B6"; + margin: -4px 0 0 -1px; + top: 50%; + left: 100%; +} diff --git a/dashed/assets/visualizations/heatmap.js b/dashed/assets/visualizations/heatmap.js new file mode 100644 index 000000000..6bc688fc8 --- /dev/null +++ b/dashed/assets/visualizations/heatmap.js @@ -0,0 +1,209 @@ +// JS +var $ = window.$ || require('jquery'); +var px = window.px || require('../javascripts/modules/dashed.js'); +var d3 = require('d3'); + +d3.tip = require('d3-tip'); //using window.d3 doesn't capture events properly bc of multiple instances + +// CSS +require('./heatmap.css'); + +// Inspired from http://bl.ocks.org/mbostock/3074470 +// https://jsfiddle.net/cyril123/h0reyumq/ +function heatmapVis(slice) { + var margins = { + t: 10, + r: 10, + b: 50, + l: 60 + }; + + function refresh() { + var width = slice.width(); + var height = slice.height(); + var hmWidth = width - (margins.l + margins.r); + var hmHeight = height - (margins.b + margins.t); + var fp = d3.format('.3p'); + d3.json(slice.jsonEndpoint(), function (error, payload) { + var matrix = {}; + if (error) { + slice.error(error.responseText); + return ''; + } + var fd = payload.form_data; + var data = payload.data; + + function ordScale(k, rangeBands, reverse) { + if (reverse === undefined) { + reverse = false; + } + var domain = {}; + $.each(data, function (i, d) { + domain[d[k]] = true; + }); + domain = Object.keys(domain).sort(function (a, b) { + return b - a; + }); + if (reverse) { + domain.reverse(); + } + if (rangeBands === undefined) { + return d3.scale.ordinal().domain(domain).range(d3.range(domain.length)); + } else { + return d3.scale.ordinal().domain(domain).rangeBands(rangeBands); + } + } + var xScale = ordScale('x'); + var yScale = ordScale('y', undefined, true); + var xRbScale = ordScale('x', [0, hmWidth]); + var yRbScale = ordScale('y', [hmHeight, 0]); + var X = 0, + Y = 1; + var heatmapDim = [xRbScale.domain().length, yRbScale.domain().length]; + + var color = px.color.colorScalerFactory(fd.linear_color_scheme); + + var scale = [ + d3.scale.linear() + .domain([0, heatmapDim[X]]) + .range([0, hmWidth]), + d3.scale.linear() + .domain([0, heatmapDim[Y]]) + .range([0, hmHeight]) + ]; + + var container = d3.select(slice.selector) + .style("left", "0px") + .style("position", "relative") + .style("top", "0px"); + + var canvas = container.append("canvas") + .attr("width", heatmapDim[X]) + .attr("height", heatmapDim[Y]) + .style("width", hmWidth + "px") + .style("height", hmHeight + "px") + .style("image-rendering", fd.canvas_image_rendering) + .style("left", margins.l + "px") + .style("top", margins.t + "px") + .style("position", "absolute"); + + var svg = container.append("svg") + .attr("width", width) + .attr("height", height) + .style("left", "0px") + .style("top", "0px") + .style("position", "absolute"); + + var rect = svg.append('g') + .attr("transform", "translate(" + margins.l + "," + margins.t + ")") + .append('rect') + .style('fill-opacity', 0) + .attr('stroke', 'black') + .attr("width", hmWidth) + .attr("height", hmHeight); + + var tip = d3.tip() + .attr('class', 'd3-tip') + .offset(function () { + var k = d3.mouse(this); + var x = k[0] - (hmWidth / 2); + return [k[1] - 20, x]; + }) + .html(function (d) { + var k = d3.mouse(this); + var m = Math.floor(scale[0].invert(k[0])); + var n = Math.floor(scale[1].invert(k[1])); + if (m in matrix && n in matrix[m]) { + var obj = matrix[m][n]; + var s = ""; + s += "
" + fd.all_columns_x + ": " + obj.x + "
"; + s += "
" + fd.all_columns_y + ": " + obj.y + "
"; + s += "
" + fd.metric + ": " + obj.v + "
"; + s += "
%: " + fp(obj.perc) + "
"; + return s; + } + }); + + rect.call(tip); + + var xAxis = d3.svg.axis() + .scale(xRbScale) + .tickValues(xRbScale.domain().filter( + function (d, i) { + return !(i % (parseInt(fd.xscale_interval, 10))); + })) + .orient("bottom"); + var yAxis = d3.svg.axis() + .scale(yRbScale) + .tickValues(yRbScale.domain().filter( + function (d, i) { + return !(i % (parseInt(fd.yscale_interval, 10))); + })) + .orient("left"); + + svg.append("g") + .attr("class", "x axis") + .attr("transform", "translate(" + margins.l + "," + (margins.t + hmHeight) + ")") + .call(xAxis) + .selectAll("text") + .style("text-anchor", "end") + .attr("transform", "rotate(-45)") + .style("font-weight", "bold"); + + svg.append("g") + .attr("class", "y axis") + .attr("transform", "translate(" + margins.l + ", 0)") + .call(yAxis); + + rect.on('mousemove', tip.show); + rect.on('mouseout', tip.hide); + + var context = canvas.node().getContext("2d"); + context.imageSmoothingEnabled = false; + createImageObj(); + + // Compute the pixel colors; scaled by CSS. + function createImageObj() { + var imageObj = new Image(); + var image = context.createImageData(heatmapDim[0], heatmapDim[1]); + var pixs = {}; + $.each(data, function (i, d) { + var c = d3.rgb(color(d.perc)); + var x = xScale(d.x); + var y = yScale(d.y); + pixs[x + (y * xScale.domain().length)] = c; + if (matrix[x] === undefined) { + matrix[x] = {}; + } + if (matrix[x][y] === undefined) { + matrix[x][y] = d; + } + }); + + var p = -1; + for (var i = 0; i < heatmapDim[0] * heatmapDim[1]; i++) { + var c = pixs[i]; + var alpha = 255; + if (c === undefined) { + c = d3.rgb('#F00'); + alpha = 0; + } + image.data[++p] = c.r; + image.data[++p] = c.g; + image.data[++p] = c.b; + image.data[++p] = alpha; + } + context.putImageData(image, 0, 0); + imageObj.src = canvas.node().toDataURL(); + } + slice.done(); + + }); + } + return { + render: refresh, + resize: refresh + }; +} + +module.exports = heatmapVis; diff --git a/dashed/assets/visualizations/iframe.js b/dashed/assets/visualizations/iframe.js new file mode 100644 index 000000000..0f9fddc8b --- /dev/null +++ b/dashed/assets/visualizations/iframe.js @@ -0,0 +1,25 @@ +var $ = window.$ || require('jquery'); + +function iframeWidget(slice) { + + function refresh() { + $('#code').attr('rows', '15'); + $.getJSON(slice.jsonEndpoint(), function (payload) { + slice.container.html(''); + var iframe = slice.container.find('iframe'); + iframe.css('height', slice.height()); + iframe.attr('src', payload.form_data.url); + slice.done(); + }) + .fail(function (xhr) { + slice.error(xhr.responseText); + }); + } + + return { + render: refresh, + resize: refresh + }; +} + +module.exports = iframeWidget; diff --git a/dashed/assets/visualizations/markup.js b/dashed/assets/visualizations/markup.js new file mode 100644 index 000000000..f202c92dd --- /dev/null +++ b/dashed/assets/visualizations/markup.js @@ -0,0 +1,23 @@ +var $ = window.$ || require('jquery'); + +function markupWidget(slice) { + + function refresh() { + $('#code').attr('rows', '15'); + + $.getJSON(slice.jsonEndpoint(), function (payload) { + slice.container.html(payload.data.html); + slice.done(); + }) + .fail(function (xhr) { + slice.error(xhr.responseText); + }); + } + + return { + render: refresh, + resize: refresh + }; +} + +module.exports = markupWidget; diff --git a/dashed/assets/visualizations/nvd3_vis.css b/dashed/assets/visualizations/nvd3_vis.css new file mode 100644 index 000000000..80c452b4d --- /dev/null +++ b/dashed/assets/visualizations/nvd3_vis.css @@ -0,0 +1,8 @@ +g.dashed path { + stroke-dasharray: 5, 5; +} + +.nvtooltip tr.highlight td { + font-weight: bold; + font-size: 15px !important; +} diff --git a/dashed/assets/visualizations/nvd3_vis.js b/dashed/assets/visualizations/nvd3_vis.js new file mode 100644 index 000000000..04a0852ab --- /dev/null +++ b/dashed/assets/visualizations/nvd3_vis.js @@ -0,0 +1,208 @@ +// JS +var $ = window.$ || require('jquery'); +var d3 = window.d3 || require('d3'); +var px = window.px || require('../javascripts/modules/dashed.js'); +var nv = require('nvd3'); + +// CSS +require('../node_modules/nvd3/build/nv.d3.min.css'); +require('./nvd3_vis.css'); + +function nvd3Vis(slice) { + var chart; + + var render = function () { + $.getJSON(slice.jsonEndpoint(), function (payload) { + var fd = payload.form_data; + var viz_type = fd.viz_type; + + var f = d3.format('.3s'); + var colorKey = 'key'; + + nv.addGraph(function () { + switch (viz_type) { + case 'line': + if (fd.show_brush) { + chart = nv.models.lineWithFocusChart(); + chart.lines2.xScale(d3.time.scale.utc()); + chart.x2Axis + .showMaxMin(fd.x_axis_showminmax) + .staggerLabels(true); + } else { + chart = nv.models.lineChart(); + } + // To alter the tooltip header + // chart.interactiveLayer.tooltip.headerFormatter(function(){return '';}); + chart.xScale(d3.time.scale.utc()); + chart.interpolate(fd.line_interpolation); + chart.xAxis + .showMaxMin(fd.x_axis_showminmax) + .staggerLabels(true); + break; + + case 'bar': + chart = nv.models.multiBarChart() + .showControls(true) + .groupSpacing(0.1); + + chart.xAxis + .showMaxMin(false) + .staggerLabels(true); + + chart.stacked(fd.bar_stacked); + break; + + case 'dist_bar': + chart = nv.models.multiBarChart() + .showControls(true) //Allow user to switch between 'Grouped' and 'Stacked' mode. + .reduceXTicks(false) + .rotateLabels(45) + .groupSpacing(0.1); //Distance between each group of bars. + + chart.xAxis + .showMaxMin(false); + + chart.stacked(fd.bar_stacked); + break; + + case 'pie': + chart = nv.models.pieChart(); + colorKey = 'x'; + chart.valueFormat(f); + if (fd.donut) { + chart.donut(true); + chart.labelsOutside(true); + } + chart.labelsOutside(true); + chart.cornerRadius(true); + break; + + case 'column': + chart = nv.models.multiBarChart() + .reduceXTicks(false) + .rotateLabels(45); + break; + + case 'compare': + chart = nv.models.cumulativeLineChart(); + chart.xScale(d3.time.scale.utc()); + chart.xAxis + .showMaxMin(false) + .staggerLabels(true); + break; + + case 'bubble': + var row = function (col1, col2) { + return "" + col1 + "" + col2 + ""; + }; + chart = nv.models.scatterChart(); + chart.showDistX(true); + chart.showDistY(true); + chart.tooltip.contentGenerator(function (obj) { + var p = obj.point; + var s = ""; + s += ''; + s += row(fd.x, f(p.x)); + s += row(fd.y, f(p.y)); + s += row(fd.size, f(p.size)); + s += "
' + p[fd.entity] + ' (' + p.group + ')
"; + return s; + }); + chart.pointRange([5, fd.max_bubble_size * fd.max_bubble_size]); + break; + + case 'area': + chart = nv.models.stackedAreaChart(); + chart.style(fd.stacked_style); + chart.xScale(d3.time.scale.utc()); + chart.xAxis + .showMaxMin(false) + .staggerLabels(true); + break; + + default: + throw new Error("Unrecognized visualization for nvd3" + viz_type); + } + + if ("showLegend" in chart && typeof fd.show_legend !== 'undefined') { + chart.showLegend(fd.show_legend); + } + + var height = slice.height(); + height -= 15; // accounting for the staggered xAxis + + if (chart.hasOwnProperty("x2Axis")) { + height += 30; + } + chart.height(height); + slice.container.css('height', height + 'px'); + + if ((viz_type === "line" || viz_type === "area") && fd.rich_tooltip) { + chart.useInteractiveGuideline(true); + } + if (fd.y_axis_zero) { + chart.forceY([0, 1]); + } else if (fd.y_log_scale) { + chart.yScale(d3.scale.log()); + } + if (fd.x_log_scale) { + chart.xScale(d3.scale.log()); + } + if (viz_type === 'bubble') { + chart.xAxis.tickFormat(d3.format('.3s')); + } else if (fd.x_axis_format === 'smart_date') { + chart.xAxis.tickFormat(px.formatDate); + } else if (fd.x_axis_format !== undefined) { + chart.xAxis.tickFormat(px.timeFormatFactory(fd.x_axis_format)); + } + if (chart.yAxis !== undefined) { + chart.yAxis.tickFormat(d3.format('.3s')); + } + + if (fd.contribution || fd.num_period_compare || viz_type === 'compare') { + chart.yAxis.tickFormat(d3.format('.3p')); + if (chart.y2Axis !== undefined) { + chart.y2Axis.tickFormat(d3.format('.3p')); + } + } else if (fd.y_axis_format) { + chart.yAxis.tickFormat(d3.format(fd.y_axis_format)); + + if (chart.y2Axis !== undefined) { + chart.y2Axis.tickFormat(d3.format(fd.y_axis_format)); + } + } + + chart.color(function (d, i) { + return px.color.category21(d[colorKey]); + }); + + d3.select(slice.selector).html(''); + d3.select(slice.selector).append("svg") + .datum(payload.data) + .transition().duration(500) + .attr('height', height) + .call(chart); + + return chart; + }); + + slice.done(payload); + }) + .fail(function (xhr) { + slice.error(xhr.responseText); + }); + }; + + var update = function () { + if (chart && chart.update) { + chart.update(); + } + }; + + return { + render: render, + resize: update + }; +} + +module.exports = nvd3Vis; diff --git a/dashed/assets/visualizations/parallel_coordinates.js b/dashed/assets/visualizations/parallel_coordinates.js new file mode 100644 index 000000000..271989b9f --- /dev/null +++ b/dashed/assets/visualizations/parallel_coordinates.js @@ -0,0 +1,92 @@ +// JS +var $ = window.$ || require('jquery'); +var d3 = window.d3 || require('d3'); +d3.parcoords = require('../vendor/parallel_coordinates/d3.parcoords.js'); +d3.divgrid = require('../vendor/parallel_coordinates/divgrid.js'); + +// CSS +require('../vendor/parallel_coordinates/d3.parcoords.css'); + +function parallelCoordVis(slice) { + + function refresh() { + $('#code').attr('rows', '15'); + $.getJSON(slice.jsonEndpoint(), function (payload) { + var data = payload.data; + var fd = payload.form_data; + var ext = d3.extent(data, function (d) { + return d[fd.secondary_metric]; + }); + ext = [ext[0], (ext[1] - ext[0]) / 2, ext[1]]; + var cScale = d3.scale.linear() + .domain(ext) + .range(['red', 'grey', 'blue']) + .interpolate(d3.interpolateLab); + + var color = function (d) { + return cScale(d[fd.secondary_metric]); + }; + var container = d3.select(slice.selector); + var eff_height = fd.show_datatable ? (slice.height() / 2) : slice.height(); + + container.append('div') + .attr('id', 'parcoords_' + slice.container_id) + .style('height', eff_height + 'px') + .classed("parcoords", true); + + var parcoords = d3.parcoords()('#parcoords_' + slice.container_id) + .width(slice.width()) + .color(color) + .alpha(0.5) + .composite("darken") + .height(eff_height) + .data(payload.data) + .render() + .createAxes() + .shadows() + .reorderable() + .brushMode("1D-axes"); + + if (fd.show_datatable) { + // create data table, row hover highlighting + var grid = d3.divgrid(); + container.append("div") + .datum(data.slice(0, 10)) + .attr('id', "grid") + .call(grid) + .classed("parcoords", true) + .selectAll(".row") + .on({ + mouseover: function (d) { + parcoords.highlight([d]); + }, + mouseout: parcoords.unhighlight + }); + // update data table on brush event + parcoords.on("brush", function (d) { + d3.select("#grid") + .datum(d.slice(0, 10)) + .call(grid) + .selectAll(".row") + .on({ + mouseover: function (d) { + parcoords.highlight([d]); + }, + mouseout: parcoords.unhighlight + }); + }); + } + slice.done(); + }) + .fail(function (xhr) { + slice.error(xhr.responseText); + }); + } + + return { + render: refresh, + resize: refresh + }; +} + +module.exports = parallelCoordVis; diff --git a/dashed/assets/visualizations/pivot_table.css b/dashed/assets/visualizations/pivot_table.css new file mode 100644 index 000000000..8aef19a85 --- /dev/null +++ b/dashed/assets/visualizations/pivot_table.css @@ -0,0 +1,13 @@ +.gridster .widget.pivot_table { + overflow: auto !important; +} + +.table tr>th { + padding: 1px 5px !important; + font-size: small !important; +} + +.table tr>td { + padding: 1px 5px !important; + font-size: small !important; +} diff --git a/dashed/assets/visualizations/pivot_table.js b/dashed/assets/visualizations/pivot_table.js new file mode 100644 index 000000000..795adbb29 --- /dev/null +++ b/dashed/assets/visualizations/pivot_table.js @@ -0,0 +1,31 @@ +var $ = window.$ = require('jquery'); +var jQuery = window.jQuery = $; + +require('datatables'); +require('./pivot_table.css'); +require('../node_modules/datatables-bootstrap3-plugin/media/css/datatables-bootstrap3.css'); + +module.exports = function (slice) { + var container = slice.container; + var form_data = slice.data.form_data; + + function refresh() { + $.getJSON(slice.jsonEndpoint(), function (json) { + container.html(json.data); + if (form_data.groupby.length === 1) { + var table = container.find('table').DataTable({ + paging: false, + searching: false + }); + table.column('-1').order('desc').draw(); + } + slice.done(json); + }).fail(function (xhr) { + slice.error(xhr.responseText); + }); + } + return { + render: refresh, + resize: refresh + }; +}; diff --git a/dashed/assets/visualizations/sankey.css b/dashed/assets/visualizations/sankey.css new file mode 100644 index 000000000..9a2a0c88a --- /dev/null +++ b/dashed/assets/visualizations/sankey.css @@ -0,0 +1,20 @@ +.sankey .node rect { + cursor: move; + fill-opacity: .9; + shape-rendering: crispEdges; +} + +.sankey .node text { + pointer-events: none; + text-shadow: 0 1px 0 #fff; +} + +.sankey .link { + fill: none; + stroke: #000; + stroke-opacity: .2; +} + +.sankey .link:hover { + stroke-opacity: .5; +} diff --git a/dashed/assets/visualizations/sankey.js b/dashed/assets/visualizations/sankey.js new file mode 100644 index 000000000..018ffe57d --- /dev/null +++ b/dashed/assets/visualizations/sankey.js @@ -0,0 +1,140 @@ +// CSS +require('./sankey.css'); +// JS +var px = window.px || require('../javascripts/modules/dashed.js'); +var d3 = window.d3 || require('d3'); +d3.sankey = require('d3-sankey').sankey; + +function sankeyVis(slice) { + var div = d3.select(slice.selector); + + var render = function () { + var margin = { + top: 5, + right: 5, + bottom: 5, + left: 5 + }; + var width = slice.width() - margin.left - margin.right; + var height = slice.height() - margin.top - margin.bottom; + + var formatNumber = d3.format(",.0f"), + format = function (d) { + return formatNumber(d) + " TWh"; + }; + + var svg = div.append("svg") + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + var sankey = d3.sankey() + .nodeWidth(15) + .nodePadding(10) + .size([width, height]); + + var path = sankey.link(); + + d3.json(slice.jsonEndpoint(), function (error, json) { + if (error !== null) { + slice.error(error.responseText); + return ''; + } + var links = json.data; + var nodes = {}; + // Compute the distinct nodes from the links. + links.forEach(function (link) { + link.source = nodes[link.source] || (nodes[link.source] = { name: link.source }); + link.target = nodes[link.target] || (nodes[link.target] = { name: link.target }); + link.value = Number(link.value); + }); + nodes = d3.values(nodes); + + sankey + .nodes(nodes) + .links(links) + .layout(32); + + var link = svg.append("g").selectAll(".link") + .data(links) + .enter().append("path") + .attr("class", "link") + .attr("d", path) + .style("stroke-width", function (d) { + return Math.max(1, d.dy); + }) + .sort(function (a, b) { + return b.dy - a.dy; + }); + + link.append("title") + .text(function (d) { + return d.source.name + " → " + d.target.name + "\n" + format(d.value); + }); + + var node = svg.append("g").selectAll(".node") + .data(nodes) + .enter().append("g") + .attr("class", "node") + .attr("transform", function (d) { + return "translate(" + d.x + "," + d.y + ")"; + }) + .call(d3.behavior.drag() + .origin(function (d) { + return d; + }) + .on("dragstart", function () { + this.parentNode.appendChild(this); + }) + .on("drag", dragmove)); + + node.append("rect") + .attr("height", function (d) { + return d.dy; + }) + .attr("width", sankey.nodeWidth()) + .style("fill", function (d) { + d.color = px.color.category21(d.name.replace(/ .*/, "")); + return d.color; + }) + .style("stroke", function (d) { + return d3.rgb(d.color).darker(2); + }) + .append("title") + .text(function (d) { + return d.name + "\n" + format(d.value); + }); + + node.append("text") + .attr("x", -6) + .attr("y", function (d) { + return d.dy / 2; + }) + .attr("dy", ".35em") + .attr("text-anchor", "end") + .attr("transform", null) + .text(function (d) { + return d.name; + }) + .filter(function (d) { + return d.x < width / 2; + }) + .attr("x", 6 + sankey.nodeWidth()) + .attr("text-anchor", "start"); + + function dragmove(d) { + d3.select(this).attr("transform", "translate(" + d.x + "," + (d.y = Math.max(0, Math.min(height - d.dy, d3.event.y))) + ")"); + sankey.relayout(); + link.attr("d", path); + } + slice.done(json); + }); + }; + return { + render: render, + resize: render + }; +} + +module.exports = sankeyVis; diff --git a/dashed/assets/visualizations/sunburst.css b/dashed/assets/visualizations/sunburst.css new file mode 100644 index 000000000..d2636faaa --- /dev/null +++ b/dashed/assets/visualizations/sunburst.css @@ -0,0 +1,39 @@ +.sunburst text { + shape-rendering: crispEdges; +} +.sunburst path { + stroke: #333; + stroke-width: 0.5px; +} +.sunburst .center-label { + text-anchor: middle; + fill: #000; + pointer-events: none; +} +.sunburst .path-percent { + font-size: 4em; +} +.sunburst .path-metrics { + font-size: 1.75em; +} +.sunburst .path-ratio { + font-size: 1.2em; +} + +.sunburst .breadcrumbs text { + font-weight: 600; + font-size: 1.2em; + text-anchor: middle; + fill: #000; +} + +/* dashboard specific */ +.dashboard .sunburst text { + font-size: 1em; +} +.dashboard .sunburst .path-percent { + font-size: 2.5em; +} +.dashboard .sunburst .path-metrics { + font-size: 1em; +} diff --git a/dashed/assets/visualizations/sunburst.js b/dashed/assets/visualizations/sunburst.js new file mode 100644 index 000000000..e195e71af --- /dev/null +++ b/dashed/assets/visualizations/sunburst.js @@ -0,0 +1,359 @@ +var d3 = window.d3 || require('d3'); +var px = require('../javascripts/modules/dashed.js'); +var wrapSvgText = require('../javascripts/modules/utils.js').wrapSvgText; + +require('./sunburst.css'); + +// Modified from http://bl.ocks.org/kerryrodden/7090426 +function sunburstVis(slice) { + var container = d3.select(slice.selector); + + var render = function () { + // vars with shared scope within this function + var margin = { top: 10, right: 5, bottom: 10, left: 5 }; + var containerWidth = slice.width(); + var containerHeight = slice.height(); + var breadcrumbHeight = containerHeight * 0.085; + var visWidth = containerWidth - margin.left - margin.right; + var visHeight = containerHeight - margin.top - margin.bottom - breadcrumbHeight; + var radius = Math.min(visWidth, visHeight) / 2; + var colorByCategory = true; // color by category if primary/secondary metrics match + + var maxBreadcrumbs, breadcrumbDims, // set based on data + totalSize, // total size of all segments; set after loading the data. + colorScale, + breadcrumbs, vis, arcs, gMiddleText; // dom handles + + // Helper + path gen functions + var partition = d3.layout.partition() + .size([2 * Math.PI, radius * radius]) + .value(function (d) { return d.m1; }); + + var arc = d3.svg.arc() + .startAngle(function (d) { + return d.x; + }) + .endAngle(function (d) { + return d.x + d.dx; + }) + .innerRadius(function (d) { + return Math.sqrt(d.y); + }) + .outerRadius(function (d) { + return Math.sqrt(d.y + d.dy); + }); + + var f = d3.format(".3s"); + var fp = d3.format(".3p"); + + container.select("svg").remove(); + + var svg = container.append("svg:svg") + .attr("width", containerWidth) + .attr("height", containerHeight); + + d3.json(slice.jsonEndpoint(), function (error, rawData) { + if (error !== null) { + slice.error(error.responseText); + return ''; + } + + createBreadcrumbs(rawData); + createVisualization(rawData); + + slice.done(rawData); + }); + + function createBreadcrumbs(rawData) { + var firstRowData = rawData.data[0]; + maxBreadcrumbs = (firstRowData.length - 2) + 1; // -2 bc row contains 2x metrics, +extra for %label and buffer + + breadcrumbDims = { + width: visWidth / maxBreadcrumbs, + height: breadcrumbHeight *0.8, // more margin + spacing: 3, + tipTailWidth: 10 + }; + + breadcrumbs = svg.append("svg:g") + .attr("class", "breadcrumbs") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + breadcrumbs.append("svg:text") + .attr("class", "end-label"); + } + + // Main function to draw and set up the visualization, once we have the data. + function createVisualization(rawData) { + var tree = buildHierarchy(rawData.data); + + vis = svg.append("svg:g") + .attr("class", "sunburst-vis") + .attr("transform", "translate(" + (margin.left + (visWidth / 2)) + "," + (margin.top + breadcrumbHeight + (visHeight / 2)) + ")") + .on("mouseleave", mouseleave); + + arcs = vis.append("svg:g") + .attr("id", "arcs"); + + gMiddleText = vis.append("svg:g") + .attr("class", "center-label"); + + // Bounding circle underneath the sunburst, to make it easier to detect + // when the mouse leaves the parent g. + arcs.append("svg:circle") + .attr("r", radius) + .style("opacity", 0); + + // For efficiency, filter nodes to keep only those large enough to see. + var nodes = partition.nodes(tree) + .filter(function (d) { + return (d.dx > 0.005); // 0.005 radians = 0.29 degrees + }); + + var ext; + + if (rawData.form_data.metric !== rawData.form_data.secondary_metric) { + colorByCategory = false; + + ext = d3.extent(nodes, function (d) { + return d.m2 / d.m1; + }); + + colorScale = d3.scale.linear() + .domain([ext[0], ext[0] + ((ext[1] - ext[0]) / 2), ext[1]]) + .range(["#00D1C1", "white", "#FFB400"]); + } + + var path = arcs.data([tree]).selectAll("path") + .data(nodes) + .enter().append("svg:path") + .attr("display", function (d) { + return d.depth ? null : "none"; + }) + .attr("d", arc) + .attr("fill-rule", "evenodd") + .style("fill", function (d) { + return colorByCategory ? px.color.category21(d.name) : colorScale(d.m2 / d.m1); + }) + .style("opacity", 1) + .on("mouseenter", mouseenter); + + // Get total size of the tree = value of root node from partition. + totalSize = path.node().__data__.value; + } + + // Fade all but the current sequence, and show it in the breadcrumb trail. + function mouseenter(d) { + + var percentage = (d.m1 / totalSize).toPrecision(3); + var percentageString = fp(percentage); + var metricsMatch = Math.abs(d.m1 - d.m2) < 0.000001; + + gMiddleText.selectAll("*").remove(); + + gMiddleText.append("text") + .attr("class", "path-percent") + .attr("y", "-10") + .text(percentageString); + + gMiddleText.append("text") + .attr("class", "path-metrics") + .attr("y", "25") + .text("m1: " + f(d.m1) + (metricsMatch ? "" : ", m2: " + f(d.m2))); + + gMiddleText.append("text") + .attr("class", "path-ratio") + .attr("y", "50") + .text("m2/m1: " + fp(d.m2 / d.m1)); + + var sequenceArray = getAncestors(d); + + // Reset and fade all the segments. + arcs.selectAll("path") + .style("stroke-width", null) + .style("stroke", null) + .style("opacity", 0.3); + + // Then highlight only those that are an ancestor of the current segment. + arcs.selectAll("path") + .filter(function (node) { + return (sequenceArray.indexOf(node) >= 0); + }) + .style("opacity", 1) + .style("stroke-width", "2px") + .style("stroke", "#000"); + + updateBreadcrumbs(sequenceArray, percentageString); + } + + // Restore everything to full opacity when moving off the visualization. + function mouseleave(d) { + + // Hide the breadcrumb trail + breadcrumbs.style("visibility", "hidden"); + + gMiddleText.selectAll("*").remove(); + + // Deactivate all segments during transition. + arcs.selectAll("path").on("mouseenter", null); + //gMiddleText.selectAll("*").remove(); + + // Transition each segment to full opacity and then reactivate it. + arcs.selectAll("path") + .transition() + .duration(200) + .style("opacity", 1) + .style("stroke", null) + .style("stroke-width", null) + .each("end", function () { + d3.select(this).on("mouseenter", mouseenter); + }); + } + + // Given a node in a partition layout, return an array of all of its ancestor + // nodes, highest first, but excluding the root. + function getAncestors(node) { + var path = []; + var current = node; + while (current.parent) { + path.unshift(current); + current = current.parent; + } + return path; + } + + // Generate a string that describes the points of a breadcrumb polygon. + function breadcrumbPoints(d, i) { + var points = []; + points.push("0,0"); + points.push(breadcrumbDims.width + ",0"); + points.push(breadcrumbDims.width + breadcrumbDims.tipTailWidth + "," + (breadcrumbDims.height / 2)); + points.push(breadcrumbDims.width+ "," + breadcrumbDims.height); + points.push("0," + breadcrumbDims.height); + if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex. + points.push(breadcrumbDims.tipTailWidth + "," + (breadcrumbDims.height / 2)); + } + return points.join(" "); + } + + function updateBreadcrumbs(sequenceArray, percentageString) { + var g = breadcrumbs.selectAll("g") + .data(sequenceArray, function (d) { + return d.name + d.depth; + }); + + // Add breadcrumb and label for entering nodes. + var entering = g.enter().append("svg:g"); + + entering.append("svg:polygon") + .attr("points", breadcrumbPoints) + .style("fill", function (d) { + return colorByCategory ? px.color.category21(d.name) : colorScale(d.m2 / d.m1); + }); + + entering.append("svg:text") + .attr("x", (breadcrumbDims.width + breadcrumbDims.tipTailWidth) / 2) + .attr("y", breadcrumbDims.height / 4) + .attr("dy", "0.35em") + .attr("class", "step-label") + .text(function (d) { return d.name; }) + .call(wrapSvgText, breadcrumbDims.width, breadcrumbDims.height / 2); + + // Set position for entering and updating nodes. + g.attr("transform", function (d, i) { + return "translate(" + i * (breadcrumbDims.width + breadcrumbDims.spacing) + ", 0)"; + }); + + // Remove exiting nodes. + g.exit().remove(); + + // Now move and update the percentage at the end. + breadcrumbs.select(".end-label") + .attr("x", (sequenceArray.length + 0.5) * (breadcrumbDims.width + breadcrumbDims.spacing)) + .attr("y", breadcrumbDims.height / 2) + .attr("dy", "0.35em") + .text(percentageString); + + // Make the breadcrumb trail visible, if it's hidden. + breadcrumbs.style("visibility", null); + } + + function buildHierarchy(rows) { + var root = { + name: "root", + children: [] + }; + for (var i = 0; i < rows.length; i++) { + var row = rows[i]; + var m1 = Number(row[row.length - 2]); + var m2 = Number(row[row.length - 1]); + var levels = row.slice(0, row.length - 2); + if (isNaN(m1)) { // e.g. if this is a header row + continue; + } + var currentNode = root; + for (var j = 0; j < levels.length; j++) { + var children = currentNode.children; + var nodeName = levels[j]; + // If the next node has the name "0", it will + var isLeafNode = (j >= levels.length - 1) || levels[j+1] === 0; + var childNode; + + if (!isLeafNode) { + // Not yet at the end of the sequence; move down the tree. + var foundChild = false; + for (var k = 0; k < children.length; k++) { + if (children[k].name === nodeName) { + childNode = children[k]; + foundChild = true; + break; + } + } + // If we don't already have a child node for this branch, create it. + if (!foundChild) { + childNode = { + name: nodeName, + children: [] + }; + children.push(childNode); + } + currentNode = childNode; + } else if (nodeName !== 0) { + // Reached the end of the sequence; create a leaf node. + childNode = { + name: nodeName, + m1: m1, + m2: m2 + }; + children.push(childNode); + } + } + } + + function recurse(node) { + if (node.children) { + var sums; + var m1 = 0; + var m2 = 0; + for (var i = 0; i < node.children.length; i++) { + sums = recurse(node.children[i]); + m1 += sums[0]; + m2 += sums[1]; + } + node.m1 = m1; + node.m2 = m2; + } + return [node.m1, node.m2]; + } + recurse(root); + return root; + } + }; + + return { + render: render, + resize: render + }; +} + +module.exports = sunburstVis; diff --git a/dashed/assets/visualizations/table.css b/dashed/assets/visualizations/table.css new file mode 100644 index 000000000..cd70e14eb --- /dev/null +++ b/dashed/assets/visualizations/table.css @@ -0,0 +1,18 @@ +.gridster .widget.table { + overflow: auto !important; +} + +.widget.table td.filtered { + background-color: #005a63; + color: white; +} + +.table tr>th { + padding: 1px 5px !important; + font-size: small !important; +} + +.table tr>td { + padding: 1px 5px !important; + font-size: small !important; +} diff --git a/dashed/assets/visualizations/table.js b/dashed/assets/visualizations/table.js new file mode 100644 index 000000000..937b0769b --- /dev/null +++ b/dashed/assets/visualizations/table.js @@ -0,0 +1,124 @@ +var $ = window.$ = require('jquery'); +var jQuery = window.jQuery = $; +var d3 = require('d3'); + +require('./table.css'); +require('datatables'); +require('../node_modules/datatables-bootstrap3-plugin/media/css/datatables-bootstrap3.css'); + +function tableVis(slice) { + var data = slice.data; + var form_data = data.form_data; + var f = d3.format('.3s'); + var fC = d3.format('0,000'); + + function refresh() { + $.getJSON(slice.jsonEndpoint(), onSuccess).fail(onError); + + function onError(xhr) { + slice.error(xhr.responseText); + } + + function onSuccess(json) { + var data = json.data; + var metrics = json.form_data.metrics; + + function col(c) { + var arr = []; + for (var i = 0; i < data.records.length; i++) { + arr.push(json.data.records[i][c]); + } + return arr; + } + var maxes = {}; + for (var i = 0; i < metrics.length; i++) { + maxes[metrics[i]] = d3.max(col(metrics[i])); + } + + var table = d3.select(slice.selector).append('table') + .classed('dataframe dataframe table table-striped table-bordered table-condensed table-hover dataTable no-footer', true); + + table.append('thead').append('tr') + .selectAll('th') + .data(data.columns).enter() + .append('th') + .text(function (d) { + return d; + }); + + table.append('tbody') + .selectAll('tr') + .data(data.records).enter() + .append('tr') + .selectAll('td') + .data(function (row, i) { + return data.columns.map(function (c) { + return { + col: c, + val: row[c], + isMetric: metrics.indexOf(c) >= 0 + }; + }); + }).enter() + .append('td') + .style('background-image', function (d) { + if (d.isMetric) { + var perc = Math.round((d.val / maxes[d.col]) * 100); + return "linear-gradient(to right, lightgrey, lightgrey " + perc + "%, rgba(0,0,0,0) " + perc + "%"; + } + }) + .attr('title', function (d) { + if (!isNaN(d.val)) { + return fC(d.val); + } + }) + .attr('data-sort', function (d) { + if (d.isMetric) { + return d.val; + } + }) + .on("click", function (d) { + if (!d.isMetric) { + var td = d3.select(this); + if (td.classed('filtered')) { + slice.removeFilter(d.col, [d.val]); + d3.select(this).classed('filtered', false); + } else { + d3.select(this).classed('filtered', true); + slice.addFilter(d.col, [d.val]); + } + } + }) + .style("cursor", function (d) { + if (!d.isMetric) { + return 'pointer'; + } + }) + .html(function (d) { + if (d.isMetric) { + return f(d.val); + } else { + return d.val; + } + }); + var datatable = slice.container.find('.dataTable').DataTable({ + paging: false, + searching: form_data.include_search + }); + // Sorting table by main column + if (form_data.metrics.length > 0) { + var main_metric = form_data.metrics[0]; + datatable.column(data.columns.indexOf(main_metric)).order('desc').draw(); + } + slice.done(json); + slice.container.parents('.widget').find('.tooltip').remove(); + } + } + + return { + render: refresh, + resize: function () {} + }; +} + +module.exports = tableVis; diff --git a/dashed/assets/visualizations/word_cloud.js b/dashed/assets/visualizations/word_cloud.js new file mode 100644 index 000000000..503807a7e --- /dev/null +++ b/dashed/assets/visualizations/word_cloud.js @@ -0,0 +1,91 @@ +var px = window.px || require('../javascripts/modules/dashed.js'); +var d3 = window.d3 || require('d3'); +var cloudLayout = require('d3-cloud'); + +function wordCloudChart(slice) { + var chart = d3.select(slice.selector); + + function refresh() { + d3.json(slice.jsonEndpoint(), function (error, json) { + if (error !== null) { + slice.error(error.responseText); + return ''; + } + var data = json.data; + var range = [ + json.form_data.size_from, + json.form_data.size_to + ]; + var rotation = json.form_data.rotation; + var f_rotation; + if (rotation === "square") { + f_rotation = function () { + return ~~(Math.random() * 2) * 90; + }; + } else if (rotation === "flat") { + f_rotation = function () { + return 0; + }; + } else { + f_rotation = function () { + return (~~(Math.random() * 6) - 3) * 30; + }; + } + var size = [slice.width(), slice.height()]; + + var scale = d3.scale.linear() + .range(range) + .domain(d3.extent(data, function (d) { + return d.size; + })); + + var layout = cloudLayout() + .size(size) + .words(data) + .padding(5) + .rotate(f_rotation) + .font("serif") + .fontSize(function (d) { + return scale(d.size); + }) + .on("end", draw); + + layout.start(); + + function draw(words) { + chart.selectAll("*").remove(); + + chart.append("svg") + .attr("width", layout.size()[0]) + .attr("height", layout.size()[1]) + .append("g") + .attr("transform", "translate(" + layout.size()[0] / 2 + "," + layout.size()[1] / 2 + ")") + .selectAll("text") + .data(words) + .enter().append("text") + .style("font-size", function (d) { + return d.size + "px"; + }) + .style("font-family", "Impact") + .style("fill", function (d) { + return px.color.category21(d.text); + }) + .attr("text-anchor", "middle") + .attr("transform", function (d) { + return "translate(" + [d.x, d.y] + ") rotate(" + d.rotate + ")"; + }) + .text(function (d) { + return d.text; + }); + } + slice.done(data); + }); + } + + return { + render: refresh, + resize: refresh + }; +} + +module.exports = wordCloudChart; diff --git a/dashed/assets/visualizations/world_map.css b/dashed/assets/visualizations/world_map.css new file mode 100644 index 000000000..99e1bf6f0 --- /dev/null +++ b/dashed/assets/visualizations/world_map.css @@ -0,0 +1,7 @@ +.world_map svg { + background-color: #feffff; +} + +.world_map { + position: relative; +} diff --git a/dashed/assets/visualizations/world_map.js b/dashed/assets/visualizations/world_map.js new file mode 100644 index 000000000..87f09b30b --- /dev/null +++ b/dashed/assets/visualizations/world_map.js @@ -0,0 +1,110 @@ +// JS +var d3 = window.d3 || require('d3'); +//var Datamap = require('../vendor/datamaps/datamaps.all.js'); +var Datamap = require('datamaps'); + +// CSS +require('./world_map.css'); + +function worldMapChart(slice) { + var render = function () { + var container = slice.container; + var div = d3.select(slice.selector); + + container.css('height', slice.height()); + + d3.json(slice.jsonEndpoint(), function (error, json) { + var fd = json.form_data; + + if (error !== null) { + slice.error(error.responseText); + return ''; + } + var ext = d3.extent(json.data, function (d) { + return d.m1; + }); + var extRadius = d3.extent(json.data, function (d) { + return d.m2; + }); + var radiusScale = d3.scale.linear() + .domain([extRadius[0], extRadius[1]]) + .range([1, fd.max_bubble_size]); + + json.data.forEach(function (d) { + d.radius = radiusScale(d.m2); + }); + + var colorScale = d3.scale.linear() + .domain([ext[0], ext[1]]) + .range(["#FFF", "black"]); + + var d = {}; + for (var i = 0; i < json.data.length; i++) { + var country = json.data[i]; + country.fillColor = colorScale(country.m1); + d[country.country] = country; + } + + var f = d3.format('.3s'); + + container.show(); + + var map = new Datamap({ + element: slice.container.get(0), + data: json.data, + fills: { + defaultFill: '#ddd' + }, + geographyConfig: { + popupOnHover: true, + highlightOnHover: true, + borderWidth: 1, + borderColor: '#fff', + highlightBorderColor: '#fff', + highlightFillColor: '#005a63', + highlightBorderWidth: 1, + popupTemplate: function (geo, data) { + return '
' + data.name + '
' + f(data.m1) + '
'; + } + }, + bubblesConfig: { + borderWidth: 1, + borderOpacity: 1, + borderColor: '#005a63', + popupOnHover: true, + radius: null, + popupTemplate: function (geo, data) { + return '
' + data.name + '
' + f(data.m2) + '
'; + }, + fillOpacity: 0.5, + animate: true, + highlightOnHover: true, + highlightFillColor: '#005a63', + highlightBorderColor: 'black', + highlightBorderWidth: 2, + highlightBorderOpacity: 1, + highlightFillOpacity: 0.85, + exitDelay: 100, + key: JSON.stringify + } + }); + + map.updateChoropleth(d); + + if (fd.show_bubbles) { + map.bubbles(json.data); + div.selectAll("circle.datamaps-bubble").style('fill', '#005a63'); + } + + slice.done(json); + + }); + }; + + return { + render: render, + resize: render + }; +} + +module.exports = worldMapChart; diff --git a/dashed/assets/webpack.config.js b/dashed/assets/webpack.config.js new file mode 100644 index 000000000..465a04e1c --- /dev/null +++ b/dashed/assets/webpack.config.js @@ -0,0 +1,51 @@ +var path = require('path'); +var APP_DIR = path.resolve(__dirname, './'); // input +var BUILD_DIR = path.resolve(__dirname, './javascripts/dist'); // output + +var config = { + // for now generate one compiled js file per entry point / html page + entry: { + 'css-theme': APP_DIR + '/javascripts/css-theme.js', + dashboard: APP_DIR + '/javascripts/dashboard.js', + explore: APP_DIR + '/javascripts/explore.js', + featured: APP_DIR + '/javascripts/featured.js', + sql: APP_DIR + '/javascripts/sql.js', + standalone: APP_DIR + '/javascripts/standalone.js' + }, + output: { + path: BUILD_DIR, + filename: '[name].entry.js' + }, + module: { + loaders: [ + { + test: /\.jsx?/, + include: APP_DIR, + exclude: APP_DIR + '/node_modules', + loader: 'babel' + }, + /* for require('*.css') */ + { + test: /\.css$/, + include: APP_DIR, + loader: "style-loader!css-loader" + }, + /* for css linking images */ + { test: /\.png$/, loader: "url-loader?limit=100000" }, + { test: /\.jpg$/, loader: "file-loader" }, + { test: /\.gif$/, loader: "file-loader" }, + /* for font-awesome */ + { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "url-loader?limit=10000&minetype=application/font-woff" }, + { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "file-loader" }, + /* for require('*.less') */ + { + test: /\.less$/, + include: APP_DIR, + loader: "style!css!less" + } + ] + }, + plugins: [] +}; + +module.exports = config; diff --git a/dashed/bin/__init__.py b/dashed/bin/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dashed/bin/dashed b/dashed/bin/dashed new file mode 100755 index 000000000..ac47bb6e9 --- /dev/null +++ b/dashed/bin/dashed @@ -0,0 +1,91 @@ +#!/usr/bin/env python + +from datetime import datetime +import logging +from subprocess import Popen + +from flask.ext.script import Manager +from dashed import app +from flask.ext.migrate import MigrateCommand +import dashed +from dashed import db +from dashed import data, utils + +config = app.config + +manager = Manager(app) +manager.add_command('db', MigrateCommand) + + +@manager.option( + '-d', '--debug', action='store_true', + help="Start the web server in debug mode") +@manager.option( + '-p', '--port', default=config.get("DASHED_WEBSERVER_PORT"), + help="Specify the port on which to run the web server") +@manager.option( + '-w', '--workers', default=config.get("DASHED_WORKERS", 16), + help="Number of gunicorn web server workers to fire up") +@manager.option( + '-t', '--timeout', default=config.get("DASHED_WEBSERVER_TIMEOUT"), + help="Specify the timeout (seconds) for the gunicorn web server") +def runserver(debug, port, timeout, workers): + """Starts a Dashed web server""" + debug = debug or config.get("DEBUG") + if debug: + app.run( + host='0.0.0.0', + port=int(port), + debug=True) + else: + cmd = ( + "gunicorn " + "-w {workers} " + "--timeout {timeout} " + "-b 0.0.0.0:{port} " + "dashed:app").format(**locals()) + print("Starting server with command: " + cmd) + Popen(cmd, shell=True).wait() + +@manager.command +def init(): + """Inits the Dashed application""" + utils.init(dashed) + +@manager.option( + '-s', '--sample', action='store_true', + help="Only load 1000 rows (faster, used for testing)") +def load_examples(sample): + """Loads a set of Slices and Dashboards and a supporting dataset """ + print("Loading examples into {}".format(db)) + + data.load_css_templates() + + print("Loading [World Bank's Health Nutrition and Population Stats]") + data.load_world_bank_health_n_pop() + + print("Loading [Birth names]") + data.load_birth_names() + +@manager.command +def refresh_druid(): + """Refresh all druid datasources""" + session = db.session() + from dashed import models + for cluster in session.query(models.DruidCluster).all(): + try: + cluster.refresh_datasources() + except Exception as e: + print( + "Error while processing cluster '{}'\n{}".format( + cluster, str(e))) + logging.exception(e) + cluster.metadata_last_refreshed = datetime.now() + print( + "Refreshed metadata from cluster " + "[" + cluster.cluster_name + "]") + session.commit() + + +if __name__ == "__main__": + manager.run() diff --git a/dashed/config.py b/dashed/config.py new file mode 100644 index 000000000..a6561114f --- /dev/null +++ b/dashed/config.py @@ -0,0 +1,118 @@ +""" +All configuration in this file can be overridden by providing a local_config +in your PYTHONPATH. + +There' a ``from local_config import *`` at the end of this file. +""" +import os +from flask_appbuilder.security.manager import AUTH_DB +# from flask_appbuilder.security.manager import ( +# AUTH_OID, AUTH_REMOTE_USER, AUTH_DB, AUTH_LDAP, AUTH_OAUTH) +BASE_DIR = os.path.abspath(os.path.dirname(__file__)) +from dateutil import tz + + +# --------------------------------------------------------- +# Dashed specifix config +# --------------------------------------------------------- +ROW_LIMIT = 50000 +WEBSERVER_THREADS = 8 + +DASHED_WEBSERVER_PORT = 8088 +DASHED_WEBSERVER_TIMEOUT = 60 + +CUSTOM_SECURITY_MANAGER = None +# --------------------------------------------------------- + +# Your App secret key +SECRET_KEY = '\2\1thisismyscretkey\1\2\e\y\y\h' # noqa + +# The SQLAlchemy connection string. +SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/dashed.db' +# SQLALCHEMY_DATABASE_URI = 'mysql://myapp@localhost/myapp' +# SQLALCHEMY_DATABASE_URI = 'postgresql://root:password@localhost/myapp' + +# Flask-WTF flag for CSRF +CSRF_ENABLED = True + +# Whether to run the web server in debug mode or not +DEBUG = True + +# Whether to show the stacktrace on 500 error +SHOW_STACKTRACE = True + +# ------------------------------ +# GLOBALS FOR APP Builder +# ------------------------------ +# Uncomment to setup Your App name +APP_NAME = "Dashed" + +# Uncomment to setup Setup an App icon +# APP_ICON = "/static/img/something.png" + +# Druid query timezone +# tz.tzutc() : Using utc timezone +# tz.tzlocal() : Using local timezone +# other tz can be overridden by providing a local_config +DRUID_TZ = tz.tzutc() + +# ---------------------------------------------------- +# AUTHENTICATION CONFIG +# ---------------------------------------------------- +# The authentication type +# AUTH_OID : Is for OpenID +# AUTH_DB : Is for database (username/password() +# AUTH_LDAP : Is for LDAP +# AUTH_REMOTE_USER : Is for using REMOTE_USER from web server +AUTH_TYPE = AUTH_DB + +# Uncomment to setup Full admin role name +# AUTH_ROLE_ADMIN = 'Admin' + +# Uncomment to setup Public role name, no authentication needed +# AUTH_ROLE_PUBLIC = 'Public' + +# Will allow user self registration +# AUTH_USER_REGISTRATION = True + +# The default user self registration role +# AUTH_USER_REGISTRATION_ROLE = "Public" + +# When using LDAP Auth, setup the ldap server +# AUTH_LDAP_SERVER = "ldap://ldapserver.new" + +# Uncomment to setup OpenID providers example for OpenID authentication +# OPENID_PROVIDERS = [ +# { 'name': 'Yahoo', 'url': 'https://me.yahoo.com' }, +# { 'name': 'AOL', 'url': 'http://openid.aol.com/' }, +# { 'name': 'Flickr', 'url': 'http://www.flickr.com/' }, +# { 'name': 'MyOpenID', 'url': 'https://www.myopenid.com' }] +# --------------------------------------------------- +# Babel config for translations +# --------------------------------------------------- +# Setup default language +BABEL_DEFAULT_LOCALE = 'en' +# Your application default translation path +BABEL_DEFAULT_FOLDER = 'translations' +# The allowed translation for you app +LANGUAGES = { + 'en': {'flag': 'us', 'name': 'English'}, +} +# --------------------------------------------------- +# Image and file configuration +# --------------------------------------------------- +# The file upload folder, when using models with files +UPLOAD_FOLDER = BASE_DIR + '/app/static/uploads/' + +# The image upload folder, when using models with images +IMG_UPLOAD_FOLDER = BASE_DIR + '/app/static/uploads/' + +# The image upload url, when using models with images +IMG_UPLOAD_URL = '/static/uploads/' +# Setup image size default is (300, 200, True) +# IMG_SIZE = (300, 200, True) + +try: + from dashed_config import * # noqa +except Exception: + pass diff --git a/dashed/data/__init__.py b/dashed/data/__init__.py new file mode 100644 index 000000000..b02339ad1 --- /dev/null +++ b/dashed/data/__init__.py @@ -0,0 +1,624 @@ +"""Loads datasets, dashboards and slices in a new dashed instance""" + +import gzip +import json +import os +import textwrap + +import pandas as pd +from sqlalchemy import String, DateTime + +from dashed import app, db, models, utils + +# Shortcuts +DB = models.Database +Slice = models.Slice +TBL = models.SqlaTable +Dash = models.Dashboard + +config = app.config + +DATA_FOLDER = os.path.join(config.get("BASE_DIR"), 'data') + + +def get_or_create_db(session): + print("Creating database reference") + dbobj = session.query(DB).filter_by(database_name='main').first() + if not dbobj: + dbobj = DB(database_name="main") + print(config.get("SQLALCHEMY_DATABASE_URI")) + dbobj.sqlalchemy_uri = config.get("SQLALCHEMY_DATABASE_URI") + session.add(dbobj) + session.commit() + return dbobj + + +def merge_slice(slc): + o = db.session.query(Slice).filter_by(slice_name=slc.slice_name).first() + if o: + db.session.delete(o) + db.session.add(slc) + db.session.commit() + + +def get_slice_json(defaults, **kwargs): + d = defaults.copy() + d.update(kwargs) + return json.dumps(d, indent=4, sort_keys=True) + + +def load_world_bank_health_n_pop(): + """Loads the world bank health dataset, slices and a dashboard""" + tbl_name = 'wb_health_population' + with gzip.open(os.path.join(DATA_FOLDER, 'countries.json.gz')) as f: + pdf = pd.read_json(f) + pdf.columns = [col.replace('.', '_') for col in pdf.columns] + pdf.year = pd.to_datetime(pdf.year) + pdf.to_sql( + tbl_name, + db.engine, + if_exists='replace', + chunksize=500, + dtype={ + 'year': DateTime(), + 'country_code': String(3), + 'country_name': String(255), + 'region': String(255), + }, + index=False) + + print("Creating table [wb_health_population] reference") + tbl = db.session.query(TBL).filter_by(table_name=tbl_name).first() + if not tbl: + tbl = TBL(table_name=tbl_name) + tbl.description = utils.readfile(os.path.join(DATA_FOLDER, 'countries.md')) + tbl.main_dttm_col = 'year' + tbl.is_featured = True + tbl.database = get_or_create_db(db.session) + db.session.merge(tbl) + db.session.commit() + tbl.fetch_metadata() + + defaults = { + "compare_lag": "10", + "compare_suffix": "o10Y", + "datasource_id": "1", + "datasource_name": "birth_names", + "datasource_type": "table", + "limit": "25", + "granularity": "year", + "groupby": [], + "metric": 'sum__SP_POP_TOTL', + "metrics": ["sum__SP_POP_TOTL"], + "row_limit": config.get("ROW_LIMIT"), + "since": "2014-01-01", + "until": "2014-01-01", + "where": "", + "markup_type": "markdown", + "country_fieldtype": "cca3", + "secondary_metric": "sum__SP_POP_TOTL", + "entity": "country_code", + "show_bubbles": "y", + } + + print("Creating slices") + slices = [ + Slice( + slice_name="Region Filter", + viz_type='filter_box', + datasource_type='table', + table=tbl, + params=get_slice_json( + defaults, + viz_type='filter_box', + groupby=['region'], + )), + Slice( + slice_name="World's Population", + viz_type='big_number', + datasource_type='table', + table=tbl, + params=get_slice_json( + defaults, + since='2000', + viz_type='big_number', + compare_lag="10", + metric='sum__SP_POP_TOTL', + compare_suffix="over 10Y")), + Slice( + slice_name="Most Populated Countries", + viz_type='table', + datasource_type='table', + table=tbl, + params=get_slice_json( + defaults, + viz_type='table', + metrics=["sum__SP_POP_TOTL"], + groupby=['country_name'])), + Slice( + slice_name="Growth Rate", + viz_type='line', + datasource_type='table', + table=tbl, + params=get_slice_json( + defaults, + viz_type='line', + since="1960-01-01", + metrics=["sum__SP_POP_TOTL"], + num_period_compare="10", + groupby=['country_name'])), + Slice( + slice_name="% Rural", + viz_type='world_map', + datasource_type='table', + table=tbl, + params=get_slice_json( + defaults, + viz_type='world_map', + metric= "sum__SP_RUR_TOTL_ZS", + num_period_compare="10",)), + Slice( + slice_name="Life Expexctancy VS Rural %", + viz_type='bubble', + datasource_type='table', + table=tbl, + params=get_slice_json( + defaults, + viz_type='bubble', + since= "2011-01-01", + until= "2011-01-01", + series="region", + limit="0", + entity="country_name", + x="sum__SP_RUR_TOTL_ZS", + y="sum__SP_DYN_LE00_IN", + size="sum__SP_POP_TOTL", + max_bubble_size="50", + flt_col_1="country_code", + flt_op_1= "not in", + flt_eq_1="TCA,MNP,DMA,MHL,MCO,SXM,CYM,TUV,IMY,KNA,ASM,ADO,AMA,PLW", + num_period_compare="10",)), + Slice( + slice_name="Rural Breakdown", + viz_type='sunburst', + datasource_type='table', + table=tbl, + params=get_slice_json( + defaults, + viz_type='sunburst', + groupby=["region", "country_name"], + secondary_metric="sum__SP_RUR_TOTL", + since= "2011-01-01", + until= "2011-01-01",)), + Slice( + slice_name="World's Pop Growth", + viz_type='area', + datasource_type='table', + table=tbl, + params=get_slice_json( + defaults, + since="1960-01-01", + until="now", + viz_type='area', + groupby=["region"],)), + ] + for slc in slices: + merge_slice(slc) + + print("Creating a World's Health Bank dashboard") + dash_name = "World's Health Bank Dashboard" + dash = db.session.query(Dash).filter_by(dashboard_title=dash_name).first() + + if dash: + db.session.delete(dash) + js = """\ +[ + { + "size_y": 1, + "size_x": 3, + "col": 1, + "slice_id": "269", + "row": 1 + }, + { + "size_y": 3, + "size_x": 3, + "col": 1, + "slice_id": "270", + "row": 2 + }, + { + "size_y": 7, + "size_x": 3, + "col": 10, + "slice_id": "271", + "row": 1 + }, + { + "size_y": 3, + "size_x": 6, + "col": 1, + "slice_id": "272", + "row": 5 + }, + { + "size_y": 4, + "size_x": 6, + "col": 4, + "slice_id": "273", + "row": 1 + }, + { + "size_y": 4, + "size_x": 6, + "col": 7, + "slice_id": "274", + "row": 8 + }, + { + "size_y": 3, + "size_x": 3, + "col": 7, + "slice_id": "275", + "row": 5 + }, + { + "size_y": 4, + "size_x": 6, + "col": 1, + "slice_id": "276", + "row": 8 + } +] + """ + l = json.loads(js) + for i, pos in enumerate(l): + pos['slice_id'] = str(slices[i].id) + dash = Dash( + dashboard_title=dash_name, + position_json=json.dumps(l, indent=4), + slug="world_health", + ) + for s in slices: + dash.slices.append(s) + db.session.commit() + + +def load_css_templates(): + """Loads 2 css templates to demonstrate the feature""" + print('Creating default CSS templates') + CSS = models.CssTemplate + + obj = db.session.query(CSS).filter_by(template_name='Flat').first() + if not obj: + obj = CSS(template_name="Flat") + css = textwrap.dedent("""\ + .gridster li.widget { + transition: background-color 0.5s ease; + background-color: #FAFAFA; + border: 1px solid #CCC; + overflow: hidden; + box-shadow: none; + border-radius: 0px; + } + .gridster li.widget:hover { + border: 1px solid #000; + background-color: #EAEAEA; + } + .navbar { + transition: opacity 0.5s ease; + opacity: 0.05; + } + .navbar:hover { + opacity: 1; + } + .chart-header .header{ + font-weight: normal; + font-size: 12px; + } + /* + var bnbColors = [ + //rausch hackb kazan babu lima beach tirol + '#ff5a5f', '#7b0051', '#007A87', '#00d1c1', '#8ce071', '#ffb400', '#b4a76c', + '#ff8083', '#cc0086', '#00a1b3', '#00ffeb', '#bbedab', '#ffd266', '#cbc29a', + '#ff3339', '#ff1ab1', '#005c66', '#00b3a5', '#55d12e', '#b37e00', '#988b4e', + ]; + */ + """) + obj.css = css + db.session.merge(obj) + db.session.commit() + + obj = ( + db.session.query(CSS).filter_by(template_name='Courier Black').first()) + if not obj: + obj = CSS(template_name="Courier Black") + css = textwrap.dedent("""\ + .gridster li.widget { + transition: background-color 0.5s ease; + background-color: #EEE; + border: 2px solid #444; + overflow: hidden; + border-radius: 15px; + box-shadow: none; + } + h2 { + color: white; + font-size: 52px; + } + .navbar { + box-shadow: none; + } + .gridster li.widget:hover { + border: 2px solid #000; + background-color: #EAEAEA; + } + .navbar { + transition: opacity 0.5s ease; + opacity: 0.05; + } + .navbar:hover { + opacity: 1; + } + .chart-header .header{ + font-weight: normal; + font-size: 12px; + } + .nvd3 text { + font-size: 12px; + font-family: inherit; + } + body{ + background: #000; + font-family: Courier, Monaco, monospace;; + } + /* + var bnbColors = [ + //rausch hackb kazan babu lima beach tirol + '#ff5a5f', '#7b0051', '#007A87', '#00d1c1', '#8ce071', '#ffb400', '#b4a76c', + '#ff8083', '#cc0086', '#00a1b3', '#00ffeb', '#bbedab', '#ffd266', '#cbc29a', + '#ff3339', '#ff1ab1', '#005c66', '#00b3a5', '#55d12e', '#b37e00', '#988b4e', + ]; + */ + """) + obj.css = css + db.session.merge(obj) + db.session.commit() + + +def load_birth_names(): + with gzip.open(os.path.join(DATA_FOLDER, 'birth_names.json.gz')) as f: + pdf = pd.read_json(f) + pdf.ds = pd.to_datetime(pdf.ds, unit='ms') + pdf.to_sql( + 'birth_names', + db.engine, + if_exists='replace', + chunksize=500, + dtype={ + 'ds': DateTime, + 'gender': String(16), + 'state': String(10), + 'name': String(255), + }, + index=False) + l = [] + print("Done loading table!") + print("-" * 80) + + print("Creating table reference") + obj = db.session.query(TBL).filter_by(table_name='birth_names').first() + if not obj: + obj = TBL(table_name = 'birth_names') + obj.main_dttm_col = 'ds' + obj.database = get_or_create_db(db.session) + obj.is_featured = True + db.session.merge(obj) + db.session.commit() + obj.fetch_metadata() + tbl = obj + + defaults = { + "compare_lag": "10", + "compare_suffix": "o10Y", + "datasource_id": "1", + "datasource_name": "birth_names", + "datasource_type": "table", + "flt_op_1": "in", + "limit": "25", + "granularity": "ds", + "groupby": [], + "metric": 'sum__num', + "metrics": ["sum__num"], + "row_limit": config.get("ROW_LIMIT"), + "since": "100 years ago", + "until": "now", + "viz_type": "table", + "where": "", + "markup_type": "markdown", + } + + print("Creating some slices") + slices = [ + Slice( + slice_name="Girls", + viz_type='table', + datasource_type='table', + table=tbl, + params=get_slice_json( + defaults, + groupby=['name'], + flt_col_1='gender', + flt_eq_1="girl", row_limit=50)), + Slice( + slice_name="Boys", + viz_type='table', + datasource_type='table', + table=tbl, + params=get_slice_json( + defaults, + groupby=['name'], + flt_col_1='gender', + flt_eq_1="boy", + row_limit=50)), + Slice( + slice_name="Participants", + viz_type='big_number', + datasource_type='table', + table=tbl, + params=get_slice_json( + defaults, + viz_type="big_number", granularity="ds", + compare_lag="5", compare_suffix="over 5Y")), + Slice( + slice_name="Genders", + viz_type='pie', + datasource_type='table', + table=tbl, + params=get_slice_json( + defaults, + viz_type="pie", groupby=['gender'])), + Slice( + slice_name="Genders by State", + viz_type='dist_bar', + datasource_type='table', + table=tbl, + params=get_slice_json( + defaults, + flt_eq_1="other", viz_type="dist_bar", + metrics=['sum__sum_girls', 'sum__sum_boys'], + groupby=['state'], flt_op_1='not in', flt_col_1='state')), + Slice( + slice_name="Trends", + viz_type='line', + datasource_type='table', + table=tbl, + params=get_slice_json( + defaults, + viz_type="line", groupby=['name'], + granularity='ds', rich_tooltip='y', show_legend='y')), + Slice( + slice_name="Title", + viz_type='markup', + datasource_type='table', + table=tbl, + params=get_slice_json( + defaults, + viz_type="markup", markup_type="html", + code="""\ +
+

Birth Names Dashboard

+

+ The source dataset came from + [here] +

+ +
+""" + )), + Slice( + slice_name="Name Cloud", + viz_type='word_cloud', + datasource_type='table', + table=tbl, + params=get_slice_json( + defaults, + viz_type="word_cloud", size_from="10", + series='name', size_to="70", rotation="square", + limit='100')), + Slice( + slice_name="Pivot Table", + viz_type='pivot_table', + datasource_type='table', + table=tbl, + params=get_slice_json( + defaults, + viz_type="pivot_table", metrics=['sum__num'], + groupby=['name'], columns=['state'])), + ] + for slc in slices: + merge_slice(slc) + + print("Creating a dashboard") + dash = db.session.query(Dash).filter_by(dashboard_title="Births").first() + + if dash: + db.session.delete(dash) + js = """ +[ + { + "size_y": 4, + "size_x": 2, + "col": 8, + "slice_id": "85", + "row": 7 + }, + { + "size_y": 4, + "size_x": 2, + "col": 10, + "slice_id": "86", + "row": 7 + }, + { + "size_y": 2, + "size_x": 2, + "col": 1, + "slice_id": "87", + "row": 1 + }, + { + "size_y": 2, + "size_x": 2, + "col": 3, + "slice_id": "88", + "row": 1 + }, + { + "size_y": 3, + "size_x": 7, + "col": 5, + "slice_id": "89", + "row": 4 + }, + { + "size_y": 4, + "size_x": 7, + "col": 1, + "slice_id": "90", + "row": 7 + }, + { + "size_y": 3, + "size_x": 3, + "col": 9, + "slice_id": "91", + "row": 1 + }, + { + "size_y": 3, + "size_x": 4, + "col": 5, + "slice_id": "92", + "row": 1 + }, + { + "size_y": 4, + "size_x": 4, + "col": 1, + "slice_id": "93", + "row": 3 + } +] + """ + l = json.loads(js) + for i, pos in enumerate(l): + pos['slice_id'] = str(slices[i].id) + dash = Dash( + dashboard_title="Births", + position_json=json.dumps(l, indent=4), + slug="births", + ) + for s in slices: + dash.slices.append(s) + db.session.commit() diff --git a/dashed/data/birth_names.csv.gz b/dashed/data/birth_names.csv.gz new file mode 100644 index 000000000..9990ab9cc Binary files /dev/null and b/dashed/data/birth_names.csv.gz differ diff --git a/dashed/data/birth_names.json.gz b/dashed/data/birth_names.json.gz new file mode 100644 index 000000000..2652cf724 Binary files /dev/null and b/dashed/data/birth_names.json.gz differ diff --git a/dashed/data/countries.json.gz b/dashed/data/countries.json.gz new file mode 100644 index 000000000..6c71c0c43 Binary files /dev/null and b/dashed/data/countries.json.gz differ diff --git a/dashed/data/countries.md b/dashed/data/countries.md new file mode 100644 index 000000000..253a68b9f --- /dev/null +++ b/dashed/data/countries.md @@ -0,0 +1,355 @@ +This data was download from the +[World's Health Organization's website](http://data.worldbank.org/data-catalog/health-nutrition-and-population-statistics) + +Here's the script that was used to massage the data: + + DIR = "" + df_country = pd.read_csv(DIR + '/HNP_Country.csv') + df_country.columns = ['country_code'] + list(df_country.columns[1:]) + df_country = df_country[['country_code', 'Region']] + df_country.columns = ['country_code', 'region'] + + df = pd.read_csv(DIR + '/HNP_Data.csv') + del df['Unnamed: 60'] + df.columns = ['country_name', 'country_code'] + list(df.columns[2:]) + ndf = df.merge(df_country, how='inner') + + dims = ('country_name', 'country_code', 'region') + vv = [str(i) for i in range(1960, 2015)] + mdf = pd.melt(ndf, id_vars=dims + ('Indicator Code',), value_vars=vv) + mdf['year'] = mdf.variable + '-01-01' + dims = dims + ('year',) + + pdf = mdf.pivot_table(values='value', columns='Indicator Code', index=dims) + pdf = pdf.reset_index() + pdf.to_csv(DIR + '/countries.csv') + pdf.to_json(DIR + '/countries.json', orient='records') + +Here's the description of the metrics available: + +Series | Code Indicator Name +--- | --- +NY.GNP.PCAP.CD | GNI per capita, Atlas method (current US$) +SE.ADT.1524.LT.FM.ZS | Literacy rate, youth (ages 15-24), gender parity index (GPI) +SE.ADT.1524.LT.MA.ZS | Literacy rate, youth male (% of males ages 15-24) +SE.ADT.1524.LT.ZS | Literacy rate, youth total (% of people ages 15-24) +SE.ADT.LITR.FE.ZS | Literacy rate, adult female (% of females ages 15 and above) +SE.ADT.LITR.MA.ZS | Literacy rate, adult male (% of males ages 15 and above) +SE.ADT.LITR.ZS | Literacy rate, adult total (% of people ages 15 and above) +SE.ENR.ORPH | Ratio of school attendance of orphans to school attendance of non-orphans ages 10-14 +SE.PRM.CMPT.FE.ZS | Primary completion rate, female (% of relevant age group) +SE.PRM.CMPT.MA.ZS | Primary completion rate, male (% of relevant age group) +SE.PRM.CMPT.ZS | Primary completion rate, total (% of relevant age group) +SE.PRM.ENRR | School enrollment, primary (% gross) +SE.PRM.ENRR.FE | School enrollment, primary, female (% gross) +SE.PRM.ENRR.MA | School enrollment, primary, male (% gross) +SE.PRM.NENR | School enrollment, primary (% net) +SE.PRM.NENR.FE | School enrollment, primary, female (% net) +SE.PRM.NENR.MA | School enrollment, primary, male (% net) +SE.SEC.ENRR | School enrollment, secondary (% gross) +SE.SEC.ENRR.FE | School enrollment, secondary, female (% gross) +SE.SEC.ENRR.MA | School enrollment, secondary, male (% gross) +SE.SEC.NENR | School enrollment, secondary (% net) +SE.SEC.NENR.FE | School enrollment, secondary, female (% net) +SE.SEC.NENR.MA | School enrollment, secondary, male (% net) +SE.TER.ENRR | School enrollment, tertiary (% gross) +SE.TER.ENRR.FE | School enrollment, tertiary, female (% gross) +SE.XPD.TOTL.GD.ZS | Government expenditure on education, total (% of GDP) +SH.ANM.CHLD.ZS | Prevalence of anemia among children (% of children under 5) +SH.ANM.NPRG.ZS | Prevalence of anemia among non-pregnant women (% of women ages 15-49) +SH.CON.1524.FE.ZS | Condom use, population ages 15-24, female (% of females ages 15-24) +SH.CON.1524.MA.ZS | Condom use, population ages 15-24, male (% of males ages 15-24) +SH.CON.AIDS.FE.ZS | Condom use at last high-risk sex, adult female (% ages 15-49) +SH.CON.AIDS.MA.ZS | Condom use at last high-risk sex, adult male (% ages 15-49) +SH.DTH.COMM.ZS | Cause of death, by communicable diseases and maternal, prenatal and nutrition conditions (% of total) +SH.DTH.IMRT | Number of infant deaths +SH.DTH.INJR.ZS | Cause of death, by injury (% of total) +SH.DTH.MORT | Number of under-five deaths +SH.DTH.NCOM.ZS | Cause of death, by non-communicable diseases (% of total) +SH.DTH.NMRT | Number of neonatal deaths +SH.DYN.AIDS | Adults (ages 15+) living with HIV +SH.DYN.AIDS.DH | AIDS estimated deaths (UNAIDS estimates) +SH.DYN.AIDS.FE.ZS | Women's share of population ages 15+ living with HIV (%) +SH.DYN.AIDS.ZS | Prevalence of HIV, total (% of population ages 15-49) +SH.DYN.MORT | Mortality rate, under-5 (per 1,000 live births) +SH.DYN.MORT.FE | Mortality rate, under-5, female (per 1,000 live births) +SH.DYN.MORT.MA | Mortality rate, under-5, male (per 1,000 live births) +SH.DYN.NMRT | Mortality rate, neonatal (per 1,000 live births) +SH.FPL.SATI.ZS | Met need for contraception (% of married women ages 15-49) +SH.H2O.SAFE.RU.ZS | Improved water source, rural (% of rural population with access) +SH.H2O.SAFE.UR.ZS | Improved water source, urban (% of urban population with access) +SH.H2O.SAFE.ZS | Improved water source (% of population with access) +SH.HIV.0014 | Children (0-14) living with HIV +SH.HIV.1524.FE.ZS | Prevalence of HIV, female (% ages 15-24) +SH.HIV.1524.KW.FE.ZS | Comprehensive correct knowledge of HIV/AIDS, ages 15-24, female (2 prevent ways and reject 3 misconceptions) +SH.HIV.1524.KW.MA.ZS | Comprehensive correct knowledge of HIV/AIDS, ages 15-24, male (2 prevent ways and reject 3 misconceptions) +SH.HIV.1524.MA.ZS | Prevalence of HIV, male (% ages 15-24) +SH.HIV.ARTC.ZS | Antiretroviral therapy coverage (% of people living with HIV) +SH.HIV.KNOW.FE.ZS | % of females ages 15-49 having comprehensive correct knowledge about HIV (2 prevent ways and reject 3 misconceptions) +SH.HIV.KNOW.MA.ZS | % of males ages 15-49 having comprehensive correct knowledge about HIV (2 prevent ways and reject 3 misconceptions) +SH.HIV.ORPH | Children orphaned by HIV/AIDS +SH.HIV.TOTL | Adults (ages 15+) and children (0-14 years) living with HIV +SH.IMM.HEPB | Immunization, HepB3 (% of one-year-old children) +SH.IMM.HIB3 | Immunization, Hib3 (% of children ages 12-23 months) +SH.IMM.IBCG | Immunization, BCG (% of one-year-old children) +SH.IMM.IDPT | Immunization, DPT (% of children ages 12-23 months) +SH.IMM.MEAS | Immunization, measles (% of children ages 12-23 months) +SH.IMM.POL3 | Immunization, Pol3 (% of one-year-old children) +SH.MED.BEDS.ZS | Hospital beds (per 1,000 people) +SH.MED.CMHW.P3 | Community health workers (per 1,000 people) +SH.MED.NUMW.P3 | Nurses and midwives (per 1,000 people) +SH.MED.PHYS.ZS | Physicians (per 1,000 people) +SH.MLR.NETS.ZS | Use of insecticide-treated bed nets (% of under-5 population) +SH.MLR.PREG.ZS | Use of any antimalarial drug (% of pregnant women) +SH.MLR.SPF2.ZS | Use of Intermittent Preventive Treatment of malaria, 2+ doses of SP/Fansidar (% of pregnant women) +SH.MLR.TRET.ZS | Children with fever receiving antimalarial drugs (% of children under age 5 with fever) +SH.MMR.DTHS | Number of maternal deaths +SH.MMR.LEVE | Number of weeks of maternity leave +SH.MMR.RISK | Lifetime risk of maternal death (1 in: rate varies by country) +SH.MMR.RISK.ZS | Lifetime risk of maternal death (%) +SH.MMR.WAGE.ZS | Maternal leave benefits (% of wages paid in covered period) +SH.PRG.ANEM | Prevalence of anemia among pregnant women (%) +SH.PRG.ARTC.ZS | Antiretroviral therapy coverage (% of pregnant women living with HIV) +SH.PRG.SYPH.ZS | Prevalence of syphilis (% of women attending antenatal care) +SH.PRV.SMOK.FE | Smoking prevalence, females (% of adults) +SH.PRV.SMOK.MA | Smoking prevalence, males (% of adults) +SH.STA.ACSN | Improved sanitation facilities (% of population with access) +SH.STA.ACSN.RU | Improved sanitation facilities, rural (% of rural population with access) +SH.STA.ACSN.UR | Improved sanitation facilities, urban (% of urban population with access) +SH.STA.ANV4.ZS | Pregnant women receiving prenatal care of at least four visits (% of pregnant women) +SH.STA.ANVC.ZS | Pregnant women receiving prenatal care (%) +SH.STA.ARIC.ZS | ARI treatment (% of children under 5 taken to a health provider) +SH.STA.BFED.ZS | Exclusive breastfeeding (% of children under 6 months) +SH.STA.BRTC.ZS | Births attended by skilled health staff (% of total) +SH.STA.BRTW.ZS | Low-birthweight babies (% of births) +SH.STA.DIAB.ZS | Diabetes prevalence (% of population ages 20 to 79) +SH.STA.IYCF.ZS | Infant and young child feeding practices, all 3 IYCF (% children ages 6-23 months) +SH.STA.MALN.FE.ZS | Prevalence of underweight, weight for age, female (% of children under 5) +SH.STA.MALN.MA.ZS | Prevalence of underweight, weight for age, male (% of children under 5) +SH.STA.MALN.ZS | Prevalence of underweight, weight for age (% of children under 5) +SH.STA.MALR | Malaria cases reported +SH.STA.MMRT | Maternal mortality ratio (modeled estimate, per 100,000 live births) +SH.STA.MMRT.NE | Maternal mortality ratio (national estimate, per 100,000 live births) +SH.STA.ORCF.ZS | Diarrhea treatment (% of children under 5 receiving oral rehydration and continued feeding) +SH.STA.ORTH | Diarrhea treatment (% of children under 5 who received ORS packet) +SH.STA.OW15.FE.ZS | Prevalence of overweight, female (% of female adults) +SH.STA.OW15.MA.ZS | Prevalence of overweight, male (% of male adults) +SH.STA.OW15.ZS | Prevalence of overweight (% of adults) +SH.STA.OWGH.FE.ZS | Prevalence of overweight, weight for height, female (% of children under 5) +SH.STA.OWGH.MA.ZS | Prevalence of overweight, weight for height, male (% of children under 5) +SH.STA.OWGH.ZS | Prevalence of overweight, weight for height (% of children under 5) +SH.STA.PNVC.ZS | Postnatal care coverage (% mothers) +SH.STA.STNT.FE.ZS | Prevalence of stunting, height for age, female (% of children under 5) +SH.STA.STNT.MA.ZS | Prevalence of stunting, height for age, male (% of children under 5) +SH.STA.STNT.ZS | Prevalence of stunting, height for age (% of children under 5) +SH.STA.WAST.FE.ZS | Prevalence of wasting, weight for height, female (% of children under 5) +SH.STA.WAST.MA.ZS | Prevalence of wasting, weight for height, male (% of children under 5) +SH.STA.WAST.ZS | Prevalence of wasting, weight for height (% of children under 5) +SH.SVR.WAST.FE.ZS | Prevalence of severe wasting, weight for height, female (% of children under 5) +SH.SVR.WAST.MA.ZS | Prevalence of severe wasting, weight for height, male (% of children under 5) +SH.SVR.WAST.ZS | Prevalence of severe wasting, weight for height (% of children under 5) +SH.TBS.CURE.ZS | Tuberculosis treatment success rate (% of new cases) +SH.TBS.DTEC.ZS | Tuberculosis case detection rate (%, all forms) +SH.TBS.INCD | Incidence of tuberculosis (per 100,000 people) +SH.TBS.MORT | Tuberculosis death rate (per 100,000 people) +SH.TBS.PREV | Prevalence of tuberculosis (per 100,000 population) +SH.VAC.TTNS.ZS | Newborns protected against tetanus (%) +SH.XPD.EXTR.ZS | External resources for health (% of total expenditure on health) +SH.XPD.OOPC.TO.ZS | Out-of-pocket health expenditure (% of total expenditure on health) +SH.XPD.OOPC.ZS | Out-of-pocket health expenditure (% of private expenditure on health) +SH.XPD.PCAP | Health expenditure per capita (current US$) +SH.XPD.PCAP.PP.KD | Health expenditure per capita, PPP (constant 2011 international $) +SH.XPD.PRIV | Health expenditure, private (% of total health expenditure) +SH.XPD.PRIV.ZS | Health expenditure, private (% of GDP) +SH.XPD.PUBL | Health expenditure, public (% of total health expenditure) +SH.XPD.PUBL.GX.ZS | Health expenditure, public (% of government expenditure) +SH.XPD.PUBL.ZS | Health expenditure, public (% of GDP) +SH.XPD.TOTL.CD | Health expenditure, total (current US$) +SH.XPD.TOTL.ZS | Health expenditure, total (% of GDP) +SI.POV.NAHC | Poverty headcount ratio at national poverty lines (% of population) +SI.POV.RUHC | Rural poverty headcount ratio at national poverty lines (% of rural population) +SI.POV.URHC | Urban poverty headcount ratio at national poverty lines (% of urban population) +SL.EMP.INSV.FE.ZS | Share of women in wage employment in the nonagricultural sector (% of total nonagricultural employment) +SL.TLF.TOTL.FE.ZS | Labor force, female (% of total labor force) +SL.TLF.TOTL.IN | Labor force, total +SL.UEM.TOTL.FE.ZS | Unemployment, female (% of female labor force) (modeled ILO estimate) +SL.UEM.TOTL.MA.ZS | Unemployment, male (% of male labor force) (modeled ILO estimate) +SL.UEM.TOTL.ZS | Unemployment, total (% of total labor force) (modeled ILO estimate) +SM.POP.NETM | Net migration +SN.ITK.DEFC | Number of people who are undernourished +SN.ITK.DEFC.ZS | Prevalence of undernourishment (% of population) +SN.ITK.SALT.ZS | Consumption of iodized salt (% of households) +SN.ITK.VITA.ZS | Vitamin A supplementation coverage rate (% of children ages 6-59 months) +SP.ADO.TFRT | Adolescent fertility rate (births per 1,000 women ages 15-19) +SP.DYN.AMRT.FE | Mortality rate, adult, female (per 1,000 female adults) +SP.DYN.AMRT.MA | Mortality rate, adult, male (per 1,000 male adults) +SP.DYN.CBRT.IN | Birth rate, crude (per 1,000 people) +SP.DYN.CDRT.IN | Death rate, crude (per 1,000 people) +SP.DYN.CONU.ZS | Contraceptive prevalence (% of women ages 15-49) +SP.DYN.IMRT.FE.IN | Mortality rate, infant, female (per 1,000 live births) +SP.DYN.IMRT.IN | Mortality rate, infant (per 1,000 live births) +SP.DYN.IMRT.MA.IN | Mortality rate, infant, male (per 1,000 live births) +SP.DYN.LE00.FE.IN | Life expectancy at birth, female (years) +SP.DYN.LE00.IN | Life expectancy at birth, total (years) +SP.DYN.LE00.MA.IN | Life expectancy at birth, male (years) +SP.DYN.SMAM.FE | Mean age at first marriage, female +SP.DYN.SMAM.MA | Mean age at first marriage, male +SP.DYN.TFRT.IN | Fertility rate, total (births per woman) +SP.DYN.TO65.FE.ZS | Survival to age 65, female (% of cohort) +SP.DYN.TO65.MA.ZS | Survival to age 65, male (% of cohort) +SP.DYN.WFRT | Wanted fertility rate (births per woman) +SP.HOU.FEMA.ZS | Female headed households (% of households with a female head) +SP.MTR.1519.ZS | Teenage mothers (% of women ages 15-19 who have had children or are currently pregnant) +SP.POP.0004.FE | Population ages 0-4, female +SP.POP.0004.FE.5Y | Population ages 0-4, female (% of female population) +SP.POP.0004.MA | Population ages 0-4, male +SP.POP.0004.MA.5Y | Population ages 0-4, male (% of male population) +SP.POP.0014.FE.ZS | Population ages 0-14, female (% of total) +SP.POP.0014.MA.ZS | Population ages 0-14, male (% of total) +SP.POP.0014.TO | Population ages 0-14, total +SP.POP.0014.TO.ZS | Population ages 0-14 (% of total) +SP.POP.0509.FE | Population ages 5-9, female +SP.POP.0509.FE.5Y | Population ages 5-9, female (% of female population) +SP.POP.0509.MA | Population ages 5-9, male +SP.POP.0509.MA.5Y | Population ages 5-9, male (% of male population) +SP.POP.1014.FE | Population ages 10-14, female +SP.POP.1014.FE.5Y | Population ages 10-14, female (% of female population) +SP.POP.1014.MA | Population ages 10-14, male +SP.POP.1014.MA.5Y | Population ages 10-14, male (% of male population) +SP.POP.1519.FE | Population ages 15-19, female +SP.POP.1519.FE.5Y | Population ages 15-19, female (% of female population) +SP.POP.1519.MA | Population ages 15-19, male +SP.POP.1519.MA.5Y | Population ages 15-19, male (% of male population) +SP.POP.1564.FE.ZS | Population ages 15-64, female (% of total) +SP.POP.1564.MA.ZS | Population ages 15-64, male (% of total) +SP.POP.1564.TO | Population ages 15-64, total +SP.POP.1564.TO.ZS | Population ages 15-64 (% of total) +SP.POP.2024.FE | Population ages 20-24, female +SP.POP.2024.FE.5Y | Population ages 20-24, female (% of female population) +SP.POP.2024.MA | Population ages 20-24, male +SP.POP.2024.MA.5Y | Population ages 20-24, male (% of male population) +SP.POP.2529.FE | Population ages 25-29, female +SP.POP.2529.FE.5Y | Population ages 25-29, female (% of female population) +SP.POP.2529.MA | Population ages 25-29, male +SP.POP.2529.MA.5Y | Population ages 25-29, male (% of male population) +SP.POP.3034.FE | Population ages 30-34, female +SP.POP.3034.FE.5Y | Population ages 30-34, female (% of female population) +SP.POP.3034.MA | Population ages 30-34, male +SP.POP.3034.MA.5Y | Population ages 30-34, male (% of male population) +SP.POP.3539.FE | Population ages 35-39, female +SP.POP.3539.FE.5Y | Population ages 35-39, female (% of female population) +SP.POP.3539.MA | Population ages 35-39, male +SP.POP.3539.MA.5Y | Population ages 35-39, male (% of male population) +SP.POP.4044.FE | Population ages 40-44, female +SP.POP.4044.FE.5Y | Population ages 40-44, female (% of female population) +SP.POP.4044.MA | Population ages 40-44, male +SP.POP.4044.MA.5Y | Population ages 40-44, male (% of male population) +SP.POP.4549.FE | Population ages 45-49, female +SP.POP.4549.FE.5Y | Population ages 45-49, female (% of female population) +SP.POP.4549.MA | Population ages 45-49, male +SP.POP.4549.MA.5Y | Population ages 45-49, male (% of male population) +SP.POP.5054.FE | Population ages 50-54, female +SP.POP.5054.FE.5Y | Population ages 50-54, female (% of female population) +SP.POP.5054.MA | Population ages 50-54, male +SP.POP.5054.MA.5Y | Population ages 50-54, male (% of male population) +SP.POP.5559.FE | Population ages 55-59, female +SP.POP.5559.FE.5Y | Population ages 55-59, female (% of female population) +SP.POP.5559.MA | Population ages 55-59, male +SP.POP.5559.MA.5Y | Population ages 55-59, male (% of male population) +SP.POP.6064.FE | Population ages 60-64, female +SP.POP.6064.FE.5Y | Population ages 60-64, female (% of female population) +SP.POP.6064.MA | Population ages 60-64, male +SP.POP.6064.MA.5Y | Population ages 60-64, male (% of male population) +SP.POP.6569.FE | Population ages 65-69, female +SP.POP.6569.FE.5Y | Population ages 65-69, female (% of female population) +SP.POP.6569.MA | Population ages 65-69, male +SP.POP.6569.MA.5Y | Population ages 65-69, male (% of male population) +SP.POP.65UP.FE.ZS | Population ages 65 and above, female (% of total) +SP.POP.65UP.MA.ZS | Population ages 65 and above, male (% of total) +SP.POP.65UP.TO | Population ages 65 and above, total +SP.POP.65UP.TO.ZS | Population ages 65 and above (% of total) +SP.POP.7074.FE | Population ages 70-74, female +SP.POP.7074.FE.5Y | Population ages 70-74, female (% of female population) +SP.POP.7074.MA | Population ages 70-74, male +SP.POP.7074.MA.5Y | Population ages 70-74, male (% of male population) +SP.POP.7579.FE | Population ages 75-79, female +SP.POP.7579.FE.5Y | Population ages 75-79, female (% of female population) +SP.POP.7579.MA | Population ages 75-79, male +SP.POP.7579.MA.5Y | Population ages 75-79, male (% of male population) +SP.POP.80UP.FE | Population ages 80 and above, female +SP.POP.80UP.FE.5Y | Population ages 80 and above, female (% of female population) +SP.POP.80UP.MA | Population ages 80 and above, male +SP.POP.80UP.MA.5Y | Population ages 80 and above, male (% of male population) +SP.POP.AG00.FE.IN | Age population, age 0, female, interpolated +SP.POP.AG00.MA.IN | Age population, age 0, male, interpolated +SP.POP.AG01.FE.IN | Age population, age 01, female, interpolated +SP.POP.AG01.MA.IN | Age population, age 01, male, interpolated +SP.POP.AG02.FE.IN | Age population, age 02, female, interpolated +SP.POP.AG02.MA.IN | Age population, age 02, male, interpolated +SP.POP.AG03.FE.IN | Age population, age 03, female, interpolated +SP.POP.AG03.MA.IN | Age population, age 03, male, interpolated +SP.POP.AG04.FE.IN | Age population, age 04, female, interpolated +SP.POP.AG04.MA.IN | Age population, age 04, male, interpolated +SP.POP.AG05.FE.IN | Age population, age 05, female, interpolated +SP.POP.AG05.MA.IN | Age population, age 05, male, interpolated +SP.POP.AG06.FE.IN | Age population, age 06, female, interpolated +SP.POP.AG06.MA.IN | Age population, age 06, male, interpolated +SP.POP.AG07.FE.IN | Age population, age 07, female, interpolated +SP.POP.AG07.MA.IN | Age population, age 07, male, interpolated +SP.POP.AG08.FE.IN | Age population, age 08, female, interpolated +SP.POP.AG08.MA.IN | Age population, age 08, male, interpolated +SP.POP.AG09.FE.IN | Age population, age 09, female, interpolated +SP.POP.AG09.MA.IN | Age population, age 09, male, interpolated +SP.POP.AG10.FE.IN | Age population, age 10, female, interpolated +SP.POP.AG10.MA.IN | Age population, age 10, male +SP.POP.AG11.FE.IN | Age population, age 11, female, interpolated +SP.POP.AG11.MA.IN | Age population, age 11, male +SP.POP.AG12.FE.IN | Age population, age 12, female, interpolated +SP.POP.AG12.MA.IN | Age population, age 12, male +SP.POP.AG13.FE.IN | Age population, age 13, female, interpolated +SP.POP.AG13.MA.IN | Age population, age 13, male +SP.POP.AG14.FE.IN | Age population, age 14, female, interpolated +SP.POP.AG14.MA.IN | Age population, age 14, male +SP.POP.AG15.FE.IN | Age population, age 15, female, interpolated +SP.POP.AG15.MA.IN | Age population, age 15, male, interpolated +SP.POP.AG16.FE.IN | Age population, age 16, female, interpolated +SP.POP.AG16.MA.IN | Age population, age 16, male, interpolated +SP.POP.AG17.FE.IN | Age population, age 17, female, interpolated +SP.POP.AG17.MA.IN | Age population, age 17, male, interpolated +SP.POP.AG18.FE.IN | Age population, age 18, female, interpolated +SP.POP.AG18.MA.IN | Age population, age 18, male, interpolated +SP.POP.AG19.FE.IN | Age population, age 19, female, interpolated +SP.POP.AG19.MA.IN | Age population, age 19, male, interpolated +SP.POP.AG20.FE.IN | Age population, age 20, female, interpolated +SP.POP.AG20.MA.IN | Age population, age 20, male, interpolated +SP.POP.AG21.FE.IN | Age population, age 21, female, interpolated +SP.POP.AG21.MA.IN | Age population, age 21, male, interpolated +SP.POP.AG22.FE.IN | Age population, age 22, female, interpolated +SP.POP.AG22.MA.IN | Age population, age 22, male, interpolated +SP.POP.AG23.FE.IN | Age population, age 23, female, interpolated +SP.POP.AG23.MA.IN | Age population, age 23, male, interpolated +SP.POP.AG24.FE.IN | Age population, age 24, female, interpolated +SP.POP.AG24.MA.IN | Age population, age 24, male, interpolated +SP.POP.AG25.FE.IN | Age population, age 25, female, interpolated +SP.POP.AG25.MA.IN | Age population, age 25, male, interpolated +SP.POP.BRTH.MF | Sex ratio at birth (male births per female births) +SP.POP.DPND | Age dependency ratio (% of working-age population) +SP.POP.DPND.OL | Age dependency ratio, old (% of working-age population) +SP.POP.DPND.YG | Age dependency ratio, young (% of working-age population) +SP.POP.GROW | Population growth (annual %) +SP.POP.TOTL | Population, total +SP.POP.TOTL.FE.IN | Population, female +SP.POP.TOTL.FE.ZS | Population, female (% of total) +SP.POP.TOTL.MA.IN | Population, male +SP.POP.TOTL.MA.ZS | Population, male (% of total) +SP.REG.BRTH.RU.ZS | Completeness of birth registration, rural (%) +SP.REG.BRTH.UR.ZS | Completeness of birth registration, urban (%) +SP.REG.BRTH.ZS | Completeness of birth registration (%) +SP.REG.DTHS.ZS | Completeness of death registration with cause-of-death information (%) +SP.RUR.TOTL | Rural population +SP.RUR.TOTL.ZG | Rural population growth (annual %) +SP.RUR.TOTL.ZS | Rural population (% of total population) +SP.URB.GROW | Urban population growth (annual %) +SP.URB.TOTL | Urban population +SP.URB.TOTL.IN.ZS | Urban population (% of total) +SP.UWT.TFRT | Unmet need for contraception (% of married women ages 15-49) diff --git a/dashed/data/countries.py b/dashed/data/countries.py new file mode 100644 index 000000000..f81ef32df --- /dev/null +++ b/dashed/data/countries.py @@ -0,0 +1,2494 @@ +""" +This module contains data related to countries and is used for geo mapping +""" + +countries = [ + { + "name": "Angola", + "area": 1246700, + "cioc": "ANG", + "cca2": "AO", + "capital": "Luanda", + "lat": -12.5, + "lng": 18.5, + "cca3": "AGO" + }, + { + "name": "Algeria", + "area": 2381741, + "cioc": "ALG", + "cca2": "DZ", + "capital": "Algiers", + "lat": 28, + "lng": 3, + "cca3": "DZA" + }, + { + "name": "Egypt", + "area": 1002450, + "cioc": "EGY", + "cca2": "EG", + "capital": "Cairo", + "lat": 27, + "lng": 30, + "cca3": "EGY" + }, + { + "name": "Bangladesh", + "area": 147570, + "cioc": "BAN", + "cca2": "BD", + "capital": "Dhaka", + "lat": 24, + "lng": 90, + "cca3": "BGD" + }, + { + "name": "Niger", + "area": 1267000, + "cioc": "NIG", + "cca2": "NE", + "capital": "Niamey", + "lat": 16, + "lng": 8, + "cca3": "NER" + }, + { + "name": "Liechtenstein", + "area": 160, + "cioc": "LIE", + "cca2": "LI", + "capital": "Vaduz", + "lat": 47.26666666, + "lng": 9.53333333, + "cca3": "LIE" + }, + { + "name": "Namibia", + "area": 825615, + "cioc": "NAM", + "cca2": "NA", + "capital": "Windhoek", + "lat": -22, + "lng": 17, + "cca3": "NAM" + }, + { + "name": "Bulgaria", + "area": 110879, + "cioc": "BUL", + "cca2": "BG", + "capital": "Sofia", + "lat": 43, + "lng": 25, + "cca3": "BGR" + }, + { + "name": "Bolivia", + "area": 1098581, + "cioc": "BOL", + "cca2": "BO", + "capital": "Sucre", + "lat": -17, + "lng": -65, + "cca3": "BOL" + }, + { + "name": "Ghana", + "area": 238533, + "cioc": "GHA", + "cca2": "GH", + "capital": "Accra", + "lat": 8, + "lng": -2, + "cca3": "GHA" + }, + { + "name": "Cocos (Keeling) Islands", + "area": 14, + "cioc": "", + "cca2": "CC", + "capital": "West Island", + "lat": -12.5, + "lng": 96.83333333, + "cca3": "CCK" + }, + { + "name": "Pakistan", + "area": 881912, + "cioc": "PAK", + "cca2": "PK", + "capital": "Islamabad", + "lat": 30, + "lng": 70, + "cca3": "PAK" + }, + { + "name": "Cape Verde", + "area": 4033, + "cioc": "CPV", + "cca2": "CV", + "capital": "Praia", + "lat": 16, + "lng": -24, + "cca3": "CPV" + }, + { + "name": "Jordan", + "area": 89342, + "cioc": "JOR", + "cca2": "JO", + "capital": "Amman", + "lat": 31, + "lng": 36, + "cca3": "JOR" + }, + { + "name": "Liberia", + "area": 111369, + "cioc": "LBR", + "cca2": "LR", + "capital": "Monrovia", + "lat": 6.5, + "lng": -9.5, + "cca3": "LBR" + }, + { + "name": "Libya", + "area": 1759540, + "cioc": "LBA", + "cca2": "LY", + "capital": "Tripoli", + "lat": 25, + "lng": 17, + "cca3": "LBY" + }, + { + "name": "Malaysia", + "area": 330803, + "cioc": "MAS", + "cca2": "MY", + "capital": "Kuala Lumpur", + "lat": 2.5, + "lng": 112.5, + "cca3": "MYS" + }, + { + "name": "Dominican Republic", + "area": 48671, + "cioc": "DOM", + "cca2": "DO", + "capital": "Santo Domingo", + "lat": 19, + "lng": -70.66666666, + "cca3": "DOM" + }, + { + "name": "Puerto Rico", + "area": 8870, + "cioc": "PUR", + "cca2": "PR", + "capital": "San Juan", + "lat": 18.25, + "lng": -66.5, + "cca3": "PRI" + }, + { + "name": "Mayotte", + "area": 374, + "cioc": "", + "cca2": "YT", + "capital": "Mamoudzou", + "lat": -12.83333333, + "lng": 45.16666666, + "cca3": "MYT" + }, + { + "name": "North Korea", + "area": 120538, + "cioc": "PRK", + "cca2": "KP", + "capital": "Pyongyang", + "lat": 40, + "lng": 127, + "cca3": "PRK" + }, + { + "name": "Palestine", + "area": 6220, + "cioc": "PLE", + "cca2": "PS", + "capital": "Ramallah", + "lat": 31.9, + "lng": 35.2, + "cca3": "PSE" + }, + { + "name": "Tanzania", + "area": 945087, + "cioc": "TAN", + "cca2": "TZ", + "capital": "Dodoma", + "lat": -6, + "lng": 35, + "cca3": "TZA" + }, + { + "name": "Botswana", + "area": 582000, + "cioc": "BOT", + "cca2": "BW", + "capital": "Gaborone", + "lat": -22, + "lng": 24, + "cca3": "BWA" + }, + { + "name": "Cambodia", + "area": 181035, + "cioc": "CAM", + "cca2": "KH", + "capital": "Phnom Penh", + "lat": 13, + "lng": 105, + "cca3": "KHM" + }, + { + "name": "Nicaragua", + "area": 130373, + "cioc": "NCA", + "cca2": "NI", + "capital": "Managua", + "lat": 13, + "lng": -85, + "cca3": "NIC" + }, + { + "name": "Trinidad and Tobago", + "area": 5130, + "cioc": "TTO", + "cca2": "TT", + "capital": "Port of Spain", + "lat": 11, + "lng": -61, + "cca3": "TTO" + }, + { + "name": "Ethiopia", + "area": 1104300, + "cioc": "ETH", + "cca2": "ET", + "capital": "Addis Ababa", + "lat": 8, + "lng": 38, + "cca3": "ETH" + }, + { + "name": "Paraguay", + "area": 406752, + "cioc": "PAR", + "cca2": "PY", + "capital": "Asuncion", + "lat": -23, + "lng": -58, + "cca3": "PRY" + }, + { + "name": "Hong Kong", + "area": 1104, + "cioc": "HKG", + "cca2": "HK", + "capital": "City of Victoria", + "lat": 22.267, + "lng": 114.188, + "cca3": "HKG" + }, + { + "name": "Saudi Arabia", + "area": 2149690, + "cioc": "KSA", + "cca2": "SA", + "capital": "Riyadh", + "lat": 25, + "lng": 45, + "cca3": "SAU" + }, + { + "name": "Lebanon", + "area": 10452, + "cioc": "LIB", + "cca2": "LB", + "capital": "Beirut", + "lat": 33.83333333, + "lng": 35.83333333, + "cca3": "LBN" + }, + { + "name": "Slovenia", + "area": 20273, + "cioc": "SLO", + "cca2": "SI", + "capital": "Ljubljana", + "lat": 46.11666666, + "lng": 14.81666666, + "cca3": "SVN" + }, + { + "name": "Burkina Faso", + "area": 272967, + "cioc": "BUR", + "cca2": "BF", + "capital": "Ouagadougou", + "lat": 13, + "lng": -2, + "cca3": "BFA" + }, + { + "name": "Switzerland", + "area": 41284, + "cioc": "SUI", + "cca2": "CH", + "capital": "Bern", + "lat": 47, + "lng": 8, + "cca3": "CHE" + }, + { + "name": "Mauritania", + "area": 1030700, + "cioc": "MTN", + "cca2": "MR", + "capital": "Nouakchott", + "lat": 20, + "lng": -12, + "cca3": "MRT" + }, + { + "name": "Croatia", + "area": 56594, + "cioc": "CRO", + "cca2": "HR", + "capital": "Zagreb", + "lat": 45.16666666, + "lng": 15.5, + "cca3": "HRV" + }, + { + "name": "Chile", + "area": 756102, + "cioc": "CHI", + "cca2": "CL", + "capital": "Santiago", + "lat": -30, + "lng": -71, + "cca3": "CHL" + }, + { + "name": "China", + "area": 9706961, + "cioc": "CHN", + "cca2": "CN", + "capital": "Beijing", + "lat": 35, + "lng": 105, + "cca3": "CHN" + }, + { + "name": "Saint Kitts and Nevis", + "area": 261, + "cioc": "SKN", + "cca2": "KN", + "capital": "Basseterre", + "lat": 17.33333333, + "lng": -62.75, + "cca3": "KNA" + }, + { + "name": "Sierra Leone", + "area": 71740, + "cioc": "SLE", + "cca2": "SL", + "capital": "Freetown", + "lat": 8.5, + "lng": -11.5, + "cca3": "SLE" + }, + { + "name": "Jamaica", + "area": 10991, + "cioc": "JAM", + "cca2": "JM", + "capital": "Kingston", + "lat": 18.25, + "lng": -77.5, + "cca3": "JAM" + }, + { + "name": "San Marino", + "area": 61, + "cioc": "SMR", + "cca2": "SM", + "capital": "City of San Marino", + "lat": 43.76666666, + "lng": 12.41666666, + "cca3": "SMR" + }, + { + "name": "Gibraltar", + "area": 6, + "cioc": "", + "cca2": "GI", + "capital": "Gibraltar", + "lat": 36.13333333, + "lng": -5.35, + "cca3": "GIB" + }, + { + "name": "Djibouti", + "area": 23200, + "cioc": "DJI", + "cca2": "DJ", + "capital": "Djibouti", + "lat": 11.5, + "lng": 43, + "cca3": "DJI" + }, + { + "name": "Guinea", + "area": 245857, + "cioc": "GUI", + "cca2": "GN", + "capital": "Conakry", + "lat": 11, + "lng": -10, + "cca3": "GIN" + }, + { + "name": "Finland", + "area": 338424, + "cioc": "FIN", + "cca2": "FI", + "capital": "Helsinki", + "lat": 64, + "lng": 26, + "cca3": "FIN" + }, + { + "name": "Uruguay", + "area": 181034, + "cioc": "URU", + "cca2": "UY", + "capital": "Montevideo", + "lat": -33, + "lng": -56, + "cca3": "URY" + }, + { + "name": "Thailand", + "area": 513120, + "cioc": "THA", + "cca2": "TH", + "capital": "Bangkok", + "lat": 15, + "lng": 100, + "cca3": "THA" + }, + { + "name": "Sao Tome and Principe", + "area": 964, + "cioc": "STP", + "cca2": "ST", + "capital": "Sao Tome", + "lat": 1, + "lng": 7, + "cca3": "STP" + }, + { + "name": "Seychelles", + "area": 452, + "cioc": "SEY", + "cca2": "SC", + "capital": "Victoria", + "lat": -4.58333333, + "lng": 55.66666666, + "cca3": "SYC" + }, + { + "name": "Nepal", + "area": 147181, + "cioc": "NEP", + "cca2": "NP", + "capital": "Kathmandu", + "lat": 28, + "lng": 84, + "cca3": "NPL" + }, + { + "name": "Christmas Island", + "area": 135, + "cioc": "", + "cca2": "CX", + "capital": "Flying Fish Cove", + "lat": -10.5, + "lng": 105.66666666, + "cca3": "CXR" + }, + { + "name": "Laos", + "area": 236800, + "cioc": "LAO", + "cca2": "LA", + "capital": "Vientiane", + "lat": 18, + "lng": 105, + "cca3": "LAO" + }, + { + "name": "Yemen", + "area": 527968, + "cioc": "YEM", + "cca2": "YE", + "capital": "Sana'a", + "lat": 15, + "lng": 48, + "cca3": "YEM" + }, + { + "name": "Bouvet Island", + "area": 49, + "cioc": "", + "cca2": "BV", + "capital": "", + "lat": -54.43333333, + "lng": 3.4, + "cca3": "BVT" + }, + { + "name": "South Africa", + "area": 1221037, + "cioc": "RSA", + "cca2": "ZA", + "capital": "Pretoria", + "lat": -29, + "lng": 24, + "cca3": "ZAF" + }, + { + "name": "Kiribati", + "area": 811, + "cioc": "KIR", + "cca2": "KI", + "capital": "South Tarawa", + "lat": 1.41666666, + "lng": 173, + "cca3": "KIR" + }, + { + "name": "Philippines", + "area": 342353, + "cioc": "PHI", + "cca2": "PH", + "capital": "Manila", + "lat": 13, + "lng": 122, + "cca3": "PHL" + }, + { + "name": "Sint Maarten", + "area": 34, + "cioc": "", + "cca2": "SX", + "capital": "Philipsburg", + "lat": 18.033333, + "lng": -63.05, + "cca3": "SXM" + }, + { + "name": "Romania", + "area": 238391, + "cioc": "ROU", + "cca2": "RO", + "capital": "Bucharest", + "lat": 46, + "lng": 25, + "cca3": "ROU" + }, + { + "name": "United States Virgin Islands", + "area": 347, + "cioc": "ISV", + "cca2": "VI", + "capital": "Charlotte Amalie", + "lat": 18.35, + "lng": -64.933333, + "cca3": "VIR" + }, + { + "name": "Syria", + "area": 185180, + "cioc": "SYR", + "cca2": "SY", + "capital": "Damascus", + "lat": 35, + "lng": 38, + "cca3": "SYR" + }, + { + "name": "Macau", + "area": 30, + "cioc": "", + "cca2": "MO", + "capital": "", + "lat": 22.16666666, + "lng": 113.55, + "cca3": "MAC" + }, + { + "name": "Saint Martin", + "area": 53, + "cioc": "", + "cca2": "MF", + "capital": "Marigot", + "lat": 18.08333333, + "lng": -63.95, + "cca3": "MAF" + }, + { + "name": "Malta", + "area": 316, + "cioc": "MLT", + "cca2": "MT", + "capital": "Valletta", + "lat": 35.83333333, + "lng": 14.58333333, + "cca3": "MLT" + }, + { + "name": "Kazakhstan", + "area": 2724900, + "cioc": "KAZ", + "cca2": "KZ", + "capital": "Astana", + "lat": 48, + "lng": 68, + "cca3": "KAZ" + }, + { + "name": "Turks and Caicos Islands", + "area": 948, + "cioc": "", + "cca2": "TC", + "capital": "Cockburn Town", + "lat": 21.75, + "lng": -71.58333333, + "cca3": "TCA" + }, + { + "name": "French Polynesia", + "area": 4167, + "cioc": "", + "cca2": "PF", + "capital": "Papeete", + "lat": -15, + "lng": -140, + "cca3": "PYF" + }, + { + "name": "Niue", + "area": 260, + "cioc": "", + "cca2": "NU", + "capital": "Alofi", + "lat": -19.03333333, + "lng": -169.86666666, + "cca3": "NIU" + }, + { + "name": "Dominica", + "area": 751, + "cioc": "DMA", + "cca2": "DM", + "capital": "Roseau", + "lat": 15.41666666, + "lng": -61.33333333, + "cca3": "DMA" + }, + { + "name": "Benin", + "area": 112622, + "cioc": "BEN", + "cca2": "BJ", + "capital": "Porto-Novo", + "lat": 9.5, + "lng": 2.25, + "cca3": "BEN" + }, + { + "name": "French Guiana", + "area": 83534, + "cioc": "", + "cca2": "GF", + "capital": "Cayenne", + "lat": 4, + "lng": -53, + "cca3": "GUF" + }, + { + "name": "Belgium", + "area": 30528, + "cioc": "BEL", + "cca2": "BE", + "capital": "Brussels", + "lat": 50.83333333, + "lng": 4, + "cca3": "BEL" + }, + { + "name": "Montserrat", + "area": 102, + "cioc": "", + "cca2": "MS", + "capital": "Plymouth", + "lat": 16.75, + "lng": -62.2, + "cca3": "MSR" + }, + { + "name": "Togo", + "area": 56785, + "cioc": "TOG", + "cca2": "TG", + "capital": "Lome", + "lat": 8, + "lng": 1.16666666, + "cca3": "TGO" + }, + { + "name": "Germany", + "area": 357114, + "cioc": "GER", + "cca2": "DE", + "capital": "Berlin", + "lat": 51, + "lng": 9, + "cca3": "DEU" + }, + { + "name": "Guam", + "area": 549, + "cioc": "GUM", + "cca2": "GU", + "capital": "Hagatna", + "lat": 13.46666666, + "lng": 144.78333333, + "cca3": "GUM" + }, + { + "name": "Sri Lanka", + "area": 65610, + "cioc": "SRI", + "cca2": "LK", + "capital": "Colombo", + "lat": 7, + "lng": 81, + "cca3": "LKA" + }, + { + "name": "South Sudan", + "area": 619745, + "cioc": "", + "cca2": "SS", + "capital": "Juba", + "lat": 7, + "lng": 30, + "cca3": "SSD" + }, + { + "name": "Falkland Islands", + "area": 12173, + "cioc": "", + "cca2": "FK", + "capital": "Stanley", + "lat": -51.75, + "lng": -59, + "cca3": "FLK" + }, + { + "name": "United Kingdom", + "area": 242900, + "cioc": "GBR", + "cca2": "GB", + "capital": "London", + "lat": 54, + "lng": -2, + "cca3": "GBR" + }, + { + "name": "Guyana", + "area": 214969, + "cioc": "GUY", + "cca2": "GY", + "capital": "Georgetown", + "lat": 5, + "lng": -59, + "cca3": "GUY" + }, + { + "name": "Costa Rica", + "area": 51100, + "cioc": "CRC", + "cca2": "CR", + "capital": "San Jose", + "lat": 10, + "lng": -84, + "cca3": "CRI" + }, + { + "name": "Cameroon", + "area": 475442, + "cioc": "CMR", + "cca2": "CM", + "capital": "Yaounde", + "lat": 6, + "lng": 12, + "cca3": "CMR" + }, + { + "name": "Morocco", + "area": 446550, + "cioc": "MAR", + "cca2": "MA", + "capital": "Rabat", + "lat": 32, + "lng": -5, + "cca3": "MAR" + }, + { + "name": "Northern Mariana Islands", + "area": 464, + "cioc": "", + "cca2": "MP", + "capital": "Saipan", + "lat": 15.2, + "lng": 145.75, + "cca3": "MNP" + }, + { + "name": "Lesotho", + "area": 30355, + "cioc": "LES", + "cca2": "LS", + "capital": "Maseru", + "lat": -29.5, + "lng": 28.5, + "cca3": "LSO" + }, + { + "name": "Hungary", + "area": 93028, + "cioc": "HUN", + "cca2": "HU", + "capital": "Budapest", + "lat": 47, + "lng": 20, + "cca3": "HUN" + }, + { + "name": "Turkmenistan", + "area": 488100, + "cioc": "TKM", + "cca2": "TM", + "capital": "Ashgabat", + "lat": 40, + "lng": 60, + "cca3": "TKM" + }, + { + "name": "Suriname", + "area": 163820, + "cioc": "SUR", + "cca2": "SR", + "capital": "Paramaribo", + "lat": 4, + "lng": -56, + "cca3": "SUR" + }, + { + "name": "Netherlands", + "area": 41850, + "cioc": "NED", + "cca2": "NL", + "capital": "Amsterdam", + "lat": 52.5, + "lng": 5.75, + "cca3": "NLD" + }, + { + "name": "Bermuda", + "area": 54, + "cioc": "BER", + "cca2": "BM", + "capital": "Hamilton", + "lat": 32.33333333, + "lng": -64.75, + "cca3": "BMU" + }, + { + "name": "Heard Island and McDonald Islands", + "area": 412, + "cioc": "", + "cca2": "HM", + "capital": "", + "lat": -53.1, + "lng": 72.51666666, + "cca3": "HMD" + }, + { + "name": "Chad", + "area": 1284000, + "cioc": "CHA", + "cca2": "TD", + "capital": "N'Djamena", + "lat": 15, + "lng": 19, + "cca3": "TCD" + }, + { + "name": "Georgia", + "area": 69700, + "cioc": "GEO", + "cca2": "GE", + "capital": "Tbilisi", + "lat": 42, + "lng": 43.5, + "cca3": "GEO" + }, + { + "name": "Montenegro", + "area": 13812, + "cioc": "MNE", + "cca2": "ME", + "capital": "Podgorica", + "lat": 42.5, + "lng": 19.3, + "cca3": "MNE" + }, + { + "name": "Mongolia", + "area": 1564110, + "cioc": "MGL", + "cca2": "MN", + "capital": "Ulan Bator", + "lat": 46, + "lng": 105, + "cca3": "MNG" + }, + { + "name": "Marshall Islands", + "area": 181, + "cioc": "MHL", + "cca2": "MH", + "capital": "Majuro", + "lat": 9, + "lng": 168, + "cca3": "MHL" + }, + { + "name": "Martinique", + "area": 1128, + "cioc": "", + "cca2": "MQ", + "capital": "Fort-de-France", + "lat": 14.666667, + "lng": -61, + "cca3": "MTQ" + }, + { + "name": "Belize", + "area": 22966, + "cioc": "BIZ", + "cca2": "BZ", + "capital": "Belmopan", + "lat": 17.25, + "lng": -88.75, + "cca3": "BLZ" + }, + { + "name": "Norfolk Island", + "area": 36, + "cioc": "", + "cca2": "NF", + "capital": "Kingston", + "lat": -29.03333333, + "lng": 167.95, + "cca3": "NFK" + }, + { + "name": "Myanmar", + "area": 676578, + "cioc": "MYA", + "cca2": "MM", + "capital": "Naypyidaw", + "lat": 22, + "lng": 98, + "cca3": "MMR" + }, + { + "name": "Afghanistan", + "area": 652230, + "cioc": "AFG", + "cca2": "AF", + "capital": "Kabul", + "lat": 33, + "lng": 65, + "cca3": "AFG" + }, + { + "name": "Burundi", + "area": 27834, + "cioc": "BDI", + "cca2": "BI", + "capital": "Bujumbura", + "lat": -3.5, + "lng": 30, + "cca3": "BDI" + }, + { + "name": "British Virgin Islands", + "area": 151, + "cioc": "IVB", + "cca2": "VG", + "capital": "Road Town", + "lat": 18.431383, + "lng": -64.62305, + "cca3": "VGB" + }, + { + "name": "Belarus", + "area": 207600, + "cioc": "BLR", + "cca2": "BY", + "capital": "Minsk", + "lat": 53, + "lng": 28, + "cca3": "BLR" + }, + { + "name": "Saint Barthelemy", + "area": 21, + "cioc": "", + "cca2": "BL", + "capital": "Gustavia", + "lat": 18.5, + "lng": -63.41666666, + "cca3": "BLM" + }, + { + "name": "Grenada", + "area": 344, + "cioc": "GRN", + "cca2": "GD", + "capital": "St. George's", + "lat": 12.11666666, + "lng": -61.66666666, + "cca3": "GRD" + }, + { + "name": "Tokelau", + "area": 12, + "cioc": "", + "cca2": "TK", + "capital": "Fakaofo", + "lat": -9, + "lng": -172, + "cca3": "TKL" + }, + { + "name": "Greece", + "area": 131990, + "cioc": "GRE", + "cca2": "GR", + "capital": "Athens", + "lat": 39, + "lng": 22, + "cca3": "GRC" + }, + { + "name": "Russia", + "area": 17098242, + "cioc": "RUS", + "cca2": "RU", + "capital": "Moscow", + "lat": 60, + "lng": 100, + "cca3": "RUS" + }, + { + "name": "Greenland", + "area": 2166086, + "cioc": "", + "cca2": "GL", + "capital": "Nuuk", + "lat": 72, + "lng": -40, + "cca3": "GRL" + }, + { + "name": "Andorra", + "area": 468, + "cioc": "AND", + "cca2": "AD", + "capital": "Andorra la Vella", + "lat": 42.5, + "lng": 1.5, + "cca3": "AND" + }, + { + "name": "Mozambique", + "area": 801590, + "cioc": "MOZ", + "cca2": "MZ", + "capital": "Maputo", + "lat": -18.25, + "lng": 35, + "cca3": "MOZ" + }, + { + "name": "Tajikistan", + "area": 143100, + "cioc": "TJK", + "cca2": "TJ", + "capital": "Dushanbe", + "lat": 39, + "lng": 71, + "cca3": "TJK" + }, + { + "name": "Haiti", + "area": 27750, + "cioc": "HAI", + "cca2": "HT", + "capital": "Port-au-Prince", + "lat": 19, + "lng": -72.41666666, + "cca3": "HTI" + }, + { + "name": "Mexico", + "area": 1964375, + "cioc": "MEX", + "cca2": "MX", + "capital": "Mexico City", + "lat": 23, + "lng": -102, + "cca3": "MEX" + }, + { + "name": "Zimbabwe", + "area": 390757, + "cioc": "ZIM", + "cca2": "ZW", + "capital": "Harare", + "lat": -20, + "lng": 30, + "cca3": "ZWE" + }, + { + "name": "Saint Lucia", + "area": 616, + "cioc": "LCA", + "cca2": "LC", + "capital": "Castries", + "lat": 13.88333333, + "lng": -60.96666666, + "cca3": "LCA" + }, + { + "name": "India", + "area": 3287590, + "cioc": "IND", + "cca2": "IN", + "capital": "New Delhi", + "lat": 20, + "lng": 77, + "cca3": "IND" + }, + { + "name": "Latvia", + "area": 64559, + "cioc": "LAT", + "cca2": "LV", + "capital": "Riga", + "lat": 57, + "lng": 25, + "cca3": "LVA" + }, + { + "name": "Bhutan", + "area": 38394, + "cioc": "BHU", + "cca2": "BT", + "capital": "Thimphu", + "lat": 27.5, + "lng": 90.5, + "cca3": "BTN" + }, + { + "name": "Saint Vincent and the Grenadines", + "area": 389, + "cioc": "VIN", + "cca2": "VC", + "capital": "Kingstown", + "lat": 13.25, + "lng": -61.2, + "cca3": "VCT" + }, + { + "name": "Vietnam", + "area": 331212, + "cioc": "VIE", + "cca2": "VN", + "capital": "Hanoi", + "lat": 16.16666666, + "lng": 107.83333333, + "cca3": "VNM" + }, + { + "name": "Norway", + "area": 323802, + "cioc": "NOR", + "cca2": "NO", + "capital": "Oslo", + "lat": 62, + "lng": 10, + "cca3": "NOR" + }, + { + "name": "Czech Republic", + "area": 78865, + "cioc": "CZE", + "cca2": "CZ", + "capital": "Prague", + "lat": 49.75, + "lng": 15.5, + "cca3": "CZE" + }, + { + "name": "French Southern and Antarctic Lands", + "area": 7747, + "cioc": "", + "cca2": "TF", + "capital": "Port-aux-Francais", + "lat": -49.25, + "lng": 69.167, + "cca3": "ATF" + }, + { + "name": "Antigua and Barbuda", + "area": 442, + "cioc": "ANT", + "cca2": "AG", + "capital": "Saint John's", + "lat": 17.05, + "lng": -61.8, + "cca3": "ATG" + }, + { + "name": "Fiji", + "area": 18272, + "cioc": "FIJ", + "cca2": "FJ", + "capital": "Suva", + "lat": -18, + "lng": 175, + "cca3": "FJI" + }, + { + "name": "British Indian Ocean Territory", + "area": 60, + "cioc": "", + "cca2": "IO", + "capital": "Diego Garcia", + "lat": -6, + "lng": 71.5, + "cca3": "IOT" + }, + { + "name": "Honduras", + "area": 112492, + "cioc": "HON", + "cca2": "HN", + "capital": "Tegucigalpa", + "lat": 15, + "lng": -86.5, + "cca3": "HND" + }, + { + "name": "Mauritius", + "area": 2040, + "cioc": "MRI", + "cca2": "MU", + "capital": "Port Louis", + "lat": -20.28333333, + "lng": 57.55, + "cca3": "MUS" + }, + { + "name": "Antarctica", + "area": 14000000, + "cioc": "", + "cca2": "AQ", + "capital": "", + "lat": -90, + "lng": 0, + "cca3": "ATA" + }, + { + "name": "Luxembourg", + "area": 2586, + "cioc": "LUX", + "cca2": "LU", + "capital": "Luxembourg", + "lat": 49.75, + "lng": 6.16666666, + "cca3": "LUX" + }, + { + "name": "Israel", + "area": 20770, + "cioc": "ISR", + "cca2": "IL", + "capital": "Jerusalem", + "lat": 31.47, + "lng": 35.13, + "cca3": "ISR" + }, + { + "name": "Micronesia", + "area": 702, + "cioc": "FSM", + "cca2": "FM", + "capital": "Palikir", + "lat": 6.91666666, + "lng": 158.25, + "cca3": "FSM" + }, + { + "name": "Peru", + "area": 1285216, + "cioc": "PER", + "cca2": "PE", + "capital": "Lima", + "lat": -10, + "lng": -76, + "cca3": "PER" + }, + { + "name": "Reunion", + "area": 2511, + "cioc": "", + "cca2": "RE", + "capital": "Saint-Denis", + "lat": -21.15, + "lng": 55.5, + "cca3": "REU" + }, + { + "name": "Indonesia", + "area": 1904569, + "cioc": "INA", + "cca2": "ID", + "capital": "Jakarta", + "lat": -5, + "lng": 120, + "cca3": "IDN" + }, + { + "name": "Vanuatu", + "area": 12189, + "cioc": "VAN", + "cca2": "VU", + "capital": "Port Vila", + "lat": -16, + "lng": 167, + "cca3": "VUT" + }, + { + "name": "Macedonia", + "area": 25713, + "cioc": "MKD", + "cca2": "MK", + "capital": "Skopje", + "lat": 41.83333333, + "lng": 22, + "cca3": "MKD" + }, + { + "name": "DR Congo", + "area": 2344858, + "cioc": "COD", + "cca2": "CD", + "capital": "Kinshasa", + "lat": 0, + "lng": 25, + "cca3": "COD" + }, + { + "name": "Republic of the Congo", + "area": 342000, + "cioc": "CGO", + "cca2": "CG", + "capital": "Brazzaville", + "lat": -1, + "lng": 15, + "cca3": "COG" + }, + { + "name": "Iceland", + "area": 103000, + "cioc": "ISL", + "cca2": "IS", + "capital": "Reykjavik", + "lat": 65, + "lng": -18, + "cca3": "ISL" + }, + { + "name": "Guadeloupe", + "area": 1628, + "cioc": "", + "cca2": "GP", + "capital": "Basse-Terre", + "lat": 16.25, + "lng": -61.583333, + "cca3": "GLP" + }, + { + "name": "Cook Islands", + "area": 236, + "cioc": "COK", + "cca2": "CK", + "capital": "Avarua", + "lat": -21.23333333, + "lng": -159.76666666, + "cca3": "COK" + }, + { + "name": "Comoros", + "area": 1862, + "cioc": "COM", + "cca2": "KM", + "capital": "Moroni", + "lat": -12.16666666, + "lng": 44.25, + "cca3": "COM" + }, + { + "name": "Colombia", + "area": 1141748, + "cioc": "COL", + "cca2": "CO", + "capital": "Bogota", + "lat": 4, + "lng": -72, + "cca3": "COL" + }, + { + "name": "Nigeria", + "area": 923768, + "cioc": "NGR", + "cca2": "NG", + "capital": "Abuja", + "lat": 10, + "lng": 8, + "cca3": "NGA" + }, + { + "name": "Timor-Leste", + "area": 14874, + "cioc": "TLS", + "cca2": "TL", + "capital": "Dili", + "lat": -8.83333333, + "lng": 125.91666666, + "cca3": "TLS" + }, + { + "name": "Taiwan", + "area": 36193, + "cioc": "TPE", + "cca2": "TW", + "capital": "Taipei", + "lat": 23.5, + "lng": 121, + "cca3": "TWN" + }, + { + "name": "Portugal", + "area": 92090, + "cioc": "POR", + "cca2": "PT", + "capital": "Lisbon", + "lat": 39.5, + "lng": -8, + "cca3": "PRT" + }, + { + "name": "Moldova", + "area": 33846, + "cioc": "MDA", + "cca2": "MD", + "capital": "Chisinau", + "lat": 47, + "lng": 29, + "cca3": "MDA" + }, + { + "name": "Guernsey", + "area": 78, + "cioc": "", + "cca2": "GG", + "capital": "St. Peter Port", + "lat": 49.46666666, + "lng": -2.58333333, + "cca3": "GGY" + }, + { + "name": "Madagascar", + "area": 587041, + "cioc": "MAD", + "cca2": "MG", + "capital": "Antananarivo", + "lat": -20, + "lng": 47, + "cca3": "MDG" + }, + { + "name": "Ecuador", + "area": 276841, + "cioc": "ECU", + "cca2": "EC", + "capital": "Quito", + "lat": -2, + "lng": -77.5, + "cca3": "ECU" + }, + { + "name": "Senegal", + "area": 196722, + "cioc": "SEN", + "cca2": "SN", + "capital": "Dakar", + "lat": 14, + "lng": -14, + "cca3": "SEN" + }, + { + "name": "New Zealand", + "area": 270467, + "cioc": "NZL", + "cca2": "NZ", + "capital": "Wellington", + "lat": -41, + "lng": 174, + "cca3": "NZL" + }, + { + "name": "Maldives", + "area": 300, + "cioc": "MDV", + "cca2": "MV", + "capital": "Male", + "lat": 3.25, + "lng": 73, + "cca3": "MDV" + }, + { + "name": "American Samoa", + "area": 199, + "cioc": "ASA", + "cca2": "AS", + "capital": "Pago Pago", + "lat": -14.33333333, + "lng": -170, + "cca3": "ASM" + }, + { + "name": "Saint Pierre and Miquelon", + "area": 242, + "cioc": "", + "cca2": "PM", + "capital": "Saint-Pierre", + "lat": 46.83333333, + "lng": -56.33333333, + "cca3": "SPM" + }, + { + "name": "Curacao", + "area": 444, + "cioc": "", + "cca2": "CW", + "capital": "Willemstad", + "lat": 12.116667, + "lng": -68.933333, + "cca3": "CUW" + }, + { + "name": "France", + "area": 551695, + "cioc": "FRA", + "cca2": "FR", + "capital": "Paris", + "lat": 46, + "lng": 2, + "cca3": "FRA" + }, + { + "name": "Lithuania", + "area": 65300, + "cioc": "LTU", + "cca2": "LT", + "capital": "Vilnius", + "lat": 56, + "lng": 24, + "cca3": "LTU" + }, + { + "name": "Rwanda", + "area": 26338, + "cioc": "RWA", + "cca2": "RW", + "capital": "Kigali", + "lat": -2, + "lng": 30, + "cca3": "RWA" + }, + { + "name": "Zambia", + "area": 752612, + "cioc": "ZAM", + "cca2": "ZM", + "capital": "Lusaka", + "lat": -15, + "lng": 30, + "cca3": "ZMB" + }, + { + "name": "Gambia", + "area": 10689, + "cioc": "GAM", + "cca2": "GM", + "capital": "Banjul", + "lat": 13.46666666, + "lng": -16.56666666, + "cca3": "GMB" + }, + { + "name": "Wallis and Futuna", + "area": 142, + "cioc": "", + "cca2": "WF", + "capital": "Mata-Utu", + "lat": -13.3, + "lng": -176.2, + "cca3": "WLF" + }, + { + "name": "Jersey", + "area": 116, + "cioc": "", + "cca2": "JE", + "capital": "Saint Helier", + "lat": 49.25, + "lng": -2.16666666, + "cca3": "JEY" + }, + { + "name": "Faroe Islands", + "area": 1393, + "cioc": "", + "cca2": "FO", + "capital": "Torshavn", + "lat": 62, + "lng": -7, + "cca3": "FRO" + }, + { + "name": "Guatemala", + "area": 108889, + "cioc": "GUA", + "cca2": "GT", + "capital": "Guatemala City", + "lat": 15.5, + "lng": -90.25, + "cca3": "GTM" + }, + { + "name": "Denmark", + "area": 43094, + "cioc": "DEN", + "cca2": "DK", + "capital": "Copenhagen", + "lat": 56, + "lng": 10, + "cca3": "DNK" + }, + { + "name": "Isle of Man", + "area": 572, + "cioc": "", + "cca2": "IM", + "capital": "Douglas", + "lat": 54.25, + "lng": -4.5, + "cca3": "IMN" + }, + { + "name": "Australia", + "area": 7692024, + "cioc": "AUS", + "cca2": "AU", + "capital": "Canberra", + "lat": -27, + "lng": 133, + "cca3": "AUS" + }, + { + "name": "Austria", + "area": 83871, + "cioc": "AUT", + "cca2": "AT", + "capital": "Vienna", + "lat": 47.33333333, + "lng": 13.33333333, + "cca3": "AUT" + }, + { + "name": "Svalbard and Jan Mayen", + "area": -1, + "cioc": "", + "cca2": "SJ", + "capital": "Longyearbyen", + "lat": 78, + "lng": 20, + "cca3": "SJM" + }, + { + "name": "Venezuela", + "area": 916445, + "cioc": "VEN", + "cca2": "VE", + "capital": "Caracas", + "lat": 8, + "lng": -66, + "cca3": "VEN" + }, + { + "name": "Kosovo", + "area": 10908, + "cioc": "KOS", + "cca2": "XK", + "capital": "Pristina", + "lat": 42.666667, + "lng": 21.166667, + "cca3": "UNK" + }, + { + "name": "Palau", + "area": 459, + "cioc": "PLW", + "cca2": "PW", + "capital": "Ngerulmud", + "lat": 7.5, + "lng": 134.5, + "cca3": "PLW" + }, + { + "name": "Kenya", + "area": 580367, + "cioc": "KEN", + "cca2": "KE", + "capital": "Nairobi", + "lat": 1, + "lng": 38, + "cca3": "KEN" + }, + { + "name": "Samoa", + "area": 2842, + "cioc": "SAM", + "cca2": "WS", + "capital": "Apia", + "lat": -13.58333333, + "lng": -172.33333333, + "cca3": "WSM" + }, + { + "name": "Turkey", + "area": 783562, + "cioc": "TUR", + "cca2": "TR", + "capital": "Ankara", + "lat": 39, + "lng": 35, + "cca3": "TUR" + }, + { + "name": "Albania", + "area": 28748, + "cioc": "ALB", + "cca2": "AL", + "capital": "Tirana", + "lat": 41, + "lng": 20, + "cca3": "ALB" + }, + { + "name": "Oman", + "area": 309500, + "cioc": "OMA", + "cca2": "OM", + "capital": "Muscat", + "lat": 21, + "lng": 57, + "cca3": "OMN" + }, + { + "name": "Tuvalu", + "area": 26, + "cioc": "TUV", + "cca2": "TV", + "capital": "Funafuti", + "lat": -8, + "lng": 178, + "cca3": "TUV" + }, + { + "name": "Aland Islands", + "area": 1580, + "cioc": "", + "cca2": "AX", + "capital": "Mariehamn", + "lat": 60.116667, + "lng": 19.9, + "cca3": "ALA" + }, + { + "name": "Brunei", + "area": 5765, + "cioc": "BRU", + "cca2": "BN", + "capital": "Bandar Seri Begawan", + "lat": 4.5, + "lng": 114.66666666, + "cca3": "BRN" + }, + { + "name": "Tunisia", + "area": 163610, + "cioc": "TUN", + "cca2": "TN", + "capital": "Tunis", + "lat": 34, + "lng": 9, + "cca3": "TUN" + }, + { + "name": "Pitcairn Islands", + "area": 47, + "cioc": "", + "cca2": "PN", + "capital": "Adamstown", + "lat": -25.06666666, + "lng": -130.1, + "cca3": "PCN" + }, + { + "name": "Barbados", + "area": 430, + "cioc": "BAR", + "cca2": "BB", + "capital": "Bridgetown", + "lat": 13.16666666, + "lng": -59.53333333, + "cca3": "BRB" + }, + { + "name": "Brazil", + "area": 8515767, + "cioc": "BRA", + "cca2": "BR", + "capital": "Brasilia", + "lat": -10, + "lng": -55, + "cca3": "BRA" + }, + { + "name": "Ivory Coast", + "area": 322463, + "cioc": "CIV", + "cca2": "CI", + "capital": "Yamoussoukro", + "lat": 8, + "lng": -5, + "cca3": "CIV" + }, + { + "name": "Serbia", + "area": 88361, + "cioc": "SRB", + "cca2": "RS", + "capital": "Belgrade", + "lat": 44, + "lng": 21, + "cca3": "SRB" + }, + { + "name": "Equatorial Guinea", + "area": 28051, + "cioc": "GEQ", + "cca2": "GQ", + "capital": "Malabo", + "lat": 2, + "lng": 10, + "cca3": "GNQ" + }, + { + "name": "United States", + "area": 9372610, + "cioc": "USA", + "cca2": "US", + "capital": "Washington D.C.", + "lat": 38, + "lng": -97, + "cca3": "USA" + }, + { + "name": "Qatar", + "area": 11586, + "cioc": "QAT", + "cca2": "QA", + "capital": "Doha", + "lat": 25.5, + "lng": 51.25, + "cca3": "QAT" + }, + { + "name": "Sweden", + "area": 450295, + "cioc": "SWE", + "cca2": "SE", + "capital": "Stockholm", + "lat": 62, + "lng": 15, + "cca3": "SWE" + }, + { + "name": "Azerbaijan", + "area": 86600, + "cioc": "AZE", + "cca2": "AZ", + "capital": "Baku", + "lat": 40.5, + "lng": 47.5, + "cca3": "AZE" + }, + { + "name": "Guinea-Bissau", + "area": 36125, + "cioc": "GBS", + "cca2": "GW", + "capital": "Bissau", + "lat": 12, + "lng": -15, + "cca3": "GNB" + }, + { + "name": "Swaziland", + "area": 17364, + "cioc": "SWZ", + "cca2": "SZ", + "capital": "Lobamba", + "lat": -26.5, + "lng": 31.5, + "cca3": "SWZ" + }, + { + "name": "Tonga", + "area": 747, + "cioc": "TGA", + "cca2": "TO", + "capital": "Nuku'alofa", + "lat": -20, + "lng": -175, + "cca3": "TON" + }, + { + "name": "Canada", + "area": 9984670, + "cioc": "CAN", + "cca2": "CA", + "capital": "Ottawa", + "lat": 60, + "lng": -95, + "cca3": "CAN" + }, + { + "name": "Ukraine", + "area": 603500, + "cioc": "UKR", + "cca2": "UA", + "capital": "Kiev", + "lat": 49, + "lng": 32, + "cca3": "UKR" + }, + { + "name": "South Korea", + "area": 100210, + "cioc": "KOR", + "cca2": "KR", + "capital": "Seoul", + "lat": 37, + "lng": 127.5, + "cca3": "KOR" + }, + { + "name": "Anguilla", + "area": 91, + "cioc": "", + "cca2": "AI", + "capital": "The Valley", + "lat": 18.25, + "lng": -63.16666666, + "cca3": "AIA" + }, + { + "name": "Central African Republic", + "area": 622984, + "cioc": "CAF", + "cca2": "CF", + "capital": "Bangui", + "lat": 7, + "lng": 21, + "cca3": "CAF" + }, + { + "name": "Slovakia", + "area": 49037, + "cioc": "SVK", + "cca2": "SK", + "capital": "Bratislava", + "lat": 48.66666666, + "lng": 19.5, + "cca3": "SVK" + }, + { + "name": "Cyprus", + "area": 9251, + "cioc": "CYP", + "cca2": "CY", + "capital": "Nicosia", + "lat": 35, + "lng": 33, + "cca3": "CYP" + }, + { + "name": "Bosnia and Herzegovina", + "area": 51209, + "cioc": "BIH", + "cca2": "BA", + "capital": "Sarajevo", + "lat": 44, + "lng": 18, + "cca3": "BIH" + }, + { + "name": "Singapore", + "area": 710, + "cioc": "SIN", + "cca2": "SG", + "capital": "Singapore", + "lat": 1.36666666, + "lng": 103.8, + "cca3": "SGP" + }, + { + "name": "South Georgia", + "area": 3903, + "cioc": "", + "cca2": "GS", + "capital": "King Edward Point", + "lat": -54.5, + "lng": -37, + "cca3": "SGS" + }, + { + "name": "Somalia", + "area": 637657, + "cioc": "SOM", + "cca2": "SO", + "capital": "Mogadishu", + "lat": 10, + "lng": 49, + "cca3": "SOM" + }, + { + "name": "Uzbekistan", + "area": 447400, + "cioc": "UZB", + "cca2": "UZ", + "capital": "Tashkent", + "lat": 41, + "lng": 64, + "cca3": "UZB" + }, + { + "name": "Eritrea", + "area": 117600, + "cioc": "ERI", + "cca2": "ER", + "capital": "Asmara", + "lat": 15, + "lng": 39, + "cca3": "ERI" + }, + { + "name": "Poland", + "area": 312679, + "cioc": "POL", + "cca2": "PL", + "capital": "Warsaw", + "lat": 52, + "lng": 20, + "cca3": "POL" + }, + { + "name": "Kuwait", + "area": 17818, + "cioc": "KUW", + "cca2": "KW", + "capital": "Kuwait City", + "lat": 29.5, + "lng": 45.75, + "cca3": "KWT" + }, + { + "name": "Gabon", + "area": 267668, + "cioc": "GAB", + "cca2": "GA", + "capital": "Libreville", + "lat": -1, + "lng": 11.75, + "cca3": "GAB" + }, + { + "name": "Cayman Islands", + "area": 264, + "cioc": "CAY", + "cca2": "KY", + "capital": "George Town", + "lat": 19.5, + "lng": -80.5, + "cca3": "CYM" + }, + { + "name": "Vatican City", + "area": 0.44, + "cioc": "", + "cca2": "VA", + "capital": "Vatican City", + "lat": 41.9, + "lng": 12.45, + "cca3": "VAT" + }, + { + "name": "Estonia", + "area": 45227, + "cioc": "EST", + "cca2": "EE", + "capital": "Tallinn", + "lat": 59, + "lng": 26, + "cca3": "EST" + }, + { + "name": "Malawi", + "area": 118484, + "cioc": "MAW", + "cca2": "MW", + "capital": "Lilongwe", + "lat": -13.5, + "lng": 34, + "cca3": "MWI" + }, + { + "name": "Spain", + "area": 505992, + "cioc": "ESP", + "cca2": "ES", + "capital": "Madrid", + "lat": 40, + "lng": -4, + "cca3": "ESP" + }, + { + "name": "Iraq", + "area": 438317, + "cioc": "IRQ", + "cca2": "IQ", + "capital": "Baghdad", + "lat": 33, + "lng": 44, + "cca3": "IRQ" + }, + { + "name": "El Salvador", + "area": 21041, + "cioc": "ESA", + "cca2": "SV", + "capital": "San Salvador", + "lat": 13.83333333, + "lng": -88.91666666, + "cca3": "SLV" + }, + { + "name": "Mali", + "area": 1240192, + "cioc": "MLI", + "cca2": "ML", + "capital": "Bamako", + "lat": 17, + "lng": -4, + "cca3": "MLI" + }, + { + "name": "Ireland", + "area": 70273, + "cioc": "IRL", + "cca2": "IE", + "capital": "Dublin", + "lat": 53, + "lng": -8, + "cca3": "IRL" + }, + { + "name": "Iran", + "area": 1648195, + "cioc": "IRI", + "cca2": "IR", + "capital": "Tehran", + "lat": 32, + "lng": 53, + "cca3": "IRN" + }, + { + "name": "Aruba", + "area": 180, + "cioc": "ARU", + "cca2": "AW", + "capital": "Oranjestad", + "lat": 12.5, + "lng": -69.96666666, + "cca3": "ABW" + }, + { + "name": "Papua New Guinea", + "area": 462840, + "cioc": "PNG", + "cca2": "PG", + "capital": "Port Moresby", + "lat": -6, + "lng": 147, + "cca3": "PNG" + }, + { + "name": "Panama", + "area": 75417, + "cioc": "PAN", + "cca2": "PA", + "capital": "Panama City", + "lat": 9, + "lng": -80, + "cca3": "PAN" + }, + { + "name": "Sudan", + "area": 1886068, + "cioc": "SUD", + "cca2": "SD", + "capital": "Khartoum", + "lat": 15, + "lng": 30, + "cca3": "SDN" + }, + { + "name": "Solomon Islands", + "area": 28896, + "cioc": "SOL", + "cca2": "SB", + "capital": "Honiara", + "lat": -8, + "lng": 159, + "cca3": "SLB" + }, + { + "name": "Western Sahara", + "area": 266000, + "cioc": "", + "cca2": "EH", + "capital": "El Aaiun", + "lat": 24.5, + "lng": -13, + "cca3": "ESH" + }, + { + "name": "Monaco", + "area": 2.02, + "cioc": "MON", + "cca2": "MC", + "capital": "Monaco", + "lat": 43.73333333, + "lng": 7.4, + "cca3": "MCO" + }, + { + "name": "Italy", + "area": 301336, + "cioc": "ITA", + "cca2": "IT", + "capital": "Rome", + "lat": 42.83333333, + "lng": 12.83333333, + "cca3": "ITA" + }, + { + "name": "Japan", + "area": 377930, + "cioc": "JPN", + "cca2": "JP", + "capital": "Tokyo", + "lat": 36, + "lng": 138, + "cca3": "JPN" + }, + { + "name": "Kyrgyzstan", + "area": 199951, + "cioc": "KGZ", + "cca2": "KG", + "capital": "Bishkek", + "lat": 41, + "lng": 75, + "cca3": "KGZ" + }, + { + "name": "Uganda", + "area": 241550, + "cioc": "UGA", + "cca2": "UG", + "capital": "Kampala", + "lat": 1, + "lng": 32, + "cca3": "UGA" + }, + { + "name": "New Caledonia", + "area": 18575, + "cioc": "", + "cca2": "NC", + "capital": "Noumea", + "lat": -21.5, + "lng": 165.5, + "cca3": "NCL" + }, + { + "name": "United Arab Emirates", + "area": 83600, + "cioc": "UAE", + "cca2": "AE", + "capital": "Abu Dhabi", + "lat": 24, + "lng": 54, + "cca3": "ARE" + }, + { + "name": "Argentina", + "area": 2780400, + "cioc": "ARG", + "cca2": "AR", + "capital": "Buenos Aires", + "lat": -34, + "lng": -64, + "cca3": "ARG" + }, + { + "name": "Bahamas", + "area": 13943, + "cioc": "BAH", + "cca2": "BS", + "capital": "Nassau", + "lat": 24.25, + "lng": -76, + "cca3": "BHS" + }, + { + "name": "Bahrain", + "area": 765, + "cioc": "BRN", + "cca2": "BH", + "capital": "Manama", + "lat": 26, + "lng": 50.55, + "cca3": "BHR" + }, + { + "name": "Armenia", + "area": 29743, + "cioc": "ARM", + "cca2": "AM", + "capital": "Yerevan", + "lat": 40, + "lng": 45, + "cca3": "ARM" + }, + { + "name": "Nauru", + "area": 21, + "cioc": "NRU", + "cca2": "NR", + "capital": "Yaren", + "lat": -0.53333333, + "lng": 166.91666666, + "cca3": "NRU" + }, + { + "name": "Cuba", + "area": 109884, + "cioc": "CUB", + "cca2": "CU", + "capital": "Havana", + "lat": 21.5, + "lng": -80, + "cca3": "CUB" + } +] + +all_lookups = {} +lookups = ['cioc', 'cca2', 'cca3', 'name'] +for lookup in lookups: + all_lookups[lookup] = {} + for country in countries: + all_lookups[lookup][country[lookup].lower()] = country + +def get(field, symbol): + """ + Get country data based on a standard code and a symbol + + >>> get('cioc', 'CUB')['name'] + "Cuba" + >>> get('cca2', 'CA')['name'] + "Canada" + """ + return all_lookups[field].get(symbol.lower()) diff --git a/dashed/forms.py b/dashed/forms.py new file mode 100644 index 000000000..e5332ce5e --- /dev/null +++ b/dashed/forms.py @@ -0,0 +1,588 @@ +from wtforms import ( + Form, SelectMultipleField, SelectField, TextField, TextAreaField, + BooleanField, IntegerField, HiddenField) +from wtforms import validators, widgets +from copy import copy +from dashed import app +from collections import OrderedDict +config = app.config + + +class BetterBooleanField(BooleanField): + + """ + Fixes behavior of html forms omitting non checked + (which doesn't distinguish False from NULL/missing ) + If value is unchecked, this hidden fills in False value + """ + + def __call__(self, **kwargs): + html = super(BetterBooleanField, self).__call__(**kwargs) + html += u''.format(self.name) + return widgets.HTMLString(html) + + +class SelectMultipleSortableField(SelectMultipleField): + + """Works along with select2sortable to preserves the sort order""" + + def iter_choices(self): + d = OrderedDict() + for value, label in self.choices: + selected = self.data is not None and self.coerce(value) in self.data + d[value] = (value, label, selected) + if self.data: + for value in self.data: + if value: + yield d.pop(value) + while d: + yield d.pop(d.keys()[0]) + + +class FreeFormSelect(widgets.Select): + + """A WTF widget that allows for free form entry""" + + def __call__(self, field, **kwargs): + kwargs.setdefault('id', field.id) + if self.multiple: + kwargs['multiple'] = True + html = ['') + return widgets.HTMLString(''.join(html)) + + +class FreeFormSelectField(SelectField): + + """ A WTF SelectField that allows for free form input """ + + widget = FreeFormSelect() + def pre_validate(self, form): + return + + +class OmgWtForm(Form): + + """Dashedification of the WTForm Form object""" + + fieldsets = {} + css_classes = dict() + + def get_field(self, fieldname): + return getattr(self, fieldname) + + def field_css_classes(self, fieldname): + if fieldname in self.css_classes: + return " ".join(self.css_classes[fieldname]) + return "" + + +class FormFactory(object): + """Used to create the forms in the explore view dynamically""" + series_limits = [0, 5, 10, 25, 50, 100, 500] + fieltype_class = { + SelectField: 'select2', + SelectMultipleField: 'select2', + FreeFormSelectField: 'select2_freeform', + SelectMultipleSortableField: 'select2Sortable', + } + + def __init__(self, viz): + self.viz = viz + from dashed.viz import viz_types + viz = self.viz + datasource = viz.datasource + default_metric = datasource.metrics_combo[0][0] + default_groupby = datasource.groupby_column_names[0] + group_by_choices = [(s, s) for s in datasource.groupby_column_names] + # Pool of all the fields that can be used in Dashed + self.field_dict = { + 'viz_type': SelectField( + 'Viz', + default='table', + choices=[(k, v.verbose_name) for k, v in viz_types.items()], + description="The type of visualization to display"), + 'metrics': SelectMultipleSortableField( + 'Metrics', choices=datasource.metrics_combo, + default=[default_metric], + description="One or many metrics to display"), + 'metric': SelectField( + 'Metric', choices=datasource.metrics_combo, + default=default_metric, + description="Chose the metric"), + 'stacked_style': SelectField( + 'Chart Style', choices=self.choicify( + ['stack', 'stream', 'expand']), + default='stack', + description=""), + 'linear_color_scheme': SelectField( + 'Color Scheme', choices=self.choicify([ + 'fire', 'blue_white_yellow', 'white_black', + 'black_white']), + default='fire', + description=""), + 'normalize_across': SelectField( + 'Normalize Across', choices=self.choicify([ + 'heatmap', 'x', 'y']), + default='heatmap', + description=( + "Color will be rendered based on a ratio " + "of the cell against the sum of across this " + "criteria")), + 'canvas_image_rendering': SelectField( + 'Rendering', choices=( + ('pixelated', 'pixelated (Sharp)'), + ('auto', 'auto (Smooth)'), + ), + default='pixelated', + description=( + "image-rendering CSS attribute of the canvas object that " + "defines how the browser scales up the image")), + 'xscale_interval': SelectField( + 'XScale Interval', choices=self.choicify(range(1, 50)), + default='1', + description=( + "Number of step to take between ticks when " + "printing the x scale")), + 'yscale_interval': SelectField( + 'YScale Interval', choices=self.choicify(range(1, 50)), + default='1', + description=( + "Number of step to take between ticks when " + "printing the y scale")), + 'bar_stacked': BetterBooleanField( + 'Stacked Bars', + default=False, + description=""), + 'secondary_metric': SelectField( + 'Color Metric', choices=datasource.metrics_combo, + default=default_metric, + description="A metric to use for color"), + 'country_fieldtype': SelectField( + 'Country Field Type', + default='cca2', + choices=( + ('name', 'Full name'), + ('cioc', 'code International Olympic Committee (cioc)'), + ('cca2', 'code ISO 3166-1 alpha-2 (cca2)'), + ('cca3', 'code ISO 3166-1 alpha-3 (cca3)'), + ), + description=( + "The country code standard that Dashed should expect " + "to find in the [country] column")), + 'groupby': SelectMultipleSortableField( + 'Group by', + choices=self.choicify(datasource.groupby_column_names), + description="One or many fields to group by"), + 'columns': SelectMultipleSortableField( + 'Columns', + choices=self.choicify(datasource.groupby_column_names), + description="One or many fields to pivot as columns"), + 'all_columns': SelectMultipleSortableField( + 'Columns', + choices=self.choicify(datasource.column_names), + description="Columns to display"), + 'all_columns_x': SelectField( + 'X', + choices=self.choicify(datasource.column_names), + description="Columns to display"), + 'all_columns_y': SelectField( + 'Y', + choices=self.choicify(datasource.column_names), + description="Columns to display"), + 'granularity': FreeFormSelectField( + 'Time Granularity', default="one day", + choices=self.choicify([ + 'all', + '5 seconds', + '30 seconds', + '1 minute', + '5 minutes', + '1 hour', + '6 hour', + '1 day', + '7 days', + ]), + description=( + "The time granularity for the visualization. Note that you " + "can type and use simple natural language as in '10 seconds', " + "'1 day' or '56 weeks'")), + 'link_length': FreeFormSelectField( + 'Link Length', default="200", + choices=self.choicify([ + '10', + '25', + '50', + '75', + '100', + '150', + '200', + '250', + ]), + description="Link length in the force layout"), + 'charge': FreeFormSelectField( + 'Charge', default="-500", + choices=self.choicify([ + '-50', + '-75', + '-100', + '-150', + '-200', + '-250', + '-500', + '-1000', + '-2500', + '-5000', + ]), + description="Charge in the force layout"), + 'granularity_sqla': SelectField( + 'Time Column', + default=datasource.main_dttm_col or datasource.any_dttm_col, + choices=self.choicify(datasource.dttm_cols), + description=( + "The time column for the visualization. Note that you " + "can define arbitrary expression that return a DATETIME " + "column in the table editor. Also note that the " + "filter bellow is applied against this column or " + "expression")), + 'resample_rule': FreeFormSelectField( + 'Resample Rule', default='', + choices=self.choicify(('1T', '1H', '1D', '7D', '1M', '1AS')), + description=("Pandas resample rule")), + 'resample_how': FreeFormSelectField( + 'Resample How', default='', + choices=self.choicify(('', 'mean', 'sum', 'median')), + description=("Pandas resample how")), + 'resample_fillmethod': FreeFormSelectField( + 'Resample Fill Method', default='', + choices=self.choicify(('', 'ffill', 'bfill')), + description=("Pandas resample fill method")), + 'since': FreeFormSelectField( + 'Since', default="7 days ago", + choices=self.choicify([ + '1 hour ago', + '12 hours ago', + '1 day ago', + '7 days ago', + '28 days ago', + '90 days ago', + '1 year ago' + ]), + description=( + "Timestamp from filter. This supports free form typing and " + "natural language as in '1 day ago', '28 days' or '3 years'")), + 'until': FreeFormSelectField('Until', default="now", + choices=self.choicify([ + 'now', + '1 day ago', + '7 days ago', + '28 days ago', + '90 days ago', + '1 year ago']) + ), + 'max_bubble_size': FreeFormSelectField( + 'Max Bubble Size', default="25", + choices=self.choicify([ + '5', + '10', + '15', + '25', + '50', + '75', + '100', + ]) + ), + 'row_limit': + FreeFormSelectField( + 'Row limit', + default=config.get("ROW_LIMIT"), + choices=self.choicify( + [10, 50, 100, 250, 500, 1000, 5000, 10000, 50000])), + 'limit': + FreeFormSelectField( + 'Series limit', + choices=self.choicify(self.series_limits), + default=50, + description=( + "Limits the number of time series that get displayed")), + 'rolling_type': SelectField( + 'Rolling', + default='None', + choices=[(s, s) for s in ['None', 'mean', 'sum', 'std', 'cumsum']], + description=( + "Defines a rolling window function to apply, works along " + "with the [Periods] text box")), + 'rolling_periods': IntegerField( + 'Periods', + validators=[validators.optional()], + description=( + "Defines the size of the rolling window function, " + "relative to the time granularity selected")), + 'series': SelectField( + 'Series', choices=group_by_choices, + default=default_groupby, + description=( + "Defines the grouping of entities. " + "Each serie is shown as a specific color on the chart and " + "has a legend toggle")), + 'entity': SelectField('Entity', choices=group_by_choices, + default=default_groupby, + description="This define the element to be plotted on the chart"), + 'x': SelectField( + 'X Axis', choices=datasource.metrics_combo, + default=default_metric, + description="Metric assigned to the [X] axis"), + 'y': SelectField('Y Axis', choices=datasource.metrics_combo, + default=default_metric, + description="Metric assigned to the [Y] axis"), + 'size': SelectField( + 'Bubble Size', + default=default_metric, + choices=datasource.metrics_combo), + 'url': TextField( + 'URL', default='www.airbnb.com',), + 'where': TextField( + 'Custom WHERE clause', default='', + description=( + "The text in this box gets included in your query's WHERE " + "clause, as an AND to other criteria. You can include " + "complex expression, parenthesis and anything else " + "supported by the backend it is directed towards.")), + 'having': TextField('Custom HAVING clause', default='', + description=( + "The text in this box gets included in your query's HAVING" + " clause, as an AND to other criteria. You can include " + "complex expression, parenthesis and anything else " + "supported by the backend it is directed towards.")), + 'compare_lag': TextField('Comparison Period Lag', + description=( + "Based on granularity, number of time periods to " + "compare against")), + 'compare_suffix': TextField('Comparison suffix', + description="Suffix to apply after the percentage display"), + 'x_axis_format': FreeFormSelectField('X axis format', + default='smart_date', + choices=[ + ('smart_date', 'Adaptative formating'), + ("%m/%d/%Y", '"%m/%d/%Y" | 01/14/2019'), + ("%Y-%m-%d", '"%Y-%m-%d" | 2019-01-14'), + ("%Y-%m-%d %H:%M:%S", + '"%Y-%m-%d %H:%M:%S" | 2019-01-14 01:32:10'), + ("%H:%M:%S", '"%H:%M:%S" | 01:32:10'), + ], + description="D3 format syntax for y axis " + "https://github.com/mbostock/\n" + "d3/wiki/Formatting"), + 'y_axis_format': FreeFormSelectField('Y axis format', + default='.3s', + choices=[ + ('.3s', '".3s" | 12.3k'), + ('.3%', '".3%" | 1234543.210%'), + ('.4r', '".4r" | 12350'), + ('.3f', '".3f" | 12345.432'), + ('+,', '"+," | +12,345.4321'), + ('$,.2f', '"$,.2f" | $12,345.43'), + ], + description="D3 format syntax for y axis " + "https://github.com/mbostock/\n" + "d3/wiki/Formatting"), + 'markup_type': SelectField( + "Markup Type", + choices=self.choicify(['markdown', 'html']), + default="markdown", + description="Pick your favorite markup language"), + 'rotation': SelectField( + "Rotation", + choices=[(s, s) for s in ['random', 'flat', 'square']], + default="random", + description="Rotation to apply to words in the cloud"), + 'line_interpolation': SelectField( + "Line Style", + choices=self.choicify([ + 'linear', 'basis', 'cardinal', 'monotone', + 'step-before', 'step-after']), + default='linear', + description="Line interpolation as defined by d3.js"), + 'code': TextAreaField( + "Code", description="Put your code here", default=''), + 'pandas_aggfunc': SelectField( + "Aggregation function", + choices=self.choicify([ + 'sum', 'mean', 'min', 'max', 'median', 'stdev', 'var']), + default='sum', + description=( + "Aggregate function to apply when pivoting and " + "computing the total rows and columns")), + 'size_from': TextField( + "Font Size From", + default="20", + description="Font size for the smallest value in the list"), + 'size_to': TextField( + "Font Size To", + default="150", + description="Font size for the biggest value in the list"), + 'show_brush': BetterBooleanField( + "Range Filter", default=False, + description=( + "Whether to display the time range interactive selector")), + 'show_datatable': BetterBooleanField( + "Data Table", default=False, + description="Whether to display the interactive data table"), + 'include_search': BetterBooleanField( + "Search Box", default=False, + description=( + "Whether to include a client side search box")), + 'show_bubbles': BetterBooleanField( + "Show Bubbles", default=False, + description=( + "Whether to display bubbles on top of countries")), + 'show_legend': BetterBooleanField( + "Legend", default=True, + description="Whether to display the legend (toggles)"), + 'x_axis_showminmax': BetterBooleanField( + "X bounds", default=True, + description=( + "Whether to display the min and max values of the X axis")), + 'rich_tooltip': BetterBooleanField( + "Rich Tooltip", default=True, + description=( + "The rich tooltip shows a list of all series for that" + " point in time")), + 'y_axis_zero': BetterBooleanField( + "Y Axis Zero", default=False, + description=( + "Force the Y axis to start at 0 instead of the minimum " + "value")), + 'y_log_scale': BetterBooleanField( + "Y Log", default=False, + description="Use a log scale for the Y axis"), + 'x_log_scale': BetterBooleanField( + "X Log", default=False, + description="Use a log scale for the X axis"), + 'donut': BetterBooleanField( + "Donut", default=False, + description="Do you want a donut or a pie?"), + 'contribution': BetterBooleanField( + "Contribution", default=False, + description="Compute the contribution to the total"), + 'num_period_compare': IntegerField( + "Period Ratio", default=None, + validators=[validators.optional()], + description=( + "[integer] Number of period to compare against, " + "this is relative to the granularity selected")), + 'time_compare': TextField( + "Time Shift", + default="", + description=( + "Overlay a timeseries from a " + "relative time period. Expects relative time delta " + "in natural language (example: 24 hours, 7 days, " + "56 weeks, 365 days")), + } + + @staticmethod + def choicify(l): + return [("{}".format(obj), "{}".format(obj)) for obj in l] + + def get_form(self): + """Returns a form object based on the viz/datasource/context""" + viz = self.viz + field_css_classes = {} + for name, obj in self.field_dict.items(): + field_css_classes[name] = ['form-control'] + s = self.fieltype_class.get(obj.field_class) + if s: + field_css_classes[name] += [s] + + for field in ('show_brush', 'show_legend', 'rich_tooltip'): + field_css_classes[field] += ['input-sm'] + + class QueryForm(OmgWtForm): + fieldsets = copy(viz.fieldsets) + css_classes = field_css_classes + standalone = HiddenField() + async = HiddenField() + extra_filters = HiddenField() + json = HiddenField() + slice_id = HiddenField() + slice_name = HiddenField() + previous_viz_type = HiddenField(default=viz.viz_type) + collapsed_fieldsets = HiddenField() + viz_type = self.field_dict.get('viz_type') + + filter_cols = viz.datasource.filterable_column_names or [''] + for i in range(10): + setattr(QueryForm, 'flt_col_' + str(i), SelectField( + 'Filter 1', + default=filter_cols[0], + choices=self.choicify(filter_cols))) + setattr(QueryForm, 'flt_op_' + str(i), SelectField( + 'Filter 1', + default='in', + choices=self.choicify(['in', 'not in']))) + setattr( + QueryForm, 'flt_eq_' + str(i), + TextField("Super", default='')) + + for field in viz.flat_form_fields(): + setattr(QueryForm, field, self.field_dict[field]) + + def add_to_form(attrs): + for attr in attrs: + setattr(QueryForm, attr, self.field_dict[attr]) + + # datasource type specific form elements + if viz.datasource.__class__.__name__ == 'SqlaTable': + QueryForm.fieldsets += ({ + 'label': 'SQL', + 'fields': ['where', 'having'], + 'description': ( + "This section exposes ways to include snippets of " + "SQL in your query"), + },) + add_to_form(('where', 'having')) + grains = viz.datasource.database.grains() + + if not viz.datasource.any_dttm_col: + return QueryForm + if grains: + time_fields = ('granularity_sqla', 'time_grain_sqla') + self.field_dict['time_grain_sqla'] = SelectField( + 'Time Grain', + choices=self.choicify((grain.name for grain in grains)), + default="Time Column", + description=( + "The time granularity for the visualization. This " + "applies a date transformation to alter " + "your time column and defines a new time granularity." + "The options here are defined on a per database " + "engine basis in the Dashed source code")) + add_to_form(time_fields) + field_css_classes['time_grain_sqla'] = ['form-control', 'select2'] + field_css_classes['granularity_sqla'] = ['form-control', 'select2'] + else: + time_fields = 'granularity_sqla' + add_to_form((time_fields, )) + else: + time_fields = 'granularity' + add_to_form(('granularity',)) + field_css_classes['granularity'] = ['form-control', 'select2'] + add_to_form(('since', 'until')) + + QueryForm.fieldsets = ({ + 'label': 'Time', + 'fields': ( + time_fields, + ('since', 'until'), + ), + 'description': "Time related form attributes", + },) + tuple(QueryForm.fieldsets) + return QueryForm diff --git a/dashed/migrations/README b/dashed/migrations/README new file mode 100755 index 000000000..98e4f9c44 --- /dev/null +++ b/dashed/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/dashed/migrations/__init__.py b/dashed/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dashed/migrations/alembic.ini b/dashed/migrations/alembic.ini new file mode 100644 index 000000000..f8ed4801f --- /dev/null +++ b/dashed/migrations/alembic.ini @@ -0,0 +1,45 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/dashed/migrations/env.py b/dashed/migrations/env.py new file mode 100755 index 000000000..e3713a3e4 --- /dev/null +++ b/dashed/migrations/env.py @@ -0,0 +1,88 @@ +from __future__ import with_statement +from alembic import context +from sqlalchemy import engine_from_config, pool +from logging.config import fileConfig +import logging +from flask.ext.appbuilder import Base + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +from flask import current_app +config.set_main_option('sqlalchemy.url', + current_app.config.get('SQLALCHEMY_DATABASE_URI')) +target_metadata = Base.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure(url=url) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.readthedocs.org/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + engine = engine_from_config(config.get_section(config.config_ini_section), + prefix='sqlalchemy.', + poolclass=pool.NullPool) + + connection = engine.connect() + context.configure(connection=connection, + target_metadata=target_metadata, + #compare_type=True, + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args) + + try: + with context.begin_transaction(): + context.run_migrations() + finally: + connection.close() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/dashed/migrations/script.py.mako b/dashed/migrations/script.py.mako new file mode 100755 index 000000000..95702017e --- /dev/null +++ b/dashed/migrations/script.py.mako @@ -0,0 +1,22 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision} +Create Date: ${create_date} + +""" + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/dashed/migrations/versions/12d55656cbca_is_featured.py b/dashed/migrations/versions/12d55656cbca_is_featured.py new file mode 100644 index 000000000..315822374 --- /dev/null +++ b/dashed/migrations/versions/12d55656cbca_is_featured.py @@ -0,0 +1,23 @@ +"""is_featured + +Revision ID: 12d55656cbca +Revises: 55179c7f25c7 +Create Date: 2015-12-14 13:37:17.374852 + +""" + +# revision identifiers, used by Alembic. +revision = '12d55656cbca' +down_revision = '55179c7f25c7' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('tables', sa.Column('is_featured', sa.Boolean(), nullable=True)) + + +def downgrade(): + op.drop_column('tables', 'is_featured') + diff --git a/dashed/migrations/versions/18e88e1cc004_making_audit_nullable.py b/dashed/migrations/versions/18e88e1cc004_making_audit_nullable.py new file mode 100644 index 000000000..0143aad58 --- /dev/null +++ b/dashed/migrations/versions/18e88e1cc004_making_audit_nullable.py @@ -0,0 +1,99 @@ +"""making audit nullable + +Revision ID: 18e88e1cc004 +Revises: 430039611635 +Create Date: 2016-03-13 21:30:24.833107 + +""" + +# revision identifiers, used by Alembic. +revision = '18e88e1cc004' +down_revision = '430039611635' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + try: + op.alter_column( + 'clusters', 'changed_on', + existing_type=sa.DATETIME(), + nullable=True) + op.alter_column( + 'clusters', 'created_on', + existing_type=sa.DATETIME(), nullable=True) + op.drop_constraint(None, 'columns', type_='foreignkey') + op.drop_constraint(None, 'columns', type_='foreignkey') + op.drop_column('columns', 'created_on') + op.drop_column('columns', 'created_by_fk') + op.drop_column('columns', 'changed_on') + op.drop_column('columns', 'changed_by_fk') + op.alter_column('css_templates', 'changed_on', + existing_type=sa.DATETIME(), + nullable=True) + op.alter_column('css_templates', 'created_on', + existing_type=sa.DATETIME(), + nullable=True) + op.alter_column('dashboards', 'changed_on', + existing_type=sa.DATETIME(), + nullable=True) + op.alter_column('dashboards', 'created_on', + existing_type=sa.DATETIME(), + nullable=True) + op.create_unique_constraint(None, 'dashboards', ['slug']) + op.alter_column('datasources', 'changed_by_fk', + existing_type=sa.INTEGER(), + nullable=True) + op.alter_column('datasources', 'changed_on', + existing_type=sa.DATETIME(), + nullable=True) + op.alter_column('datasources', 'created_by_fk', + existing_type=sa.INTEGER(), + nullable=True) + op.alter_column('datasources', 'created_on', + existing_type=sa.DATETIME(), + nullable=True) + op.alter_column('dbs', 'changed_on', + existing_type=sa.DATETIME(), + nullable=True) + op.alter_column('dbs', 'created_on', + existing_type=sa.DATETIME(), + nullable=True) + op.alter_column('slices', 'changed_on', + existing_type=sa.DATETIME(), + nullable=True) + op.alter_column('slices', 'created_on', + existing_type=sa.DATETIME(), + nullable=True) + op.alter_column('sql_metrics', 'changed_on', + existing_type=sa.DATETIME(), + nullable=True) + op.alter_column('sql_metrics', 'created_on', + existing_type=sa.DATETIME(), + nullable=True) + op.alter_column('table_columns', 'changed_on', + existing_type=sa.DATETIME(), + nullable=True) + op.alter_column('table_columns', 'created_on', + existing_type=sa.DATETIME(), + nullable=True) + op.alter_column('tables', 'changed_on', + existing_type=sa.DATETIME(), + nullable=True) + op.alter_column('tables', 'created_on', + existing_type=sa.DATETIME(), + nullable=True) + op.alter_column('url', 'changed_on', + existing_type=sa.DATETIME(), + nullable=True) + op.alter_column('url', 'created_on', + existing_type=sa.DATETIME(), + nullable=True) + ### end Alembic commands ### + except: + pass + + +def downgrade(): + pass diff --git a/dashed/migrations/versions/1a48a5411020_adding_slug_to_dash.py b/dashed/migrations/versions/1a48a5411020_adding_slug_to_dash.py new file mode 100644 index 000000000..c6b88642b --- /dev/null +++ b/dashed/migrations/versions/1a48a5411020_adding_slug_to_dash.py @@ -0,0 +1,26 @@ +"""adding slug to dash + +Revision ID: 1a48a5411020 +Revises: 289ce07647b +Create Date: 2015-12-04 09:42:16.973264 + +""" + +# revision identifiers, used by Alembic. +revision = '1a48a5411020' +down_revision = '289ce07647b' + +from alembic import op +import sqlalchemy as sa + +def upgrade(): + op.add_column('dashboards', sa.Column('slug', sa.String(length=255), nullable=True)) + try: + op.create_unique_constraint('idx_unique_slug', 'dashboards', ['slug']) + except: + pass + + +def downgrade(): + op.drop_constraint(None, 'dashboards', type_='unique') + op.drop_column('dashboards', 'slug') diff --git a/dashed/migrations/versions/1e2841a4128_.py b/dashed/migrations/versions/1e2841a4128_.py new file mode 100644 index 000000000..330b3b217 --- /dev/null +++ b/dashed/migrations/versions/1e2841a4128_.py @@ -0,0 +1,21 @@ +"""empty message + +Revision ID: 1e2841a4128 +Revises: 5a7bad26f2a7 +Create Date: 2015-10-05 22:11:00.537054 + +""" + +# revision identifiers, used by Alembic. +revision = '1e2841a4128' +down_revision = '5a7bad26f2a7' + +from alembic import op +import sqlalchemy as sa + +def upgrade(): + op.add_column('table_columns', sa.Column('expression', sa.Text(), nullable=True)) + + +def downgrade(): + op.drop_column('table_columns', 'expression') diff --git a/dashed/migrations/versions/2591d77e9831_user_id.py b/dashed/migrations/versions/2591d77e9831_user_id.py new file mode 100644 index 000000000..4fac61ce9 --- /dev/null +++ b/dashed/migrations/versions/2591d77e9831_user_id.py @@ -0,0 +1,26 @@ +"""user_id + +Revision ID: 2591d77e9831 +Revises: 12d55656cbca +Create Date: 2015-12-15 17:02:45.128709 + +""" + +# revision identifiers, used by Alembic. +revision = '2591d77e9831' +down_revision = '12d55656cbca' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + with op.batch_alter_table('tables') as batch_op: + batch_op.add_column(sa.Column('user_id', sa.Integer())) + batch_op.create_foreign_key('user_id', 'ab_user', ['user_id'], ['id']) + + +def downgrade(): + with op.batch_alter_table('tables') as batch_op: + batch_op.drop_constraint('user_id', type_='foreignkey') + batch_op.drop_column('user_id') diff --git a/dashed/migrations/versions/289ce07647b_add_encrypted_password_field.py b/dashed/migrations/versions/289ce07647b_add_encrypted_password_field.py new file mode 100644 index 000000000..6d64887b2 --- /dev/null +++ b/dashed/migrations/versions/289ce07647b_add_encrypted_password_field.py @@ -0,0 +1,28 @@ +"""Add encrypted password field + +Revision ID: 289ce07647b +Revises: 2929af7925ed +Create Date: 2015-11-21 11:18:00.650587 + +""" + +# revision identifiers, used by Alembic. +revision = '289ce07647b' +down_revision = '2929af7925ed' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy_utils.types.encrypted import EncryptedType + + +def upgrade(): + op.add_column( + 'dbs', + sa.Column( + 'password', + EncryptedType(sa.String(1024)), + nullable=True)) + + +def downgrade(): + op.drop_column('dbs', 'password') diff --git a/dashed/migrations/versions/2929af7925ed_tz_offsets_in_data_sources.py b/dashed/migrations/versions/2929af7925ed_tz_offsets_in_data_sources.py new file mode 100644 index 000000000..85b54bc5c --- /dev/null +++ b/dashed/migrations/versions/2929af7925ed_tz_offsets_in_data_sources.py @@ -0,0 +1,23 @@ +"""TZ offsets in data sources + +Revision ID: 2929af7925ed +Revises: 1e2841a4128 +Create Date: 2015-10-19 20:54:00.565633 + +""" + +# revision identifiers, used by Alembic. +revision = '2929af7925ed' +down_revision = '1e2841a4128' + +from alembic import op +import sqlalchemy as sa + +def upgrade(): + op.add_column('datasources', sa.Column('offset', sa.Integer(), nullable=True)) + op.add_column('tables', sa.Column('offset', sa.Integer(), nullable=True)) + + +def downgrade(): + op.drop_column('tables', 'offset') + op.drop_column('datasources', 'offset') diff --git a/dashed/migrations/versions/315b3f4da9b0_adding_log_model.py b/dashed/migrations/versions/315b3f4da9b0_adding_log_model.py new file mode 100644 index 000000000..d9fdfacce --- /dev/null +++ b/dashed/migrations/versions/315b3f4da9b0_adding_log_model.py @@ -0,0 +1,30 @@ +"""adding log model + +Revision ID: 315b3f4da9b0 +Revises: 1a48a5411020 +Create Date: 2015-12-04 11:16:58.226984 + +""" + +# revision identifiers, used by Alembic. +revision = '315b3f4da9b0' +down_revision = '1a48a5411020' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table('logs', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('action', sa.String(length=512), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('json', sa.Text(), nullable=True), + sa.Column('dttm', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['ab_user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + + +def downgrade(): + op.drop_table('logs') diff --git a/dashed/migrations/versions/430039611635_log_more.py b/dashed/migrations/versions/430039611635_log_more.py new file mode 100644 index 000000000..aec2b32ed --- /dev/null +++ b/dashed/migrations/versions/430039611635_log_more.py @@ -0,0 +1,23 @@ +"""log more + +Revision ID: 430039611635 +Revises: d827694c7555 +Create Date: 2016-02-10 08:47:28.950891 + +""" + +# revision identifiers, used by Alembic. +revision = '430039611635' +down_revision = 'd827694c7555' + +from alembic import op +import sqlalchemy as sa + +def upgrade(): + op.add_column('logs', sa.Column('dashboard_id', sa.Integer(), nullable=True)) + op.add_column('logs', sa.Column('slice_id', sa.Integer(), nullable=True)) + + +def downgrade(): + op.drop_column('logs', 'slice_id') + op.drop_column('logs', 'dashboard_id') diff --git a/dashed/migrations/versions/43df8de3a5f4_dash_json.py b/dashed/migrations/versions/43df8de3a5f4_dash_json.py new file mode 100644 index 000000000..c56ddc8f5 --- /dev/null +++ b/dashed/migrations/versions/43df8de3a5f4_dash_json.py @@ -0,0 +1,22 @@ +"""empty message + +Revision ID: 43df8de3a5f4 +Revises: 7dbf98566af7 +Create Date: 2016-01-18 23:43:16.073483 + +""" + +# revision identifiers, used by Alembic. +revision = '43df8de3a5f4' +down_revision = '7dbf98566af7' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('dashboards', sa.Column('json_metadata', sa.Text(), nullable=True)) + + +def downgrade(): + op.drop_column('dashboards', 'json_metadata') diff --git a/dashed/migrations/versions/4e6a06bad7a8_init.py b/dashed/migrations/versions/4e6a06bad7a8_init.py new file mode 100644 index 000000000..3b8a9bff4 --- /dev/null +++ b/dashed/migrations/versions/4e6a06bad7a8_init.py @@ -0,0 +1,215 @@ +"""Init + +Revision ID: 4e6a06bad7a8 +Revises: None +Create Date: 2015-09-21 17:30:38.442998 + +""" + +# revision identifiers, used by Alembic. +revision = '4e6a06bad7a8' +down_revision = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.create_table('clusters', + sa.Column('created_on', sa.DateTime(), nullable=False), + sa.Column('changed_on', sa.DateTime(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('cluster_name', sa.String(length=250), nullable=True), + sa.Column('coordinator_host', sa.String(length=256), nullable=True), + sa.Column('coordinator_port', sa.Integer(), nullable=True), + sa.Column('coordinator_endpoint', sa.String(length=256), nullable=True), + sa.Column('broker_host', sa.String(length=256), nullable=True), + sa.Column('broker_port', sa.Integer(), nullable=True), + sa.Column('broker_endpoint', sa.String(length=256), nullable=True), + sa.Column('metadata_last_refreshed', sa.DateTime(), nullable=True), + sa.Column('created_by_fk', sa.Integer(), nullable=True), + sa.Column('changed_by_fk', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ), + sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('cluster_name') + ) + op.create_table('dashboards', + sa.Column('created_on', sa.DateTime(), nullable=False), + sa.Column('changed_on', sa.DateTime(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('dashboard_title', sa.String(length=500), nullable=True), + sa.Column('position_json', sa.Text(), nullable=True), + sa.Column('created_by_fk', sa.Integer(), nullable=True), + sa.Column('changed_by_fk', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ), + sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('dbs', + sa.Column('created_on', sa.DateTime(), nullable=False), + sa.Column('changed_on', sa.DateTime(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('database_name', sa.String(length=250), nullable=True), + sa.Column('sqlalchemy_uri', sa.String(length=1024), nullable=True), + sa.Column('created_by_fk', sa.Integer(), nullable=True), + sa.Column('changed_by_fk', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ), + sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('database_name') + ) + op.create_table('datasources', + sa.Column('created_on', sa.DateTime(), nullable=False), + sa.Column('changed_on', sa.DateTime(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('datasource_name', sa.String(length=250), nullable=True), + sa.Column('is_featured', sa.Boolean(), nullable=True), + sa.Column('is_hidden', sa.Boolean(), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('default_endpoint', sa.Text(), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('cluster_name', sa.String(length=250), nullable=True), + sa.Column('changed_by_fk', sa.Integer(), nullable=False), + sa.Column('created_by_fk', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ), + sa.ForeignKeyConstraint(['cluster_name'], ['clusters.cluster_name'], ), + sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['ab_user.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('datasource_name') + ) + op.create_table('tables', + sa.Column('created_on', sa.DateTime(), nullable=False), + sa.Column('changed_on', sa.DateTime(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('table_name', sa.String(length=250), nullable=True), + sa.Column('main_dttm_col', sa.String(length=250), nullable=True), + sa.Column('default_endpoint', sa.Text(), nullable=True), + sa.Column('database_id', sa.Integer(), nullable=False), + sa.Column('created_by_fk', sa.Integer(), nullable=True), + sa.Column('changed_by_fk', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ), + sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ), + sa.ForeignKeyConstraint(['database_id'], ['dbs.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('table_name') + ) + op.create_table('columns', + sa.Column('created_on', sa.DateTime(), nullable=False), + sa.Column('changed_on', sa.DateTime(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('datasource_name', sa.String(length=250), nullable=True), + sa.Column('column_name', sa.String(length=256), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('type', sa.String(length=32), nullable=True), + sa.Column('groupby', sa.Boolean(), nullable=True), + sa.Column('count_distinct', sa.Boolean(), nullable=True), + sa.Column('sum', sa.Boolean(), nullable=True), + sa.Column('max', sa.Boolean(), nullable=True), + sa.Column('min', sa.Boolean(), nullable=True), + sa.Column('filterable', sa.Boolean(), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('created_by_fk', sa.Integer(), nullable=True), + sa.Column('changed_by_fk', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ), + sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ), + sa.ForeignKeyConstraint(['datasource_name'], ['datasources.datasource_name'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('metrics', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('metric_name', sa.String(length=512), nullable=True), + sa.Column('verbose_name', sa.String(length=1024), nullable=True), + sa.Column('metric_type', sa.String(length=32), nullable=True), + sa.Column('datasource_name', sa.String(length=250), nullable=True), + sa.Column('json', sa.Text(), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['datasource_name'], ['datasources.datasource_name'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('slices', + sa.Column('created_on', sa.DateTime(), nullable=False), + sa.Column('changed_on', sa.DateTime(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('slice_name', sa.String(length=250), nullable=True), + sa.Column('druid_datasource_id', sa.Integer(), nullable=True), + sa.Column('table_id', sa.Integer(), nullable=True), + sa.Column('datasource_type', sa.String(length=200), nullable=True), + sa.Column('datasource_name', sa.String(length=2000), nullable=True), + sa.Column('viz_type', sa.String(length=250), nullable=True), + sa.Column('params', sa.Text(), nullable=True), + sa.Column('created_by_fk', sa.Integer(), nullable=True), + sa.Column('changed_by_fk', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ), + sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ), + sa.ForeignKeyConstraint(['druid_datasource_id'], ['datasources.id'], ), + sa.ForeignKeyConstraint(['table_id'], ['tables.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('sql_metrics', + sa.Column('created_on', sa.DateTime(), nullable=False), + sa.Column('changed_on', sa.DateTime(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('metric_name', sa.String(length=512), nullable=True), + sa.Column('verbose_name', sa.String(length=1024), nullable=True), + sa.Column('metric_type', sa.String(length=32), nullable=True), + sa.Column('table_id', sa.Integer(), nullable=True), + sa.Column('expression', sa.Text(), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('created_by_fk', sa.Integer(), nullable=True), + sa.Column('changed_by_fk', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ), + sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ), + sa.ForeignKeyConstraint(['table_id'], ['tables.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('table_columns', + sa.Column('created_on', sa.DateTime(), nullable=False), + sa.Column('changed_on', sa.DateTime(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('table_id', sa.Integer(), nullable=True), + sa.Column('column_name', sa.String(length=256), nullable=True), + sa.Column('is_dttm', sa.Boolean(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('type', sa.String(length=32), nullable=True), + sa.Column('groupby', sa.Boolean(), nullable=True), + sa.Column('count_distinct', sa.Boolean(), nullable=True), + sa.Column('sum', sa.Boolean(), nullable=True), + sa.Column('max', sa.Boolean(), nullable=True), + sa.Column('min', sa.Boolean(), nullable=True), + sa.Column('filterable', sa.Boolean(), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('created_by_fk', sa.Integer(), nullable=True), + sa.Column('changed_by_fk', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ), + sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ), + sa.ForeignKeyConstraint(['table_id'], ['tables.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('dashboard_slices', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('dashboard_id', sa.Integer(), nullable=True), + sa.Column('slice_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['dashboard_id'], ['dashboards.id'], ), + sa.ForeignKeyConstraint(['slice_id'], ['slices.id'], ), + sa.PrimaryKeyConstraint('id') + ) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_table('dashboard_slices') + op.drop_table('table_columns') + op.drop_table('sql_metrics') + op.drop_table('slices') + op.drop_table('metrics') + op.drop_table('columns') + op.drop_table('tables') + op.drop_table('datasources') + op.drop_table('dbs') + op.drop_table('dashboards') + op.drop_table('clusters') + ### end Alembic commands ### diff --git a/dashed/migrations/versions/55179c7f25c7_sqla_descr.py b/dashed/migrations/versions/55179c7f25c7_sqla_descr.py new file mode 100644 index 000000000..aade0b930 --- /dev/null +++ b/dashed/migrations/versions/55179c7f25c7_sqla_descr.py @@ -0,0 +1,22 @@ +"""sqla_descr + +Revision ID: 55179c7f25c7 +Revises: 315b3f4da9b0 +Create Date: 2015-12-13 08:38:43.704145 + +""" + +# revision identifiers, used by Alembic. +revision = '55179c7f25c7' +down_revision = '315b3f4da9b0' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('tables', sa.Column('description', sa.Text(), nullable=True)) + + +def downgrade(): + op.drop_column('tables', 'description') diff --git a/dashed/migrations/versions/5a7bad26f2a7_.py b/dashed/migrations/versions/5a7bad26f2a7_.py new file mode 100644 index 000000000..66dc20aae --- /dev/null +++ b/dashed/migrations/versions/5a7bad26f2a7_.py @@ -0,0 +1,24 @@ +"""empty message + +Revision ID: 5a7bad26f2a7 +Revises: 4e6a06bad7a8 +Create Date: 2015-10-05 10:32:15.850753 + +""" + +# revision identifiers, used by Alembic. +revision = '5a7bad26f2a7' +down_revision = '4e6a06bad7a8' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('dashboards', sa.Column('css', sa.Text(), nullable=True)) + op.add_column('dashboards', sa.Column('description', sa.Text(), nullable=True)) + + +def downgrade(): + op.drop_column('dashboards', 'description') + op.drop_column('dashboards', 'css') diff --git a/dashed/migrations/versions/7dbf98566af7_slice_description.py b/dashed/migrations/versions/7dbf98566af7_slice_description.py new file mode 100644 index 000000000..329af9ef2 --- /dev/null +++ b/dashed/migrations/versions/7dbf98566af7_slice_description.py @@ -0,0 +1,20 @@ +"""empty message + +Revision ID: 7dbf98566af7 +Revises: 8e80a26a31db +Create Date: 2016-01-17 22:00:23.640788 + +""" + +# revision identifiers, used by Alembic. +revision = '7dbf98566af7' +down_revision = '8e80a26a31db' + +from alembic import op +import sqlalchemy as sa + +def upgrade(): + op.add_column('slices', sa.Column('description', sa.Text(), nullable=True)) + +def downgrade(): + op.drop_column('slices', 'description') diff --git a/dashed/migrations/versions/8e80a26a31db_.py b/dashed/migrations/versions/8e80a26a31db_.py new file mode 100644 index 000000000..54edc58a8 --- /dev/null +++ b/dashed/migrations/versions/8e80a26a31db_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: 8e80a26a31db +Revises: 2591d77e9831 +Create Date: 2016-01-13 20:24:45.256437 + +""" + +# revision identifiers, used by Alembic. +revision = '8e80a26a31db' +down_revision = '2591d77e9831' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table('url', + sa.Column('created_on', sa.DateTime(), nullable=False), + sa.Column('changed_on', sa.DateTime(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('url', sa.Text(), nullable=True), + sa.Column('created_by_fk', sa.Integer(), nullable=True), + sa.Column('changed_by_fk', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ), + sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + + +def downgrade(): + op.drop_table('url') diff --git a/dashed/migrations/versions/d827694c7555_css_templates.py b/dashed/migrations/versions/d827694c7555_css_templates.py new file mode 100644 index 000000000..3b20e4405 --- /dev/null +++ b/dashed/migrations/versions/d827694c7555_css_templates.py @@ -0,0 +1,33 @@ +"""css templates + +Revision ID: d827694c7555 +Revises: 43df8de3a5f4 +Create Date: 2016-02-03 17:41:10.944019 + +""" + +# revision identifiers, used by Alembic. +revision = 'd827694c7555' +down_revision = '43df8de3a5f4' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table('css_templates', + sa.Column('created_on', sa.DateTime(), nullable=False), + sa.Column('changed_on', sa.DateTime(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('template_name', sa.String(length=250), nullable=True), + sa.Column('css', sa.Text(), nullable=True), + sa.Column('changed_by_fk', sa.Integer(), nullable=True), + sa.Column('created_by_fk', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ), + sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + + +def downgrade(): + op.drop_table('css_templates') diff --git a/dashed/models.py b/dashed/models.py new file mode 100644 index 000000000..1db3fd62d --- /dev/null +++ b/dashed/models.py @@ -0,0 +1,1198 @@ +""" +A collection of ORM sqlalchemy models for Dashed +""" + +from copy import deepcopy, copy +from collections import namedtuple +from datetime import timedelta, datetime +import functools +import json +import logging +from six import string_types +import sqlparse +import requests + +from dateutil.parser import parse +from flask import flash, request, g +from flask.ext.appbuilder import Model +from flask.ext.appbuilder.models.mixins import AuditMixin +import pandas as pd +from pydruid import client +from pydruid.utils.filters import Dimension, Filter + +import sqlalchemy as sqla +from sqlalchemy import ( + Column, Integer, String, ForeignKey, Text, Boolean, DateTime, + Table, create_engine, MetaData, desc, select, and_, func) +from sqlalchemy.engine import reflection +from sqlalchemy.orm import relationship +from sqlalchemy.sql import table, literal_column, text, column +from sqlalchemy.sql.elements import ColumnClause +from sqlalchemy_utils import EncryptedType + +from dashed import app, db, get_session, utils +from dashed.viz import viz_types +from sqlalchemy.ext.declarative import declared_attr + +config = app.config + +QueryResult = namedtuple('namedtuple', ['df', 'query', 'duration']) + + +class AuditMixinNullable(AuditMixin): + + """Altering the AuditMixin to use nullable fields + + Allows creating objects programmatically outside of CRUD + """ + + created_on = Column(DateTime, default=datetime.now, nullable=True) + changed_on = Column( + DateTime, default=datetime.now, + onupdate=datetime.now, nullable=True) + + @declared_attr + def created_by_fk(cls): + return Column(Integer, ForeignKey('ab_user.id'), + default=cls.get_user_id, nullable=True) + + @declared_attr + def changed_by_fk(cls): + return Column(Integer, ForeignKey('ab_user.id'), + default=cls.get_user_id, onupdate=cls.get_user_id, nullable=True) + + @property + def created_by_(self): + return '{}'.format(self.created_by or '') + + @property # noqa + def changed_by_(self): + return '{}'.format(self.changed_by or '') + + +class Url(Model, AuditMixinNullable): + + """Used for the short url feature""" + + __tablename__ = 'url' + id = Column(Integer, primary_key=True) + url = Column(Text) + + +class CssTemplate(Model, AuditMixinNullable): + + """CSS templates for dashboards""" + + __tablename__ = 'css_templates' + id = Column(Integer, primary_key=True) + template_name = Column(String(250)) + css = Column(Text, default='') + + +class Slice(Model, AuditMixinNullable): + + """A slice is essentially a report or a view on data""" + + __tablename__ = 'slices' + id = Column(Integer, primary_key=True) + slice_name = Column(String(250)) + druid_datasource_id = Column(Integer, ForeignKey('datasources.id')) + table_id = Column(Integer, ForeignKey('tables.id')) + datasource_type = Column(String(200)) + datasource_name = Column(String(2000)) + viz_type = Column(String(250)) + params = Column(Text) + description = Column(Text) + + table = relationship( + 'SqlaTable', foreign_keys=[table_id], backref='slices') + druid_datasource = relationship( + 'DruidDatasource', foreign_keys=[druid_datasource_id], backref='slices') + + def __repr__(self): + return self.slice_name + + @property + def datasource(self): + return self.table or self.druid_datasource + + @property + def datasource_link(self): + if self.table: + return self.table.link + elif self.druid_datasource: + return self.druid_datasource.link + + @property + @utils.memoized + def viz(self): + d = json.loads(self.params) + viz = viz_types[self.viz_type]( + self.datasource, + form_data=d) + return viz + + @property + def description_markeddown(self): + return utils.markdown(self.description) + + @property + def datasource_id(self): + return self.table_id or self.druid_datasource_id + + @property + def data(self): + d = self.viz.data + d['slice_id'] = self.id + return d + + @property + def json_data(self): + return json.dumps(self.data) + + @property + def slice_url(self): + """Defines the url to access the slice""" + try: + slice_params = json.loads(self.params) + except Exception as e: + logging.exception(e) + slice_params = {} + slice_params['slice_id'] = self.id + slice_params['slice_name'] = self.slice_name + from werkzeug.urls import Href + href = Href( + "/dashed/explore/{self.datasource_type}/" + "{self.datasource_id}/".format(self=self)) + return href(slice_params) + + @property + def edit_url(self): + return "/slicemodelview/edit/{}".format(self.id) + + @property + def slice_link(self): + url = self.slice_url + return '{self.slice_name}'.format( + url=url, self=self) + + +dashboard_slices = Table('dashboard_slices', Model.metadata, + Column('id', Integer, primary_key=True), + Column('dashboard_id', Integer, ForeignKey('dashboards.id')), + Column('slice_id', Integer, ForeignKey('slices.id')), +) + + +class Dashboard(Model, AuditMixinNullable): + + """The dashboard object!""" + + __tablename__ = 'dashboards' + id = Column(Integer, primary_key=True) + dashboard_title = Column(String(500)) + position_json = Column(Text) + description = Column(Text) + css = Column(Text) + json_metadata = Column(Text) + slug = Column(String(255), unique=True) + slices = relationship( + 'Slice', secondary=dashboard_slices, backref='dashboards') + + def __repr__(self): + return self.dashboard_title + + @property + def url(self): + return "/dashed/dashboard/{}/".format(self.slug or self.id) + + @property + def metadata_dejson(self): + if self.json_metadata: + return json.loads(self.json_metadata) + else: + return {} + + def dashboard_link(self): + return '{self.dashboard_title}'.format(self=self) + + @property + def json_data(self): + d = { + 'id': self.id, + 'metadata': self.metadata_dejson, + 'dashboard_title': self.dashboard_title, + 'slug': self.slug, + 'slices': [slc.data for slc in self.slices], + } + return json.dumps(d) + + +class Queryable(object): + """A common interface to objects that are queryable (tables and datasources)""" + @property + def column_names(self): + return sorted([c.column_name for c in self.columns]) + + @property + def main_dttm_col(self): + return "timestamp" + + @property + def groupby_column_names(self): + return sorted([c.column_name for c in self.columns if c.groupby]) + + @property + def filterable_column_names(self): + return sorted([c.column_name for c in self.columns if c.filterable]) + + @property + def dttm_cols(self): + return [] + + +class Database(Model, AuditMixinNullable): + + """An ORM object that stores Database related information""" + + __tablename__ = 'dbs' + id = Column(Integer, primary_key=True) + database_name = Column(String(250), unique=True) + sqlalchemy_uri = Column(String(1024)) + password = Column(EncryptedType(String(1024), config.get('SECRET_KEY'))) + + def __repr__(self): + return self.database_name + + def get_sqla_engine(self): + return create_engine(self.sqlalchemy_uri_decrypted) + + def safe_sqlalchemy_uri(self): + return self.sqlalchemy_uri + + def grains(self): + """Defines time granularity database-specific expressions. + + The idea here is to make it easy for users to change the time grain + form a datetime (maybe the source grain is arbitrary timestamps, daily + or 5 minutes increments) to another, "truncated" datetime. Since + each database has slightly different but similar datetime functions, + this allows a mapping between database engines and actual functions. + """ + Grain = namedtuple('Grain', 'name function') + DB_TIME_GRAINS = { + 'presto': ( + Grain('Time Column', '{col}'), + Grain('week', "date_trunc('week', CAST({col} AS DATE))"), + Grain('month', "date_trunc('month', CAST({col} AS DATE))"), + ), + 'mysql': ( + Grain('Time Column', '{col}'), + Grain('day', 'DATE({col})'), + Grain('week', 'DATE_SUB({col}, INTERVAL DAYOFWEEK({col}) - 1 DAY)'), + Grain('month', 'DATE_SUB({col}, INTERVAL DAYOFMONTH({col}) - 1 DAY)'), + ), + } + for db_type, grains in DB_TIME_GRAINS.items(): + if self.sqlalchemy_uri.startswith(db_type): + return grains + + def grains_dict(self): + return {grain.name: grain for grain in self.grains()} + + def get_table(self, table_name): + meta = MetaData() + return Table( + table_name, meta, + autoload=True, + autoload_with=self.get_sqla_engine()) + + def get_columns(self, table_name): + engine = self.get_sqla_engine() + insp = reflection.Inspector.from_engine(engine) + return insp.get_columns(table_name) + + @property + def sqlalchemy_uri_decrypted(self): + conn = sqla.engine.url.make_url(self.sqlalchemy_uri) + conn.password = self.password + return str(conn) + + @property + def sql_url(self): + return '/dashed/sql/{}/'.format(self.id) + + @property + def sql_link(self): + return 'SQL'.format(self.sql_url) + + +class SqlaTable(Model, Queryable, AuditMixinNullable): + + """An ORM object for SqlAlchemy table references""" + + type = "table" + + __tablename__ = 'tables' + id = Column(Integer, primary_key=True) + table_name = Column(String(250), unique=True) + main_dttm_col = Column(String(250)) + description = Column(Text) + default_endpoint = Column(Text) + database_id = Column(Integer, ForeignKey('dbs.id'), nullable=False) + is_featured = Column(Boolean, default=False) + user_id = Column(Integer, ForeignKey('ab_user.id')) + owner = relationship('User', backref='tables', foreign_keys=[user_id]) + database = relationship( + 'Database', backref='tables', foreign_keys=[database_id]) + offset = Column(Integer, default=0) + + baselink = "tablemodelview" + + def __repr__(self): + return self.table_name + + @property + def description_markeddown(self): + return utils.markdown(self.description) + + @property + def url(self): + return '/tablemodelview/edit/{}'.format(self.id) + + @property + def link(self): + return '{self.table_name}'.format(**locals()) + + @property + def perm(self): + return ( + "[{self.database}].[{self.table_name}]" + "(id:{self.id})").format(self=self) + + @property + def full_name(self): + return "[{self.database}].[{self.table_name}]".format(self=self) + + @property + def dttm_cols(self): + l = [c.column_name for c in self.columns if c.is_dttm] + if self.main_dttm_col not in l: + l.append(self.main_dttm_col) + return l + + @property + def any_dttm_col(self): + cols = self.dttm_cols + if cols: + return cols[0] + + @property + def html(self): + t = ((c.column_name, c.type) for c in self.columns) + df = pd.DataFrame(t) + df.columns = ['field', 'type'] + return df.to_html( + index=False, + classes=( + "dataframe table table-striped table-bordered " + "table-condensed")) + + @property + def name(self): + return self.table_name + + @property + def table_link(self): + url = "/dashed/explore/{self.type}/{self.id}/".format(self=self) + return '{self.table_name}'.format( + url=url, self=self) + + @property + def metrics_combo(self): + return sorted( + [ + (m.metric_name, m.verbose_name or m.metric_name) + for m in self.metrics], + key=lambda x: x[1]) + + @property + def sql_url(self): + return self.database.sql_url + "?table_name=" + str(self.table_name) + + @property + def sql_link(self): + return 'SQL'.format(self.sql_url) + + def query( + self, groupby, metrics, + granularity, + from_dttm, to_dttm, + filter=None, # noqa + is_timeseries=True, + timeseries_limit=15, row_limit=None, + inner_from_dttm=None, inner_to_dttm=None, + extras=None, + columns=None): + + # For backward compatibility + if granularity not in self.dttm_cols: + granularity = self.main_dttm_col + + cols = {col.column_name: col for col in self.columns} + qry_start_dttm = datetime.now() + + if not granularity and is_timeseries: + raise Exception( + "Datetime column not provided as part table configuration " + "and is required by this type of chart") + + metrics_exprs = [ + literal_column(m.expression).label(m.metric_name) + for m in self.metrics if m.metric_name in metrics] + + if metrics: + main_metric_expr = literal_column([ + m.expression for m in self.metrics + if m.metric_name == metrics[0]][0]) + else: + main_metric_expr = literal_column("COUNT(*)") + + select_exprs = [] + groupby_exprs = [] + + if groupby: + select_exprs = [] + inner_select_exprs = [] + inner_groupby_exprs = [] + for s in groupby: + col = cols[s] + expr = col.expression + if expr: + outer = literal_column(expr).label(s) + inner = literal_column(expr).label('__' + s) + else: + outer = column(s).label(s) + inner = column(s).label('__' + s) + + groupby_exprs.append(outer) + select_exprs.append(outer) + inner_groupby_exprs.append(inner) + inner_select_exprs.append(inner) + elif columns: + for s in columns: + select_exprs.append(s) + metrics_exprs = [] + + if granularity: + dttm_expr = cols[granularity].expression or granularity + timestamp = literal_column(dttm_expr).label('timestamp') + + # Transforming time grain into an expression based on configuration + time_grain_sqla = extras.get('time_grain_sqla') + if time_grain_sqla: + udf = self.database.grains_dict().get(time_grain_sqla, '{col}') + timestamp_grain = literal_column( + udf.function.format(col=dttm_expr)).label('timestamp') + else: + timestamp_grain = timestamp + + if is_timeseries: + select_exprs += [timestamp_grain] + groupby_exprs += [timestamp_grain] + + tf = '%Y-%m-%d %H:%M:%S.%f' + time_filter = [ + timestamp >= from_dttm.strftime(tf), + timestamp <= to_dttm.strftime(tf), + ] + inner_time_filter = copy(time_filter) + if inner_from_dttm: + inner_time_filter[0] = timestamp >= inner_from_dttm.strftime(tf) + if inner_to_dttm: + inner_time_filter[1] = timestamp <= inner_to_dttm.strftime(tf) + + select_exprs += metrics_exprs + qry = select(select_exprs) + from_clause = table(self.table_name) + if not columns: + qry = qry.group_by(*groupby_exprs) + + where_clause_and = [] + having_clause_and = [] + for col, op, eq in filter: + col_obj = cols[col] + if op in ('in', 'not in'): + values = eq.split(",") + if col_obj.expression: + cond = ColumnClause( + col_obj.expression, is_literal=True).in_(values) + else: + cond = column(col).in_(values) + if op == 'not in': + cond = ~cond + where_clause_and.append(cond) + if extras and 'where' in extras: + where_clause_and += [text(extras['where'])] + if extras and 'having' in extras: + having_clause_and += [text(extras['having'])] + if granularity: + qry = qry.where(and_(*(time_filter + where_clause_and))) + qry = qry.having(and_(*having_clause_and)) + if groupby: + qry = qry.order_by(desc(main_metric_expr)) + qry = qry.limit(row_limit) + + if timeseries_limit and groupby: + subq = select(inner_select_exprs) + subq = subq.select_from(table(self.table_name)) + subq = subq.where(and_(*(where_clause_and + inner_time_filter))) + subq = subq.group_by(*inner_groupby_exprs) + subq = subq.order_by(desc(main_metric_expr)) + subq = subq.limit(timeseries_limit) + on_clause = [] + for i, gb in enumerate(groupby): + on_clause.append( + groupby_exprs[i] == column("__" + gb)) + + from_clause = from_clause.join(subq.alias(), and_(*on_clause)) + + qry = qry.select_from(from_clause) + + engine = self.database.get_sqla_engine() + sql = "{}".format( + qry.compile(engine, compile_kwargs={"literal_binds": True})) + df = pd.read_sql_query( + sql=sql, + con=engine + ) + sql = sqlparse.format(sql, reindent=True) + return QueryResult( + df=df, duration=datetime.now() - qry_start_dttm, query=sql) + + def fetch_metadata(self): + """Fetches the metadata for the table and merges it in""" + table = self.database.get_table(self.table_name) + try: + table = self.database.get_table(self.table_name) + except Exception as e: + flash(str(e)) + flash( + "Table doesn't seem to exist in the specified database, " + "couldn't fetch column information", "danger") + return + + TC = TableColumn + M = SqlMetric + metrics = [] + any_date_col = None + for col in table.columns: + try: + datatype = str(col.type) + except Exception as e: + datatype = "UNKNOWN" + dbcol = ( + db.session + .query(TC) + .filter(TC.table == self) + .filter(TC.column_name == col.name) + .first() + ) + db.session.flush() + if not dbcol: + dbcol = TableColumn(column_name=col.name) + + if ( + str(datatype).startswith('VARCHAR') or + str(datatype).startswith('STRING')): + dbcol.groupby = True + dbcol.filterable = True + elif str(datatype).upper() in ('DOUBLE', 'FLOAT', 'INT', 'BIGINT'): + dbcol.sum = True + db.session.merge(self) + self.columns.append(dbcol) + + if not any_date_col and 'date' in datatype.lower(): + any_date_col = col.name + + quoted = "{}".format( + column(dbcol.column_name).compile(dialect=db.engine.dialect)) + if dbcol.sum: + metrics.append(M( + metric_name='sum__' + dbcol.column_name, + verbose_name='sum__' + dbcol.column_name, + metric_type='sum', + expression="SUM({})".format(quoted) + )) + if dbcol.max: + metrics.append(M( + metric_name='max__' + dbcol.column_name, + verbose_name='max__' + dbcol.column_name, + metric_type='max', + expression="MAX({})".format(quoted) + )) + if dbcol.min: + metrics.append(M( + metric_name='min__' + dbcol.column_name, + verbose_name='min__' + dbcol.column_name, + metric_type='min', + expression="MIN({})".format(quoted) + )) + if dbcol.count_distinct: + metrics.append(M( + metric_name='count_distinct__' + dbcol.column_name, + verbose_name='count_distinct__' + dbcol.column_name, + metric_type='count_distinct', + expression="COUNT(DISTINCT {})".format(quoted) + )) + dbcol.type = datatype + db.session.merge(self) + db.session.commit() + + metrics.append(M( + metric_name='count', + verbose_name='COUNT(*)', + metric_type='count', + expression="COUNT(*)" + )) + for metric in metrics: + m = ( + db.session.query(M) + .filter(M.metric_name == metric.metric_name) + .filter(M.table_id == self.id) + .first() + ) + metric.table_id = self.id + if not m: + db.session.add(metric) + db.session.commit() + if not self.main_dttm_col: + self.main_dttm_col = any_date_col + + +class SqlMetric(Model, AuditMixinNullable): + + """ORM object for metrics, each table can have multiple metrics""" + + __tablename__ = 'sql_metrics' + id = Column(Integer, primary_key=True) + metric_name = Column(String(512)) + verbose_name = Column(String(1024)) + metric_type = Column(String(32)) + table_id = Column(Integer, ForeignKey('tables.id')) + table = relationship( + 'SqlaTable', backref='metrics', foreign_keys=[table_id]) + expression = Column(Text) + description = Column(Text) + + +class TableColumn(Model, AuditMixinNullable): + + """ORM object for table columns, each table can have multiple columns""" + + __tablename__ = 'table_columns' + id = Column(Integer, primary_key=True) + table_id = Column(Integer, ForeignKey('tables.id')) + table = relationship( + 'SqlaTable', backref='columns', foreign_keys=[table_id]) + column_name = Column(String(256)) + is_dttm = Column(Boolean, default=False) + is_active = Column(Boolean, default=True) + type = Column(String(32), default='') + groupby = Column(Boolean, default=False) + count_distinct = Column(Boolean, default=False) + sum = Column(Boolean, default=False) + max = Column(Boolean, default=False) + min = Column(Boolean, default=False) + filterable = Column(Boolean, default=False) + expression = Column(Text, default='') + description = Column(Text, default='') + + def __repr__(self): + return self.column_name + + @property + def isnum(self): + return self.type in ('LONG', 'DOUBLE', 'FLOAT') + + +class DruidCluster(Model, AuditMixinNullable): + + """ORM object referencing the Druid clusters""" + + __tablename__ = 'clusters' + id = Column(Integer, primary_key=True) + cluster_name = Column(String(250), unique=True) + coordinator_host = Column(String(256)) + coordinator_port = Column(Integer) + coordinator_endpoint = Column( + String(256), default='druid/coordinator/v1/metadata') + broker_host = Column(String(256)) + broker_port = Column(Integer) + broker_endpoint = Column(String(256), default='druid/v2') + metadata_last_refreshed = Column(DateTime) + + def __repr__(self): + return self.cluster_name + + def get_pydruid_client(self): + cli = client.PyDruid( + "http://{0}:{1}/".format(self.broker_host, self.broker_port), + self.broker_endpoint) + return cli + + def refresh_datasources(self): + endpoint = ( + "http://{self.coordinator_host}:{self.coordinator_port}/" + "{self.coordinator_endpoint}/datasources" + ).format(self=self) + + datasources = json.loads(requests.get(endpoint).text) + for datasource in datasources: + DruidDatasource.sync_to_db(datasource, self) + + +class DruidDatasource(Model, AuditMixinNullable, Queryable): + + """ORM object referencing Druid datasources (tables)""" + + type = "druid" + + baselink = "datasourcemodelview" + + __tablename__ = 'datasources' + id = Column(Integer, primary_key=True) + datasource_name = Column(String(250), unique=True) + is_featured = Column(Boolean, default=False) + is_hidden = Column(Boolean, default=False) + description = Column(Text) + default_endpoint = Column(Text) + user_id = Column(Integer, ForeignKey('ab_user.id')) + owner = relationship('User', backref='datasources', foreign_keys=[user_id]) + cluster_name = Column( + String(250), ForeignKey('clusters.cluster_name')) + cluster = relationship( + 'DruidCluster', backref='datasources', foreign_keys=[cluster_name]) + offset = Column(Integer, default=0) + + @property + def metrics_combo(self): + return sorted( + [(m.metric_name, m.verbose_name) for m in self.metrics], + key=lambda x: x[1]) + + @property + def name(self): + return self.datasource_name + + @property + def perm(self): + return ( + "[{self.cluster_name}].[{self.datasource_name}]" + "(id:{self.id})").format(self=self) + + @property + def url(self): + return '/datasourcemodelview/edit/{}'.format(self.id) + + @property + def link(self): + return ( + '' + '{self.datasource_name}').format(**locals()) + + @property + def full_name(self): + return ( + "[{self.cluster_name}]." + "[{self.datasource_name}]").format(self=self) + + def __repr__(self): + return self.datasource_name + + @property + def datasource_link(self): + url = "/dashed/explore/{self.type}/{self.id}/".format(self=self) + return '{self.datasource_name}'.format( + url=url, self=self) + + def get_metric_obj(self, metric_name): + return [ + m.json_obj for m in self.metrics + if m.metric_name == metric_name + ][0] + + def latest_metadata(self): + """Returns segment metadata from the latest segment""" + client = self.cluster.get_pydruid_client() + results = client.time_boundary(datasource=self.datasource_name) + if not results: + return + max_time = results[0]['result']['maxTime'] + max_time = parse(max_time) + intervals = (max_time - timedelta(seconds=1)).isoformat() + '/' + intervals += (max_time + timedelta(seconds=1)).isoformat() + segment_metadata = client.segment_metadata( + datasource=self.datasource_name, + intervals=intervals) + if segment_metadata: + return segment_metadata[-1]['columns'] + + def generate_metrics(self): + for col in self.columns: + col.generate_metrics() + + @classmethod + def sync_to_db(cls, name, cluster): + """Fetches metadata for that datasource and merges the Dashed db""" + print("Syncing Druid datasource [{}]".format(name)) + session = get_session() + datasource = session.query(cls).filter_by(datasource_name=name).first() + if not datasource: + datasource = cls(datasource_name=name) + session.add(datasource) + flash("Adding new datasource [{}]".format(name), "success") + else: + flash("Refreshing datasource [{}]".format(name), "info") + datasource.cluster = cluster + + cols = datasource.latest_metadata() + if not cols: + return + for col in cols: + col_obj = ( + session + .query(DruidColumn) + .filter_by(datasource_name=name, column_name=col) + .first() + ) + datatype = cols[col]['type'] + if not col_obj: + col_obj = DruidColumn(datasource_name=name, column_name=col) + session.add(col_obj) + if datatype == "STRING": + col_obj.groupby = True + col_obj.filterable = True + if col_obj: + col_obj.type = cols[col]['type'] + col_obj.datasource = datasource + col_obj.generate_metrics() + + def query( + self, groupby, metrics, + granularity, + from_dttm, to_dttm, + filter=None, # noqa + is_timeseries=True, + timeseries_limit=None, + row_limit=None, + inner_from_dttm=None, inner_to_dttm=None, + extras=None, # noqa + select=None): + """Runs a query against Druid and returns a dataframe. + + This query interface is common to SqlAlchemy and Druid + """ + # TODO refactor into using a TBD Query object + qry_start_dttm = datetime.now() + + inner_from_dttm = inner_from_dttm or from_dttm + inner_to_dttm = inner_to_dttm or to_dttm + + # add tzinfo to native datetime with config + from_dttm = from_dttm.replace(tzinfo=config.get("DRUID_TZ")) + to_dttm = to_dttm.replace(tzinfo=config.get("DRUID_TZ")) + + query_str = "" + aggregations = { + m.metric_name: m.json_obj + for m in self.metrics if m.metric_name in metrics + } + granularity = granularity or "all" + if granularity != "all": + granularity = utils.parse_human_timedelta( + granularity).total_seconds() * 1000 + if not isinstance(granularity, string_types): + granularity = {"type": "duration", "duration": granularity} + + qry = dict( + datasource=self.datasource_name, + dimensions=groupby, + aggregations=aggregations, + granularity=granularity, + intervals=from_dttm.isoformat() + '/' + to_dttm.isoformat(), + ) + filters = None + for col, op, eq in filter: + cond = None + if op == '==': + cond = Dimension(col) == eq + elif op == '!=': + cond = ~(Dimension(col) == eq) + elif op in ('in', 'not in'): + fields = [] + splitted = eq.split(',') + if len(splitted) > 1: + for s in eq.split(','): + s = s.strip() + fields.append(Filter.build_filter(Dimension(col) == s)) + cond = Filter(type="or", fields=fields) + else: + cond = Dimension(col) == eq + if op == 'not in': + cond = ~cond + if filters: + filters = Filter(type="and", fields=[ + Filter.build_filter(cond), + Filter.build_filter(filters) + ]) + else: + filters = cond + + if filters: + qry['filter'] = filters + + client = self.cluster.get_pydruid_client() + orig_filters = filters + if timeseries_limit and is_timeseries: + # Limit on the number of timeseries, doing a two-phases query + pre_qry = deepcopy(qry) + pre_qry['granularity'] = "all" + pre_qry['limit_spec'] = { + "type": "default", + "limit": timeseries_limit, + 'intervals': ( + inner_from_dttm.isoformat() + '/' + + inner_to_dttm.isoformat()), + "columns": [{ + "dimension": metrics[0] if metrics else self.metrics[0], + "direction": "descending", + }], + } + client.groupby(**pre_qry) + query_str += "// Two phase query\n// Phase 1\n" + query_str += json.dumps(client.query_dict, indent=2) + "\n" + query_str += "//\nPhase 2 (built based on phase one's results)\n" + df = client.export_pandas() + if df is not None and not df.empty: + dims = qry['dimensions'] + filters = [] + for _, row in df.iterrows(): + fields = [] + for dim in dims: + f = Filter.build_filter(Dimension(dim) == row[dim]) + fields.append(f) + if len(fields) > 1: + filt = Filter(type="and", fields=fields) + filters.append(Filter.build_filter(filt)) + elif fields: + filters.append(fields[0]) + + if filters: + ff = Filter(type="or", fields=filters) + if not orig_filters: + qry['filter'] = ff + else: + qry['filter'] = Filter(type="and", fields=[ + Filter.build_filter(ff), + Filter.build_filter(orig_filters)]) + qry['limit_spec'] = None + if row_limit: + qry['limit_spec'] = { + "type": "default", + "limit": row_limit, + "columns": [{ + "dimension": metrics[0] if metrics else self.metrics[0], + "direction": "descending", + }], + } + client.groupby(**qry) + query_str += json.dumps(client.query_dict, indent=2) + df = client.export_pandas() + if df is None or df.size == 0: + raise Exception("No data was returned.") + + if ( + not is_timeseries and + granularity == "all" and + 'timestamp' in df.columns): + del df['timestamp'] + + # Reordering columns + cols = [] + if 'timestamp' in df.columns: + cols += ['timestamp'] + cols += [col for col in groupby if col in df.columns] + cols += [col for col in metrics if col in df.columns] + cols += [col for col in df.columns if col not in cols] + df = df[cols] + return QueryResult( + df=df, + query=query_str, + duration=datetime.now() - qry_start_dttm) + + +class Log(Model): + + """ORM object used to log Dashed actions to the database""" + + __tablename__ = 'logs' + + id = Column(Integer, primary_key=True) + action = Column(String(512)) + user_id = Column(Integer, ForeignKey('ab_user.id')) + dashboard_id = Column(Integer) + slice_id = Column(Integer) + user_id = Column(Integer, ForeignKey('ab_user.id')) + json = Column(Text) + user = relationship('User', backref='logs', foreign_keys=[user_id]) + dttm = Column(DateTime, default=func.now()) + + @classmethod + def log_this(cls, f): + """Decorator to log user actions""" + @functools.wraps(f) + def wrapper(*args, **kwargs): + user_id = None + if g.user: + user_id = g.user.id + d = request.args.to_dict() + d.update(kwargs) + log = cls( + action=f.__name__, + json=json.dumps(d), + dashboard_id=d.get('dashboard_id') or None, + slice_id=d.get('slice_id') or None, + user_id=user_id) + db.session.add(log) + db.session.commit() + return f(*args, **kwargs) + return wrapper + + + + +class DruidMetric(Model): + + """ORM object referencing Druid metrics for a datasource""" + + __tablename__ = 'metrics' + id = Column(Integer, primary_key=True) + metric_name = Column(String(512)) + verbose_name = Column(String(1024)) + metric_type = Column(String(32)) + datasource_name = Column( + String(250), + ForeignKey('datasources.datasource_name')) + datasource = relationship('DruidDatasource', backref='metrics') + json = Column(Text) + description = Column(Text) + + @property + def json_obj(self): + try: + obj = json.loads(self.json) + except Exception: + obj = {} + return obj + + +class DruidColumn(Model): + + """ORM model for storing Druid datasource column metadata""" + + __tablename__ = 'columns' + id = Column(Integer, primary_key=True) + datasource_name = Column( + String(250), + ForeignKey('datasources.datasource_name')) + datasource = relationship('DruidDatasource', backref='columns') + column_name = Column(String(256)) + is_active = Column(Boolean, default=True) + type = Column(String(32)) + groupby = Column(Boolean, default=False) + count_distinct = Column(Boolean, default=False) + sum = Column(Boolean, default=False) + max = Column(Boolean, default=False) + min = Column(Boolean, default=False) + filterable = Column(Boolean, default=False) + description = Column(Text) + + def __repr__(self): + return self.column_name + + @property + def isnum(self): + return self.type in ('LONG', 'DOUBLE', 'FLOAT') + + def generate_metrics(self): + """Generate metrics based on the column metadata""" + M = DruidMetric + metrics = [] + metrics.append(DruidMetric( + metric_name='count', + verbose_name='COUNT(*)', + metric_type='count', + json=json.dumps({'type': 'count', 'name': 'count'}) + )) + # Somehow we need to reassign this for UDAFs + if self.type in ('DOUBLE', 'FLOAT'): + corrected_type = 'DOUBLE' + else: + corrected_type = self.type + + if self.sum and self.isnum: + mt = corrected_type.lower() + 'Sum' + name = 'sum__' + self.column_name + metrics.append(DruidMetric( + metric_name=name, + metric_type='sum', + verbose_name='SUM({})'.format(self.column_name), + json=json.dumps({ + 'type': mt, 'name': name, 'fieldName': self.column_name}) + )) + if self.min and self.isnum: + mt = corrected_type.lower() + 'Min' + name = 'min__' + self.column_name + metrics.append(DruidMetric( + metric_name=name, + metric_type='min', + verbose_name='MIN({})'.format(self.column_name), + json=json.dumps({ + 'type': mt, 'name': name, 'fieldName': self.column_name}) + )) + if self.max and self.isnum: + mt = corrected_type.lower() + 'Max' + name = 'max__' + self.column_name + metrics.append(DruidMetric( + metric_name=name, + metric_type='max', + verbose_name='MAX({})'.format(self.column_name), + json=json.dumps({ + 'type': mt, 'name': name, 'fieldName': self.column_name}) + )) + if self.count_distinct: + mt = 'count_distinct' + name = 'count_distinct__' + self.column_name + metrics.append(DruidMetric( + metric_name=name, + verbose_name='COUNT(DISTINCT {})'.format(self.column_name), + metric_type='count_distinct', + json=json.dumps({ + 'type': 'cardinality', + 'name': name, + 'fieldNames': [self.column_name]}) + )) + session = get_session() + for metric in metrics: + m = ( + session.query(M) + .filter(M.metric_name == metric.metric_name) + .filter(M.datasource_name == self.datasource_name) + .filter(DruidCluster.cluster_name == self.datasource.cluster_name) + .first() + ) + metric.datasource_name = self.datasource_name + if not m: + session.add(metric) + session.commit() diff --git a/dashed/static/assets b/dashed/static/assets new file mode 120000 index 000000000..ec2e4be2f --- /dev/null +++ b/dashed/static/assets @@ -0,0 +1 @@ +../assets \ No newline at end of file diff --git a/dashed/static/docs b/dashed/static/docs new file mode 120000 index 000000000..932170420 --- /dev/null +++ b/dashed/static/docs @@ -0,0 +1 @@ +../../docs/_build/html/ \ No newline at end of file diff --git a/dashed/static/favicon.png b/dashed/static/favicon.png new file mode 100644 index 000000000..50c8c9a45 Binary files /dev/null and b/dashed/static/favicon.png differ diff --git a/dashed/static/img/bubble.png b/dashed/static/img/bubble.png new file mode 100644 index 000000000..a65d5ed8b Binary files /dev/null and b/dashed/static/img/bubble.png differ diff --git a/dashed/static/img/cardash.jpg b/dashed/static/img/cardash.jpg new file mode 100644 index 000000000..e8dbcc49c Binary files /dev/null and b/dashed/static/img/cardash.jpg differ diff --git a/dashed/static/img/cloud.png b/dashed/static/img/cloud.png new file mode 100644 index 000000000..9478806cf Binary files /dev/null and b/dashed/static/img/cloud.png differ diff --git a/dashed/static/img/dash.png b/dashed/static/img/dash.png new file mode 100644 index 000000000..83ecf8e57 Binary files /dev/null and b/dashed/static/img/dash.png differ diff --git a/dashed/static/img/dashed_screenshot.png b/dashed/static/img/dashed_screenshot.png new file mode 100644 index 000000000..804576455 Binary files /dev/null and b/dashed/static/img/dashed_screenshot.png differ diff --git a/dashed/static/img/favicon.png b/dashed/static/img/favicon.png new file mode 100644 index 000000000..35fc3c161 Binary files /dev/null and b/dashed/static/img/favicon.png differ diff --git a/dashed/static/img/gallery.jpg b/dashed/static/img/gallery.jpg new file mode 100644 index 000000000..42ebad239 Binary files /dev/null and b/dashed/static/img/gallery.jpg differ diff --git a/dashed/static/img/loading.gif b/dashed/static/img/loading.gif new file mode 100644 index 000000000..01ae3939c Binary files /dev/null and b/dashed/static/img/loading.gif differ diff --git a/dashed/static/img/penguins.png b/dashed/static/img/penguins.png new file mode 100644 index 000000000..14bfc440e Binary files /dev/null and b/dashed/static/img/penguins.png differ diff --git a/dashed/static/img/serpe.jpg b/dashed/static/img/serpe.jpg new file mode 100644 index 000000000..79a91666a Binary files /dev/null and b/dashed/static/img/serpe.jpg differ diff --git a/dashed/static/img/servers.jpg b/dashed/static/img/servers.jpg new file mode 100644 index 000000000..2d4604b6b Binary files /dev/null and b/dashed/static/img/servers.jpg differ diff --git a/dashed/static/img/slice.jpg b/dashed/static/img/slice.jpg new file mode 100644 index 000000000..68c53c4e8 Binary files /dev/null and b/dashed/static/img/slice.jpg differ diff --git a/dashed/templates/appbuilder/baselayout.html b/dashed/templates/appbuilder/baselayout.html new file mode 100644 index 000000000..33110ce16 --- /dev/null +++ b/dashed/templates/appbuilder/baselayout.html @@ -0,0 +1,40 @@ +{% extends 'appbuilder/init.html' %} +{% import 'appbuilder/baselib.html' as baselib %} + +{% block body %} + {% include 'appbuilder/general/confirm.html' %} + {% include 'appbuilder/general/alert.html' %} + + {% block navbar %} +
+ {% include 'appbuilder/navbar.html' %} +
+ {% endblock %} + + {% block uncontained %}{% endblock %} + +
+
+ {% block messages %} + {% include 'appbuilder/flash.html' %} + {% endblock %} + {% block content %} + {% endblock %} +
+
+
+ {% block content_fluid %} + {% endblock %} +
+ + {% block footer %} +
+ +
+ {% endblock %} +{% endblock %} + diff --git a/dashed/templates/appbuilder/general/widgets/list.html b/dashed/templates/appbuilder/general/widgets/list.html new file mode 100644 index 000000000..b0c28eda9 --- /dev/null +++ b/dashed/templates/appbuilder/general/widgets/list.html @@ -0,0 +1,81 @@ +{% import 'appbuilder/general/lib.html' as lib %} +{% extends 'appbuilder/general/widgets/base_list.html' %} + + + {% block begin_content scoped %} +
+ + {% endblock %} + + {% block begin_loop_header scoped %} + + + {% if actions %} + + {% endif %} + + {% if can_show or can_edit or can_delete %} + + {% endif %} + + {% for item in include_columns %} + {% if item in order_columns %} + {% set res = item | get_link_order(modelview_name) %} + {% if res == 2 %} + + {% elif res == 1 %} + + {% else %} + + {% endif %} + {% else %} + + {% endif %} + {% endfor %} + + + {% endblock %} + + {% block begin_loop_values %} + {% for item in value_columns %} + {% set pk = pks[loop.index-1] %} + + {% if actions %} + + {% endif %} + {% if can_show or can_edit or can_delete %} + + {% endif %} + {% for value in include_columns %} + + {% endfor %} + + {% endfor %} + {% endblock %} + + {% block end_content scoped %} +
+ + {{label_columns.get(item)}} + {{label_columns.get(item)}} + {{label_columns.get(item)}} + {{label_columns.get(item)}}
+ +
+ {{ lib.btn_crud(can_show, can_edit, can_delete, pk, modelview_name, filters) }} +
+ {% if item[value].__class__.__name__ == 'bool' %} + + {% else %} + {{ item[value]|safe }} + {% endif %} +
+
+ {% endblock %} + diff --git a/dashed/templates/appbuilder/navbar.html b/dashed/templates/appbuilder/navbar.html new file mode 100644 index 000000000..d0d260c2c --- /dev/null +++ b/dashed/templates/appbuilder/navbar.html @@ -0,0 +1,35 @@ + + + +{% set menu = appbuilder.menu %} +{% set languages = appbuilder.languages %} + + diff --git a/dashed/templates/appbuilder/navbar_right.html b/dashed/templates/appbuilder/navbar_right.html new file mode 100644 index 000000000..433bf1bf4 --- /dev/null +++ b/dashed/templates/appbuilder/navbar_right.html @@ -0,0 +1,43 @@ + +{% macro locale_menu(languages) %} +{% set locale = session['locale'] %} +{% if not locale %} + {% set locale = 'en' %} +{% endif %} + +{% endmacro %} + + + +{% if not current_user.is_anonymous() %} + +{% else %} +
  • + {{_("Login")}}
  • +{% endif %} diff --git a/dashed/templates/dashed/ajah.html b/dashed/templates/dashed/ajah.html new file mode 100644 index 000000000..b5d122680 --- /dev/null +++ b/dashed/templates/dashed/ajah.html @@ -0,0 +1 @@ +{{ content |safe }} diff --git a/dashed/templates/dashed/base.html b/dashed/templates/dashed/base.html new file mode 100644 index 000000000..da9e1cee9 --- /dev/null +++ b/dashed/templates/dashed/base.html @@ -0,0 +1,12 @@ +{% extends "appbuilder/baselayout.html" %} + + {% block head_css %} + {{super()}} + + + {% endblock %} + + {% block head_js %} + {{super()}} + + {% endblock %} diff --git a/dashed/templates/dashed/basic.html b/dashed/templates/dashed/basic.html new file mode 100644 index 000000000..8d0cba178 --- /dev/null +++ b/dashed/templates/dashed/basic.html @@ -0,0 +1,36 @@ + + + + {% block title %} + {% if appbuilder and appbuilder.app_name %} {{ appbuilder.app_name }} {% endif %} + {% endblock %} + + {% block head_meta %}{% endblock %} + {% block head_css %} + + + {% endblock %} + {% block head_js %} + + {% endblock %} + + + + {% block navbar %} + {% if not viz or viz.request.args.get("standalone") != "true" %} +
    + {% include 'appbuilder/navbar.html' %} +
    + {% endif %} + {% endblock %} + + {% block body %} +
    + Oops! React.js is not working properly. +
    + {% endblock %} + + {% block tail_js %} + {% endblock %} + + diff --git a/dashed/templates/dashed/dashboard.html b/dashed/templates/dashed/dashboard.html new file mode 100644 index 000000000..905435cfe --- /dev/null +++ b/dashed/templates/dashed/dashboard.html @@ -0,0 +1,139 @@ +{% extends "dashed/basic.html" %} + +{% block head_js %} + {{ super() }} + +{% endblock %} + +{% block body %} +
    + + + + +
    +
    +
    +
    +

    + {{ dashboard.dashboard_title }} +

    +
    +
    +
    + + + + + + +
    +
    +
    +
    + +
    +{% endblock %} diff --git a/dashed/templates/dashed/explore.html b/dashed/templates/dashed/explore.html new file mode 100644 index 000000000..106bd487a --- /dev/null +++ b/dashed/templates/dashed/explore.html @@ -0,0 +1,212 @@ +{% extends "dashed/basic.html" %} + +{% block body %} + {% set datasource = viz.datasource %} + {% set form = viz.form %} + + {% macro panofield(fieldname)%} +
    + {% set field = form.get_field(fieldname)%} +
    + {{ viz.get_form_override(fieldname, 'label') or field.label }} + {% if field.description %} + + {% endif %} + {{ field(class_=form.field_css_classes(field.name)) }} +
    +
    + {% endmacro %} + +
    + +
    +{% endblock %} + +{% block tail_js %} + {{ super() }} + +{% endblock %} diff --git a/dashed/templates/dashed/featured.html b/dashed/templates/dashed/featured.html new file mode 100644 index 000000000..8b41f6c9b --- /dev/null +++ b/dashed/templates/dashed/featured.html @@ -0,0 +1,42 @@ +{% extends "dashed/basic.html" %} + +{% block head_js %} + {{ super() }} + +{% endblock %} + +{% block body %} +
    +
    +

    Featured Datasets

    +
    +
    + + + + + + + + + + + {% for dataset in featured_datasets %} + + + + + + + {% endfor %} + + +
    +
    +{% endblock %} + diff --git a/dashed/templates/dashed/index.html b/dashed/templates/dashed/index.html new file mode 100644 index 000000000..71ed52780 --- /dev/null +++ b/dashed/templates/dashed/index.html @@ -0,0 +1,6 @@ +{% extends "dashed/basic.html" %} + +{% block tail_js %} + {{ super() }} + +{% endblock %} diff --git a/dashed/templates/dashed/models/database/add.html b/dashed/templates/dashed/models/database/add.html new file mode 100644 index 000000000..79b6cf6cb --- /dev/null +++ b/dashed/templates/dashed/models/database/add.html @@ -0,0 +1,7 @@ +{% extends "appbuilder/general/model/add.html" %} + +{% import "dashed/models/database/macros.html" as macros %} +{% block tail_js %} + {{ super() }} + {{ macros.testconn() }} +{% endblock %} diff --git a/dashed/templates/dashed/models/database/edit.html b/dashed/templates/dashed/models/database/edit.html new file mode 100644 index 000000000..b1db50f2b --- /dev/null +++ b/dashed/templates/dashed/models/database/edit.html @@ -0,0 +1,7 @@ +{% extends "appbuilder/general/model/edit.html" %} + +{% import "dashed/models/database/macros.html" as macros %} +{% block tail_js %} + {{ super() }} + {{ macros.testconn() }} +{% endblock %} diff --git a/dashed/templates/dashed/models/database/macros.html b/dashed/templates/dashed/models/database/macros.html new file mode 100644 index 000000000..dac8b6d50 --- /dev/null +++ b/dashed/templates/dashed/models/database/macros.html @@ -0,0 +1,29 @@ +{% macro testconn() %} + +{% endmacro %} diff --git a/dashed/templates/dashed/no_data.html b/dashed/templates/dashed/no_data.html new file mode 100644 index 000000000..a8de807a6 --- /dev/null +++ b/dashed/templates/dashed/no_data.html @@ -0,0 +1,5 @@ +{% extends "dashed/datasource.html" %} + +{% block viz %} +No data: review your incantations. +{% endblock %} diff --git a/dashed/templates/dashed/sql.html b/dashed/templates/dashed/sql.html new file mode 100644 index 000000000..fd660eac3 --- /dev/null +++ b/dashed/templates/dashed/sql.html @@ -0,0 +1,99 @@ +{% extends "dashed/basic.html" %} + +{% block head_css %} + {{super()}} + + + +{% endblock %} + +{% block body %} +
    + +
    +{% endblock %} + +{% block tail_js %} +{{ super() }} + +{% endblock %} diff --git a/dashed/templates/dashed/standalone.html b/dashed/templates/dashed/standalone.html new file mode 100644 index 000000000..319aad13c --- /dev/null +++ b/dashed/templates/dashed/standalone.html @@ -0,0 +1,21 @@ + + + + + {% set CSS_THEME = appbuilder.get_app.config.get("CSS_THEME") %} + {% set height = request.args.get("height", 700) %} + {% if CSS_THEME %} + + {% endif %} + + +
    + loading +
    +
    + + diff --git a/dashed/templates/dashed/traceback.html b/dashed/templates/dashed/traceback.html new file mode 100644 index 000000000..97fc6b496 --- /dev/null +++ b/dashed/templates/dashed/traceback.html @@ -0,0 +1,10 @@ + + + +
    {{ art }}
    +{{ title }}
    +{{ error_msg }}
    +
    +
    + + diff --git a/dashed/templates/dashed/viz.html b/dashed/templates/dashed/viz.html new file mode 100644 index 000000000..ea1dc7fc0 --- /dev/null +++ b/dashed/templates/dashed/viz.html @@ -0,0 +1,11 @@ +{% if viz.form_data.get("json") == "true" %} + {{ viz.get_json() }} +{% else %} + + {% if viz.request.args.get("standalone") == "true" %} + {% extends 'dashed/standalone.html' %} + {% else %} + {% extends 'dashed/explore.html' %} + {% endif %} + +{% endif %} diff --git a/dashed/templates/index.html b/dashed/templates/index.html new file mode 100644 index 000000000..e2f16ba7e --- /dev/null +++ b/dashed/templates/index.html @@ -0,0 +1,116 @@ +{% extends "appbuilder/base.html" %} + +{% block uncontained %} +
    + +
    +
    +
    +
    + +

    Dashboards

    +

    Browse the dashboards list

    +

    + +

    +
    +
    + +

    Slices

    +

    "Slices" are individual views into a single dataset

    +

    + +

    +
    +
    + Generic placeholder image +

    Gallery

    +

    Navigate through the growing set of visualizations

    +

    + +

    +
    +
    +
    +
    +{% endblock %} + +{% block tail_js %} +{{ super() }} + +{% endblock %} diff --git a/dashed/utils.py b/dashed/utils.py new file mode 100644 index 000000000..dee2cdd9b --- /dev/null +++ b/dashed/utils.py @@ -0,0 +1,248 @@ +"""Utility functions used across Dashed""" + +from datetime import datetime +import hashlib +import functools +import json +import logging + +from dateutil.parser import parse +from sqlalchemy.types import TypeDecorator, TEXT +from markdown import markdown as md +import parsedatetime +from flask_appbuilder.security.sqla import models as ab_models + + +class memoized(object): + + """Decorator that caches a function's return value each time it is called. + If called later with the same arguments, the cached value is returned, and + not re-evaluated. + """ + + def __init__(self, func): + self.func = func + self.cache = {} + def __call__(self, *args): + try: + return self.cache[args] + except KeyError: + value = self.func(*args) + self.cache[args] = value + return value + except TypeError: + # uncachable -- for instance, passing a list as an argument. + # Better to not cache than to blow up entirely. + return self.func(*args) + def __repr__(self): + """Return the function's docstring.""" + return self.func.__doc__ + def __get__(self, obj, objtype): + """Support instance methods.""" + return functools.partial(self.__call__, obj) + +def list_minus(l, minus): + """Returns l without what is in minus + + >>> list_minus([1, 2, 3], [2]) + [1, 3] + """ + return [o for o in l if o not in minus] + +def parse_human_datetime(s): + """ + Returns ``datetime.datetime`` from human readable strings + + >>> from datetime import date, timedelta + >>> from dateutil.relativedelta import relativedelta + >>> parse_human_datetime('2015-04-03') + datetime.datetime(2015, 4, 3, 0, 0) + >>> parse_human_datetime('2/3/1969') + datetime.datetime(1969, 2, 3, 0, 0) + >>> parse_human_datetime("now") <= datetime.now() + True + >>> parse_human_datetime("yesterday") <= datetime.now() + True + >>> date.today() - timedelta(1) == parse_human_datetime('yesterday').date() + True + >>> year_ago_1 = parse_human_datetime('one year ago').date() + >>> year_ago_2 = (datetime.now() - relativedelta(years=1) ).date() + >>> year_ago_1 == year_ago_2 + True + """ + try: + dttm = parse(s) + except Exception: + try: + cal = parsedatetime.Calendar() + dttm = dttm_from_timtuple(cal.parse(s)[0]) + except Exception as e: + logging.exception(e) + raise ValueError("Couldn't parse date string [{}]".format(s)) + return dttm + + +def dttm_from_timtuple(d): + return datetime( + d.tm_year, d.tm_mon, d.tm_mday, d.tm_hour, d.tm_min, d.tm_sec) + + +def merge_perm(sm, permission_name, view_menu_name): + pv = sm.find_permission_view_menu(permission_name, view_menu_name) + if not pv: + sm.add_permission_view_menu(permission_name, view_menu_name) + + +def parse_human_timedelta(s): + """ + Returns ``datetime.datetime`` from natural language time deltas + + >>> parse_human_datetime("now") <= datetime.now() + True + """ + cal = parsedatetime.Calendar() + dttm = dttm_from_timtuple(datetime.now().timetuple()) + d = cal.parse(s, dttm)[0] + d = datetime( + d.tm_year, d.tm_mon, d.tm_mday, d.tm_hour, d.tm_min, d.tm_sec) + return d - dttm + + +class JSONEncodedDict(TypeDecorator): + + """Represents an immutable structure as a json-encoded string.""" + + impl = TEXT + def process_bind_param(self, value, dialect): + if value is not None: + value = json.dumps(value) + + return value + + def process_result_value(self, value, dialect): + if value is not None: + value = json.loads(value) + return value + + +class ColorFactory(object): + + """Used to generated arrays of colors server side""" + + BNB_COLORS = [ + #rausch hackb kazan babu lima beach barol + '#ff5a5f', '#7b0051', '#007A87', '#00d1c1', '#8ce071', '#ffb400', '#b4a76c', + '#ff8083', '#cc0086', '#00a1b3', '#00ffeb', '#bbedab', '#ffd266', '#cbc29a', + '#ff3339', '#ff1ab1', '#005c66', '#00b3a5', '#55d12e', '#b37e00', '#988b4e', + ] + + def __init__(self, hash_based=False): + self.d = {} + self.hash_based = hash_based + + def get(self, s): + """Gets a color from a string and memoize the association + + >>> cf = ColorFactory() + >>> cf.get('item_1') + '#ff5a5f' + >>> cf.get('item_2') + '#7b0051' + >>> cf.get('item_1') + '#ff5a5f' + """ + if self.hash_based: + s = s.encode('utf-8') + h = hashlib.md5(s) + i = int(h.hexdigest(), 16) + else: + if s not in self.d: + self.d[s] = len(self.d) + i = self.d[s] + return self.BNB_COLORS[i % len(self.BNB_COLORS)] + + +def init(dashed): + """Inits the Dashed application with security roles and such""" + db = dashed.db + models = dashed.models + sm = dashed.appbuilder.sm + alpha = sm.add_role("Alpha") + admin = sm.add_role("Admin") + + merge_perm(sm, 'all_datasource_access', 'all_datasource_access') + + perms = db.session.query(ab_models.PermissionView).all() + for perm in perms: + if perm.permission.name == 'datasource_access': + continue + if perm.view_menu.name not in ( + 'UserDBModelView', 'RoleModelView', 'ResetPasswordView', + 'Security'): + sm.add_permission_role(alpha, perm) + sm.add_permission_role(admin, perm) + gamma = sm.add_role("Gamma") + for perm in perms: + if( + perm.view_menu.name not in ( + 'ResetPasswordView', + 'RoleModelView', + 'UserDBModelView', + 'Security') and + perm.permission.name not in ( + 'all_datasource_access', + 'can_add', + 'can_download', + 'can_delete', + 'can_edit', + 'can_save', + 'datasource_access', + 'muldelete', + )): + sm.add_permission_role(gamma, perm) + session = db.session() + table_perms = [ + table.perm for table in session.query(models.SqlaTable).all()] + table_perms += [ + table.perm for table in session.query(models.DruidDatasource).all()] + for table_perm in table_perms: + merge_perm(sm, 'datasource_access', table_perm) + + +def datetime_f(dttm): + """Formats datetime to take less room when it is recent""" + if dttm: + dttm = dttm.isoformat() + now_iso = datetime.now().isoformat() + if now_iso[:10] == dttm[:10]: + dttm = dttm[11:] + elif now_iso[:4] == dttm[:4]: + dttm = dttm[5:] + return "{}".format(dttm) + + +def json_iso_dttm_ser(obj): + """ + json serializer that deals with dates + + >>> dttm = datetime(1970, 1, 1) + >>> json.dumps({'dttm': dttm}, default=json_iso_dttm_ser) + '{"dttm": "1970-01-01T00:00:00"}' + """ + if isinstance(obj, datetime): + obj = obj.isoformat() + return obj + + +def markdown(s): + s = s or '' + return md(s, [ + 'markdown.extensions.tables', + 'markdown.extensions.fenced_code', + 'markdown.extensions.codehilite',]) + + +def readfile(filepath): + with open(filepath) as f: + content = f.read() + return content diff --git a/dashed/views.py b/dashed/views.py new file mode 100644 index 000000000..9720dbdd4 --- /dev/null +++ b/dashed/views.py @@ -0,0 +1,774 @@ +"""Flask web views for Dashed""" + +from datetime import datetime +import json +import logging +import re +import traceback + +from flask import request, redirect, flash, Response, render_template, Markup +from flask.ext.appbuilder import ModelView, CompactCRUDMixin, BaseView, expose +from flask.ext.appbuilder.actions import action +from flask.ext.appbuilder.models.sqla.interface import SQLAInterface +from flask.ext.appbuilder.security.decorators import has_access +from pydruid.client import doublesum +from sqlalchemy import create_engine +import sqlalchemy as sqla +from wtforms.validators import ValidationError +import pandas as pd +from sqlalchemy import select, text +from sqlalchemy.sql.expression import TextAsFrom + +from dashed import appbuilder, db, models, viz, utils, app, sm, ascii_art + +config = app.config +log_this = models.Log.log_this + + +def validate_json(form, field): # noqa + try: + json.loads(field.data) + except Exception as e: + logging.exception(e) + raise ValidationError("json isn't valid") + + +class DeleteMixin(object): + @action( + "muldelete", "Delete", "Delete all Really?", "fa-trash", single=False) + def muldelete(self, items): + self.datamodel.delete_all(items) + self.update_redirect() + return redirect(self.get_redirect()) + + +class DashedModelView(ModelView): + page_size = 500 + + +class TableColumnInlineView(CompactCRUDMixin, DashedModelView): # noqa + datamodel = SQLAInterface(models.TableColumn) + can_delete = False + edit_columns = [ + 'column_name', 'description', 'groupby', 'filterable', 'table', + 'count_distinct', 'sum', 'min', 'max', 'expression', 'is_dttm'] + add_columns = edit_columns + list_columns = [ + 'column_name', 'type', 'groupby', 'filterable', 'count_distinct', + 'sum', 'min', 'max', 'is_dttm'] + page_size = 500 + description_columns = { + 'is_dttm': ( + "Whether to make this column available as a " + "[Time Granularity] option, column has to be DATETIME or " + "DATETIME-like"), + } +appbuilder.add_view_no_menu(TableColumnInlineView) + +appbuilder.add_link( + "Featured Datasets", + href='/dashed/featured', + category='Sources', + category_icon='fa-table', + icon="fa-star") + +appbuilder.add_separator("Sources") + + +class DruidColumnInlineView(CompactCRUDMixin, DashedModelView): # noqa + datamodel = SQLAInterface(models.DruidColumn) + edit_columns = [ + 'column_name', 'description', 'datasource', 'groupby', + 'count_distinct', 'sum', 'min', 'max'] + list_columns = [ + 'column_name', 'type', 'groupby', 'filterable', 'count_distinct', + 'sum', 'min', 'max'] + can_delete = False + page_size = 500 + + def post_update(self, col): + col.generate_metrics() + +appbuilder.add_view_no_menu(DruidColumnInlineView) + + +class SqlMetricInlineView(CompactCRUDMixin, DashedModelView): # noqa + datamodel = SQLAInterface(models.SqlMetric) + list_columns = ['metric_name', 'verbose_name', 'metric_type'] + edit_columns = [ + 'metric_name', 'description', 'verbose_name', 'metric_type', + 'expression', 'table'] + add_columns = edit_columns + page_size = 500 +appbuilder.add_view_no_menu(SqlMetricInlineView) + + +class DruidMetricInlineView(CompactCRUDMixin, DashedModelView): # noqa + datamodel = SQLAInterface(models.DruidMetric) + list_columns = ['metric_name', 'verbose_name', 'metric_type'] + edit_columns = [ + 'metric_name', 'description', 'verbose_name', 'metric_type', + 'datasource', 'json'] + add_columns = [ + 'metric_name', 'verbose_name', 'metric_type', 'datasource', 'json'] + page_size = 500 + validators_columns = { + 'json': [validate_json], + } +appbuilder.add_view_no_menu(DruidMetricInlineView) + + +class DatabaseView(DashedModelView, DeleteMixin): # noqa + datamodel = SQLAInterface(models.Database) + list_columns = ['database_name', 'sql_link', 'created_by_', 'changed_on'] + order_columns = utils.list_minus(list_columns, ['created_by_']) + add_columns = ['database_name', 'sqlalchemy_uri'] + search_exclude_columns = ('password',) + edit_columns = add_columns + add_template = "dashed/models/database/add.html" + edit_template = "dashed/models/database/edit.html" + base_order = ('changed_on','desc') + description_columns = { + 'sqlalchemy_uri': ( + "Refer to the SqlAlchemy docs for more information on how " + "to structure your URI here: " + "http://docs.sqlalchemy.org/en/rel_1_0/core/engines.html") + } + def pre_add(self, db): + conn = sqla.engine.url.make_url(db.sqlalchemy_uri) + db.password = conn.password + conn.password = "X" * 10 if conn.password else None + db.sqlalchemy_uri = str(conn) # hides the password + + def pre_update(self, db): + self.pre_add(db) + + +appbuilder.add_view( + DatabaseView, + "Databases", + icon="fa-database", + category="Sources", + category_icon='fa-database',) + + +class TableModelView(DashedModelView, DeleteMixin): # noqa + datamodel = SQLAInterface(models.SqlaTable) + list_columns = [ + 'table_link', 'database', 'sql_link', 'is_featured', + 'changed_by_', 'changed_on'] + add_columns = ['table_name', 'database', 'default_endpoint', 'offset'] + edit_columns = [ + 'table_name', 'is_featured', 'database', 'description', 'owner', + 'main_dttm_col', 'default_endpoint', 'offset'] + related_views = [TableColumnInlineView, SqlMetricInlineView] + base_order = ('changed_on','desc') + description_columns = { + 'offset': "Timezone offset (in hours) for this datasource", + 'description': Markup( + "Supports " + "markdown"), + } + + def post_add(self, table): + try: + table.fetch_metadata() + except Exception as e: + logging.exception(e) + flash( + "Table [{}] doesn't seem to exist, " + "couldn't fetch metadata".format(table.table_name), + "danger") + utils.merge_perm(sm, 'datasource_access', table.perm) + + def post_update(self, table): + self.post_add(table) + +appbuilder.add_view( + TableModelView, + "Tables", + category="Sources", + icon='fa-table',) + + +appbuilder.add_separator("Sources") + + +class DruidClusterModelView(DashedModelView, DeleteMixin): # noqa + datamodel = SQLAInterface(models.DruidCluster) + add_columns = [ + 'cluster_name', + 'coordinator_host', 'coordinator_port', 'coordinator_endpoint', + 'broker_host', 'broker_port', 'broker_endpoint', + ] + edit_columns = add_columns + list_columns = ['cluster_name', 'metadata_last_refreshed'] + +appbuilder.add_view( + DruidClusterModelView, + "Druid Clusters", + icon="fa-cubes", + category="Sources", + category_icon='fa-database',) + + +class SliceModelView(DashedModelView, DeleteMixin): # noqa + datamodel = SQLAInterface(models.Slice) + can_add = False + list_columns = [ + 'slice_link', 'viz_type', + 'datasource_link', 'created_by_', 'changed_on'] + order_columns = utils.list_minus(list_columns, ['created_by_']) + edit_columns = [ + 'slice_name', 'description', 'viz_type', 'druid_datasource', + 'table', 'dashboards', 'params'] + base_order = ('changed_on','desc') + description_columns = { + 'description': Markup( + "The content here can be displayed as widget headers in the " + "dashboard view. Supports " + "" + "markdown"), + } + + +appbuilder.add_view( + SliceModelView, + "Slices", + icon="fa-bar-chart", + category="", + category_icon='',) + + +class DashboardModelView(DashedModelView, DeleteMixin): # noqa + datamodel = SQLAInterface(models.Dashboard) + list_columns = ['dashboard_link', 'created_by_', 'changed_on'] + order_columns = utils.list_minus(list_columns, ['created_by_']) + edit_columns = [ + 'dashboard_title', 'slug', 'slices', 'position_json', 'css', + 'json_metadata'] + add_columns = edit_columns + base_order = ('changed_on','desc') + description_columns = { + 'position_json': ( + "This json object describes the positioning of the widgets in " + "the dashboard. It is dynamically generated when adjusting " + "the widgets size and positions by using drag & drop in " + "the dashboard view"), + 'css': ( + "The css for individual dashboards can be altered here, or " + "in the dashboard view where changes are immediately " + "visible"), + 'slug': "To get a readable URL for your dashboard", + } + def pre_add(self, obj): + obj.slug = obj.slug.strip() or None + if obj.slug: + obj.slug = obj.slug.replace(" ", "-") + obj.slug = re.sub(r'\W+', '', obj.slug) + + def pre_update(self, obj): + self.pre_add(obj) + + +appbuilder.add_view( + DashboardModelView, + "Dashboards", + icon="fa-dashboard", + category="", + category_icon='',) + + +class LogModelView(DashedModelView): + datamodel = SQLAInterface(models.Log) + list_columns = ('user', 'action', 'dttm') + edit_columns = ('user', 'action', 'dttm', 'json') + base_order = ('dttm','desc') + +appbuilder.add_view( + LogModelView, + "Action Log", + category="Security", + icon="fa-list-ol") + + +class DruidDatasourceModelView(DashedModelView, DeleteMixin): # noqa + datamodel = SQLAInterface(models.DruidDatasource) + list_columns = [ + 'datasource_link', 'cluster', 'owner', + 'created_by_', 'created_on', + 'changed_by_', 'changed_on', + 'offset'] + order_columns = utils.list_minus( + list_columns, ['created_by_', 'changed_by_']) + related_views = [DruidColumnInlineView, DruidMetricInlineView] + edit_columns = [ + 'datasource_name', 'cluster', 'description', 'owner', + 'is_featured', 'is_hidden', 'default_endpoint', 'offset'] + page_size = 500 + base_order = ('datasource_name', 'asc') + description_columns = { + 'offset': "Timezone offset (in hours) for this datasource", + 'description': Markup( + "Supports markdown"), + } + + def post_add(self, datasource): + datasource.generate_metrics() + utils.merge_perm(sm, 'datasource_access', datasource.perm) + + def post_update(self, datasource): + self.post_add(datasource) + +appbuilder.add_view( + DruidDatasourceModelView, + "Druid Datasources", + category="Sources", + icon="fa-cube") + + +@app.route('/health') +def health(): + return "OK" + + +@app.route('/ping') +def ping(): + return "OK" + + +class R(BaseView): + + @log_this + @expose("/") + def index(self, url_id): + url = db.session.query(models.Url).filter_by(id=url_id).first() + if url: + print(url.url) + return redirect('/' + url.url) + else: + flash("URL to nowhere...", "danger") + return redirect('/') + + @log_this + @expose("/shortner/", methods=['POST', 'GET']) + def shortner(self): + url = request.form.get('data') + obj = models.Url(url=url) + db.session.add(obj) + db.session.commit() + return("{request.headers[Host]}/r/{obj.id}".format( + request=request, obj=obj)) + +appbuilder.add_view_no_menu(R) + + +class Dashed(BaseView): + + """The base views for Dashed!""" + + @has_access + @expose("/explore///") + @expose("/datasource///") # Legacy url + @log_this + def explore(self, datasource_type, datasource_id): + if datasource_type == "table": + datasource = ( + db.session + .query(models.SqlaTable) + .filter_by(id=datasource_id) + .first() + ) + else: + datasource = ( + db.session + .query(models.DruidDatasource) + .filter_by(id=datasource_id) + .first() + ) + + all_datasource_access = self.appbuilder.sm.has_access( + 'all_datasource_access', 'all_datasource_access') + datasource_access = self.appbuilder.sm.has_access( + 'datasource_access', datasource.perm) + if not (all_datasource_access or datasource_access): + flash( + "You don't seem to have access to this datasource", + "danger") + return redirect('/slicemodelview/list/') + action = request.args.get('action') + if action in ('save', 'overwrite'): + session = db.session() + + # TODO use form processing form wtforms + d = request.args.to_dict(flat=False) + del d['action'] + del d['previous_viz_type'] + as_list = ('metrics', 'groupby', 'columns') + for k in d: + v = d.get(k) + if k in as_list and not isinstance(v, list): + d[k] = [v] if v else [] + if k not in as_list and isinstance(v, list): + d[k] = v[0] + + table_id = druid_datasource_id = None + datasource_type = request.args.get('datasource_type') + if datasource_type in ('datasource', 'druid'): + druid_datasource_id = request.args.get('datasource_id') + elif datasource_type == 'table': + table_id = request.args.get('datasource_id') + + slice_name = request.args.get('slice_name') + + if action == "save": + slc = models.Slice() + msg = "Slice [{}] has been saved".format(slice_name) + elif action == "overwrite": + slc = ( + session.query(models.Slice) + .filter_by(id=request.args.get("slice_id")) + .first() + ) + msg = "Slice [{}] has been overwritten".format(slice_name) + + slc.params = json.dumps(d, indent=4, sort_keys=True) + slc.datasource_name = request.args.get('datasource_name') + slc.viz_type = request.args.get('viz_type') + slc.druid_datasource_id = druid_datasource_id + slc.table_id = table_id + slc.datasource_type = datasource_type + slc.slice_name = slice_name + + session.merge(slc) + session.commit() + flash(msg, "info") + return redirect(slc.slice_url) + + + if not datasource: + flash("The datasource seem to have been deleted", "alert") + viz_type = request.args.get("viz_type") + if not viz_type and datasource.default_endpoint: + return redirect(datasource.default_endpoint) + if not viz_type: + viz_type = "table" + obj = viz.viz_types[viz_type]( + datasource, + form_data=request.args) + if request.args.get("csv") == "true": + status = 200 + payload = obj.get_csv() + return Response( + payload, + status=status, + mimetype="application/csv") + + slice_id = request.args.get("slice_id") + slc = None + if slice_id: + slc = ( + db.session.query(models.Slice) + .filter_by(id=request.args.get("slice_id")) + .first() + ) + if request.args.get("json") == "true": + status = 200 + if config.get("DEBUG"): + payload = obj.get_json() + else: + try: + payload = obj.get_json() + except Exception as e: + logging.exception(e) + payload = str(e) + status = 500 + return Response( + payload, + status=status, + mimetype="application/json") + else: + if config.get("DEBUG"): + resp = self.render_template( + "dashed/viz.html", viz=obj, slice=slc) + try: + resp = self.render_template( + "dashed/viz.html", viz=obj, slice=slc) + except Exception as e: + if config.get("DEBUG"): + raise(e) + return Response( + str(e), + status=500, + mimetype="application/json") + return resp + + @has_access + @expose("/checkbox////", methods=['GET']) + def checkbox(self, model_view, id_, attr, value): + """endpoint for checking/unchecking any boolean in a sqla model""" + model = None + if model_view == 'TableColumnInlineView': + model = models.TableColumn + elif model_view == 'DruidColumnInlineView': + model = models.DruidColumn + + obj = db.session.query(model).filter_by(id=id_).first() + if obj: + setattr(obj, attr, value=='true') + db.session.commit() + return Response("OK", mimetype="application/json") + + + @has_access + @expose("/save_dash//", methods=['GET', 'POST']) + def save_dash(self, dashboard_id): + """Save a dashboard's metadata""" + data = json.loads(request.form.get('data')) + positions = data['positions'] + slice_ids = [int(d['slice_id']) for d in positions] + session = db.session() + Dash = models.Dashboard + dash = session.query(Dash).filter_by(id=dashboard_id).first() + dash.slices = [o for o in dash.slices if o.id in slice_ids] + dash.position_json = json.dumps(data['positions'], indent=4) + md = dash.metadata_dejson + if 'filter_immune_slices' not in md: + md['filter_immune_slices'] = [] + md['expanded_slices'] = data['expanded_slices'] + dash.json_metadata = json.dumps(md, indent=4) + dash.css = data['css'] + session.merge(dash) + session.commit() + session.close() + return "SUCCESS" + + @has_access + @expose("/testconn", methods=["POST", "GET"]) + def testconn(self): + """Tests a sqla connection""" + try: + uri = request.form.get('uri') + engine = create_engine(uri) + engine.connect() + return json.dumps(engine.table_names(), indent=4) + except Exception: + return Response( + traceback.format_exc(), + status=500, + mimetype="application/json") + + @has_access + @expose("/dashboard//") + def dashboard(self, dashboard_id): + """Server side rendering for a dashboard""" + session = db.session() + qry = session.query(models.Dashboard) + if dashboard_id.isdigit(): + qry = qry.filter_by(id=int(dashboard_id)) + else: + qry = qry.filter_by(slug=dashboard_id) + + templates = session.query(models.CssTemplate).all() + + dash = qry.first() + + # Hack to log the dashboard_id properly, even when getting a slug + @log_this + def dashboard(**kwargs): # noqa + pass + dashboard(dashboard_id=dash.id) + + pos_dict = {} + if dash.position_json: + pos_dict = { + int(o['slice_id']):o + for o in json.loads(dash.position_json)} + return self.render_template( + "dashed/dashboard.html", dashboard=dash, + templates=templates, + pos_dict=pos_dict) + + @has_access + @expose("/sql//") + @log_this + def sql(self, database_id): + mydb = db.session.query( + models.Database).filter_by(id=database_id).first() + engine = mydb.get_sqla_engine() + tables = engine.table_names() + + table_name=request.args.get('table_name') + return self.render_template( + "dashed/sql.html", + tables=tables, + table_name=table_name, + db=mydb) + + @has_access + @expose("/table///") + @log_this + def table(self, database_id, table_name): + mydb = db.session.query( + models.Database).filter_by(id=database_id).first() + cols = mydb.get_columns(table_name) + df = pd.DataFrame([(c['name'], c['type']) for c in cols]) + df.columns = ['col', 'type'] + return self.render_template( + "dashed/ajah.html", + content=df.to_html( + index=False, + na_rep='', + classes=( + "dataframe table table-striped table-bordered " + "table-condensed sql_results"))) + + @has_access + @expose("/select_star///") + @log_this + def select_star(self, database_id, table_name): + mydb = db.session.query( + models.Database).filter_by(id=database_id).first() + t = mydb.get_table(table_name) + fields = ", ".join( + [c.name for c in t.columns] or "*") + s = "SELECT\n{}\nFROM {}".format(fields, table_name) + return self.render_template( + "dashed/ajah.html", + content=s + ) + + @has_access + @expose("/runsql/", methods=['POST', 'GET']) + @log_this + def runsql(self): + session = db.session() + limit = 1000 + data = json.loads(request.form.get('data')) + sql = data.get('sql') + database_id = data.get('database_id') + mydb = session.query(models.Database).filter_by(id=database_id).first() + content = "" + if mydb: + eng = mydb.get_sqla_engine() + if limit: + sql = sql.strip().strip(';') + qry = ( + select('*') + .select_from(TextAsFrom(text(sql), ['*']).alias('inner_qry')) + .limit(limit) + ) + sql= str(qry.compile(eng, compile_kwargs={"literal_binds": True})) + try: + df = pd.read_sql_query(sql=sql, con=eng) + content = df.to_html( + index=False, + na_rep='', + classes=( + "dataframe table table-striped table-bordered " + "table-condensed sql_results")) + except Exception as e: + content = ( + '
    ' + "{}
    " + ).format(e.message) + session.commit() + return content + + @has_access + @expose("/refresh_datasources/") + def refresh_datasources(self): + session = db.session() + for cluster in session.query(models.DruidCluster).all(): + try: + cluster.refresh_datasources() + except Exception as e: + flash( + "Error while processing cluster '{}'\n{}".format( + cluster, str(e)), + "danger") + logging.exception(e) + return redirect('/druidclustermodelview/list/') + cluster.metadata_last_refreshed = datetime.now() + flash( + "Refreshed metadata from cluster " + "[" + cluster.cluster_name + "]", + 'info') + session.commit() + return redirect("/datasourcemodelview/list/") + + @expose("/autocomplete///") + def autocomplete(self, datasource, column): + client = utils.get_pydruid_client() + top = client.topn( + datasource=datasource, + granularity='all', + intervals='2013-10-04/2020-10-10', + aggregations={"count": doublesum("count")}, + dimension=column, + metric='count', + threshold=1000, + ) + values = sorted([d[column] for d in top[0]['result']]) + return json.dumps(values) + + @app.errorhandler(500) + def show_traceback(self): + if config.get("SHOW_STACKTRACE"): + error_msg = traceback.format_exc() + else: + error_msg = "FATAL ERROR\n" + error_msg = ( + "Stacktrace is hidden. Change the SHOW_STACKTRACE " + "configuration setting to enable it") + return render_template( + 'dashed/traceback.html', + error_msg=error_msg, + title=ascii_art.stacktrace, + art=ascii_art.error), 500 + + @has_access + @expose("/featured", methods=['GET']) + def featured(self): + session = db.session() + datasets_sqla = ( + session.query(models.SqlaTable) + .filter_by(is_featured=True) + .all() + ) + datasets_druid = ( + session.query(models.DruidDatasource) + .filter_by(is_featured=True) + .all() + ) + featured_datasets = datasets_sqla + datasets_druid + return self.render_template( + 'dashed/featured.html', + featured_datasets=featured_datasets, + utils=utils) + +appbuilder.add_view_no_menu(Dashed) +appbuilder.add_link( + "Refresh Druid Metadata", + href='/dashed/refresh_datasources/', + category='Sources', + category_icon='fa-database', + icon="fa-cog") + + +class CssTemplateModelView(DashedModelView, DeleteMixin): + datamodel = SQLAInterface(models.CssTemplate) + list_columns = ['template_name'] + edit_columns = ['template_name', 'css'] + add_columns = edit_columns + +appbuilder.add_separator("Sources") +appbuilder.add_view( + CssTemplateModelView, + "CSS Templates", + icon="fa-css3", + category="Sources", + category_icon='',) + + diff --git a/dashed/viz.py b/dashed/viz.py new file mode 100644 index 000000000..56b15a70b --- /dev/null +++ b/dashed/viz.py @@ -0,0 +1,1228 @@ +""" +This module contains the "Viz" objects that represent the backend of all +the visualizations that Dashed can render +""" + +from collections import OrderedDict, defaultdict +from datetime import datetime, timedelta +import json +import uuid + +from flask import flash, request, Markup +from markdown import markdown +from pandas.io.json import dumps +from werkzeug.datastructures import ImmutableMultiDict +from werkzeug.urls import Href +import pandas as pd + +from dashed import app, utils +from dashed.forms import FormFactory + +from six import string_types + +config = app.config + + +class BaseViz(object): + + """All visualizations derive this base class""" + + viz_type = None + verbose_name = "Base Viz" + is_timeseries = False + fieldsets = ( + { + 'label': None, + 'fields': ( + 'metrics', 'groupby', + ) + },) + form_overrides = {} + + def __init__(self, datasource, form_data): + self.orig_form_data = form_data + self.datasource = datasource + self.request = request + self.viz_type = form_data.get("viz_type") + + # TODO refactor all form related logic out of here and into forms.py + ff = FormFactory(self) + form_class = ff.get_form() + defaults = form_class().data.copy() + previous_viz_type = form_data.get('previous_viz_type') + if isinstance(form_data, ImmutableMultiDict): + form = form_class(form_data) + else: + form = form_class(**form_data) + data = form.data.copy() + + if not form.validate(): + for k, v in form.errors.items(): + if not data.get('json') and not data.get('async'): + flash("{}: {}".format(k, " ".join(v)), 'danger') + if previous_viz_type != self.viz_type: + data = { + k: form.data[k] + for k in form_data.keys() + if k in form.data} + defaults.update(data) + self.form_data = defaults + self.query = "" + + self.form_data['previous_viz_type'] = self.viz_type + self.token = self.form_data.get( + 'token', 'token_' + uuid.uuid4().hex[:8]) + + self.metrics = self.form_data.get('metrics') or [] + self.groupby = self.form_data.get('groupby') or [] + self.reassignments() + + def get_form_override(self, fieldname, attr): + if ( + fieldname in self.form_overrides and + attr in self.form_overrides[fieldname]): + s = self.form_overrides[fieldname][attr] + if attr == 'label': + s = ''.format(**locals()) + s = Markup(s) + return s + + @classmethod + def flat_form_fields(cls): + l = set() + for d in cls.fieldsets: + for obj in d['fields']: + if obj and isinstance(obj, (tuple, list)): + l |= {a for a in obj if a} + elif obj: + l.add(obj) + return tuple(l) + + def reassignments(self): + pass + + def get_url(self, **kwargs): + d = self.orig_form_data.copy() + if 'json' in d: + del d['json'] + if 'action' in d: + del d['action'] + d.update(kwargs) + # Remove unchecked checkboxes because HTML is weird like that + for key in d.keys(): + if d[key] is False: + del d[key] + href = Href( + '/dashed/explore/{self.datasource.type}/' + '{self.datasource.id}/'.format(**locals())) + return href(d) + + def get_df(self, query_obj=None): + """Returns a pandas dataframe based on the query object""" + if not query_obj: + query_obj = self.query_obj() + + self.error_msg = "" + self.results = None + + # The datasource here can be different backend but the interface is common + self.results = self.datasource.query(**query_obj) + self.query = self.results.query + df = self.results.df + if df is None or df.empty: + raise Exception("No data, review your incantations!") + else: + if 'timestamp' in df.columns: + df.timestamp = pd.to_datetime(df.timestamp, utc=False) + if self.datasource.offset: + df.timestamp += timedelta(hours=self.datasource.offset) + df = df.fillna(0) + return df + + @property + def form(self): + return self.form_class(**self.form_data) + + @property + def form_class(self): + return FormFactory(self).get_form() + + def query_filters(self): + """Processes the filters for the query""" + form_data = self.form_data + # Building filters + filters = [] + for i in range(1, 10): + col = form_data.get("flt_col_" + str(i)) + op = form_data.get("flt_op_" + str(i)) + eq = form_data.get("flt_eq_" + str(i)) + if col and op and eq: + filters.append((col, op, eq)) + + # Extra filters (coming from dashboard) + extra_filters = form_data.get('extra_filters') + if extra_filters: + extra_filters = json.loads(extra_filters) + for slice_filters in extra_filters.values(): + for col, vals in slice_filters.items(): + if col and vals: + filters += [(col, 'in', ",".join(vals))] + return filters + + def query_obj(self): + """Building a query object""" + form_data = self.form_data + groupby = form_data.get("groupby") or [] + metrics = form_data.get("metrics") or ['count'] + granularity = \ + form_data.get("granularity") or form_data.get("granularity_sqla") + limit = int(form_data.get("limit", 0)) + row_limit = int( + form_data.get("row_limit", config.get("ROW_LIMIT"))) + since = form_data.get("since", "1 year ago") + from_dttm = utils.parse_human_datetime(since) + if from_dttm > datetime.now(): + from_dttm = datetime.now() - (from_dttm-datetime.now()) + until = form_data.get("until", "now") + to_dttm = utils.parse_human_datetime(until) + if from_dttm > to_dttm: + flash("The date range doesn't seem right.", "danger") + from_dttm = to_dttm # Making them identical to not raise + + # extras are used to query elements specific to a datasource type + # for instance the extra where clause that applies only to Tables + extras = { + 'where': form_data.get("where", ''), + 'having': form_data.get("having", ''), + 'time_grain_sqla': form_data.get("time_grain_sqla", ''), + } + d = { + 'granularity': granularity, + 'from_dttm': from_dttm, + 'to_dttm': to_dttm, + 'is_timeseries': self.is_timeseries, + 'groupby': groupby, + 'metrics': metrics, + 'row_limit': row_limit, + 'filter': self.query_filters(), + 'timeseries_limit': limit, + 'extras': extras, + } + return d + + def get_json(self): + payload = { + 'data': json.loads(self.get_json_data()), + 'query': self.query, + 'form_data': self.form_data, + 'json_endpoint': self.json_endpoint, + 'csv_endpoint': self.csv_endpoint, + 'standalone_endpoint': self.standalone_endpoint, + } + return json.dumps(payload) + + def get_csv(self): + df = self.get_df() + return df.to_csv(index=False) + + def get_json_data(self): + return json.dumps([]) + + @property + def json_endpoint(self): + return self.get_url(json="true") + + @property + def csv_endpoint(self): + return self.get_url(csv="true") + + @property + def standalone_endpoint(self): + return self.get_url(standalone="true") + + @property + def data(self): + content = { + 'viz_name': self.viz_type, + 'json_endpoint': self.json_endpoint, + 'csv_endpoint': self.csv_endpoint, + 'standalone_endpoint': self.standalone_endpoint, + 'token': self.token, + 'form_data': self.form_data, + } + return content + + @property + def json_data(self): + return dumps(self.data) + +class TableViz(BaseViz): + viz_type = "table" + verbose_name = "Table View" + fieldsets = ( + { + 'label': "Chart Options", + 'fields': ( + 'row_limit', + ('include_search', None), + ) + }, + { + 'label': "GROUP BY", + 'fields': ( + 'groupby', + 'metrics', + ) + }, + { + 'label': "NOT GROUPED BY", + 'fields': ( + 'all_columns', + ) + },) + is_timeseries = False + + + def query_obj(self): + d = super(TableViz, self).query_obj() + fd = self.form_data + if fd.get('all_columns') and (fd.get('groupby') or fd.get('metrics')): + raise Exception( + "Choose either fields to [Group By] and [Metrics] or " + "[Columns], not both") + if fd.get('all_columns'): + d['columns'] = fd.get('all_columns') + d['groupby'] = [] + return d + + def get_df(self, query_obj=None): + df = super(TableViz, self).get_df(query_obj) + if ( + self.form_data.get("granularity") == "all" and + 'timestamp' in df): + del df['timestamp'] + return df + + def get_json_data(self): + df = self.get_df() + return json.dumps( + dict( + records=df.to_dict(orient="records"), + columns=list(df.columns), + ), + default=utils.json_iso_dttm_ser, + ) + + +class PivotTableViz(BaseViz): + viz_type = "pivot_table" + verbose_name = "Pivot Table" + is_timeseries = False + fieldsets = ( + { + 'label': None, + 'fields': ( + 'groupby', + 'columns', + 'metrics', + 'pandas_aggfunc', + ) + },) + + def query_obj(self): + d = super(PivotTableViz, self).query_obj() + groupby = self.form_data.get('groupby') + columns = self.form_data.get('columns') + metrics = self.form_data.get('metrics') + if not columns: + columns = [] + if not groupby: + groupby = [] + if not groupby: + raise Exception("Please choose at least one \"Group by\" field ") + if not metrics: + raise Exception("Please choose at least one metric") + if ( + any(v in groupby for v in columns) or + any(v in columns for v in groupby)): + raise Exception("groupby and columns can't overlap") + + d['groupby'] = list(set(groupby) | set(columns)) + return d + + def get_df(self, query_obj=None): + df = super(PivotTableViz, self).get_df(query_obj) + if ( + self.form_data.get("granularity") == "all" and + 'timestamp' in df): + del df['timestamp'] + df = df.pivot_table( + index=self.form_data.get('groupby'), + columns=self.form_data.get('columns'), + values=self.form_data.get('metrics'), + aggfunc=self.form_data.get('pandas_aggfunc'), + margins=True, + ) + return df + + def get_json_data(self): + return dumps(self.get_df().to_html( + na_rep='', + classes=( + "dataframe table table-striped table-bordered " + "table-condensed table-hover"))) + + +class MarkupViz(BaseViz): + viz_type = "markup" + verbose_name = "Markup Widget" + fieldsets = ( + { + 'label': None, + 'fields': ('markup_type', 'code') + },) + is_timeseries = False + + def rendered(self): + markup_type = self.form_data.get("markup_type") + code = self.form_data.get("code", '') + if markup_type == "markdown": + return markdown(code) + elif markup_type == "html": + return code + + def get_json_data(self): + return dumps(dict(html=self.rendered())) + + +class WordCloudViz(BaseViz): + + """Integration with the nice library at: + + https://github.com/jasondavies/d3-cloud + """ + + viz_type = "word_cloud" + verbose_name = "Word Cloud" + is_timeseries = False + fieldsets = ( + { + 'label': None, + 'fields': ( + 'series', 'metric', 'limit', + ('size_from', 'size_to'), + 'rotation', + ) + },) + + def query_obj(self): + d = super(WordCloudViz, self).query_obj() + + d['metrics'] = [self.form_data.get('metric')] + d['groupby'] = [self.form_data.get('series')] + return d + + def get_json_data(self): + df = self.get_df() + # Ordering the columns + df = df[[self.form_data.get('series'), self.form_data.get('metric')]] + # Labeling the columns for uniform json schema + df.columns = ['text', 'size'] + return df.to_json(orient="records") + + +class NVD3Viz(BaseViz): + + """Base class for all nvd3 vizs""" + + viz_type = None + verbose_name = "Base NVD3 Viz" + is_timeseries = False + + +class BubbleViz(NVD3Viz): + + """Based on the NVD3 bubble chart""" + + viz_type = "bubble" + verbose_name = "Bubble Chart" + is_timeseries = False + fieldsets = ( + { + 'label': None, + 'fields': ( + 'series', 'entity', + 'x', 'y', + 'size', 'limit', + ) + }, + { + 'label': 'Chart Options', + 'fields': ( + ('x_log_scale', 'y_log_scale'), + ('show_legend', None), + 'max_bubble_size', + ) + },) + + def query_obj(self): + form_data = self.form_data + d = super(BubbleViz, self).query_obj() + d['groupby'] = list({ + form_data.get('series'), + form_data.get('entity') + }) + self.x_metric = form_data.get('x') + self.y_metric = form_data.get('y') + self.z_metric = form_data.get('size') + self.entity = form_data.get('entity') + self.series = form_data.get('series') + + d['metrics'] = [ + self.z_metric, + self.x_metric, + self.y_metric, + ] + if not all(d['metrics'] + [self.entity, self.series]): + raise Exception("Pick a metric for x, y and size") + return d + + def get_df(self, query_obj=None): + df = super(BubbleViz, self).get_df(query_obj) + df = df.fillna(0) + df['x'] = df[[self.x_metric]] + df['y'] = df[[self.y_metric]] + df['size'] = df[[self.z_metric]] + df['shape'] = 'circle' + df['group'] = df[[self.series]] + return df + + def get_json_data(self): + df = self.get_df() + series = defaultdict(list) + for row in df.to_dict(orient='records'): + series[row['group']].append(row) + chart_data = [] + for k, v in series.items(): + chart_data.append({ + 'key': k, + 'values': v }) + return dumps(chart_data) + +class BigNumberViz(BaseViz): + viz_type = "big_number" + verbose_name = "Big Number" + is_timeseries = True + fieldsets = ( + { + 'label': None, + 'fields': ( + 'metric', + 'compare_lag', + 'compare_suffix', + 'y_axis_format', + ) + },) + form_overrides = { + 'y_axis_format': { + 'label': 'Number format', + } + } + + def reassignments(self): + metric = self.form_data.get('metric') + if not metric: + self.form_data['metric'] = self.orig_form_data.get('metrics') + + + def query_obj(self): + d = super(BigNumberViz, self).query_obj() + metric = self.form_data.get('metric') + if not metric: + raise Exception("Pick a metric!") + d['metrics'] = [self.form_data.get('metric')] + self.form_data['metric'] = metric + return d + + def get_json_data(self): + form_data = self.form_data + df = self.get_df() + df = df.sort(columns=df.columns[0]) + compare_lag = form_data.get("compare_lag", "") + compare_lag = int(compare_lag) if compare_lag and compare_lag.isdigit() else 0 + d = { + 'data': df.values.tolist(), + 'compare_lag': compare_lag, + 'compare_suffix': form_data.get('compare_suffix', ''), + } + return dumps(d) + + +class NVD3TimeSeriesViz(NVD3Viz): + viz_type = "line" + verbose_name = "Time Series - Line Chart" + sort_series = False + is_timeseries = True + fieldsets = ( + { + 'label': None, + 'fields': ( + 'metrics', + 'groupby', 'limit', + ), + }, { + 'label': 'Chart Options', + 'fields': ( + ('show_brush', 'show_legend'), + ('rich_tooltip', 'y_axis_zero'), + ('y_log_scale', 'contribution'), + ('x_axis_format', 'y_axis_format'), + ('line_interpolation', 'x_axis_showminmax'), + ), + }, { + 'label': 'Advanced Analytics', + 'description': ( + "This section contains options " + "that allow for advanced analytical post processing " + "of query results"), + 'fields': ( + ('rolling_type', 'rolling_periods'), + 'time_compare', + 'num_period_compare', + None, + ('resample_how', 'resample_rule',), 'resample_fillmethod' + ), + }, + ) + + def get_df(self, query_obj=None): + form_data = self.form_data + df = super(NVD3TimeSeriesViz, self).get_df(query_obj) + + df = df.fillna(0) + if form_data.get("granularity") == "all": + raise Exception("Pick a time granularity for your time series") + + df = df.pivot_table( + index="timestamp", + columns=form_data.get('groupby'), + values=form_data.get('metrics')) + + fm = form_data.get("resample_fillmethod") + if not fm: + fm = None + how = form_data.get("resample_how") + rule = form_data.get("resample_rule") + if how and rule: + df = df.resample(rule, how=how, fill_method=fm) + if not fm: + df = df.fillna(0) + + + if self.sort_series: + dfs = df.sum() + dfs.sort(ascending=False) + df = df[dfs.index] + + if form_data.get("contribution"): + dft = df.T + df = (dft / dft.sum()).T + + num_period_compare = form_data.get("num_period_compare") + if num_period_compare: + num_period_compare = int(num_period_compare) + df = (df / df.shift(num_period_compare)) - 1 + df = df[num_period_compare:] + + rolling_periods = form_data.get("rolling_periods") + rolling_type = form_data.get("rolling_type") + + if rolling_type in ('mean', 'std', 'sum') and rolling_periods: + if rolling_type == 'mean': + df = pd.rolling_mean(df, int(rolling_periods), min_periods=0) + elif rolling_type == 'std': + df = pd.rolling_std(df, int(rolling_periods), min_periods=0) + elif rolling_type == 'sum': + df = pd.rolling_sum(df, int(rolling_periods), min_periods=0) + elif rolling_type == 'cumsum': + df = df.cumsum() + return df + + def to_series(self, df, classed='', title_suffix=''): + series = df.to_dict('series') + + chart_data = [] + for name in df.T.index.tolist(): + ys = series[name] + if df[name].dtype.kind not in "biufc": + continue + df['timestamp'] = pd.to_datetime(df.index, utc=False) + if isinstance(name, string_types): + series_title = name + else: + name = ["{}".format(s) for s in name] + if len(self.form_data.get('metrics')) > 1: + series_title = ", ".join(name) + else: + series_title = ", ".join(name[1:]) + if title_suffix: + series_title += title_suffix + + d = { + "key": series_title, + "classed": classed, + "values": [{'x': ds, 'y': ys[ds]} for ds in df.timestamp], + } + chart_data.append(d) + return chart_data + + def get_json_data(self): + df = self.get_df() + chart_data = self.to_series(df) + + time_compare = self.form_data.get('time_compare') + if time_compare: + query_object = self.query_obj() + delta = utils.parse_human_timedelta(time_compare) + query_object['inner_from_dttm'] = query_object['from_dttm'] + query_object['inner_to_dttm'] = query_object['to_dttm'] + query_object['from_dttm'] -= delta + query_object['to_dttm'] -= delta + + df2 = self.get_df(query_object) + df2.index += delta + chart_data += self.to_series( + df2, classed='dashed', title_suffix="---") + chart_data = sorted(chart_data, key=lambda x: x['key']) + return dumps(chart_data) + + +class NVD3TimeSeriesBarViz(NVD3TimeSeriesViz): + viz_type = "bar" + sort_series = True + verbose_name = "Time Series - Bar Chart" + fieldsets = [NVD3TimeSeriesViz.fieldsets[0]] + [{ + 'label': 'Chart Options', + 'fields': ( + ('show_brush', 'show_legend'), + ('rich_tooltip', 'y_axis_zero'), + ('y_log_scale', 'contribution'), + ('x_axis_format', 'y_axis_format'), + ('line_interpolation', 'bar_stacked'), + ('x_axis_showminmax', None), + ), }] + [NVD3TimeSeriesViz.fieldsets[2]] + + +class NVD3CompareTimeSeriesViz(NVD3TimeSeriesViz): + viz_type = 'compare' + verbose_name = "Time Series - Percent Change" + + +class NVD3TimeSeriesStackedViz(NVD3TimeSeriesViz): + viz_type = "area" + verbose_name = "Time Series - Stacked" + sort_series = True + fieldsets = [NVD3TimeSeriesViz.fieldsets[0]] + [{ + 'label': 'Chart Options', + 'fields': ( + ('show_brush', 'show_legend'), + ('rich_tooltip', 'y_axis_zero'), + ('y_log_scale', 'contribution'), + ('x_axis_format', 'y_axis_format'), + ('x_axis_showminmax'), + ('line_interpolation', 'stacked_style'), + ), }] + [NVD3TimeSeriesViz.fieldsets[2]] + + +class DistributionPieViz(NVD3Viz): + viz_type = "pie" + verbose_name = "Distribution - NVD3 - Pie Chart" + is_timeseries = False + fieldsets = ( + { + 'label': None, + 'fields': ( + 'metrics', 'groupby', + 'limit', + ('donut', 'show_legend'), + ) + },) + + def query_obj(self): + d = super(DistributionPieViz, self).query_obj() + d['is_timeseries'] = False + return d + + def get_df(self, query_obj=None): + df = super(DistributionPieViz, self).get_df(query_obj) + df = df.pivot_table( + index=self.groupby, + values=[self.metrics[0]]) + df = df.sort(self.metrics[0], ascending=False) + return df + + def get_json_data(self): + df = self.get_df() + df = df.reset_index() + df.columns = ['x', 'y'] + return dumps(df.to_dict(orient="records")) + + +class DistributionBarViz(DistributionPieViz): + viz_type = "dist_bar" + verbose_name = "Distribution - Bar Chart" + is_timeseries = False + fieldsets = ( + { + 'label': 'Chart Options', + 'fields': ( + 'groupby', + 'columns', + 'metrics', + 'row_limit', + ('show_legend', 'bar_stacked'), + ) + },) + form_overrides = { + 'groupby': { + 'label': 'Series', + }, + 'columns': { + 'label': 'Breakdowns', + 'description': "Defines how each series is broken down", + }, + } + + def query_obj(self): + d = super(DistributionPieViz, self).query_obj() # noqa + fd = self.form_data + d['is_timeseries'] = False + gb = fd.get('groupby') or [] + cols = fd.get('columns') or [] + d['groupby'] = set(gb + cols) + if len(d['groupby']) < len(gb) + len(cols): + raise Exception("Can't have overlap between Series and Breakdowns") + if not self.metrics: + raise Exception("Pick at least one metric") + if not self.groupby: + raise Exception("Pick at least one field for [Series]") + return d + + def get_df(self, query_obj=None): + df = super(DistributionPieViz, self).get_df(query_obj) # noqa + fd = self.form_data + + row = df.groupby(self.groupby).sum()[self.metrics[0]].copy() + row.sort(ascending=False) + columns = fd.get('columns') or [] + pt = df.pivot_table( + index=self.groupby, + columns=columns, + values=self.metrics) + pt = pt.reindex(row.index) + return pt + + def get_json_data(self): + df = self.get_df() + series = df.to_dict('series') + chart_data = [] + for name, ys in series.items(): + if df[name].dtype.kind not in "biufc": + continue + if isinstance(name, string_types): + series_title = name + elif len(self.metrics) > 1: + series_title = ", ".join(name) + else: + l = [str(s) for s in name[1:]] + series_title = ", ".join(l) + d = { + "key": series_title, + "values": [ + {'x': i, 'y': v} + for i, v in ys.iteritems()] + } + chart_data.append(d) + return dumps(chart_data) + + +class SunburstViz(BaseViz): + viz_type = "sunburst" + verbose_name = "Sunburst" + is_timeseries = False + fieldsets = ( + { + 'label': None, + 'fields': ( + 'groupby', + 'metric', 'secondary_metric', + 'row_limit', + ) + },) + form_overrides = { + 'metric': { + 'label': 'Primary Metric', + 'description': ( + "The primary metric is used to " + "define the arc segment sizes"), + }, + 'secondary_metric': { + 'label': 'Secondary Metric', + 'description': ( + "This secondary metric is used to " + "define the color as a ratio against the primary metric. " + "If the two metrics match, color is mapped level groups"), + }, + 'groupby': { + 'label': 'Hierarchy', + 'description': "This defines the level of the hierarchy", + }, + } + + def get_df(self, query_obj=None): + df = super(SunburstViz, self).get_df(query_obj) + return df + + def get_json_data(self): + df = self.get_df() + + # if m1 == m2 duplicate the metric column + cols = self.form_data.get('groupby') + metric = self.form_data.get('metric') + secondary_metric = self.form_data.get('secondary_metric') + if metric == secondary_metric: + ndf = df[cols] + ndf['m1'] = df[metric] + ndf['m2'] = df[metric] + else: + cols += [ + self.form_data['metric'], self.form_data['secondary_metric']] + ndf = df[cols] + return ndf.to_json(orient="values") + + def query_obj(self): + qry = super(SunburstViz, self).query_obj() + qry['metrics'] = [ + self.form_data['metric'], self.form_data['secondary_metric']] + return qry + + +class SankeyViz(BaseViz): + viz_type = "sankey" + verbose_name = "Sankey" + is_timeseries = False + fieldsets = ( + { + 'label': None, + 'fields': ( + 'groupby', + 'metric', + 'row_limit', + ) + },) + form_overrides = { + 'groupby': { + 'label': 'Source / Target', + 'description': "Choose a source and a target", + }, + } + + def query_obj(self): + qry = super(SankeyViz, self).query_obj() + if len(qry['groupby']) != 2: + raise Exception("Pick exactly 2 columns as [Source / Target]") + qry['metrics'] = [ + self.form_data['metric']] + return qry + + def get_json_data(self): + df = self.get_df() + df.columns = ['source', 'target', 'value'] + d = df.to_dict(orient='records') + return dumps(d) + + +class DirectedForceViz(BaseViz): + viz_type = "directed_force" + verbose_name = "Directed Force Layout" + is_timeseries = False + fieldsets = ( + { + 'label': None, + 'fields': ( + 'groupby', + 'metric', + 'row_limit', + ) + }, + { + 'label': 'Force Layout', + 'fields': ( + 'link_length', + 'charge', + ) + },) + form_overrides = { + 'groupby': { + 'label': 'Source / Target', + 'description': "Choose a source and a target", + }, + } + def query_obj(self): + qry = super(DirectedForceViz, self).query_obj() + if len(self.form_data['groupby']) != 2: + raise Exception("Pick exactly 2 columns to 'Group By'") + qry['metrics'] = [self.form_data['metric']] + return qry + + def get_json_data(self): + df = self.get_df() + df.columns = ['source', 'target', 'value'] + d = df.to_dict(orient='records') + return dumps(d) + + +class WorldMapViz(BaseViz): + viz_type = "world_map" + verbose_name = "World Map" + is_timeseries = False + fieldsets = ( + { + 'label': None, + 'fields': ( + 'entity', + 'country_fieldtype', + 'metric', + ) + }, + { + 'label': 'Bubbles', + 'fields': ( + ('show_bubbles', None), + 'secondary_metric', + 'max_bubble_size', + ) + }) + form_overrides = { + 'entity': { + 'label': 'Country Field', + 'description': "3 letter code of the country", + }, + 'metric': { + 'label': 'Metric for color', + 'description': ("Metric that defines the color of the country"), + }, + 'secondary_metric': { + 'label': 'Bubble size', + 'description': ("Metric that defines the size of the bubble"), + }, + } + def query_obj(self): + qry = super(WorldMapViz, self).query_obj() + qry['metrics'] = [ + self.form_data['metric'], self.form_data['secondary_metric']] + qry['groupby'] = [self.form_data['entity']] + return qry + + def get_json_data(self): + from dashed.data import countries + df = self.get_df() + cols = [self.form_data.get('entity')] + metric = self.form_data.get('metric') + secondary_metric = self.form_data.get('secondary_metric') + if metric == secondary_metric: + ndf = df[cols] + ndf['m1'] = df[metric] + ndf['m2'] = df[metric] + else: + cols += [metric, secondary_metric] + ndf = df[cols] + df = ndf + df.columns = ['country', 'm1', 'm2'] + d = df.to_dict(orient='records') + for row in d: + country = countries.get( + self.form_data.get('country_fieldtype'), row['country']) + if country: + row['country'] = country['cca3'] + row['latitude'] = country['lat'] + row['longitude'] = country['lng'] + row['name'] = country['name'] + else: + row['country'] = "XXX" + return dumps(d) + + +class FilterBoxViz(BaseViz): + viz_type = "filter_box" + verbose_name = "Filters" + is_timeseries = False + fieldsets = ( + { + 'label': None, + 'fields': ( + 'groupby', + 'metric', + ) + },) + form_overrides = { + 'groupby': { + 'label': 'Filter fields', + 'description': "The fields you want to filter on", + }, + } + def query_obj(self): + qry = super(FilterBoxViz, self).query_obj() + groupby = self.form_data['groupby'] + if len(groupby) < 1: + raise Exception("Pick at least one filter field") + qry['metrics'] = [ + self.form_data['metric']] + return qry + + def get_df(self, query_obj=None): + qry = self.query_obj() + + filters = [g for g in qry['groupby']] + d = {} + for flt in filters: + qry['groupby'] = [flt] + df = super(FilterBoxViz, self).get_df(qry) + d[flt] = [ + {'id': row[0], + 'text': row[0], + 'filter': flt, + 'metric': row[1]} + for row in df.itertuples(index=False)] + return d + + def get_json_data(self): + d = self.get_df() + return dumps(d) + + +class IFrameViz(BaseViz): + viz_type = "iframe" + verbose_name = "iFrame" + is_timeseries = False + fieldsets = ( + { + 'label': None, + 'fields': ('url',) + },) + + +class ParallelCoordinatesViz(BaseViz): + viz_type = "para" + verbose_name = "Parallel Coordinates" + is_timeseries = False + fieldsets = ( + { + 'label': None, + 'fields': ( + 'series', + 'metrics', + 'secondary_metric', + 'limit', + ('show_datatable', None), + ) + },) + def query_obj(self): + d = super(ParallelCoordinatesViz, self).query_obj() + fd = self.form_data + d['metrics'] = fd.get('metrics') + second = fd.get('secondary_metric') + if second not in d['metrics']: + d['metrics'] += [second] + d['groupby'] = [fd.get('series')] + return d + + def get_json_data(self): + df = self.get_df() + df = df[[self.form_data.get('series')] + self.form_data.get('metrics')] + return df.to_json(orient="records") + +class HeatmapViz(BaseViz): + viz_type = "heatmap" + verbose_name = "Heatmap" + is_timeseries = False + fieldsets = ( + { + 'label': None, + 'fields': ( + 'all_columns_x', + 'all_columns_y', + 'metric', + ) + }, + { + 'label': 'Heatmap Options', + 'fields': ( + 'linear_color_scheme', + ('xscale_interval', 'yscale_interval'), + 'canvas_image_rendering', + 'normalize_across', + ) + },) + def query_obj(self): + d = super(HeatmapViz, self).query_obj() + fd = self.form_data + d['metrics'] = [fd.get('metric')] + d['groupby'] = [fd.get('all_columns_x'), fd.get('all_columns_y')] + return d + + def get_json_data(self): + df = self.get_df() + fd = self.form_data + x = fd.get('all_columns_x') + y = fd.get('all_columns_y') + v = fd.get('metric') + if x == y: + df.columns = ['x', 'y', 'v'] + else: + df = df[[x, y, v]] + df.columns = ['x', 'y', 'v'] + norm = fd.get('normalize_across') + overall = False + if norm == 'heatmap': + overall = True + else: + gb = df.groupby(norm, group_keys=False) + if len(gb) <= 1: + overall = True + else: + df['perc'] = ( + gb.apply( + lambda x: (x.v - x.v.min()) / (x.v.max() - x.v.min())) + ) + if overall: + v = df.v + min_ = v.min() + df['perc'] = (v - min_) / (v.max() - min_) + return df.to_json(orient="records") + + +viz_types_list = [ + TableViz, + PivotTableViz, + NVD3TimeSeriesViz, + NVD3CompareTimeSeriesViz, + NVD3TimeSeriesStackedViz, + NVD3TimeSeriesBarViz, + DistributionBarViz, + DistributionPieViz, + BubbleViz, + MarkupViz, + WordCloudViz, + BigNumberViz, + SunburstViz, + DirectedForceViz, + SankeyViz, + WorldMapViz, + FilterBoxViz, + IFrameViz, + ParallelCoordinatesViz, + HeatmapViz, +] + +viz_types = OrderedDict([(v.viz_type, v) for v in viz_types_list]) diff --git a/docs/Makefile b/docs/Makefile index 56f29876d..7b78c404d 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -87,9 +87,9 @@ qthelp: @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/panoramix.qhcp" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/dashed.qhcp" @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/panoramix.qhc" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/dashed.qhc" applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @@ -104,8 +104,8 @@ devhelp: @echo @echo "Build finished." @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/panoramix" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/panoramix" + @echo "# mkdir -p $$HOME/.local/share/devhelp/dashed" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/dashed" @echo "# devhelp" epub: diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html index 7bb04b52f..8dc2374d4 100644 --- a/docs/_templates/layout.html +++ b/docs/_templates/layout.html @@ -21,7 +21,7 @@