├── .coveralls.yml ├── .gitignore ├── .idea ├── .name ├── misc.xml ├── modules.xml ├── panoramix.iml ├── vcs.xml └── workspace.xml ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── TODO.md ├── alembic.ini ├── babel ├── babel.cfg └── messages.pot ├── docs ├── Makefile ├── _static │ └── docs.css ├── _templates │ └── layout.html ├── build.sh ├── conf.py ├── img ├── index.rst └── user_guide.rst ├── panoramix ├── __init__.py ├── ascii_art.py ├── bin │ ├── __init__.py │ └── panoramix ├── config.py ├── data │ ├── __init__.py │ ├── birth_names.csv │ ├── birth_names.csv.gz │ ├── birth_names.json │ ├── birth_names.json.gz │ ├── countries.json │ ├── countries.json.gz │ ├── countries.md │ └── countries.py ├── forms.py ├── migrations │ ├── README │ ├── __init__.py │ ├── alembic.ini │ ├── env.py │ ├── script.py.mako │ └── versions │ │ ├── 12d55656cbca_is_featured.py │ │ ├── 1a48a5411020_adding_slug_to_dash.py │ │ ├── 1e2841a4128_.py │ │ ├── 2591d77e9831_user_id.py │ │ ├── 289ce07647b_add_encrypted_password_field.py │ │ ├── 2929af7925ed_tz_offsets_in_data_sources.py │ │ ├── 315b3f4da9b0_adding_log_model.py │ │ ├── 4e6a06bad7a8_init.py │ │ ├── 55179c7f25c7_sqla_descr.py │ │ └── 5a7bad26f2a7_.py ├── models.py ├── static │ ├── bootstrap.css │ ├── docs │ ├── img │ │ ├── bubble.png │ │ ├── cardash.jpg │ │ ├── chaudron-blue.png │ │ ├── chaudron.png │ │ ├── chaudron_white.png │ │ ├── cloud.png │ │ ├── dash.png │ │ ├── favicon.png │ │ ├── gallery.jpg │ │ ├── loading.gif │ │ ├── panoramix.jpg │ │ ├── panoramix.png │ │ ├── panoramix_screenshot.png │ │ ├── penguins.png │ │ ├── serpe.jpg │ │ ├── servers.jpg │ │ ├── slice.jpg │ │ └── tux_panoramix.png │ ├── lib │ │ ├── d3-sankey.js │ │ ├── d3.layout.cloud.js │ │ ├── d3.min.js │ │ ├── dataTables │ │ │ ├── dataTables.bootstrap.css │ │ │ ├── dataTables.bootstrap.js │ │ │ ├── jquery.dataTables.min.css │ │ │ └── jquery.dataTables.min.js │ │ ├── datamaps.all.js │ │ ├── datamaps.world.min.js │ │ ├── gridster │ │ │ ├── jquery.gridster.min.css │ │ │ └── jquery.gridster.with-extras.min.js │ │ ├── jquery-ui │ │ │ ├── external │ │ │ │ └── jquery │ │ │ │ │ └── jquery.js │ │ │ ├── images │ │ │ │ ├── ui-bg_diagonals-thick_18_b81900_40x40.png │ │ │ │ ├── ui-bg_diagonals-thick_20_666666_40x40.png │ │ │ │ ├── ui-bg_flat_10_000000_40x100.png │ │ │ │ ├── ui-bg_glass_100_f6f6f6_1x400.png │ │ │ │ ├── ui-bg_glass_100_fdf5ce_1x400.png │ │ │ │ ├── ui-bg_glass_65_ffffff_1x400.png │ │ │ │ ├── ui-bg_gloss-wave_35_f6a828_500x100.png │ │ │ │ ├── ui-bg_highlight-soft_100_eeeeee_1x100.png │ │ │ │ ├── ui-bg_highlight-soft_75_ffe45c_1x100.png │ │ │ │ ├── ui-icons_222222_256x240.png │ │ │ │ ├── ui-icons_228ef1_256x240.png │ │ │ │ ├── ui-icons_ef8c08_256x240.png │ │ │ │ ├── ui-icons_ffd27a_256x240.png │ │ │ │ └── ui-icons_ffffff_256x240.png │ │ │ ├── index.html │ │ │ ├── jquery-ui.css │ │ │ ├── jquery-ui.js │ │ │ ├── jquery-ui.min.css │ │ │ ├── jquery-ui.min.js │ │ │ ├── jquery-ui.structure.css │ │ │ ├── jquery-ui.structure.min.css │ │ │ ├── jquery-ui.theme.css │ │ │ └── jquery-ui.theme.min.css │ │ ├── nvd3 │ │ │ ├── nv.d3.css │ │ │ └── nv.d3.min.js │ │ ├── select2.sortable.js │ │ ├── select2 │ │ │ ├── select2-bootstrap.css │ │ │ ├── select2.min.css │ │ │ └── select2.min.js │ │ └── topojson.min.js │ ├── panoramix-bootstrap-theme.css │ ├── panoramix.css │ ├── panoramix.js │ └── widgets │ │ ├── viz_bignumber.css │ │ ├── viz_bignumber.js │ │ ├── viz_directed_force.css │ │ ├── viz_directed_force.js │ │ ├── viz_filter_box.css │ │ ├── viz_filter_box.js │ │ ├── viz_markup.js │ │ ├── viz_nvd3.css │ │ ├── viz_nvd3.js │ │ ├── viz_pivot_table.css │ │ ├── viz_pivot_table.js │ │ ├── viz_sankey.css │ │ ├── viz_sankey.js │ │ ├── viz_sunburst.css │ │ ├── viz_sunburst.js │ │ ├── viz_table.css │ │ ├── viz_table.js │ │ ├── viz_wordcloud.js │ │ ├── viz_world_map.css │ │ └── viz_world_map.js ├── templates │ ├── appbuilder │ │ ├── baselayout.html │ │ ├── general │ │ │ └── widgets │ │ │ │ └── list.html │ │ └── navbar.html │ ├── index.html │ └── panoramix │ │ ├── base.html │ │ ├── dashboard.html │ │ ├── explore.html │ │ ├── featured_datasets.html │ │ ├── models │ │ └── database │ │ │ ├── add.html │ │ │ ├── edit.html │ │ │ └── macros.html │ │ ├── no_data.html │ │ ├── standalone.html │ │ ├── traceback.html │ │ └── viz.html ├── utils.py ├── views.py └── viz.py ├── requirements.txt ├── run_tests.sh ├── setup.py └── tests ├── __init__.py ├── core_tests.py └── panoramix_test_config.py /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: EMkVRVEKYgUESKaNN9QyOhPnFnKNqyDcJ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | .coverage 4 | _build 5 | panoramix/bin/panoramixc 6 | build 7 | *.db 8 | tmp 9 | panoramix_config.py 10 | local_config.py 11 | env 12 | dist 13 | panoramix.egg-info/ 14 | app.db 15 | *.bak 16 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | panoramix -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/panoramix.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | cache: 6 | directories: 7 | - $HOME/.wheelhouse/ 8 | install: 9 | - pip wheel -w $HOME/.wheelhouse -f $HOME/.wheelhouse -r requirements.txt 10 | - pip install --find-links=$HOME/.wheelhouse --no-index -rrequirements.txt 11 | - python setup.py install 12 | 13 | # command to run tests 14 | script: bash run_tests.sh 15 | after_success: coveralls 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome and are greatly appreciated! Every 4 | little bit helps, and credit will always be given. 5 | 6 | You can contribute in many ways: 7 | 8 | ## Types of Contributions 9 | 10 | ### Report Bugs 11 | 12 | Report bugs through Github 13 | 14 | If you are reporting a bug, please include: 15 | 16 | - Your operating system name and version. 17 | - Any details about your local setup that might be helpful in 18 | troubleshooting. 19 | - Detailed steps to reproduce the bug. 20 | 21 | ### Fix Bugs 22 | 23 | Look through the GitHub issues for bugs. Anything tagged with "bug" is 24 | open to whoever wants to implement it. 25 | 26 | ### Implement Features 27 | 28 | Look through the GitHub issues for features. Anything tagged with 29 | "feature" is open to whoever wants to implement it. 30 | 31 | We've created the operators, hooks, macros and executors we needed, but we 32 | made sure that this part of Airflow is extensible. New operators, 33 | hooks and operators are very welcomed! 34 | 35 | ### Documentation 36 | 37 | Panoramix could always use better documentation, 38 | whether as part of the official Panoramix docs, 39 | in docstrings, `docs/*.rst` or even on the web as blog posts or 40 | articles. 41 | 42 | ### Submit Feedback 43 | 44 | The best way to send feedback is to file an issue on Github. 45 | 46 | If you are proposing a feature: 47 | 48 | - Explain in detail how it would work. 49 | - Keep the scope as narrow as possible, to make it easier to 50 | implement. 51 | - Remember that this is a volunteer-driven project, and that 52 | contributions are welcome :) 53 | 54 | ## Latest Documentation 55 | 56 | [API Documentation](http://pythonhosted.com/panoramix) 57 | 58 | ## Setting up a development environment 59 | 60 | Install development requirements: 61 | 62 | pip install -r requirements.txt 63 | 64 | Tests can then be run with: 65 | 66 | ./run_unit_tests.sh 67 | 68 | Lint the project with: 69 | 70 | flake8 changes tests 71 | 72 | ## API documentation 73 | 74 | Generate the documentation with: 75 | 76 | cd docs && ./build.sh 77 | 78 | 79 | ## Pull Request Guidelines 80 | 81 | Before you submit a pull request from your forked repo, check that it 82 | meets these guidelines: 83 | 84 | 1. The pull request should include tests, either as doctests, 85 | unit tests, or both. 86 | 2. If the pull request adds functionality, the docs should be updated 87 | as part of the same PR. Doc string are often sufficient, make 88 | sure to follow the sphinx compatible standards. 89 | 3. The pull request should work for Python 2.6, 2.7, and ideally python 3.3. 90 | `from __future__ import ` will be required in every `.py` file soon. 91 | 4. Code will be reviewed by re running the unittests, flake8 and syntax 92 | should be as rigorous as the core Python project. 93 | 5. Please rebase and resolve all conflicts before submitting. 94 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include panoramix/templates * 2 | recursive-include panoramix/static * 3 | recursive-include panoramix/data * 4 | recursive-include panoramix/migrations * 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Panoramix 2 | ========= 3 | 4 | [![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) 5 | ![img](https://travis-ci.org/mistercrunch/panoramix.svg?branch=master) 6 | [![Coverage Status](https://coveralls.io/repos/mistercrunch/panoramix/badge.svg?branch=master&service=github)](https://coveralls.io/github/mistercrunch/panoramix?branch=master) 7 | 8 | Panoramix is a data exploration platform designed to be visual, intuitive 9 | and interactive. 10 | 11 | ![img](http://i.imgur.com/bi09J9X.png) 12 | ![img](http://i.imgur.com/aOaH0ty.png) 13 | 14 | Panoramix 15 | --------- 16 | Panoramix's main goal is to make it easy to slice, dice and visualize data. 17 | It empowers its user to perform **analytics at the speed of thought**. 18 | 19 | Panoramix provides: 20 | * A quick way to intuitively visualize datasets 21 | * Create and share interactive dashboards 22 | * A rich set of visualizations to analyze your data, as well as a flexible 23 | way to extend the capabilities 24 | * An extensible, high granularity security model allowing intricate rules 25 | on who can access which features, and integration with major 26 | authentication providers (database, OpenID, LDAP, OAuth & REMOTE_USER 27 | through Flask AppBuiler) 28 | * A simple semantic layer, allowing to control how data sources are 29 | displayed in the UI, 30 | by defining which fields should show up in which dropdown and which 31 | aggregation and function (metrics) are made available to the user 32 | * Deep integration with Druid allows for Panoramix to stay blazing fast while 33 | slicing and dicing large, realtime datasets 34 | 35 | 36 | Buzz Phrases 37 | ------------ 38 | 39 | * Analytics at the speed of thought! 40 | * Instantaneous learning curve 41 | * Realtime analytics when querying [Druid.io](http://druid.io) 42 | * Extentsible to infinity 43 | 44 | Database Support 45 | ---------------- 46 | 47 | Panoramix was originally designed on to of Druid.io, but quickly broadened 48 | its scope to support other databases through the use of SqlAlchemy, a Python 49 | ORM that is compatible with 50 | [most common databases](http://docs.sqlalchemy.org/en/rel_1_0/core/engines.html). 51 | 52 | 53 | What is Druid? 54 | ------------- 55 | From their website at http://druid.io 56 | 57 | *Druid is an open-source analytics data store designed for 58 | business intelligence (OLAP) queries on event data. Druid provides low 59 | latency (real-time) data ingestion, flexible data exploration, 60 | and fast data aggregation. Existing Druid deployments have scaled to 61 | trillions of events and petabytes of data. Druid is best used to 62 | power analytic dashboards and applications.* 63 | 64 | 65 | Installation 66 | ------------ 67 | 68 | Panoramix is currently only tested using Python 2.7.*. Python 3 support is 69 | on the roadmap, Python 2.6 won't be supported. 70 | 71 | Follow these few simple steps to install Panoramix. 72 | 73 | ``` 74 | # Install panoramix 75 | pip install panoramix 76 | 77 | # Create an admin user 78 | fabmanager create-admin --app panoramix 79 | 80 | # Initialize the database 81 | panoramix db upgrade 82 | 83 | # Create default roles and permissions 84 | panoramix init 85 | 86 | # Load some data to play with 87 | panoramix load_examples 88 | 89 | # Start the development web server 90 | panoramix runserver -d 91 | ``` 92 | 93 | After installation, you should be able to point your browser to the right 94 | hostname:port [http://localhost:8088](http://localhost:8088), login using 95 | the credential you entered while creating the admin account, and navigate to 96 | `Menu -> Admin -> Refresh Metadata`. This action should bring in all of 97 | your datasources for Panoramix to be aware of, and they should show up in 98 | `Menu -> Datasources`, from where you can start playing with your data! 99 | 100 | Configuration 101 | ------------- 102 | 103 | To configure your application, you need to create a file (module) 104 | `panoramix_config.py` and make sure it is in your PYTHONPATH. Here are some 105 | of the parameters you can copy / paste in that configuration module: 106 | 107 | ``` 108 | #--------------------------------------------------------- 109 | # Panoramix specifix config 110 | #--------------------------------------------------------- 111 | ROW_LIMIT = 5000 112 | WEBSERVER_THREADS = 8 113 | 114 | PANORAMIX_WEBSERVER_PORT = 8088 115 | #--------------------------------------------------------- 116 | 117 | #--------------------------------------------------------- 118 | # Flask App Builder configuration 119 | #--------------------------------------------------------- 120 | # Your App secret key 121 | SECRET_KEY = '\2\1thisismyscretkey\1\2\e\y\y\h' 122 | 123 | # The SQLAlchemy connection string. 124 | SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/panoramix.db' 125 | 126 | # Flask-WTF flag for CSRF 127 | CSRF_ENABLED = True 128 | 129 | # Whether to run the web server in debug mode or not 130 | DEBUG = True 131 | ``` 132 | 133 | This file also allows you to define configuration parameters used by 134 | Flask App Builder, the web framework used by Panoramix. Please consult 135 | the [Flask App Builder Documentation](http://flask-appbuilder.readthedocs.org/en/latest/config.html) for more information on how to configure Panoramix. 136 | 137 | 138 | * From the UI, enter the information about your clusters in the 139 | ``Admin->Clusters`` menu by hitting the + sign. 140 | 141 | * Once the Druid cluster connection information is entered, hit the 142 | ``Admin->Refresh Metadata`` menu item to populate 143 | 144 | * Navigate to your datasources 145 | 146 | More screenshots 147 | ---------------- 148 | 149 | ![img](http://i.imgur.com/Rt6gNQ9.png) 150 | ![img](http://i.imgur.com/t7VOtqQ.png) 151 | ![img](http://i.imgur.com/PaiFQnH.png) 152 | ![img](http://i.imgur.com/CdcGHuC.png) 153 | 154 | Related Links 155 | ------------- 156 | * [Panoramix Google Group] (https://groups.google.com/forum/#!forum/airbnb_panoramix) 157 | * [Gitter (live chat) Channel](https://gitter.im/mistercrunch/panoramix) 158 | 159 | 160 | Tip of the Hat 161 | -------------- 162 | 163 | Panoramix would not be possible without these great frameworks / libs 164 | 165 | * Flask App Builder - Allowing us to focus on building the app quickly while 166 | getting the foundation for free 167 | * The Flask ecosystem - Simply amazing. So much Plug, easy play. 168 | * NVD3 - One of the best charting library out there 169 | * Much more, check out the requirements.txt file! 170 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | List of TODO items for Panoramix 3 | 4 | ## Features 5 | * **URL shortner** 6 | * **Dashboard URL filters:** `{dash_url}#fltin__fieldname__value1,value2` 7 | * **Default slice:** choose a default slice for the dataset instead of default endpoint 8 | * **refresh freq**: specifying the refresh frequency of a dashboard and specific slices within it, some randomization would be nice 9 | * **Color hash in JS:** it'd be nice to use the same hash function for color attribution of series 10 | on the js side as on the python side (`panoramix.utils.color`) 11 | * **Widget sets / chart grids:** a way to have all charts support making a series of charts and putting them in a grid. 12 | the same way that you can groupby for series, you could chart by. The form fieldset would be common and use 13 | a single field to "grid by", a limit number of chart as an N * N grid size. 14 | * **Free form SQL editor:** Having an Airpal-like easy SQL editor 15 | * **Advanced dashboard configuration:** define which slices are immune to which filters, how often widgets should refresh, 16 | maybe this should start as a json blob... 17 | * **Getting proper JS testing:** unit tests on the Python side are pretty solid, but now we need a test 18 | suite for the JS part of the site, testing all the ajax-type calls 19 | * **Annotations layers:** allow for people to maintain data annotations, 20 | attached to a layer and time range. These layers can be added on top of some visualizations as annotations. 21 | An example of a layer might be "holidays" or "site outages", ... 22 | * **Worth doing? User defined groups:** People could define mappings in the UI of say "Countries I follow" and apply it to different datasets. For now, this is done by writing CASE-WHEN-type expression which is probably good enough. 23 | 24 | ## Easy-ish fix 25 | * datasource in explore mode could be a dropdown 26 | * Create a set of slices and dashboard on top of the World Bank dataset that ship with load_examples 27 | * [sql] make "Test Connection" test further, run an actual dummy query 28 | * [druid] Allow for post aggregations (ratios!) 29 | * in/notin filters autocomplete 30 | 31 | ## New viz 32 | * Animated scatter plots 33 | * Horizon charts 34 | * Chord diagram 35 | * ... 36 | 37 | ## Community 38 | * Creat a proper user documentation (started using Sphinx and boostrap...) 39 | * Usage vid 40 | -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = migrations 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # max length of characters to apply to the 11 | # "slug" field 12 | #truncate_slug_length = 40 13 | 14 | # set to 'true' to run the environment during 15 | # the 'revision' command, regardless of autogenerate 16 | # revision_environment = false 17 | 18 | # set to 'true' to allow .pyc and .pyo files without 19 | # a source .py file to be detected as revisions in the 20 | # versions/ directory 21 | # sourceless = false 22 | 23 | # version location specification; this defaults 24 | # to help/versions. When using multiple version 25 | # directories, initial revisions must be specified with --version-path 26 | # version_locations = %(here)s/bar %(here)s/bat help/versions 27 | 28 | # the output encoding used when revision files 29 | # are written from script.py.mako 30 | # output_encoding = utf-8 31 | 32 | sqlalchemy.url = scheme://localhost/panoramix 33 | 34 | # Logging configuration 35 | [loggers] 36 | keys = root,sqlalchemy,alembic 37 | 38 | [handlers] 39 | keys = console 40 | 41 | [formatters] 42 | keys = generic 43 | 44 | [logger_root] 45 | level = WARN 46 | handlers = console 47 | qualname = 48 | 49 | [logger_sqlalchemy] 50 | level = WARN 51 | handlers = 52 | qualname = sqlalchemy.engine 53 | 54 | [logger_alembic] 55 | level = INFO 56 | handlers = 57 | qualname = alembic 58 | 59 | [handler_console] 60 | class = StreamHandler 61 | args = (sys.stderr,) 62 | level = NOTSET 63 | formatter = generic 64 | 65 | [formatter_generic] 66 | format = %(levelname)-5.5s [%(name)s] %(message)s 67 | datefmt = %H:%M:%S 68 | -------------------------------------------------------------------------------- /babel/babel.cfg: -------------------------------------------------------------------------------- 1 | [python: **.py] 2 | [jinja2: **/templates/**.html] 3 | encoding = utf-8 4 | -------------------------------------------------------------------------------- /babel/messages.pot: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/panoramix.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/panoramix.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/panoramix" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/panoramix" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/_static/docs.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 0px; 3 | } 4 | 5 | div.navbar { 6 | margin-bottom: 0px; 7 | } 8 | 9 | .carousel img { 10 | max-height: 500px; 11 | } 12 | .carousel { 13 | overflow: hidden; 14 | height: 500px; 15 | } 16 | .carousel-caption h1 { 17 | font-size: 80px; 18 | } 19 | .carousel-caption p { 20 | font-size: 20px; 21 | } 22 | div.carousel-caption{ 23 | background: rgba(0,0,0,0.5); 24 | border-radius: 20px; 25 | top: 150px; 26 | bottom: auto !important; 27 | } 28 | .carousel-inner > .item > img { 29 | margin: 0 auto; 30 | } 31 | { 32 | margin: -20px; 33 | } 34 | .carousel-indicators li { 35 | background-color: #AAA; 36 | border: 1px solid black; 37 | } 38 | 39 | .carousel-indicators .active { 40 | background-color: #000; 41 | border: 5px solid black; 42 | } 43 | -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | 3 | {% set bootswatch_css_custom = ['_static/docs.css'] %} 4 | 5 | {%- block content %} 6 | {{ navBar() }} 7 | {% if pagename == 'index' %} 8 | 80 |
81 |
82 |
83 |

Panoramix

84 |

85 | is an open source data visualization platform that provides easy 86 | exploration of your data and allows you to create and share 87 | beautiful charts and dashboards 88 |

89 |
90 |
91 | {% endif %} 92 |
93 | {% block body %}{% endblock %} 94 |
95 | {%- endblock %} 96 | -------------------------------------------------------------------------------- /docs/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | rm -r _build 3 | make html 4 | -------------------------------------------------------------------------------- /docs/img: -------------------------------------------------------------------------------- 1 | ../panoramix/static/img/ -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. panoramix documentation master file, created by 2 | sphinx-quickstart on Thu Dec 17 15:42:06 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. image:: img/tux_panoramix.png 7 | 8 | Overview 9 | ======================================= 10 | 11 | Features 12 | --------- 13 | 14 | - A rich set of data visualizations, integrated from some of the best visualization libraries 15 | - Create and share simple dashboards 16 | - An extensible, high-granularity security/permission model allowing intricate rules on who can access individual features and the dataset 17 | - Enterprise-ready authentication with integration with major authentication providers (database, OpenID, LDAP, OAuth & REMOTE_USER through Flask AppBuilder) 18 | - A simple semantic layer, allowing users to control how data sources are displayed in the UI by defining which fields should show up in which drop-down and which aggregation and function metrics are made available to the user 19 | - Integration with most RDBMS through SqlAlchemy 20 | - Deep integration with Druid.io 21 | 22 | Contents 23 | --------- 24 | 25 | .. toctree:: 26 | :maxdepth: 2 27 | 28 | user_guide 29 | 30 | 31 | 32 | Indices and tables 33 | ------------------ 34 | 35 | * :ref:`genindex` 36 | * :ref:`modindex` 37 | * :ref:`search` 38 | 39 | -------------------------------------------------------------------------------- /docs/user_guide.rst: -------------------------------------------------------------------------------- 1 | User Guide 2 | ========== 3 | Panoramix is easy to use, so this User Guide should be short. 4 | -------------------------------------------------------------------------------- /panoramix/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from flask import Flask 4 | from flask.ext.appbuilder import SQLA, AppBuilder, IndexView 5 | from flask.ext.migrate import Migrate 6 | from panoramix import config 7 | 8 | 9 | APP_DIR = os.path.dirname(__file__) 10 | CONFIG_MODULE = os.environ.get('PANORAMIX_CONFIG', 'panoramix.config') 11 | 12 | # Logging configuration 13 | logging.basicConfig(format='%(asctime)s:%(levelname)s:%(name)s:%(message)s') 14 | logging.getLogger().setLevel(logging.DEBUG) 15 | 16 | app = Flask(__name__) 17 | app.config.from_object(CONFIG_MODULE) 18 | db = SQLA(app) 19 | migrate = Migrate(app, db, directory=APP_DIR + "/migrations") 20 | 21 | 22 | class MyIndexView(IndexView): 23 | index_template = 'index.html' 24 | 25 | appbuilder = AppBuilder( 26 | app, db.session, base_template='panoramix/base.html', 27 | indexview=MyIndexView, 28 | security_manager_class=app.config.get("CUSTOM_SECURITY_MANAGER")) 29 | 30 | sm = appbuilder.sm 31 | 32 | get_session = appbuilder.get_session 33 | from panoramix import views 34 | -------------------------------------------------------------------------------- /panoramix/ascii_art.py: -------------------------------------------------------------------------------- 1 | error = ( 2 | "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+ 3 | "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+ 4 | "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMM8OI++=~~~~~~=+?IODMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+ 5 | "MMMMMMMMMMMMMMMMMMMMMMMMMD$~~~~~~~~~~~~~~~~~~~~~~~=$MMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+ 6 | "MMMMMMMMMMMMMMMMMMMMMMN8?:~~~~~~~~~~~~~~~~~~~~~~~~~~=+8NMMMMMMMMMMMMMMMMMMMMMMMM\n"+ 7 | "MMMMMMMMMMMMMMMMMMMMO=~~~~~~~~~~~~~~~~~+I??~~~~~~~~~~~~~+DMMMMMMMMMMMMMMMMMMMMMM\n"+ 8 | "MMMMMMMMMMMMMMMMMMNI~~~~~~~~~~~~~~~~~~IIIII=~~~~~~~~~~~~~~=NMMMMMMMMMMMMMMMMMMMM\n"+ 9 | "MMMMMMMMMMMMMMMMM+=~~~~~~~~~~~~~~~~~~~=III+~~~~~~~~~~~~~~~~~?8MMMMMMMMMMMMMMMMMM\n"+ 10 | "MMMMMMMMMMMMMMMM?~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+++=~~~~8MMMMMMMMMMMMMMMMM\n"+ 11 | "MMMMMMMMMMMMMMI=~~~~~~~~~~~~~~~~~~~~~~~~~III?I~~~~~~~~,:++++++~~8MMMMMMMMMMMMMMM\n"+ 12 | "MMMMMMMMMMMMN7~~~~~~~~~~~~~~~~==+=~~~~~~=IIIII~~~~~~:. ..:=++=~=MMMMMMMMMMMMMMM\n"+ 13 | "MMMMMMMMMMMO=~~~~~~~~~~~~~~~~+++=~~~~~~~~??I?I~~~~~~. ...,~~~~IMMMMMMMMMMMMM\n"+ 14 | "MMMMMMMMMMM~~~~~~~~~~~~~~~~~+++:,~~~~~~~~~~~?=~~~~~:. ..~~~~~OMMMMMMMMMMMM\n"+ 15 | "MMMMMMMMM$=~~~~~~~~~~~~~~~=++:.. ..~~~~~~~~~~~~~~~~,. . . :~~~~~OMMMMMMMMMMM\n"+ 16 | "MMMMMMMMM~~~~~~~~~~~~~~~~+++,. .~~~~~~~~~~~~~~~.. .. . .~~~~~=OMMMMMMMMMM\n"+ 17 | "MMMMMMMM?~~~~~~~~~~~~~~~=+~. .~~~~~~~~~~~~~~. ,MMMMM,=~~~~~~NMMMMMMMMM\n"+ 18 | "MMMMMMMN~~~~~~~~~~~~~~~~~,. .,~~~~~~~~~~~~~.. ZMMM,+Z:~~~~~~$MMMMMMMMM\n"+ 19 | "MMMMMM8?~~~~~~~~~~~~~~~~~.. ..~~~~~~~~~~~~~:. DMMM,+D~~~~~~~~IMMMMMMMM\n"+ 20 | "MMMMMMI~~~~~~~~~~~~~~~~~~.. :MMMO~~~~~~~~~~~~~~~,.. ?MMMMMI~~~~~~~~~MMMMMMMM\n"+ 21 | "MMMMMM=~~~~~~~~~~~~~~~~~~.. MMM+=M:~~~~~~~~~~~~~:. .:IM$~~~~~~~~~~~8MMMMMMM\n"+ 22 | "MMMMMD~~~~~~~~~~~~~~~~~~~:. MMM:,M:~~~~~~~~~~~~~~~.......:~~~~~~~~~~$MMMMMMM\n"+ 23 | "MMMMMI~~~~~~~~~~~~~~~~~~~~, MMMMMM~~~~~~~~~~~~~~~~~~,..:~~~~~~~~~~~~+MMMMMMM\n"+ 24 | "MMMMD+~~~~~~~~~~~~~~~~~~~~~. $MMMM$~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=MMMMMMM\n"+ 25 | "MMMM8~~~~~~~~~~~~~~~~~~~~~~:. . .:~~~~~~,..:. .=~~~~~~~~~~~~~~~~~~~~MMMMMMM\n"+ 26 | "MMMMO~~~~~~~~~~~~~~~~~~~~~~~:, .:~~~~~=8.. .+ . =8ZI~~~~~~~~~~~~~~~~=MMMMMMM\n"+ 27 | "MMMMZ=~~~~~~~~~~~~~~~~~~~~~~~~:,,,:~~~~~~IZ8:. .O....888?~~~~~~~~~~~~~~~+MMMMMMM\n"+ 28 | "MMMMO=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~?888=...I~I88888O?~~~~~~~~~~~~~~7MMMMMMM\n"+ 29 | "MMMMO~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Z888OO88888888888O?~~~~~~~~~~~~~OMMMMMMM\n"+ 30 | "MMMMD+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=8888888888888888888~~~~~~~~~~~~+MMMMMMMM\n"+ 31 | "MMMMM7~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~?8888888888888888888?~~~~~~~~~~=$MMMMMMMM\n"+ 32 | "MMMMMD~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=$8888888888888888888O~~~~~~~~~~8MMMMMMMMM\n"+ 33 | "MMMMMN=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+Z88888888888888888ZZ7=~~~~~~~~?MMMMMMMMMM\n"+ 34 | "MMMMMMZ=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+Z88888888Z7I===~~~~~~~~~~~~~=OMMMMMMMMMMM\n"+ 35 | "MMMMMMN$~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=$88888O7?=~~~~~~~~~~~~~~~~~~OMMMMMMMMMMMM\n"+ 36 | "MMMMMMMM?~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~I8OZ+~~~~~~~~~~~~~~~~~~~~=DMMMMMMMMMMMMMM\n"+ 37 | "MMMMMMMM8=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+$+=~~~~~~~~~~~~~~~~~~~~+MMMMMMMMMMMMMMMM\n"+ 38 | "MMMMMMMMMD7~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=$DMMMMMMMMMMMMMMMMMM\n"+ 39 | "MMMMMMMMMMM?~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=$OMMMMMMMMMMMMMMMMMMMMM\n"+ 40 | "MMMMMMMMMMMMD7=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ZMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+ 41 | "MMMMMMMMMMMMMMZ7=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~78MMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+ 42 | "MMMMMMMMMMMMMMMMM8OI=~~~~~~~~~~~~~~~~~~~=+?ZDNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+ 43 | "MMMMMMMMMMMMMMMMMMMMNDZ7?++~=~==~+?IONMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+ 44 | "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+ 45 | "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+ 46 | "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+ 47 | "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM") 48 | 49 | stacktrace=""" 50 | ------------------------------------------------------------------------------------------------------- 51 | ======================================================================================================= 52 | ------------------------------------------------------------------------------------------------------- 53 | ___ ___ ___ 54 | ( ) ( ) ( ) 55 | .--. | |_ .---. .--. | | ___ | |_ ___ .-. .---. .--. .--. 56 | / _ \ ( __) / .-, \ / \ | | ( ) ( __) ( ) \ / .-, \ / \ / \\ 57 | . .' `. ; | | (__) ; | | .-. ; | | ' / | | | ' .-. ; (__) ; | | .-. ; | .-. ; 58 | | ' | | | | ___ .'` | | |(___) | |,' / | | ___ | / (___) .'` | | |(___) | | | | 59 | _\_`.(___) | |( ) / .'| | | | | . '. | |( ) | | / .'| | | | | |/ | 60 | ( ). '. | | | | | / | | | | ___ | | `. \ | | | | | | | / | | | | ___ | ' _.' 61 | | | `\ | | ' | | ; | ; | | '( ) | | \ \ | ' | | | | ; | ; | | '( ) | .'.-. 62 | ; '._,' ' ' `-' ; ' `-' | ' `-' | | | \ . ' `-' ; | | ' `-' | ' `-' | ' `-' / 63 | '.___.' `.__. `.__.'_. `.__,' (___ ) (___) `.__. (___) `.__.'_. `.__,' `.__.' 64 | 65 | ------------------------------------------------------------------------------------------------------- 66 | ======================================================================================================= 67 | ------------------------------------------------------------------------------------------------------- 68 | """ 69 | -------------------------------------------------------------------------------- /panoramix/bin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclairvoyant/panoramix/32876f283456373ccecd83064a3af343d7014d29/panoramix/bin/__init__.py -------------------------------------------------------------------------------- /panoramix/bin/panoramix: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from subprocess import Popen 4 | 5 | from flask.ext.script import Manager 6 | from panoramix import app 7 | from flask.ext.migrate import MigrateCommand 8 | from panoramix import db 9 | from panoramix import data, utils 10 | 11 | config = app.config 12 | 13 | manager = Manager(app) 14 | manager.add_command('db', MigrateCommand) 15 | 16 | 17 | @manager.option( 18 | '-d', '--debug', action='store_true', 19 | help="Start the web server in debug mode") 20 | @manager.option( 21 | '-p', '--port', default=config.get("PANORAMIX_WEBSERVER_PORT"), 22 | help="Specify the port on which to run the web server") 23 | @manager.option( 24 | '-t', '--timeout', default=config.get("PANORAMIX_WEBSERVER_TIMEOUT"), 25 | help="Specify the timeout (seconds) for the gunicorn web server") 26 | def runserver(debug, port, timeout): 27 | """Starts a Panoramix web server""" 28 | debug = debug or config.get("DEBUG") 29 | if debug: 30 | app.run( 31 | host='0.0.0.0', 32 | port=int(port), 33 | debug=True) 34 | else: 35 | cmd = ( 36 | "gunicorn " 37 | "-w 8 " 38 | "--timeout {timeout} " 39 | "-b 0.0.0.0:{port} " 40 | "panoramix:app").format(**locals()) 41 | print("Starting server with command: " + cmd) 42 | Popen(cmd, shell=True).wait() 43 | 44 | @manager.command 45 | def init(): 46 | """Inits the Panoramix application""" 47 | utils.init() 48 | 49 | @manager.option( 50 | '-s', '--sample', action='store_true', 51 | help="Only load 1000 rows (faster, used for testing)") 52 | def load_examples(sample): 53 | """Loads a set of Slices and Dashboards and a supporting dataset """ 54 | print("Loading examples into {}".format(db)) 55 | 56 | print("Loading [World Bank's Health Nutrition and Population Stats]") 57 | data.load_world_bank_health_n_pop() 58 | print("Loading [Birth names]") 59 | data.load_birth_names() 60 | 61 | 62 | if __name__ == "__main__": 63 | manager.run() 64 | -------------------------------------------------------------------------------- /panoramix/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask_appbuilder.security.manager import AUTH_DB 3 | # from flask_appbuilder.security.manager import ( 4 | # AUTH_OID, AUTH_REMOTE_USER, AUTH_DB, AUTH_LDAP, AUTH_OAUTH) 5 | BASE_DIR = os.path.abspath(os.path.dirname(__file__)) 6 | from dateutil import tz 7 | 8 | """ 9 | All configuration in this file can be overridden by providing a local_config 10 | in your PYTHONPATH. 11 | 12 | There' a ``from local_config import *`` at the end of this file. 13 | """ 14 | 15 | # --------------------------------------------------------- 16 | # Panoramix specifix config 17 | # --------------------------------------------------------- 18 | ROW_LIMIT = 10000 19 | WEBSERVER_THREADS = 8 20 | 21 | PANORAMIX_WEBSERVER_PORT = 8088 22 | PANORAMIX_WEBSERVER_TIMEOUT = 60 23 | 24 | CUSTOM_SECURITY_MANAGER = None 25 | # --------------------------------------------------------- 26 | 27 | # Your App secret key 28 | SECRET_KEY = '\2\1thisismyscretkey\1\2\e\y\y\h' 29 | 30 | # The SQLAlchemy connection string. 31 | SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/panoramix.db' 32 | # SQLALCHEMY_DATABASE_URI = 'mysql://myapp@localhost/myapp' 33 | # SQLALCHEMY_DATABASE_URI = 'postgresql://root:password@localhost/myapp' 34 | 35 | # Flask-WTF flag for CSRF 36 | CSRF_ENABLED = True 37 | 38 | # Whether to run the web server in debug mode or not 39 | DEBUG = True 40 | 41 | # Whether to show the stacktrace on 500 error 42 | SHOW_STACKTRACE = True 43 | 44 | # ------------------------------ 45 | # GLOBALS FOR APP Builder 46 | # ------------------------------ 47 | # Uncomment to setup Your App name 48 | APP_NAME = "Panoramix" 49 | 50 | # Uncomment to setup Setup an App icon 51 | APP_ICON = "/static/img/chaudron_blue.png" 52 | 53 | # Druid query timezone 54 | # tz.tzutc() : Using utc timezone 55 | # tz.tzlocal() : Using local timezone 56 | # other tz can be overridden by providing a local_config 57 | DRUID_TZ = tz.tzutc() 58 | 59 | # ---------------------------------------------------- 60 | # AUTHENTICATION CONFIG 61 | # ---------------------------------------------------- 62 | # The authentication type 63 | # AUTH_OID : Is for OpenID 64 | # AUTH_DB : Is for database (username/password() 65 | # AUTH_LDAP : Is for LDAP 66 | # AUTH_REMOTE_USER : Is for using REMOTE_USER from web server 67 | AUTH_TYPE = AUTH_DB 68 | 69 | # Uncomment to setup Full admin role name 70 | # AUTH_ROLE_ADMIN = 'Admin' 71 | 72 | # Uncomment to setup Public role name, no authentication needed 73 | # AUTH_ROLE_PUBLIC = 'Public' 74 | 75 | # Will allow user self registration 76 | # AUTH_USER_REGISTRATION = True 77 | 78 | # The default user self registration role 79 | # AUTH_USER_REGISTRATION_ROLE = "Public" 80 | 81 | # When using LDAP Auth, setup the ldap server 82 | # AUTH_LDAP_SERVER = "ldap://ldapserver.new" 83 | 84 | # Uncomment to setup OpenID providers example for OpenID authentication 85 | # OPENID_PROVIDERS = [ 86 | # { 'name': 'Yahoo', 'url': 'https://me.yahoo.com' }, 87 | # { 'name': 'AOL', 'url': 'http://openid.aol.com/' }, 88 | # { 'name': 'Flickr', 'url': 'http://www.flickr.com/' }, 89 | # { 'name': 'MyOpenID', 'url': 'https://www.myopenid.com' }] 90 | # --------------------------------------------------- 91 | # Babel config for translations 92 | # --------------------------------------------------- 93 | # Setup default language 94 | BABEL_DEFAULT_LOCALE = 'en' 95 | # Your application default translation path 96 | BABEL_DEFAULT_FOLDER = 'translations' 97 | # The allowed translation for you app 98 | LANGUAGES = { 99 | 'en': {'flag': 'us', 'name': 'English'}, 100 | 'fr': {'flag': 'fr', 'name': 'French'}, 101 | } 102 | """ 103 | 'pt': {'flag':'pt', 'name':'Portuguese'}, 104 | 'pt_BR': {'flag':'br', 'name': 'Pt Brazil'}, 105 | 'es': {'flag':'es', 'name':'Spanish'}, 106 | 'de': {'flag':'de', 'name':'German'}, 107 | 'zh': {'flag':'cn', 'name':'Chinese'}, 108 | 'ru': {'flag':'ru', 'name':'Russian'} 109 | """ 110 | # --------------------------------------------------- 111 | # Image and file configuration 112 | # --------------------------------------------------- 113 | # The file upload folder, when using models with files 114 | UPLOAD_FOLDER = BASE_DIR + '/app/static/uploads/' 115 | 116 | # The image upload folder, when using models with images 117 | IMG_UPLOAD_FOLDER = BASE_DIR + '/app/static/uploads/' 118 | 119 | # The image upload url, when using models with images 120 | IMG_UPLOAD_URL = '/static/uploads/' 121 | # Setup image size default is (300, 200, True) 122 | # IMG_SIZE = (300, 200, True) 123 | 124 | # --------------------------------------------------- 125 | # Theme configuration 126 | # these are located on static/appbuilder/css/themes 127 | # you can create your own and easily use them placing them on the 128 | # same dir structure to override 129 | # --------------------------------------------------- 130 | # APP_THEME = "bootstrap-theme.css" # default bootstrap 131 | # APP_THEME = "cerulean.css" 132 | # APP_THEME = "amelia.css" 133 | # APP_THEME = "cosmo.css" 134 | # APP_THEME = "cyborg.css" 135 | # APP_THEME = "flatly.css" 136 | # APP_THEME = "journal.css" 137 | # APP_THEME = "readable.css" 138 | # APP_THEME = "simplex.css" 139 | # APP_THEME = "slate.css" 140 | # APP_THEME = "spacelab.css" 141 | # APP_THEME = "united.css" 142 | # APP_THEME = "yeti.css" 143 | 144 | try: 145 | from panoramix_config import * 146 | except: 147 | pass 148 | -------------------------------------------------------------------------------- /panoramix/data/__init__.py: -------------------------------------------------------------------------------- 1 | import gzip 2 | import json 3 | import os 4 | 5 | import pandas as pd 6 | from sqlalchemy import String, DateTime 7 | 8 | from panoramix import app, db, models, utils 9 | 10 | config = app.config 11 | 12 | DATA_FOLDER = os.path.join(config.get("BASE_DIR"), 'data') 13 | 14 | 15 | def get_or_create_db(session): 16 | print("Creating database reference") 17 | DB = models.Database 18 | dbobj = session.query(DB).filter_by(database_name='main').first() 19 | if not dbobj: 20 | dbobj = DB(database_name="main") 21 | print(config.get("SQLALCHEMY_DATABASE_URI")) 22 | dbobj.sqlalchemy_uri = config.get("SQLALCHEMY_DATABASE_URI") 23 | session.add(dbobj) 24 | session.commit() 25 | return dbobj 26 | 27 | 28 | def load_world_bank_health_n_pop(): 29 | tbl = 'wb_health_population' 30 | with gzip.open(os.path.join(DATA_FOLDER, 'countries.json.gz')) as f: 31 | pdf = pd.read_json(f) 32 | pdf.year = pd.to_datetime(pdf.year) 33 | pdf.to_sql( 34 | tbl, 35 | db.engine, 36 | if_exists='replace', 37 | chunksize=500, 38 | dtype={ 39 | 'year': DateTime(), 40 | 'country_code': String(3), 41 | 'country_name': String(255), 42 | 'region': String(255), 43 | }, 44 | index=False) 45 | print("Creating table reference") 46 | TBL = models.SqlaTable 47 | obj = db.session.query(TBL).filter_by(table_name=tbl).first() 48 | if not obj: 49 | obj = TBL(table_name='wb_health_population') 50 | obj.description = utils.readfile(os.path.join(DATA_FOLDER, 'countries.md')) 51 | obj.main_dttm_col = 'year' 52 | obj.database = get_or_create_db(db.session) 53 | db.session.merge(obj) 54 | db.session.commit() 55 | obj.fetch_metadata() 56 | 57 | 58 | def load_birth_names(): 59 | session = db.session 60 | with gzip.open(os.path.join(DATA_FOLDER, 'birth_names.json.gz')) as f: 61 | pdf = pd.read_json(f) 62 | pdf.ds = pd.to_datetime(pdf.ds, unit='ms') 63 | pdf.to_sql( 64 | 'birth_names', 65 | db.engine, 66 | if_exists='replace', 67 | chunksize=500, 68 | dtype={ 69 | 'ds': DateTime, 70 | 'gender': String(16), 71 | 'state': String(10), 72 | 'name': String(255), 73 | }, 74 | index=False) 75 | l = [] 76 | print("Done loading table!") 77 | print("-" * 80) 78 | 79 | print("Creating table reference") 80 | TBL = models.SqlaTable 81 | obj = db.session.query(TBL).filter_by(table_name='birth_names').first() 82 | if not obj: 83 | obj = TBL(table_name = 'birth_names') 84 | obj.main_dttm_col = 'ds' 85 | obj.database = get_or_create_db(db.session) 86 | models.Table 87 | db.session.merge(obj) 88 | db.session.commit() 89 | obj.fetch_metadata() 90 | tbl = obj 91 | 92 | print("Creating some slices") 93 | def get_slice_json(**kwargs): 94 | defaults = { 95 | "compare_lag": "10", 96 | "compare_suffix": "o10Y", 97 | "datasource_id": "1", 98 | "datasource_name": "birth_names", 99 | "datasource_type": "table", 100 | "limit": "25", 101 | "flt_col_1": "gender", 102 | "flt_eq_1": "", 103 | "flt_op_1": "in", 104 | "granularity": "ds", 105 | "groupby": [], 106 | "metric": 'sum__num', 107 | "metrics": ["sum__num"], 108 | "row_limit": config.get("ROW_LIMIT"), 109 | "since": "100 years", 110 | "until": "now", 111 | "viz_type": "table", 112 | "where": "", 113 | "markup_type": "markdown", 114 | } 115 | d = defaults.copy() 116 | d.update(kwargs) 117 | return json.dumps(d, indent=4, sort_keys=True) 118 | Slice = models.Slice 119 | slices = [] 120 | 121 | def merge_slice(slc): 122 | o = db.session.query( 123 | Slice).filter_by(slice_name=slc.slice_name).first() 124 | if o: 125 | db.session.delete(slc) 126 | db.session.add(slc) 127 | session.commit() 128 | slices.append(slc) 129 | 130 | merge_slice( 131 | Slice( 132 | slice_name="Girls", 133 | viz_type='table', 134 | datasource_type='table', 135 | table=tbl, 136 | params=get_slice_json( 137 | groupby=['name'], flt_eq_1="girl", row_limit=50))) 138 | 139 | merge_slice( 140 | Slice( 141 | slice_name="Boys", 142 | viz_type='table', 143 | datasource_type='table', 144 | table=tbl, 145 | params=get_slice_json( 146 | groupby=['name'], flt_eq_1="boy", row_limit=50))) 147 | 148 | merge_slice( 149 | Slice( 150 | slice_name="Participants", 151 | viz_type='big_number', 152 | datasource_type='table', 153 | table=tbl, 154 | params=get_slice_json( 155 | viz_type="big_number", granularity="ds", 156 | compare_lag="5", compare_suffix="over 5Y"))) 157 | 158 | merge_slice( 159 | Slice( 160 | slice_name="Genders", 161 | viz_type='pie', 162 | datasource_type='table', 163 | table=tbl, 164 | params=get_slice_json( 165 | viz_type="pie", groupby=['gender']))) 166 | 167 | merge_slice( 168 | Slice( 169 | slice_name="Genders by State", 170 | viz_type='dist_bar', 171 | datasource_type='table', 172 | table=tbl, 173 | params=get_slice_json( 174 | flt_eq_1="other", viz_type="dist_bar", 175 | metrics=['sum__sum_girls', 'sum__sum_boys'], 176 | groupby=['state'], flt_op_1='not in', flt_col_1='state'))) 177 | 178 | merge_slice( 179 | Slice( 180 | slice_name="Trends", 181 | viz_type='line', 182 | datasource_type='table', 183 | table=tbl, 184 | params=get_slice_json( 185 | viz_type="line", groupby=['name'], 186 | granularity='ds', rich_tooltip='y', show_legend='y'))) 187 | 188 | code = """ 189 |
190 |

Birth Names Dashboard

191 |

The source dataset came from [here]

192 | 193 |
194 | """ 195 | merge_slice( 196 | Slice( 197 | slice_name="Title", 198 | viz_type='markup', 199 | datasource_type='table', 200 | table=tbl, 201 | params=get_slice_json( 202 | viz_type="markup", markup_type="html", 203 | code=code))) 204 | 205 | merge_slice( 206 | Slice( 207 | slice_name="Name Cloud", 208 | viz_type='word_cloud', 209 | datasource_type='table', 210 | table=tbl, 211 | params=get_slice_json( 212 | viz_type="word_cloud", size_from="10", 213 | series='name', size_to="70", rotation="square", 214 | limit='100'))) 215 | 216 | merge_slice( 217 | Slice( 218 | slice_name="Pivot Table", 219 | viz_type='pivot_table', 220 | datasource_type='table', 221 | table=tbl, 222 | params=get_slice_json( 223 | viz_type="pivot_table", metrics=['sum__num'], 224 | groupby=['name'], columns=['state']))) 225 | 226 | print("Creating a dashboard") 227 | Dash = models.Dashboard 228 | dash = session.query(Dash).filter_by(dashboard_title="Births").first() 229 | 230 | if dash: 231 | db.session.delete(dash) 232 | js = """ 233 | [ 234 | { 235 | "size_y": 4, 236 | "size_x": 2, 237 | "col": 8, 238 | "slice_id": "85", 239 | "row": 7 240 | }, 241 | { 242 | "size_y": 4, 243 | "size_x": 2, 244 | "col": 10, 245 | "slice_id": "86", 246 | "row": 7 247 | }, 248 | { 249 | "size_y": 2, 250 | "size_x": 2, 251 | "col": 1, 252 | "slice_id": "87", 253 | "row": 1 254 | }, 255 | { 256 | "size_y": 2, 257 | "size_x": 2, 258 | "col": 3, 259 | "slice_id": "88", 260 | "row": 1 261 | }, 262 | { 263 | "size_y": 3, 264 | "size_x": 7, 265 | "col": 5, 266 | "slice_id": "89", 267 | "row": 4 268 | }, 269 | { 270 | "size_y": 4, 271 | "size_x": 7, 272 | "col": 1, 273 | "slice_id": "90", 274 | "row": 7 275 | }, 276 | { 277 | "size_y": 3, 278 | "size_x": 3, 279 | "col": 9, 280 | "slice_id": "91", 281 | "row": 1 282 | }, 283 | { 284 | "size_y": 3, 285 | "size_x": 4, 286 | "col": 5, 287 | "slice_id": "92", 288 | "row": 1 289 | }, 290 | { 291 | "size_y": 4, 292 | "size_x": 4, 293 | "col": 1, 294 | "slice_id": "93", 295 | "row": 3 296 | } 297 | ] 298 | """ 299 | l = json.loads(js) 300 | for i, pos in enumerate(l): 301 | pos['slice_id'] = str(slices[i].id) 302 | dash = Dash( 303 | dashboard_title="Births", 304 | position_json=json.dumps(l, indent=4), 305 | ) 306 | for s in slices: 307 | dash.slices.append(s) 308 | session.commit() 309 | -------------------------------------------------------------------------------- /panoramix/data/birth_names.csv.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclairvoyant/panoramix/32876f283456373ccecd83064a3af343d7014d29/panoramix/data/birth_names.csv.gz -------------------------------------------------------------------------------- /panoramix/data/birth_names.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclairvoyant/panoramix/32876f283456373ccecd83064a3af343d7014d29/panoramix/data/birth_names.json.gz -------------------------------------------------------------------------------- /panoramix/data/countries.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclairvoyant/panoramix/32876f283456373ccecd83064a3af343d7014d29/panoramix/data/countries.json.gz -------------------------------------------------------------------------------- /panoramix/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /panoramix/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclairvoyant/panoramix/32876f283456373ccecd83064a3af343d7014d29/panoramix/migrations/__init__.py -------------------------------------------------------------------------------- /panoramix/migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /panoramix/migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from alembic import context 3 | from sqlalchemy import engine_from_config, pool 4 | from logging.config import fileConfig 5 | import logging 6 | from panoramix import db, models 7 | from flask.ext.appbuilder import Base 8 | 9 | # this is the Alembic Config object, which provides 10 | # access to the values within the .ini file in use. 11 | config = context.config 12 | 13 | # Interpret the config file for Python logging. 14 | # This line sets up loggers basically. 15 | fileConfig(config.config_file_name) 16 | logger = logging.getLogger('alembic.env') 17 | 18 | # add your model's MetaData object here 19 | # for 'autogenerate' support 20 | # from myapp import mymodel 21 | from flask import current_app 22 | config.set_main_option('sqlalchemy.url', 23 | current_app.config.get('SQLALCHEMY_DATABASE_URI')) 24 | target_metadata = Base.metadata 25 | 26 | # other values from the config, defined by the needs of env.py, 27 | # can be acquired: 28 | # my_important_option = config.get_main_option("my_important_option") 29 | # ... etc. 30 | 31 | 32 | def run_migrations_offline(): 33 | """Run migrations in 'offline' mode. 34 | 35 | This configures the context with just a URL 36 | and not an Engine, though an Engine is acceptable 37 | here as well. By skipping the Engine creation 38 | we don't even need a DBAPI to be available. 39 | 40 | Calls to context.execute() here emit the given string to the 41 | script output. 42 | 43 | """ 44 | url = config.get_main_option("sqlalchemy.url") 45 | context.configure(url=url) 46 | 47 | with context.begin_transaction(): 48 | context.run_migrations() 49 | 50 | 51 | def run_migrations_online(): 52 | """Run migrations in 'online' mode. 53 | 54 | In this scenario we need to create an Engine 55 | and associate a connection with the context. 56 | 57 | """ 58 | 59 | # this callback is used to prevent an auto-migration from being generated 60 | # when there are no changes to the schema 61 | # reference: http://alembic.readthedocs.org/en/latest/cookbook.html 62 | def process_revision_directives(context, revision, directives): 63 | if getattr(config.cmd_opts, 'autogenerate', False): 64 | script = directives[0] 65 | if script.upgrade_ops.is_empty(): 66 | directives[:] = [] 67 | logger.info('No changes in schema detected.') 68 | 69 | engine = engine_from_config(config.get_section(config.config_ini_section), 70 | prefix='sqlalchemy.', 71 | poolclass=pool.NullPool) 72 | 73 | connection = engine.connect() 74 | context.configure(connection=connection, 75 | target_metadata=target_metadata, 76 | #compare_type=True, 77 | process_revision_directives=process_revision_directives, 78 | **current_app.extensions['migrate'].configure_args) 79 | 80 | try: 81 | with context.begin_transaction(): 82 | context.run_migrations() 83 | finally: 84 | connection.close() 85 | 86 | if context.is_offline_mode(): 87 | run_migrations_offline() 88 | else: 89 | run_migrations_online() 90 | -------------------------------------------------------------------------------- /panoramix/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = ${repr(up_revision)} 11 | down_revision = ${repr(down_revision)} 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | ${imports if imports else ""} 16 | 17 | def upgrade(): 18 | ${upgrades if upgrades else "pass"} 19 | 20 | 21 | def downgrade(): 22 | ${downgrades if downgrades else "pass"} 23 | -------------------------------------------------------------------------------- /panoramix/migrations/versions/12d55656cbca_is_featured.py: -------------------------------------------------------------------------------- 1 | """is_featured 2 | 3 | Revision ID: 12d55656cbca 4 | Revises: 55179c7f25c7 5 | Create Date: 2015-12-14 13:37:17.374852 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '12d55656cbca' 11 | down_revision = '55179c7f25c7' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('tables', sa.Column('is_featured', sa.Boolean(), nullable=True)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('tables', 'is_featured') 23 | 24 | -------------------------------------------------------------------------------- /panoramix/migrations/versions/1a48a5411020_adding_slug_to_dash.py: -------------------------------------------------------------------------------- 1 | """adding slug to dash 2 | 3 | Revision ID: 1a48a5411020 4 | Revises: 289ce07647b 5 | Create Date: 2015-12-04 09:42:16.973264 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '1a48a5411020' 11 | down_revision = '289ce07647b' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | def upgrade(): 17 | op.add_column('dashboards', sa.Column('slug', sa.String(length=255), nullable=True)) 18 | try: 19 | op.create_unique_constraint('idx_unique_slug', 'dashboards', ['slug']) 20 | except: 21 | pass 22 | 23 | 24 | def downgrade(): 25 | op.drop_constraint(None, 'dashboards', type_='unique') 26 | op.drop_column('dashboards', 'slug') 27 | -------------------------------------------------------------------------------- /panoramix/migrations/versions/1e2841a4128_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 1e2841a4128 4 | Revises: 5a7bad26f2a7 5 | Create Date: 2015-10-05 22:11:00.537054 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '1e2841a4128' 11 | down_revision = '5a7bad26f2a7' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | from sqlalchemy.dialects import mysql 16 | 17 | def upgrade(): 18 | ### commands auto generated by Alembic - please adjust! ### 19 | op.add_column('table_columns', sa.Column('expression', sa.Text(), nullable=True)) 20 | ### end Alembic commands ### 21 | 22 | 23 | def downgrade(): 24 | ### commands auto generated by Alembic - please adjust! ### 25 | op.drop_column('table_columns', 'expression') 26 | ### end Alembic commands ### 27 | -------------------------------------------------------------------------------- /panoramix/migrations/versions/2591d77e9831_user_id.py: -------------------------------------------------------------------------------- 1 | """user_id 2 | 3 | Revision ID: 2591d77e9831 4 | Revises: 12d55656cbca 5 | Create Date: 2015-12-15 17:02:45.128709 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '2591d77e9831' 11 | down_revision = '12d55656cbca' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | with op.batch_alter_table('tables') as batch_op: 19 | batch_op.add_column(sa.Column('user_id', sa.Integer())) 20 | batch_op.create_foreign_key('user_id', 'ab_user', ['user_id'], ['id']) 21 | 22 | 23 | def downgrade(): 24 | with op.batch_alter_table('tables') as batch_op: 25 | batch_op.drop_constraint('user_id', type_='foreignkey') 26 | batch_op.drop_column('user_id') 27 | -------------------------------------------------------------------------------- /panoramix/migrations/versions/289ce07647b_add_encrypted_password_field.py: -------------------------------------------------------------------------------- 1 | """Add encrypted password field 2 | 3 | Revision ID: 289ce07647b 4 | Revises: 2929af7925ed 5 | Create Date: 2015-11-21 11:18:00.650587 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '289ce07647b' 11 | down_revision = '2929af7925ed' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | from sqlalchemy_utils.types.encrypted import EncryptedType 16 | 17 | 18 | def upgrade(): 19 | op.add_column( 20 | 'dbs', 21 | sa.Column( 22 | 'password', 23 | EncryptedType(sa.String(1024)), 24 | nullable=True)) 25 | 26 | 27 | def downgrade(): 28 | op.drop_column('dbs', 'password') 29 | -------------------------------------------------------------------------------- /panoramix/migrations/versions/2929af7925ed_tz_offsets_in_data_sources.py: -------------------------------------------------------------------------------- 1 | """TZ offsets in data sources 2 | 3 | Revision ID: 2929af7925ed 4 | Revises: 1e2841a4128 5 | Create Date: 2015-10-19 20:54:00.565633 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '2929af7925ed' 11 | down_revision = '1e2841a4128' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | from sqlalchemy.dialects import mysql 16 | 17 | def upgrade(): 18 | op.add_column('datasources', sa.Column('offset', sa.Integer(), nullable=True)) 19 | op.add_column('tables', sa.Column('offset', sa.Integer(), nullable=True)) 20 | 21 | 22 | def downgrade(): 23 | op.drop_column('tables', 'offset') 24 | op.drop_column('datasources', 'offset') 25 | -------------------------------------------------------------------------------- /panoramix/migrations/versions/315b3f4da9b0_adding_log_model.py: -------------------------------------------------------------------------------- 1 | """adding log model 2 | 3 | Revision ID: 315b3f4da9b0 4 | Revises: 1a48a5411020 5 | Create Date: 2015-12-04 11:16:58.226984 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '315b3f4da9b0' 11 | down_revision = '1a48a5411020' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.create_table('logs', 19 | sa.Column('id', sa.Integer(), nullable=False), 20 | sa.Column('action', sa.String(length=512), nullable=True), 21 | sa.Column('user_id', sa.Integer(), nullable=True), 22 | sa.Column('json', sa.Text(), nullable=True), 23 | sa.Column('dttm', sa.DateTime(), nullable=True), 24 | sa.ForeignKeyConstraint(['user_id'], ['ab_user.id'], ), 25 | sa.PrimaryKeyConstraint('id') 26 | ) 27 | 28 | 29 | def downgrade(): 30 | op.drop_table('logs') 31 | -------------------------------------------------------------------------------- /panoramix/migrations/versions/55179c7f25c7_sqla_descr.py: -------------------------------------------------------------------------------- 1 | """sqla_descr 2 | 3 | Revision ID: 55179c7f25c7 4 | Revises: 315b3f4da9b0 5 | Create Date: 2015-12-13 08:38:43.704145 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '55179c7f25c7' 11 | down_revision = '315b3f4da9b0' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.add_column('tables', sa.Column('description', sa.Text(), nullable=True)) 19 | 20 | 21 | def downgrade(): 22 | op.drop_column('tables', 'description') 23 | -------------------------------------------------------------------------------- /panoramix/migrations/versions/5a7bad26f2a7_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 5a7bad26f2a7 4 | Revises: 4e6a06bad7a8 5 | Create Date: 2015-10-05 10:32:15.850753 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '5a7bad26f2a7' 11 | down_revision = '4e6a06bad7a8' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | from sqlalchemy.dialects import mysql 16 | 17 | def upgrade(): 18 | op.add_column('dashboards', sa.Column('css', sa.Text(), nullable=True)) 19 | op.add_column('dashboards', sa.Column('description', sa.Text(), nullable=True)) 20 | 21 | 22 | def downgrade(): 23 | op.drop_column('dashboards', 'description') 24 | op.drop_column('dashboards', 'css') 25 | -------------------------------------------------------------------------------- /panoramix/static/docs: -------------------------------------------------------------------------------- 1 | ../../docs/_build/html/ -------------------------------------------------------------------------------- /panoramix/static/img/bubble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclairvoyant/panoramix/32876f283456373ccecd83064a3af343d7014d29/panoramix/static/img/bubble.png -------------------------------------------------------------------------------- /panoramix/static/img/cardash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclairvoyant/panoramix/32876f283456373ccecd83064a3af343d7014d29/panoramix/static/img/cardash.jpg -------------------------------------------------------------------------------- /panoramix/static/img/chaudron-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclairvoyant/panoramix/32876f283456373ccecd83064a3af343d7014d29/panoramix/static/img/chaudron-blue.png -------------------------------------------------------------------------------- /panoramix/static/img/chaudron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclairvoyant/panoramix/32876f283456373ccecd83064a3af343d7014d29/panoramix/static/img/chaudron.png -------------------------------------------------------------------------------- /panoramix/static/img/chaudron_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclairvoyant/panoramix/32876f283456373ccecd83064a3af343d7014d29/panoramix/static/img/chaudron_white.png -------------------------------------------------------------------------------- /panoramix/static/img/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclairvoyant/panoramix/32876f283456373ccecd83064a3af343d7014d29/panoramix/static/img/cloud.png -------------------------------------------------------------------------------- /panoramix/static/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclairvoyant/panoramix/32876f283456373ccecd83064a3af343d7014d29/panoramix/static/img/dash.png -------------------------------------------------------------------------------- /panoramix/static/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclairvoyant/panoramix/32876f283456373ccecd83064a3af343d7014d29/panoramix/static/img/favicon.png -------------------------------------------------------------------------------- /panoramix/static/img/gallery.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclairvoyant/panoramix/32876f283456373ccecd83064a3af343d7014d29/panoramix/static/img/gallery.jpg -------------------------------------------------------------------------------- /panoramix/static/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclairvoyant/panoramix/32876f283456373ccecd83064a3af343d7014d29/panoramix/static/img/loading.gif -------------------------------------------------------------------------------- /panoramix/static/img/panoramix.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclairvoyant/panoramix/32876f283456373ccecd83064a3af343d7014d29/panoramix/static/img/panoramix.jpg -------------------------------------------------------------------------------- /panoramix/static/img/panoramix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclairvoyant/panoramix/32876f283456373ccecd83064a3af343d7014d29/panoramix/static/img/panoramix.png -------------------------------------------------------------------------------- /panoramix/static/img/panoramix_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclairvoyant/panoramix/32876f283456373ccecd83064a3af343d7014d29/panoramix/static/img/panoramix_screenshot.png -------------------------------------------------------------------------------- /panoramix/static/img/penguins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclairvoyant/panoramix/32876f283456373ccecd83064a3af343d7014d29/panoramix/static/img/penguins.png -------------------------------------------------------------------------------- /panoramix/static/img/serpe.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclairvoyant/panoramix/32876f283456373ccecd83064a3af343d7014d29/panoramix/static/img/serpe.jpg -------------------------------------------------------------------------------- /panoramix/static/img/servers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclairvoyant/panoramix/32876f283456373ccecd83064a3af343d7014d29/panoramix/static/img/servers.jpg -------------------------------------------------------------------------------- /panoramix/static/img/slice.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclairvoyant/panoramix/32876f283456373ccecd83064a3af343d7014d29/panoramix/static/img/slice.jpg -------------------------------------------------------------------------------- /panoramix/static/img/tux_panoramix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclairvoyant/panoramix/32876f283456373ccecd83064a3af343d7014d29/panoramix/static/img/tux_panoramix.png -------------------------------------------------------------------------------- /panoramix/static/lib/d3-sankey.js: -------------------------------------------------------------------------------- 1 | d3.sankey = function() { 2 | var sankey = {}, 3 | nodeWidth = 24, 4 | nodePadding = 8, 5 | size = [1, 1], 6 | nodes = [], 7 | links = []; 8 | 9 | sankey.nodeWidth = function(_) { 10 | if (!arguments.length) return nodeWidth; 11 | nodeWidth = +_; 12 | return sankey; 13 | }; 14 | 15 | sankey.nodePadding = function(_) { 16 | if (!arguments.length) return nodePadding; 17 | nodePadding = +_; 18 | return sankey; 19 | }; 20 | 21 | sankey.nodes = function(_) { 22 | if (!arguments.length) return nodes; 23 | nodes = _; 24 | return sankey; 25 | }; 26 | 27 | sankey.links = function(_) { 28 | if (!arguments.length) return links; 29 | links = _; 30 | return sankey; 31 | }; 32 | 33 | sankey.size = function(_) { 34 | if (!arguments.length) return size; 35 | size = _; 36 | return sankey; 37 | }; 38 | 39 | sankey.layout = function(iterations) { 40 | computeNodeLinks(); 41 | computeNodeValues(); 42 | computeNodeBreadths(); 43 | computeNodeDepths(iterations); 44 | computeLinkDepths(); 45 | return sankey; 46 | }; 47 | 48 | sankey.relayout = function() { 49 | computeLinkDepths(); 50 | return sankey; 51 | }; 52 | 53 | sankey.link = function() { 54 | var curvature = .5; 55 | 56 | function link(d) { 57 | var x0 = d.source.x + d.source.dx, 58 | x1 = d.target.x, 59 | xi = d3.interpolateNumber(x0, x1), 60 | x2 = xi(curvature), 61 | x3 = xi(1 - curvature), 62 | y0 = d.source.y + d.sy + d.dy / 2, 63 | y1 = d.target.y + d.ty + d.dy / 2; 64 | return "M" + x0 + "," + y0 65 | + "C" + x2 + "," + y0 66 | + " " + x3 + "," + y1 67 | + " " + x1 + "," + y1; 68 | } 69 | 70 | link.curvature = function(_) { 71 | if (!arguments.length) return curvature; 72 | curvature = +_; 73 | return link; 74 | }; 75 | 76 | return link; 77 | }; 78 | 79 | // Populate the sourceLinks and targetLinks for each node. 80 | // Also, if the source and target are not objects, assume they are indices. 81 | function computeNodeLinks() { 82 | nodes.forEach(function(node) { 83 | node.sourceLinks = []; 84 | node.targetLinks = []; 85 | }); 86 | links.forEach(function(link) { 87 | var source = link.source, 88 | target = link.target; 89 | if (typeof source === "number") source = link.source = nodes[link.source]; 90 | if (typeof target === "number") target = link.target = nodes[link.target]; 91 | source.sourceLinks.push(link); 92 | target.targetLinks.push(link); 93 | }); 94 | } 95 | 96 | // Compute the value (size) of each node by summing the associated links. 97 | function computeNodeValues() { 98 | nodes.forEach(function(node) { 99 | node.value = Math.max( 100 | d3.sum(node.sourceLinks, value), 101 | d3.sum(node.targetLinks, value) 102 | ); 103 | }); 104 | } 105 | 106 | // Iteratively assign the breadth (x-position) for each node. 107 | // Nodes are assigned the maximum breadth of incoming neighbors plus one; 108 | // nodes with no incoming links are assigned breadth zero, while 109 | // nodes with no outgoing links are assigned the maximum breadth. 110 | function computeNodeBreadths() { 111 | var remainingNodes = nodes, 112 | nextNodes, 113 | x = 0; 114 | 115 | while (remainingNodes.length) { 116 | nextNodes = []; 117 | remainingNodes.forEach(function(node) { 118 | node.x = x; 119 | node.dx = nodeWidth; 120 | node.sourceLinks.forEach(function(link) { 121 | if (nextNodes.indexOf(link.target) < 0) { 122 | nextNodes.push(link.target); 123 | } 124 | }); 125 | }); 126 | remainingNodes = nextNodes; 127 | ++x; 128 | } 129 | 130 | // 131 | moveSinksRight(x); 132 | scaleNodeBreadths((size[0] - nodeWidth) / (x - 1)); 133 | } 134 | 135 | function moveSourcesRight() { 136 | nodes.forEach(function(node) { 137 | if (!node.targetLinks.length) { 138 | node.x = d3.min(node.sourceLinks, function(d) { return d.target.x; }) - 1; 139 | } 140 | }); 141 | } 142 | 143 | function moveSinksRight(x) { 144 | nodes.forEach(function(node) { 145 | if (!node.sourceLinks.length) { 146 | node.x = x - 1; 147 | } 148 | }); 149 | } 150 | 151 | function scaleNodeBreadths(kx) { 152 | nodes.forEach(function(node) { 153 | node.x *= kx; 154 | }); 155 | } 156 | 157 | function computeNodeDepths(iterations) { 158 | var nodesByBreadth = d3.nest() 159 | .key(function(d) { return d.x; }) 160 | .sortKeys(d3.ascending) 161 | .entries(nodes) 162 | .map(function(d) { return d.values; }); 163 | 164 | // 165 | initializeNodeDepth(); 166 | resolveCollisions(); 167 | for (var alpha = 1; iterations > 0; --iterations) { 168 | relaxRightToLeft(alpha *= .99); 169 | resolveCollisions(); 170 | relaxLeftToRight(alpha); 171 | resolveCollisions(); 172 | } 173 | 174 | function initializeNodeDepth() { 175 | var ky = d3.min(nodesByBreadth, function(nodes) { 176 | return (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, value); 177 | }); 178 | 179 | nodesByBreadth.forEach(function(nodes) { 180 | nodes.forEach(function(node, i) { 181 | node.y = i; 182 | node.dy = node.value * ky; 183 | }); 184 | }); 185 | 186 | links.forEach(function(link) { 187 | link.dy = link.value * ky; 188 | }); 189 | } 190 | 191 | function relaxLeftToRight(alpha) { 192 | nodesByBreadth.forEach(function(nodes, breadth) { 193 | nodes.forEach(function(node) { 194 | if (node.targetLinks.length) { 195 | var y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, value); 196 | node.y += (y - center(node)) * alpha; 197 | } 198 | }); 199 | }); 200 | 201 | function weightedSource(link) { 202 | return center(link.source) * link.value; 203 | } 204 | } 205 | 206 | function relaxRightToLeft(alpha) { 207 | nodesByBreadth.slice().reverse().forEach(function(nodes) { 208 | nodes.forEach(function(node) { 209 | if (node.sourceLinks.length) { 210 | var y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value); 211 | node.y += (y - center(node)) * alpha; 212 | } 213 | }); 214 | }); 215 | 216 | function weightedTarget(link) { 217 | return center(link.target) * link.value; 218 | } 219 | } 220 | 221 | function resolveCollisions() { 222 | nodesByBreadth.forEach(function(nodes) { 223 | var node, 224 | dy, 225 | y0 = 0, 226 | n = nodes.length, 227 | i; 228 | 229 | // Push any overlapping nodes down. 230 | nodes.sort(ascendingDepth); 231 | for (i = 0; i < n; ++i) { 232 | node = nodes[i]; 233 | dy = y0 - node.y; 234 | if (dy > 0) node.y += dy; 235 | y0 = node.y + node.dy + nodePadding; 236 | } 237 | 238 | // If the bottommost node goes outside the bounds, push it back up. 239 | dy = y0 - nodePadding - size[1]; 240 | if (dy > 0) { 241 | y0 = node.y -= dy; 242 | 243 | // Push any overlapping nodes back up. 244 | for (i = n - 2; i >= 0; --i) { 245 | node = nodes[i]; 246 | dy = node.y + node.dy + nodePadding - y0; 247 | if (dy > 0) node.y -= dy; 248 | y0 = node.y; 249 | } 250 | } 251 | }); 252 | } 253 | 254 | function ascendingDepth(a, b) { 255 | return a.y - b.y; 256 | } 257 | } 258 | 259 | function computeLinkDepths() { 260 | nodes.forEach(function(node) { 261 | node.sourceLinks.sort(ascendingTargetDepth); 262 | node.targetLinks.sort(ascendingSourceDepth); 263 | }); 264 | nodes.forEach(function(node) { 265 | var sy = 0, ty = 0; 266 | node.sourceLinks.forEach(function(link) { 267 | link.sy = sy; 268 | sy += link.dy; 269 | }); 270 | node.targetLinks.forEach(function(link) { 271 | link.ty = ty; 272 | ty += link.dy; 273 | }); 274 | }); 275 | 276 | function ascendingSourceDepth(a, b) { 277 | return a.source.y - b.source.y; 278 | } 279 | 280 | function ascendingTargetDepth(a, b) { 281 | return a.target.y - b.target.y; 282 | } 283 | } 284 | 285 | function center(node) { 286 | return node.y + node.dy / 2; 287 | } 288 | 289 | function value(link) { 290 | return link.value; 291 | } 292 | 293 | return sankey; 294 | }; 295 | -------------------------------------------------------------------------------- /panoramix/static/lib/dataTables/dataTables.bootstrap.css: -------------------------------------------------------------------------------- 1 | div.dataTables_length label { 2 | font-weight: normal; 3 | text-align: left; 4 | white-space: nowrap; 5 | } 6 | 7 | div.dataTables_length select { 8 | width: 75px; 9 | display: inline-block; 10 | } 11 | 12 | div.dataTables_filter { 13 | text-align: right; 14 | } 15 | 16 | div.dataTables_filter label { 17 | font-weight: normal; 18 | white-space: nowrap; 19 | text-align: left; 20 | } 21 | 22 | div.dataTables_filter input { 23 | margin-left: 0.5em; 24 | display: inline-block; 25 | width: auto; 26 | } 27 | 28 | div.dataTables_info { 29 | padding-top: 8px; 30 | white-space: nowrap; 31 | } 32 | 33 | div.dataTables_paginate { 34 | margin: 0; 35 | white-space: nowrap; 36 | text-align: right; 37 | } 38 | 39 | div.dataTables_paginate ul.pagination { 40 | margin: 2px 0; 41 | white-space: nowrap; 42 | } 43 | 44 | @media screen and (max-width: 767px) { 45 | div.dataTables_wrapper > div.row > div, 46 | div.dataTables_length, 47 | div.dataTables_filter, 48 | div.dataTables_info, 49 | div.dataTables_paginate { 50 | text-align: center; 51 | } 52 | 53 | div.DTTT { 54 | margin-bottom: 0.5em; 55 | } 56 | } 57 | 58 | 59 | table.dataTable td, 60 | table.dataTable th { 61 | -webkit-box-sizing: content-box; 62 | -moz-box-sizing: content-box; 63 | box-sizing: content-box; 64 | } 65 | 66 | 67 | table.dataTable { 68 | clear: both; 69 | margin-top: 6px !important; 70 | margin-bottom: 6px !important; 71 | max-width: none !important; 72 | } 73 | 74 | table.dataTable thead .sorting, 75 | table.dataTable thead .sorting_asc, 76 | table.dataTable thead .sorting_desc, 77 | table.dataTable thead .sorting_asc_disabled, 78 | table.dataTable thead .sorting_desc_disabled { 79 | cursor: pointer; 80 | position: relative; 81 | } 82 | 83 | table.dataTable thead .sorting:after, 84 | table.dataTable thead .sorting_asc:after, 85 | table.dataTable thead .sorting_desc:after { 86 | position: absolute; 87 | top: 8px; 88 | right: 8px; 89 | display: block; 90 | font-family: 'Glyphicons Halflings'; 91 | opacity: 0.5; 92 | } 93 | table.dataTable thead .sorting:after { 94 | opacity: 0.2; 95 | content: "\e150"; /* sort */ 96 | } 97 | table.dataTable thead .sorting_asc:after { 98 | content: "\e155"; /* sort-by-attributes */ 99 | } 100 | table.dataTable thead .sorting_desc:after { 101 | content: "\e156"; /* sort-by-attributes-alt */ 102 | } 103 | div.dataTables_scrollBody table.dataTable thead .sorting:after, 104 | div.dataTables_scrollBody table.dataTable thead .sorting_asc:after, 105 | div.dataTables_scrollBody table.dataTable thead .sorting_desc:after { 106 | display: none; 107 | } 108 | 109 | table.dataTable thead .sorting_asc_disabled:after, 110 | table.dataTable thead .sorting_desc_disabled:after { 111 | color: #eee; 112 | } 113 | 114 | table.dataTable thead > tr > th { 115 | padding-right: 30px; 116 | } 117 | 118 | table.dataTable th:active { 119 | outline: none; 120 | } 121 | 122 | 123 | /* Condensed */ 124 | table.dataTable.table-condensed thead > tr > th { 125 | padding-right: 20px; 126 | } 127 | 128 | table.dataTable.table-condensed thead .sorting:after, 129 | table.dataTable.table-condensed thead .sorting_asc:after, 130 | table.dataTable.table-condensed thead .sorting_desc:after { 131 | top: 6px; 132 | right: 6px; 133 | } 134 | 135 | /* Scrolling */ 136 | div.dataTables_scrollHead table { 137 | margin-bottom: 0 !important; 138 | border-bottom-left-radius: 0; 139 | border-bottom-right-radius: 0; 140 | } 141 | 142 | div.dataTables_scrollHead table thead tr:last-child th:first-child, 143 | div.dataTables_scrollHead table thead tr:last-child td:first-child { 144 | border-bottom-left-radius: 0 !important; 145 | border-bottom-right-radius: 0 !important; 146 | } 147 | 148 | div.dataTables_scrollBody table { 149 | border-top: none; 150 | margin-top: 0 !important; 151 | margin-bottom: 0 !important; 152 | } 153 | 154 | div.dataTables_scrollBody tbody tr:first-child th, 155 | div.dataTables_scrollBody tbody tr:first-child td { 156 | border-top: none; 157 | } 158 | 159 | div.dataTables_scrollFoot table { 160 | margin-top: 0 !important; 161 | border-top: none; 162 | } 163 | 164 | /* Frustratingly the border-collapse:collapse used by Bootstrap makes the column 165 | width calculations when using scrolling impossible to align columns. We have 166 | to use separate 167 | */ 168 | table.table-bordered.dataTable { 169 | border-collapse: separate !important; 170 | } 171 | table.table-bordered thead th, 172 | table.table-bordered thead td { 173 | border-left-width: 0; 174 | border-top-width: 0; 175 | } 176 | table.table-bordered tbody th, 177 | table.table-bordered tbody td { 178 | border-left-width: 0; 179 | border-bottom-width: 0; 180 | } 181 | table.table-bordered tfoot th, 182 | table.table-bordered tfoot td { 183 | border-left-width: 0; 184 | border-bottom-width: 0; 185 | } 186 | table.table-bordered th:last-child, 187 | table.table-bordered td:last-child { 188 | border-right-width: 0; 189 | } 190 | div.dataTables_scrollHead table.table-bordered { 191 | border-bottom-width: 0; 192 | } 193 | 194 | 195 | 196 | 197 | /* 198 | * TableTools styles 199 | */ 200 | .table.dataTable tbody tr.active td, 201 | .table.dataTable tbody tr.active th { 202 | background-color: #08C; 203 | color: white; 204 | } 205 | 206 | .table.dataTable tbody tr.active:hover td, 207 | .table.dataTable tbody tr.active:hover th { 208 | background-color: #0075b0 !important; 209 | } 210 | 211 | .table.dataTable tbody tr.active th > a, 212 | .table.dataTable tbody tr.active td > a { 213 | color: white; 214 | } 215 | 216 | .table-striped.dataTable tbody tr.active:nth-child(odd) td, 217 | .table-striped.dataTable tbody tr.active:nth-child(odd) th { 218 | background-color: #017ebc; 219 | } 220 | 221 | table.DTTT_selectable tbody tr { 222 | cursor: pointer; 223 | } 224 | 225 | div.DTTT .btn:hover { 226 | text-decoration: none !important; 227 | } 228 | 229 | ul.DTTT_dropdown.dropdown-menu { 230 | z-index: 2003; 231 | } 232 | 233 | ul.DTTT_dropdown.dropdown-menu a { 234 | color: #333 !important; /* needed only when demo_page.css is included */ 235 | } 236 | 237 | ul.DTTT_dropdown.dropdown-menu li { 238 | position: relative; 239 | } 240 | 241 | ul.DTTT_dropdown.dropdown-menu li:hover a { 242 | background-color: #0088cc; 243 | color: white !important; 244 | } 245 | 246 | div.DTTT_collection_background { 247 | z-index: 2002; 248 | } 249 | 250 | /* TableTools information display */ 251 | div.DTTT_print_info { 252 | position: fixed; 253 | top: 50%; 254 | left: 50%; 255 | width: 400px; 256 | height: 150px; 257 | margin-left: -200px; 258 | margin-top: -75px; 259 | text-align: center; 260 | color: #333; 261 | padding: 10px 30px; 262 | opacity: 0.95; 263 | 264 | background-color: white; 265 | border: 1px solid rgba(0, 0, 0, 0.2); 266 | border-radius: 6px; 267 | 268 | -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.5); 269 | box-shadow: 0 3px 7px rgba(0, 0, 0, 0.5); 270 | } 271 | 272 | div.DTTT_print_info h6 { 273 | font-weight: normal; 274 | font-size: 28px; 275 | line-height: 28px; 276 | margin: 1em; 277 | } 278 | 279 | div.DTTT_print_info p { 280 | font-size: 14px; 281 | line-height: 20px; 282 | } 283 | 284 | div.dataTables_processing { 285 | position: absolute; 286 | top: 50%; 287 | left: 50%; 288 | width: 100%; 289 | height: 60px; 290 | margin-left: -50%; 291 | margin-top: -25px; 292 | padding-top: 20px; 293 | padding-bottom: 20px; 294 | text-align: center; 295 | font-size: 1.2em; 296 | background-color: white; 297 | background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255,255,255,0)), color-stop(25%, rgba(255,255,255,0.9)), color-stop(75%, rgba(255,255,255,0.9)), color-stop(100%, rgba(255,255,255,0))); 298 | background: -webkit-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%); 299 | background: -moz-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%); 300 | background: -ms-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%); 301 | background: -o-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%); 302 | background: linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%); 303 | } 304 | 305 | 306 | 307 | /* 308 | * FixedColumns styles 309 | */ 310 | div.DTFC_LeftHeadWrapper table, 311 | div.DTFC_LeftFootWrapper table, 312 | div.DTFC_RightHeadWrapper table, 313 | div.DTFC_RightFootWrapper table, 314 | table.DTFC_Cloned tr.even { 315 | background-color: white; 316 | margin-bottom: 0; 317 | } 318 | 319 | div.DTFC_RightHeadWrapper table , 320 | div.DTFC_LeftHeadWrapper table { 321 | border-bottom: none !important; 322 | margin-bottom: 0 !important; 323 | border-top-right-radius: 0 !important; 324 | border-bottom-left-radius: 0 !important; 325 | border-bottom-right-radius: 0 !important; 326 | } 327 | 328 | div.DTFC_RightHeadWrapper table thead tr:last-child th:first-child, 329 | div.DTFC_RightHeadWrapper table thead tr:last-child td:first-child, 330 | div.DTFC_LeftHeadWrapper table thead tr:last-child th:first-child, 331 | div.DTFC_LeftHeadWrapper table thead tr:last-child td:first-child { 332 | border-bottom-left-radius: 0 !important; 333 | border-bottom-right-radius: 0 !important; 334 | } 335 | 336 | div.DTFC_RightBodyWrapper table, 337 | div.DTFC_LeftBodyWrapper table { 338 | border-top: none; 339 | margin: 0 !important; 340 | } 341 | 342 | div.DTFC_RightBodyWrapper tbody tr:first-child th, 343 | div.DTFC_RightBodyWrapper tbody tr:first-child td, 344 | div.DTFC_LeftBodyWrapper tbody tr:first-child th, 345 | div.DTFC_LeftBodyWrapper tbody tr:first-child td { 346 | border-top: none; 347 | } 348 | 349 | div.DTFC_RightFootWrapper table, 350 | div.DTFC_LeftFootWrapper table { 351 | border-top: none; 352 | margin-top: 0 !important; 353 | } 354 | 355 | 356 | div.DTFC_LeftBodyWrapper table.dataTable thead .sorting:after, 357 | div.DTFC_LeftBodyWrapper table.dataTable thead .sorting_asc:after, 358 | div.DTFC_LeftBodyWrapper table.dataTable thead .sorting_desc:after, 359 | div.DTFC_RightBodyWrapper table.dataTable thead .sorting:after, 360 | div.DTFC_RightBodyWrapper table.dataTable thead .sorting_asc:after, 361 | div.DTFC_RightBodyWrapper table.dataTable thead .sorting_desc:after { 362 | display: none; 363 | } 364 | 365 | 366 | /* 367 | * FixedHeader styles 368 | */ 369 | div.FixedHeader_Cloned table { 370 | margin: 0 !important 371 | } 372 | 373 | -------------------------------------------------------------------------------- /panoramix/static/lib/dataTables/dataTables.bootstrap.js: -------------------------------------------------------------------------------- 1 | /*! DataTables Bootstrap 3 integration 2 | * ©2011-2014 SpryMedia Ltd - datatables.net/license 3 | */ 4 | 5 | /** 6 | * DataTables integration for Bootstrap 3. This requires Bootstrap 3 and 7 | * DataTables 1.10 or newer. 8 | * 9 | * This file sets the defaults and adds options to DataTables to style its 10 | * controls using Bootstrap. See http://datatables.net/manual/styling/bootstrap 11 | * for further information. 12 | */ 13 | (function(window, document, undefined){ 14 | 15 | var factory = function( $, DataTable ) { 16 | "use strict"; 17 | 18 | 19 | /* Set the defaults for DataTables initialisation */ 20 | $.extend( true, DataTable.defaults, { 21 | dom: 22 | "<'row'<'col-sm-6'l><'col-sm-6'f>>" + 23 | "<'row'<'col-sm-12'tr>>" + 24 | "<'row'<'col-sm-5'i><'col-sm-7'p>>", 25 | renderer: 'bootstrap' 26 | } ); 27 | 28 | 29 | /* Default class modification */ 30 | $.extend( DataTable.ext.classes, { 31 | sWrapper: "dataTables_wrapper form-inline dt-bootstrap", 32 | sFilterInput: "form-control input-sm", 33 | sLengthSelect: "form-control input-sm" 34 | } ); 35 | 36 | 37 | /* Bootstrap paging button renderer */ 38 | DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, buttons, page, pages ) { 39 | var api = new DataTable.Api( settings ); 40 | var classes = settings.oClasses; 41 | var lang = settings.oLanguage.oPaginate; 42 | var btnDisplay, btnClass, counter=0; 43 | 44 | var attach = function( container, buttons ) { 45 | var i, ien, node, button; 46 | var clickHandler = function ( e ) { 47 | e.preventDefault(); 48 | if ( !$(e.currentTarget).hasClass('disabled') ) { 49 | api.page( e.data.action ).draw( false ); 50 | } 51 | }; 52 | 53 | for ( i=0, ien=buttons.length ; i 0 ? 72 | '' : ' disabled'); 73 | break; 74 | 75 | case 'previous': 76 | btnDisplay = lang.sPrevious; 77 | btnClass = button + (page > 0 ? 78 | '' : ' disabled'); 79 | break; 80 | 81 | case 'next': 82 | btnDisplay = lang.sNext; 83 | btnClass = button + (page < pages-1 ? 84 | '' : ' disabled'); 85 | break; 86 | 87 | case 'last': 88 | btnDisplay = lang.sLast; 89 | btnClass = button + (page < pages-1 ? 90 | '' : ' disabled'); 91 | break; 92 | 93 | default: 94 | btnDisplay = button + 1; 95 | btnClass = page === button ? 96 | 'active' : ''; 97 | break; 98 | } 99 | 100 | if ( btnDisplay ) { 101 | node = $('
  • ', { 102 | 'class': classes.sPageButton+' '+btnClass, 103 | 'id': idx === 0 && typeof button === 'string' ? 104 | settings.sTableId +'_'+ button : 105 | null 106 | } ) 107 | .append( $('', { 108 | 'href': '#', 109 | 'aria-controls': settings.sTableId, 110 | 'data-dt-idx': counter, 111 | 'tabindex': settings.iTabIndex 112 | } ) 113 | .html( btnDisplay ) 114 | ) 115 | .appendTo( container ); 116 | 117 | settings.oApi._fnBindAction( 118 | node, {action: button}, clickHandler 119 | ); 120 | 121 | counter++; 122 | } 123 | } 124 | } 125 | }; 126 | 127 | // IE9 throws an 'unknown error' if document.activeElement is used 128 | // inside an iframe or frame. 129 | var activeEl; 130 | 131 | try { 132 | // Because this approach is destroying and recreating the paging 133 | // elements, focus is lost on the select button which is bad for 134 | // accessibility. So we want to restore focus once the draw has 135 | // completed 136 | activeEl = $(document.activeElement).data('dt-idx'); 137 | } 138 | catch (e) {} 139 | 140 | attach( 141 | $(host).empty().html('