Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e64367149e | ||
|
|
4c78b4f34e | ||
|
|
89e92904e1 | ||
|
|
703b6f612b | ||
|
|
f1a64c0988 | ||
|
|
8d4c3ea381 | ||
|
|
aea304d814 | ||
|
|
1d6d821008 | ||
|
|
82bf5437cd | ||
|
|
2d23feed1f | ||
|
|
c4b24cb9cc | ||
|
|
f95e84442f | ||
|
|
83e37f66fa | ||
|
|
74ae4ee704 | ||
|
|
9fab75bb1f | ||
|
|
e854798730 | ||
|
|
caafb1ef4c | ||
|
|
63d55c9f93 | ||
|
|
71274ae609 | ||
|
|
e126ab4eb8 | ||
|
|
f395c83bae | ||
|
|
591c90d0c0 | ||
|
|
2cbc2e36e3 | ||
|
|
a28d9d4bb1 | ||
|
|
11c291b9a1 | ||
|
|
24882884b8 | ||
|
|
f5e355a26f | ||
|
|
e4a5f34f71 | ||
|
|
449441fed5 | ||
|
|
3d8fbaa966 | ||
|
|
ff29f905c9 | ||
|
|
4d1d3ad0f6 | ||
|
|
e1b3c7e63b | ||
|
|
2cbe25c6b5 | ||
|
|
c09dca514d | ||
|
|
72ec6ae570 | ||
|
|
256193ce9f | ||
|
|
6c8f268587 | ||
|
|
8ac35bd610 | ||
|
|
e0d6d20993 | ||
|
|
4edbbd350d | ||
|
|
6f1fa5152a | ||
|
|
e5e2988e2d | ||
|
|
116b1c01f5 | ||
|
|
16550b9753 | ||
|
|
c1f28a3e74 | ||
|
|
cd09b0ddef | ||
|
|
f6753afa75 | ||
|
|
521b000ab6 | ||
|
|
359a81eee3 | ||
|
|
0bc2e71ac6 | ||
|
|
6daf92e3c1 | ||
|
|
a5b896414d | ||
|
|
95b080160f | ||
|
|
36351918c9 | ||
|
|
e755854c29 | ||
|
|
5f20a080f4 | ||
|
|
ca3959783c | ||
|
|
9a63a312b6 | ||
|
|
5825f4539d | ||
|
|
53fe171466 | ||
|
|
fab0670669 | ||
|
|
67c5f637d1 | ||
|
|
9858304468 | ||
|
|
7f9d96b024 | ||
|
|
1283bc0788 | ||
|
|
6dd81a3e95 | ||
|
|
483935cc12 | ||
|
|
bca1e15e44 | ||
|
|
11aaaf3e11 | ||
|
|
c95c2522ab | ||
|
|
58c2186f56 | ||
|
|
94ef801c6d | ||
|
|
efc54e0f65 |
5
.gitignore
vendored
@@ -1,7 +1,12 @@
|
||||
*.pyc
|
||||
.DS_Store
|
||||
.coverage
|
||||
build
|
||||
*.db
|
||||
tmp
|
||||
panoramix_config.py
|
||||
local_config.py
|
||||
env
|
||||
dist
|
||||
panoramix.egg-info/
|
||||
app.db
|
||||
|
||||
4
MANIFEST.in
Normal file
@@ -0,0 +1,4 @@
|
||||
recursive-include panoramix/templates *
|
||||
recursive-include panoramix/static *
|
||||
recursive-include panoramix/data *
|
||||
recursive-include panoramix/migrations *
|
||||
76
README.md
@@ -4,25 +4,49 @@ Panoramix
|
||||
Panoramix is a data exploration platform designed to be visual, intuitive
|
||||
and interactive.
|
||||
|
||||

|
||||

|
||||
|
||||
Panoramix
|
||||
---------
|
||||
Panoramix's main goal is to make it easy to slice, dice and visualize data.
|
||||
It empowers its user to perform **analytics at the speed of thought**.
|
||||
|
||||
Panoramix provides:
|
||||
* A quick way to intuitively visualize datasets
|
||||
* Create and share simple dashboards
|
||||
* A rich set of visualizations to analyze your data, as well as a flexible
|
||||
way to extend the capabilities
|
||||
* An extensible, high granularity security model allowing intricate rules
|
||||
on who can access which features, and integration with major
|
||||
authentication providers (database, OpenID, LDAP, OAuth & REMOTE_USER
|
||||
through Flask AppBuiler)
|
||||
* A simple semantic layer, allowing to control how data sources are
|
||||
displayed in the UI,
|
||||
by defining which fields should show up in which dropdown and which
|
||||
aggregation and function (metrics) are made available to the user
|
||||
* Deep integration with Druid allows for Panoramix to stay blazing fast while
|
||||
slicing and dicing large, realtime datasets
|
||||
|
||||
|
||||
Buzz Phrases
|
||||
------------
|
||||
|
||||
* Analytics at the speed of thought!
|
||||
* Analytics at the speed of thought!
|
||||
* Instantaneous learning curve
|
||||
* Realtime analytics when querying [Druid.io](http://druid.io)
|
||||
* Extentsible to infinity
|
||||
|
||||

|
||||
|
||||
Database Support
|
||||
----------------
|
||||
|
||||
Panoramix was originally designed on to of Druid.io, but quickly broadened
|
||||
Panoramix was originally designed on to of Druid.io, but quickly broadened
|
||||
its scope to support other databases through the use of SqlAlchemy, a Python
|
||||
ORM that is compatible with
|
||||
[many external databases](http://docs.sqlalchemy.org/en/rel_1_0/core/engines.html).
|
||||
ORM that is compatible with
|
||||
[most common databases](http://docs.sqlalchemy.org/en/rel_1_0/core/engines.html).
|
||||
|
||||
What's Druid?
|
||||
|
||||
What is Druid?
|
||||
-------------
|
||||
From their website at http://druid.io
|
||||
|
||||
@@ -33,27 +57,6 @@ and fast data aggregation. Existing Druid deployments have scaled to
|
||||
trillions of events and petabytes of data. Druid is best used to
|
||||
power analytic dashboards and applications.*
|
||||
|
||||
Panoramix
|
||||
---------
|
||||
Panoramix's main goal is to make it easy to slice, dice and visualize data
|
||||
out of Druid. It empowers its user to perform **analytics
|
||||
at the speed of thought**.
|
||||
|
||||
Panoramix started as a hackathon project at Airbnb in while running a POC
|
||||
(proof of concept) on using Druid.
|
||||
|
||||
Panoramix provides:
|
||||
* A way to query intuitively a Druid dataset, allowing for grouping, filtering
|
||||
limiting and defining a time granularity
|
||||
* Many charts and visualization to analyze your data, as well as a flexible
|
||||
way to extend the visualization capabilities
|
||||
* An extensible, high granularity security model allowing intricate rules
|
||||
on who can access which features, and integration with major
|
||||
authentication providers (through Flask AppBuiler)
|
||||
* A simple semantic layer, allowing to control how Druid datasources are
|
||||
displayed in the UI,
|
||||
by defining which fields should show up in which dropdown and which
|
||||
aggregation and function (metrics) are made available to the user
|
||||
|
||||
Installation
|
||||
------------
|
||||
@@ -67,8 +70,11 @@ pip install panoramix
|
||||
# Create an admin user
|
||||
fabmanager create-admin --app panoramix
|
||||
|
||||
# Start the web server
|
||||
panoramix
|
||||
# Load some data to play with
|
||||
panoramix load_examples
|
||||
|
||||
# Start the development web server
|
||||
panoramix runserver -d
|
||||
```
|
||||
|
||||
After installation, you should be able to point your browser to the right
|
||||
@@ -102,7 +108,7 @@ PANORAMIX_WEBSERVER_PORT = 8088
|
||||
SECRET_KEY = '\2\1thisismyscretkey\1\2\e\y\y\h'
|
||||
|
||||
# The SQLAlchemy connection string.
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///tmp/panoramix.db'
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/panoramix.db'
|
||||
|
||||
# Flask-WTF flag for CSRF
|
||||
CSRF_ENABLED = True
|
||||
@@ -123,3 +129,11 @@ the [Flask App Builder Documentation](http://flask-appbuilder.readthedocs.org/en
|
||||
``Admin->Refresh Metadata`` menu item to populate
|
||||
|
||||
* Navigate to your datasources
|
||||
|
||||
More screenshots
|
||||
----------------
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
21
TODO.md
@@ -1,7 +1,26 @@
|
||||
# TODO
|
||||
* Add a per-datasource permission
|
||||
|
||||
* in/notin filters autocomplete
|
||||
* DRUID: Allow for post aggregations (ratios!)
|
||||
* compare time ranges
|
||||
* csv export out of table view
|
||||
* Save / bookmark / url shortener
|
||||
* SQL: Find a way to manage granularity
|
||||
* Create ~/.panoramix/ to host DB and config, generate default config there
|
||||
* Reintroduce query and stopwatch
|
||||
* Sort tooltip
|
||||
* Make "Test Connection" test further
|
||||
* Consistent colors for same entities
|
||||
* Contribution to total
|
||||
* Arbitrary expressions
|
||||
* Group bucketing
|
||||
* ToT
|
||||
* Layers
|
||||
|
||||
## Test
|
||||
* Line types
|
||||
* Intelligence around series name
|
||||
* Shapes
|
||||
* Line highlighting - draw attention
|
||||
|
||||
## Bug
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
import logging
|
||||
import os
|
||||
from flask import Flask
|
||||
from flask.ext.appbuilder import SQLA, AppBuilder, IndexView
|
||||
from flask.ext.migrate import Migrate
|
||||
from panoramix import config
|
||||
|
||||
"""
|
||||
Logging configuration
|
||||
"""
|
||||
APP_DIR = os.path.dirname(__file__)
|
||||
CONFIG_MODULE = os.environ.get('PANORAMIX_CONFIG', 'panoramix.config')
|
||||
|
||||
# Logging configuration
|
||||
logging.basicConfig(format='%(asctime)s:%(levelname)s:%(name)s:%(message)s')
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object('panoramix.config')
|
||||
app.config.from_object(CONFIG_MODULE)
|
||||
db = SQLA(app)
|
||||
migrate = Migrate(app, db, directory=APP_DIR + "/migrations")
|
||||
|
||||
|
||||
class MyIndexView(IndexView):
|
||||
index_template = 'index.html'
|
||||
|
||||
appbuilder = AppBuilder(
|
||||
app, db.session, base_template='panoramix/base.html',
|
||||
indexview=MyIndexView)
|
||||
indexview=MyIndexView,
|
||||
security_manager_class=app.config.get("CUSTOM_SECURITY_MANAGER"))
|
||||
|
||||
get_session = appbuilder.get_session
|
||||
|
||||
from panoramix import views
|
||||
|
||||
0
panoramix/bin/__init__.py
Normal file
@@ -1,19 +1,340 @@
|
||||
#!/usr/bin/env python
|
||||
from panoramix import app, config
|
||||
|
||||
import csv
|
||||
import gzip
|
||||
import json
|
||||
from subprocess import Popen
|
||||
|
||||
from flask.ext.script import Manager
|
||||
from flask.ext.migrate import MigrateCommand
|
||||
from panoramix import db
|
||||
from sqlalchemy import Column, Integer, String, Table
|
||||
|
||||
if __name__ == "__main__":
|
||||
if config.DEBUG:
|
||||
from panoramix import app
|
||||
from panoramix import models
|
||||
|
||||
|
||||
config = app.config
|
||||
|
||||
manager = Manager(app)
|
||||
manager.add_command('db', MigrateCommand)
|
||||
|
||||
from flask.ext.appbuilder import Base
|
||||
|
||||
@manager.option(
|
||||
'-d', '--debug', action='store_true',
|
||||
help="Start the web server in debug mode")
|
||||
@manager.option(
|
||||
'-p', '--port', default=config.get("PANORAMIX_WEBSERVER_PORT"),
|
||||
help="Specify the port on which to run the web server")
|
||||
def runserver(debug, port):
|
||||
"""Starts a Panoramix web server"""
|
||||
debug = debug or config.get("DEBUG")
|
||||
if debug:
|
||||
app.run(
|
||||
host='0.0.0.0',
|
||||
port=int(config.PANORAMIX_WEBSERVER_PORT),
|
||||
port=int(port),
|
||||
debug=True)
|
||||
else:
|
||||
cmd = (
|
||||
"gunicorn "
|
||||
"-w 8 "
|
||||
"-b 0.0.0.0:{config.PANORAMIX_WEBSERVER_PORT} "
|
||||
"-b 0.0.0.0:{port} "
|
||||
"panoramix:app").format(**locals())
|
||||
print("Starting server with command: " + cmd)
|
||||
Popen(cmd, shell=True).wait()
|
||||
|
||||
@manager.option(
|
||||
'-s', '--sample', action='store_true',
|
||||
help="Only load 1000 rows (faster, used for testing)")
|
||||
def load_examples(sample):
|
||||
"""Loads a set of Slices and Dashboards and a supporting dataset """
|
||||
print("Loading examples into {}".format(db))
|
||||
|
||||
|
||||
BirthNames = Table(
|
||||
"birth_names", Base.metadata,
|
||||
Column("id", Integer, primary_key=True),
|
||||
Column("state", String(10)),
|
||||
Column("year", Integer),
|
||||
Column("name", String(128)),
|
||||
Column("num", Integer),
|
||||
Column("ds", String(20)),
|
||||
Column("gender", String(10)),
|
||||
)
|
||||
try:
|
||||
BirthNames.drop(db.engine)
|
||||
except:
|
||||
pass
|
||||
|
||||
BirthNames.create(db.engine)
|
||||
session = db.session()
|
||||
with gzip.open(config.get("BASE_DIR") + '/data/birth_names.csv.gz') as f:
|
||||
bb_csv = csv.reader(f)
|
||||
for i, (state, year, name, gender, num) in enumerate(bb_csv):
|
||||
if i == 0:
|
||||
continue
|
||||
if num == "NA":
|
||||
num = 0
|
||||
ds = str(year) + '-01-01'
|
||||
db.engine.execute(
|
||||
BirthNames.insert(),
|
||||
state=state,
|
||||
year=year,
|
||||
ds=ds,
|
||||
name=name, num=num, gender=gender)
|
||||
if i % 5000 == 0:
|
||||
print("{} loaded out of 82527 rows".format(i))
|
||||
session.commit()
|
||||
session.commit()
|
||||
if sample and i>1000: break
|
||||
print("Done loading table!")
|
||||
print("-" * 80)
|
||||
|
||||
print("Creating database reference")
|
||||
DB = models.Database
|
||||
dbobj = session.query(DB).filter_by(database_name='main').first()
|
||||
if not dbobj:
|
||||
dbobj = DB(database_name="main")
|
||||
print config.get("SQLALCHEMY_DATABASE_URI")
|
||||
dbobj.sqlalchemy_uri = config.get("SQLALCHEMY_DATABASE_URI")
|
||||
session.add(dbobj)
|
||||
session.commit()
|
||||
|
||||
print("Creating table reference")
|
||||
TBL = models.Table
|
||||
obj = session.query(TBL).filter_by(table_name='birth_names').first()
|
||||
if not obj:
|
||||
obj = TBL(table_name = 'birth_names')
|
||||
obj.main_dttm_col = 'ds'
|
||||
obj.default_endpoint = "/panoramix/datasource/table/1/?viz_type=table&granularity=one+day&since=100+years&until=now&row_limit=10&where=&flt_col_0=ds&flt_op_0=in&flt_eq_0=&flt_col_1=ds&flt_op_1=in&flt_eq_1=&slice_name=TEST&datasource_name=birth_names&datasource_id=1&datasource_type=table"
|
||||
obj.database = dbobj
|
||||
obj.columns = [models.TableColumn(
|
||||
column_name="num", sum=True, type="INTEGER")]
|
||||
models.Table
|
||||
session.add(obj)
|
||||
session.commit()
|
||||
obj.fetch_metadata()
|
||||
tbl = obj
|
||||
|
||||
print("Creating some slices")
|
||||
def get_slice_json(slice_name, **kwargs):
|
||||
defaults = {
|
||||
"compare_lag": "10",
|
||||
"compare_suffix": "o10Y",
|
||||
"datasource_id": "1",
|
||||
"datasource_name": "birth_names",
|
||||
"datasource_type": "table",
|
||||
"limit": "25",
|
||||
"flt_col_1": "gender",
|
||||
"flt_eq_1": "",
|
||||
"flt_op_1": "in",
|
||||
"granularity": "all",
|
||||
"groupby": [],
|
||||
"metric": 'sum__num',
|
||||
"metrics": ["sum__num"],
|
||||
"row_limit": config.get("ROW_LIMIT"),
|
||||
"since": "100 years",
|
||||
"slice_name": slice_name,
|
||||
"until": "now",
|
||||
"viz_type": "table",
|
||||
"where": "",
|
||||
"markup_type": "markdown",
|
||||
}
|
||||
d = defaults.copy()
|
||||
d.update(kwargs)
|
||||
return json.dumps(d, indent=4, sort_keys=True)
|
||||
Slice = models.Slice
|
||||
slices = []
|
||||
|
||||
slice_name = "Girls"
|
||||
slc = session.query(Slice).filter_by(slice_name=slice_name).first()
|
||||
if not slc:
|
||||
slc = Slice(
|
||||
slice_name=slice_name,
|
||||
viz_type='table',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
slice_name, groupby=['name'], flt_eq_1="girl", row_limit=50))
|
||||
session.add(slc)
|
||||
slices.append(slc)
|
||||
|
||||
slice_name = "Boys"
|
||||
slc = session.query(Slice).filter_by(slice_name=slice_name).first()
|
||||
if not slc:
|
||||
slc = Slice(
|
||||
slice_name=slice_name,
|
||||
viz_type='table',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
slice_name, groupby=['name'], flt_eq_1="boy", row_limit=50))
|
||||
session.add(slc)
|
||||
slices.append(slc)
|
||||
|
||||
slice_name = "Participants"
|
||||
slc = session.query(Slice).filter_by(slice_name=slice_name).first()
|
||||
if not slc:
|
||||
slc = Slice(
|
||||
slice_name=slice_name,
|
||||
viz_type='big_number',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
slice_name, viz_type="big_number", granularity="1 day",
|
||||
compare_lag="5", compare_suffix="over 5Y"))
|
||||
session.add(slc)
|
||||
slices.append(slc)
|
||||
|
||||
slice_name = "Genders"
|
||||
slc = session.query(Slice).filter_by(slice_name=slice_name).first()
|
||||
if not slc:
|
||||
slc = Slice(
|
||||
slice_name=slice_name,
|
||||
viz_type='pie',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
slice_name, viz_type="pie", groupby=['gender']))
|
||||
session.add(slc)
|
||||
slices.append(slc)
|
||||
|
||||
slice_name = "States"
|
||||
slc = session.query(Slice).filter_by(slice_name=slice_name).first()
|
||||
if not slc:
|
||||
slc = Slice(
|
||||
slice_name=slice_name,
|
||||
viz_type='dist_bar',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
slice_name, flt_eq_1="other", viz_type="dist_bar",
|
||||
groupby=['state'], flt_op_1='not in', flt_col_1='state'))
|
||||
session.add(slc)
|
||||
slices.append(slc)
|
||||
|
||||
slice_name = "Trends"
|
||||
slc = session.query(Slice).filter_by(slice_name=slice_name).first()
|
||||
if not slc:
|
||||
slc = Slice(
|
||||
slice_name=slice_name,
|
||||
viz_type='line',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
slice_name, viz_type="line", groupby=['name'],
|
||||
granularity='1 day'))
|
||||
session.add(slc)
|
||||
slices.append(slc)
|
||||
|
||||
slice_name = "Title"
|
||||
slc = session.query(Slice).filter_by(slice_name=slice_name).first()
|
||||
code = """
|
||||
### Birth Names Dashboard
|
||||
The source dataset came from [here](https://github.com/hadley/babynames)
|
||||
|
||||

|
||||
"""
|
||||
if not slc:
|
||||
slc = Slice(
|
||||
slice_name=slice_name,
|
||||
viz_type='markup',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
slice_name, viz_type="markup", markup_type="markdown",
|
||||
code=code))
|
||||
session.add(slc)
|
||||
slices.append(slc)
|
||||
|
||||
slice_name = "Name Cloud"
|
||||
slc = session.query(Slice).filter_by(slice_name=slice_name).first()
|
||||
if not slc:
|
||||
slc = Slice(
|
||||
slice_name=slice_name,
|
||||
viz_type='word_cloud',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
slice_name, viz_type="word_cloud", size_from="10",
|
||||
groupby=['name'], size_to="70", rotation="square",
|
||||
limit='100'))
|
||||
session.add(slc)
|
||||
slices.append(slc)
|
||||
|
||||
print("Creating a dashboard")
|
||||
Dash = models.Dashboard
|
||||
dash = session.query(Dash).filter_by(dashboard_title="Births").first()
|
||||
if not dash:
|
||||
dash = Dash(
|
||||
dashboard_title="Births",
|
||||
position_json="""
|
||||
[
|
||||
{
|
||||
"size_y": 4,
|
||||
"size_x": 2,
|
||||
"col": 3,
|
||||
"slice_id": "1",
|
||||
"row": 3
|
||||
},
|
||||
{
|
||||
"size_y": 4,
|
||||
"size_x": 2,
|
||||
"col": 1,
|
||||
"slice_id": "2",
|
||||
"row": 3
|
||||
},
|
||||
{
|
||||
"size_y": 2,
|
||||
"size_x": 2,
|
||||
"col": 1,
|
||||
"slice_id": "3",
|
||||
"row": 1
|
||||
},
|
||||
{
|
||||
"size_y": 2,
|
||||
"size_x": 2,
|
||||
"col": 3,
|
||||
"slice_id": "4",
|
||||
"row": 1
|
||||
},
|
||||
{
|
||||
"size_y": 3,
|
||||
"size_x": 7,
|
||||
"col": 5,
|
||||
"slice_id": "5",
|
||||
"row": 4
|
||||
},
|
||||
{
|
||||
"size_y": 5,
|
||||
"size_x": 11,
|
||||
"col": 1,
|
||||
"slice_id": "6",
|
||||
"row": 7
|
||||
},
|
||||
{
|
||||
"size_y": 3,
|
||||
"size_x": 3,
|
||||
"col": 9,
|
||||
"slice_id": "7",
|
||||
"row": 1
|
||||
},
|
||||
{
|
||||
"size_y": 3,
|
||||
"size_x": 4,
|
||||
"col": 5,
|
||||
"slice_id": "8",
|
||||
"row": 1
|
||||
}
|
||||
]
|
||||
"""
|
||||
)
|
||||
session.add(dash)
|
||||
for s in slices:
|
||||
dash.slices.append(s)
|
||||
session.commit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
manager.run()
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import os
|
||||
from flask_appbuilder.security.manager import AUTH_OID, AUTH_REMOTE_USER, AUTH_DB, AUTH_LDAP, AUTH_OAUTH
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
from flask_appbuilder.security.manager import AUTH_DB
|
||||
# from flask_appbuilder.security.manager import (
|
||||
# AUTH_OID, AUTH_REMOTE_USER, AUTH_DB, AUTH_LDAP, AUTH_OAUTH)
|
||||
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
from dateutil import tz
|
||||
|
||||
"""
|
||||
All configuration in this file can be overridden by providing a local_config
|
||||
@@ -9,41 +12,49 @@ in your PYTHONPATH.
|
||||
There' a ``from local_config import *`` at the end of this file.
|
||||
"""
|
||||
|
||||
#---------------------------------------------------------
|
||||
# ---------------------------------------------------------
|
||||
# Panoramix specifix config
|
||||
#---------------------------------------------------------
|
||||
ROW_LIMIT = 5000
|
||||
# ---------------------------------------------------------
|
||||
ROW_LIMIT = 50000
|
||||
WEBSERVER_THREADS = 8
|
||||
|
||||
PANORAMIX_WEBSERVER_PORT = 8088
|
||||
#---------------------------------------------------------
|
||||
|
||||
CUSTOM_SECURITY_MANAGER = None
|
||||
# ---------------------------------------------------------
|
||||
|
||||
# Your App secret key
|
||||
SECRET_KEY = '\2\1thisismyscretkey\1\2\e\y\y\h'
|
||||
|
||||
# The SQLAlchemy connection string.
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'app.db')
|
||||
#SQLALCHEMY_DATABASE_URI = 'mysql://myapp@localhost/myapp'
|
||||
#SQLALCHEMY_DATABASE_URI = 'postgresql://root:password@localhost/myapp'
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/panoramix.db'
|
||||
# SQLALCHEMY_DATABASE_URI = 'mysql://myapp@localhost/myapp'
|
||||
# SQLALCHEMY_DATABASE_URI = 'postgresql://root:password@localhost/myapp'
|
||||
|
||||
# Flask-WTF flag for CSRF
|
||||
CSRF_ENABLED = True
|
||||
|
||||
#Whether to run the web server in debug mode or not
|
||||
# Whether to run the web server in debug mode or not
|
||||
DEBUG = True
|
||||
|
||||
#------------------------------
|
||||
# ------------------------------
|
||||
# GLOBALS FOR APP Builder
|
||||
#------------------------------
|
||||
# ------------------------------
|
||||
# Uncomment to setup Your App name
|
||||
APP_NAME = "Panoramix"
|
||||
|
||||
# Uncomment to setup Setup an App icon
|
||||
APP_ICON = "/static/chaudron_white.png"
|
||||
|
||||
#----------------------------------------------------
|
||||
# Druid query timezone
|
||||
# tz.tzutc() : Using utc timezone
|
||||
# tz.tzlocal() : Using local timezone
|
||||
# other tz can be overridden by providing a local_config
|
||||
DRUID_TZ = tz.tzutc()
|
||||
|
||||
# ----------------------------------------------------
|
||||
# AUTHENTICATION CONFIG
|
||||
#----------------------------------------------------
|
||||
# ----------------------------------------------------
|
||||
# The authentication type
|
||||
# AUTH_OID : Is for OpenID
|
||||
# AUTH_DB : Is for database (username/password()
|
||||
@@ -52,37 +63,37 @@ APP_ICON = "/static/chaudron_white.png"
|
||||
AUTH_TYPE = AUTH_DB
|
||||
|
||||
# Uncomment to setup Full admin role name
|
||||
#AUTH_ROLE_ADMIN = 'Admin'
|
||||
# AUTH_ROLE_ADMIN = 'Admin'
|
||||
|
||||
# Uncomment to setup Public role name, no authentication needed
|
||||
#AUTH_ROLE_PUBLIC = 'Public'
|
||||
# AUTH_ROLE_PUBLIC = 'Public'
|
||||
|
||||
# Will allow user self registration
|
||||
#AUTH_USER_REGISTRATION = True
|
||||
# AUTH_USER_REGISTRATION = True
|
||||
|
||||
# The default user self registration role
|
||||
#AUTH_USER_REGISTRATION_ROLE = "Public"
|
||||
# AUTH_USER_REGISTRATION_ROLE = "Public"
|
||||
|
||||
# When using LDAP Auth, setup the ldap server
|
||||
#AUTH_LDAP_SERVER = "ldap://ldapserver.new"
|
||||
# AUTH_LDAP_SERVER = "ldap://ldapserver.new"
|
||||
|
||||
# Uncomment to setup OpenID providers example for OpenID authentication
|
||||
#OPENID_PROVIDERS = [
|
||||
# OPENID_PROVIDERS = [
|
||||
# { 'name': 'Yahoo', 'url': 'https://me.yahoo.com' },
|
||||
# { 'name': 'AOL', 'url': 'http://openid.aol.com/<username>' },
|
||||
# { 'name': 'Flickr', 'url': 'http://www.flickr.com/<username>' },
|
||||
# { 'name': 'MyOpenID', 'url': 'https://www.myopenid.com' }]
|
||||
#---------------------------------------------------
|
||||
# ---------------------------------------------------
|
||||
# Babel config for translations
|
||||
#---------------------------------------------------
|
||||
# ---------------------------------------------------
|
||||
# Setup default language
|
||||
BABEL_DEFAULT_LOCALE = 'en'
|
||||
# Your application default translation path
|
||||
BABEL_DEFAULT_FOLDER = 'translations'
|
||||
# The allowed translation for you app
|
||||
LANGUAGES = {
|
||||
'en': {'flag':'us', 'name':'English'},
|
||||
'fr': {'flag':'fr', 'name':'French'},
|
||||
'en': {'flag': 'us', 'name': 'English'},
|
||||
'fr': {'flag': 'fr', 'name': 'French'},
|
||||
}
|
||||
"""
|
||||
'pt': {'flag':'pt', 'name':'Portuguese'},
|
||||
@@ -92,36 +103,39 @@ LANGUAGES = {
|
||||
'zh': {'flag':'cn', 'name':'Chinese'},
|
||||
'ru': {'flag':'ru', 'name':'Russian'}
|
||||
"""
|
||||
#---------------------------------------------------
|
||||
# ---------------------------------------------------
|
||||
# Image and file configuration
|
||||
#---------------------------------------------------
|
||||
# ---------------------------------------------------
|
||||
# The file upload folder, when using models with files
|
||||
UPLOAD_FOLDER = basedir + '/app/static/uploads/'
|
||||
UPLOAD_FOLDER = BASE_DIR + '/app/static/uploads/'
|
||||
|
||||
# The image upload folder, when using models with images
|
||||
IMG_UPLOAD_FOLDER = basedir + '/app/static/uploads/'
|
||||
IMG_UPLOAD_FOLDER = BASE_DIR + '/app/static/uploads/'
|
||||
|
||||
# The image upload url, when using models with images
|
||||
IMG_UPLOAD_URL = '/static/uploads/'
|
||||
# Setup image size default is (300, 200, True)
|
||||
#IMG_SIZE = (300, 200, True)
|
||||
# IMG_SIZE = (300, 200, True)
|
||||
|
||||
# ---------------------------------------------------
|
||||
# Theme configuration
|
||||
# these are located on static/appbuilder/css/themes
|
||||
# you can create your own and easily use them placing them on the same dir structure to override
|
||||
#APP_THEME = "bootstrap-theme.css" # default bootstrap
|
||||
#APP_THEME = "cerulean.css"
|
||||
#APP_THEME = "amelia.css"
|
||||
#APP_THEME = "cosmo.css"
|
||||
#APP_THEME = "cyborg.css"
|
||||
#APP_THEME = "flatly.css"
|
||||
#APP_THEME = "journal.css"
|
||||
#APP_THEME = "readable.css"
|
||||
#APP_THEME = "simplex.css"
|
||||
#APP_THEME = "slate.css"
|
||||
#APP_THEME = "spacelab.css"
|
||||
#APP_THEME = "united.css"
|
||||
#APP_THEME = "yeti.css"
|
||||
# you can create your own and easily use them placing them on the
|
||||
# same dir structure to override
|
||||
# ---------------------------------------------------
|
||||
# APP_THEME = "bootstrap-theme.css" # default bootstrap
|
||||
# APP_THEME = "cerulean.css"
|
||||
# APP_THEME = "amelia.css"
|
||||
# APP_THEME = "cosmo.css"
|
||||
# APP_THEME = "cyborg.css"
|
||||
# APP_THEME = "flatly.css"
|
||||
# APP_THEME = "journal.css"
|
||||
# APP_THEME = "readable.css"
|
||||
# APP_THEME = "simplex.css"
|
||||
# APP_THEME = "slate.css"
|
||||
# APP_THEME = "spacelab.css"
|
||||
# APP_THEME = "united.css"
|
||||
# APP_THEME = "yeti.css"
|
||||
|
||||
try:
|
||||
from panoramix_config import *
|
||||
|
||||
0
panoramix/data/__init__.py
Normal file
BIN
panoramix/data/birth_names.csv.gz
Normal file
159
panoramix/forms.py
Normal file
@@ -0,0 +1,159 @@
|
||||
from wtforms import (
|
||||
Field, Form, SelectMultipleField, SelectField, TextField, TextAreaField,
|
||||
BooleanField)
|
||||
from copy import copy
|
||||
|
||||
|
||||
class OmgWtForm(Form):
|
||||
field_order = tuple()
|
||||
css_classes = dict()
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
fields = []
|
||||
for field in self.field_order:
|
||||
if hasattr(self, field):
|
||||
obj = getattr(self, field)
|
||||
if isinstance(obj, Field):
|
||||
fields.append(getattr(self, field))
|
||||
return fields
|
||||
|
||||
def get_field(self, fieldname):
|
||||
return getattr(self, fieldname)
|
||||
|
||||
def field_css_classes(self, fieldname):
|
||||
if fieldname in self.css_classes:
|
||||
return " ".join(self.css_classes[fieldname])
|
||||
return ""
|
||||
|
||||
|
||||
def form_factory(viz):
|
||||
datasource = viz.datasource
|
||||
from panoramix.viz import viz_types
|
||||
row_limits = [10, 50, 100, 500, 1000, 5000, 10000]
|
||||
series_limits = [0, 5, 10, 25, 50, 100, 500]
|
||||
group_by_choices = [(s, s) for s in datasource.groupby_column_names]
|
||||
# Pool of all the fields that can be used in Panoramix
|
||||
px_form_fields = {
|
||||
'viz_type': SelectField(
|
||||
'Viz',
|
||||
choices=[(k, v.verbose_name) for k, v in viz_types.items()],
|
||||
description="The type of visualization to display"),
|
||||
'metrics': SelectMultipleField(
|
||||
'Metrics', choices=datasource.metrics_combo,
|
||||
description="One or many metrics to display"),
|
||||
'metric': SelectField(
|
||||
'Metric', choices=datasource.metrics_combo,
|
||||
description="One or many metrics to display"),
|
||||
'groupby': SelectMultipleField(
|
||||
'Group by',
|
||||
choices=[(s, s) for s in datasource.groupby_column_names],
|
||||
description="One or many fields to group by"),
|
||||
'granularity': TextField(
|
||||
'Time Granularity', default="one day",
|
||||
description=(
|
||||
"The time granularity for the visualization. Note that you "
|
||||
"can type and use simple natural language as in '10 seconds', "
|
||||
"'1 day' or '56 weeks'")),
|
||||
'since': TextField(
|
||||
'Since', default="one day ago", description=(
|
||||
"Timestamp from filter. This supports free form typing and "
|
||||
"natural language as in '1 day ago', '28 days' or '3 years'")),
|
||||
'until': TextField('Until', default="now"),
|
||||
'row_limit':
|
||||
SelectField(
|
||||
'Row limit', choices=[(s, s) for s in row_limits]),
|
||||
'limit':
|
||||
SelectField(
|
||||
'Series limit', choices=[(s, s) for s in series_limits],
|
||||
description=(
|
||||
"Limits the number of time series that get displayed")),
|
||||
'rolling_type': SelectField(
|
||||
'Rolling',
|
||||
choices=[(s, s) for s in ['mean', 'sum', 'std']],
|
||||
description=(
|
||||
"Defines a rolling window function to apply")),
|
||||
'rolling_periods': TextField('Periods', description=(
|
||||
"Defines the size of the rolling window function, "
|
||||
"relative to the 'granularity' field")),
|
||||
'series': SelectField('Series', choices=group_by_choices),
|
||||
'entity': SelectField('Entity', choices=group_by_choices),
|
||||
'x': SelectField('X Axis', choices=datasource.metrics_combo),
|
||||
'y': SelectField('Y Axis', choices=datasource.metrics_combo),
|
||||
'size': SelectField('Bubble Size', choices=datasource.metrics_combo),
|
||||
'where': TextField('Custom WHERE clause'),
|
||||
'compare_lag': TextField('Comparison Period Lag',
|
||||
description="Based on granularity, number of time periods to compare against"),
|
||||
'compare_suffix': TextField('Comparison suffix',
|
||||
description="Suffix to apply after the percentage display"),
|
||||
'markup_type': SelectField(
|
||||
"Markup Type",
|
||||
choices=[(s, s) for s in ['markdown', 'html']],
|
||||
default="markdown",
|
||||
description="Pick your favorite markup language"),
|
||||
'rotation': SelectField(
|
||||
"Rotation",
|
||||
choices=[(s, s) for s in ['random', 'flat', 'square']],
|
||||
default="random",
|
||||
description="Rotation to apply to words in the cloud"),
|
||||
'code': TextAreaField("Code", description="Put your code here"),
|
||||
'size_from': TextField(
|
||||
"Font Size From",
|
||||
default="20",
|
||||
description="Font size for the smallest value in the list"),
|
||||
'size_to': TextField(
|
||||
"Font Size To",
|
||||
default="150",
|
||||
description="Font size for the biggest value in the list"),
|
||||
'show_brush': BooleanField(
|
||||
"Range Selector", default=True,
|
||||
description="Whether to display the time range interactive selector"),
|
||||
'show_legend': BooleanField(
|
||||
"Legend", default=True,
|
||||
description="Whether to display the legend (toggles)"),
|
||||
'rich_tooltip': BooleanField(
|
||||
"Rich Tooltip", default=True,
|
||||
description="The rich tooltip shows a list of all series for that point in time"),
|
||||
'y_axis_zero': BooleanField(
|
||||
"Y Axis Zero", default=False,
|
||||
description="Force the Y axis to start at 0 instead of the minimum value"),
|
||||
'y_log_scale': BooleanField(
|
||||
"Y Log", default=False,
|
||||
description="Use a log scale for the Y axis"),
|
||||
}
|
||||
field_css_classes = {k: ['form-control'] for k in px_form_fields.keys()}
|
||||
select2 = [
|
||||
'viz_type', 'metrics', 'groupby',
|
||||
'row_limit', 'rolling_type', 'series',
|
||||
'entity', 'x', 'y', 'size', 'rotation', 'metric', 'limit',
|
||||
'markup_type',]
|
||||
field_css_classes['since'] += ['select2_free_since']
|
||||
field_css_classes['until'] += ['select2_free_until']
|
||||
field_css_classes['granularity'] += ['select2_free_granularity']
|
||||
for field in ('show_brush', 'show_legend', 'rich_tooltip'):
|
||||
field_css_classes[field] += ['input-sm']
|
||||
for field in select2:
|
||||
field_css_classes[field] += ['select2']
|
||||
|
||||
class QueryForm(OmgWtForm):
|
||||
field_order = copy(viz.form_fields)
|
||||
css_classes = field_css_classes
|
||||
|
||||
for i in range(10):
|
||||
setattr(QueryForm, 'flt_col_' + str(i), SelectField(
|
||||
'Filter 1',
|
||||
choices=[(s, s) for s in datasource.filterable_column_names]))
|
||||
setattr(QueryForm, 'flt_op_' + str(i), SelectField(
|
||||
'Filter 1', choices=[(m, m) for m in ['in', 'not in']]))
|
||||
setattr(QueryForm, 'flt_eq_' + str(i), TextField("Super"))
|
||||
for ff in viz.form_fields:
|
||||
if isinstance(ff, basestring):
|
||||
ff = [ff]
|
||||
for s in ff:
|
||||
setattr(QueryForm, s, px_form_fields[s])
|
||||
|
||||
# datasource type specific form elements
|
||||
if datasource.__class__.__name__ == 'Table':
|
||||
QueryForm.field_order += ['where']
|
||||
setattr(QueryForm, 'where', px_form_fields['where'])
|
||||
return QueryForm
|
||||
@@ -1,7 +1,5 @@
|
||||
import pandas
|
||||
from collections import defaultdict
|
||||
import copy
|
||||
import json
|
||||
from pandas.io.json import dumps
|
||||
|
||||
|
||||
@@ -9,16 +7,21 @@ class BaseHighchart(object):
|
||||
stockchart = False
|
||||
tooltip_formatter = ""
|
||||
target_div = 'chart'
|
||||
|
||||
@property
|
||||
def javascript_cmd(self):
|
||||
def json(self):
|
||||
js = dumps(self.chart)
|
||||
js = (
|
||||
return (
|
||||
js.replace('"{{TOOLTIP_FORMATTER}}"', self.tooltip_formatter)
|
||||
.replace("\n", " ")
|
||||
)
|
||||
|
||||
@property
|
||||
def javascript_cmd(self):
|
||||
js = self.json
|
||||
if self.stockchart:
|
||||
return "new Highcharts.StockChart(%s);" % js
|
||||
return "new Highcharts.Chart(%s);" %js
|
||||
return "new Highcharts.Chart(%s);" % js
|
||||
|
||||
|
||||
class Highchart(BaseHighchart):
|
||||
@@ -84,7 +87,7 @@ class Highchart(BaseHighchart):
|
||||
if sort_legend_y:
|
||||
if 'tooltip' not in chart:
|
||||
chart['tooltip'] = {
|
||||
'formatter': "{{TOOLTIP_FORMATTER}}"
|
||||
#'formatter': "{{TOOLTIP_FORMATTER}}"
|
||||
}
|
||||
if self.zoom:
|
||||
chart["zoomType"] = self.zoom
|
||||
@@ -127,7 +130,6 @@ class Highchart(BaseHighchart):
|
||||
"""
|
||||
return tf
|
||||
|
||||
|
||||
def serialize_series(self):
|
||||
df = self.df
|
||||
chart = self.chart
|
||||
@@ -140,7 +142,8 @@ class Highchart(BaseHighchart):
|
||||
continue
|
||||
sec = name in self.secondary_y
|
||||
d = {
|
||||
"name": name if not sec or self.mark_right else name + " (right)",
|
||||
"name":
|
||||
name if not sec or self.mark_right else name + " (right)",
|
||||
"yAxis": int(sec),
|
||||
"data": zip(df.index, data.tolist())
|
||||
}
|
||||
@@ -150,8 +153,6 @@ class Highchart(BaseHighchart):
|
||||
d['compare'] = self.compare # either `value` or `percent`
|
||||
if self.chart_type in ("area", "column", "bar") and self.stacked:
|
||||
d["stacking"] = 'normal'
|
||||
#if kwargs.get("style"):
|
||||
# d["dashStyle"] = pd2hc_linestyle(kwargs["style"].get(name, "-"))
|
||||
chart["series"].append(d)
|
||||
|
||||
def serialize_xaxis(self):
|
||||
@@ -195,7 +196,7 @@ class Highchart(BaseHighchart):
|
||||
|
||||
|
||||
class HighchartBubble(BaseHighchart):
|
||||
def __init__(self, df, target_div='chart', height=800):
|
||||
def __init__(self, df, target_div=None, height=None):
|
||||
self.df = df
|
||||
self.chart = {
|
||||
'chart': {
|
||||
@@ -219,7 +220,6 @@ class HighchartBubble(BaseHighchart):
|
||||
chart['chart']["height"] = height
|
||||
|
||||
def series(self):
|
||||
#df = self.df[['name', 'x', 'y', 'z']]
|
||||
df = self.df
|
||||
series = defaultdict(list)
|
||||
for row in df.to_dict(orient='records'):
|
||||
|
||||
1
panoramix/migrations/README
Executable file
@@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
||||
0
panoramix/migrations/__init__.py
Normal file
45
panoramix/migrations/alembic.ini
Normal file
@@ -0,0 +1,45 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
89
panoramix/migrations/env.py
Executable file
@@ -0,0 +1,89 @@
|
||||
from __future__ import with_statement
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
from logging.config import fileConfig
|
||||
import logging
|
||||
from panoramix import db, models
|
||||
from flask.ext.appbuilder import Base
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
logger = logging.getLogger('alembic.env')
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
from flask import current_app
|
||||
config.set_main_option('sqlalchemy.url',
|
||||
current_app.config.get('SQLALCHEMY_DATABASE_URI'))
|
||||
target_metadata = Base.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(url=url)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
# this callback is used to prevent an auto-migration from being generated
|
||||
# when there are no changes to the schema
|
||||
# reference: http://alembic.readthedocs.org/en/latest/cookbook.html
|
||||
def process_revision_directives(context, revision, directives):
|
||||
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
logger.info('No changes in schema detected.')
|
||||
|
||||
engine = engine_from_config(config.get_section(config.config_ini_section),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool)
|
||||
|
||||
connection = engine.connect()
|
||||
context.configure(connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
#compare_type=True,
|
||||
process_revision_directives=process_revision_directives,
|
||||
**current_app.extensions['migrate'].configure_args)
|
||||
|
||||
try:
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
22
panoramix/migrations/script.py.mako
Executable file
@@ -0,0 +1,22 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
||||
215
panoramix/migrations/versions/4e6a06bad7a8_init.py
Normal file
@@ -0,0 +1,215 @@
|
||||
"""Init
|
||||
|
||||
Revision ID: 4e6a06bad7a8
|
||||
Revises: None
|
||||
Create Date: 2015-09-21 17:30:38.442998
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4e6a06bad7a8'
|
||||
down_revision = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('clusters',
|
||||
sa.Column('created_on', sa.DateTime(), nullable=False),
|
||||
sa.Column('changed_on', sa.DateTime(), nullable=False),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('cluster_name', sa.String(length=250), nullable=True),
|
||||
sa.Column('coordinator_host', sa.String(length=256), nullable=True),
|
||||
sa.Column('coordinator_port', sa.Integer(), nullable=True),
|
||||
sa.Column('coordinator_endpoint', sa.String(length=256), nullable=True),
|
||||
sa.Column('broker_host', sa.String(length=256), nullable=True),
|
||||
sa.Column('broker_port', sa.Integer(), nullable=True),
|
||||
sa.Column('broker_endpoint', sa.String(length=256), nullable=True),
|
||||
sa.Column('metadata_last_refreshed', sa.DateTime(), nullable=True),
|
||||
sa.Column('created_by_fk', sa.Integer(), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('cluster_name')
|
||||
)
|
||||
op.create_table('dashboards',
|
||||
sa.Column('created_on', sa.DateTime(), nullable=False),
|
||||
sa.Column('changed_on', sa.DateTime(), nullable=False),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('dashboard_title', sa.String(length=500), nullable=True),
|
||||
sa.Column('position_json', sa.Text(), nullable=True),
|
||||
sa.Column('created_by_fk', sa.Integer(), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('dbs',
|
||||
sa.Column('created_on', sa.DateTime(), nullable=False),
|
||||
sa.Column('changed_on', sa.DateTime(), nullable=False),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('database_name', sa.String(length=250), nullable=True),
|
||||
sa.Column('sqlalchemy_uri', sa.String(length=1024), nullable=True),
|
||||
sa.Column('created_by_fk', sa.Integer(), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('database_name')
|
||||
)
|
||||
op.create_table('datasources',
|
||||
sa.Column('created_on', sa.DateTime(), nullable=False),
|
||||
sa.Column('changed_on', sa.DateTime(), nullable=False),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('datasource_name', sa.String(length=250), nullable=True),
|
||||
sa.Column('is_featured', sa.Boolean(), nullable=True),
|
||||
sa.Column('is_hidden', sa.Boolean(), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('default_endpoint', sa.Text(), nullable=True),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('cluster_name', sa.String(length=250), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), nullable=False),
|
||||
sa.Column('created_by_fk', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['cluster_name'], ['clusters.cluster_name'], ),
|
||||
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['ab_user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('datasource_name')
|
||||
)
|
||||
op.create_table('tables',
|
||||
sa.Column('created_on', sa.DateTime(), nullable=False),
|
||||
sa.Column('changed_on', sa.DateTime(), nullable=False),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('table_name', sa.String(length=250), nullable=True),
|
||||
sa.Column('main_dttm_col', sa.String(length=250), nullable=True),
|
||||
sa.Column('default_endpoint', sa.Text(), nullable=True),
|
||||
sa.Column('database_id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_by_fk', sa.Integer(), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['database_id'], ['dbs.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('table_name')
|
||||
)
|
||||
op.create_table('columns',
|
||||
sa.Column('created_on', sa.DateTime(), nullable=False),
|
||||
sa.Column('changed_on', sa.DateTime(), nullable=False),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('datasource_name', sa.String(length=250), nullable=True),
|
||||
sa.Column('column_name', sa.String(length=256), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||
sa.Column('type', sa.String(length=32), nullable=True),
|
||||
sa.Column('groupby', sa.Boolean(), nullable=True),
|
||||
sa.Column('count_distinct', sa.Boolean(), nullable=True),
|
||||
sa.Column('sum', sa.Boolean(), nullable=True),
|
||||
sa.Column('max', sa.Boolean(), nullable=True),
|
||||
sa.Column('min', sa.Boolean(), nullable=True),
|
||||
sa.Column('filterable', sa.Boolean(), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('created_by_fk', sa.Integer(), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['datasource_name'], ['datasources.datasource_name'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('metrics',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('metric_name', sa.String(length=512), nullable=True),
|
||||
sa.Column('verbose_name', sa.String(length=1024), nullable=True),
|
||||
sa.Column('metric_type', sa.String(length=32), nullable=True),
|
||||
sa.Column('datasource_name', sa.String(length=250), nullable=True),
|
||||
sa.Column('json', sa.Text(), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['datasource_name'], ['datasources.datasource_name'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('slices',
|
||||
sa.Column('created_on', sa.DateTime(), nullable=False),
|
||||
sa.Column('changed_on', sa.DateTime(), nullable=False),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('slice_name', sa.String(length=250), nullable=True),
|
||||
sa.Column('druid_datasource_id', sa.Integer(), nullable=True),
|
||||
sa.Column('table_id', sa.Integer(), nullable=True),
|
||||
sa.Column('datasource_type', sa.String(length=200), nullable=True),
|
||||
sa.Column('datasource_name', sa.String(length=2000), nullable=True),
|
||||
sa.Column('viz_type', sa.String(length=250), nullable=True),
|
||||
sa.Column('params', sa.Text(), nullable=True),
|
||||
sa.Column('created_by_fk', sa.Integer(), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['druid_datasource_id'], ['datasources.id'], ),
|
||||
sa.ForeignKeyConstraint(['table_id'], ['tables.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('sql_metrics',
|
||||
sa.Column('created_on', sa.DateTime(), nullable=False),
|
||||
sa.Column('changed_on', sa.DateTime(), nullable=False),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('metric_name', sa.String(length=512), nullable=True),
|
||||
sa.Column('verbose_name', sa.String(length=1024), nullable=True),
|
||||
sa.Column('metric_type', sa.String(length=32), nullable=True),
|
||||
sa.Column('table_id', sa.Integer(), nullable=True),
|
||||
sa.Column('expression', sa.Text(), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('created_by_fk', sa.Integer(), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['table_id'], ['tables.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('table_columns',
|
||||
sa.Column('created_on', sa.DateTime(), nullable=False),
|
||||
sa.Column('changed_on', sa.DateTime(), nullable=False),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('table_id', sa.Integer(), nullable=True),
|
||||
sa.Column('column_name', sa.String(length=256), nullable=True),
|
||||
sa.Column('is_dttm', sa.Boolean(), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||
sa.Column('type', sa.String(length=32), nullable=True),
|
||||
sa.Column('groupby', sa.Boolean(), nullable=True),
|
||||
sa.Column('count_distinct', sa.Boolean(), nullable=True),
|
||||
sa.Column('sum', sa.Boolean(), nullable=True),
|
||||
sa.Column('max', sa.Boolean(), nullable=True),
|
||||
sa.Column('min', sa.Boolean(), nullable=True),
|
||||
sa.Column('filterable', sa.Boolean(), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('created_by_fk', sa.Integer(), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['table_id'], ['tables.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('dashboard_slices',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('dashboard_id', sa.Integer(), nullable=True),
|
||||
sa.Column('slice_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['dashboard_id'], ['dashboards.id'], ),
|
||||
sa.ForeignKeyConstraint(['slice_id'], ['slices.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('dashboard_slices')
|
||||
op.drop_table('table_columns')
|
||||
op.drop_table('sql_metrics')
|
||||
op.drop_table('slices')
|
||||
op.drop_table('metrics')
|
||||
op.drop_table('columns')
|
||||
op.drop_table('tables')
|
||||
op.drop_table('datasources')
|
||||
op.drop_table('dbs')
|
||||
op.drop_table('dashboards')
|
||||
op.drop_table('clusters')
|
||||
### end Alembic commands ###
|
||||
@@ -1,32 +1,161 @@
|
||||
from flask.ext.appbuilder import Model
|
||||
from datetime import timedelta
|
||||
from flask.ext.appbuilder.models.mixins import AuditMixin
|
||||
from flask import request, redirect, flash, Response
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, Text, Boolean, DateTime
|
||||
from sqlalchemy import create_engine, MetaData, desc
|
||||
from sqlalchemy import Table as sqlaTable
|
||||
from sqlalchemy.orm import relationship
|
||||
from dateutil.parser import parse
|
||||
from flask import flash
|
||||
from flask.ext.appbuilder import Model
|
||||
from flask.ext.appbuilder.models.mixins import AuditMixin
|
||||
from pandas import read_sql_query
|
||||
from pydruid import client
|
||||
from pydruid.utils.filters import Dimension, Filter
|
||||
from pandas import read_sql_query
|
||||
from sqlalchemy.sql import table, literal_column
|
||||
from sqlalchemy import select, and_, text, String
|
||||
from sqlalchemy import (
|
||||
Column, Integer, String, ForeignKey, Text, Boolean, DateTime)
|
||||
from sqlalchemy import Table as sqlaTable
|
||||
from sqlalchemy import create_engine, MetaData, desc, select, and_, Table
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import table, literal_column, text
|
||||
from flask import request
|
||||
|
||||
from copy import deepcopy, copy
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import json
|
||||
import sqlparse
|
||||
import requests
|
||||
import textwrap
|
||||
|
||||
from panoramix import db, get_session
|
||||
from panoramix import app, db, get_session, utils
|
||||
from panoramix.viz import viz_types
|
||||
from sqlalchemy.ext.declarative import declared_attr
|
||||
|
||||
config = app.config
|
||||
|
||||
QueryResult = namedtuple('namedtuple', ['df', 'query', 'duration'])
|
||||
|
||||
|
||||
class AuditMixinNullable(AuditMixin):
|
||||
@declared_attr
|
||||
def created_by_fk(cls):
|
||||
return Column(Integer, ForeignKey('ab_user.id'),
|
||||
default=cls.get_user_id, nullable=True)
|
||||
@declared_attr
|
||||
def changed_by_fk(cls):
|
||||
return Column(Integer, ForeignKey('ab_user.id'),
|
||||
default=cls.get_user_id, onupdate=cls.get_user_id, nullable=True)
|
||||
|
||||
|
||||
class Slice(Model, AuditMixinNullable):
|
||||
"""A slice is essentially a report or a view on data"""
|
||||
__tablename__ = 'slices'
|
||||
id = Column(Integer, primary_key=True)
|
||||
slice_name = Column(String(250))
|
||||
druid_datasource_id = Column(Integer, ForeignKey('datasources.id'))
|
||||
table_id = Column(Integer, ForeignKey('tables.id'))
|
||||
datasource_type = Column(String(200))
|
||||
datasource_name = Column(String(2000))
|
||||
viz_type = Column(String(250))
|
||||
params = Column(Text)
|
||||
|
||||
table = relationship(
|
||||
'Table', foreign_keys=[table_id], backref='slices')
|
||||
druid_datasource = relationship(
|
||||
'Datasource', foreign_keys=[druid_datasource_id], backref='slices')
|
||||
|
||||
def __repr__(self):
|
||||
return self.slice_name
|
||||
|
||||
@property
|
||||
def datasource(self):
|
||||
return self.table or self.druid_datasource
|
||||
|
||||
@property
|
||||
@utils.memoized
|
||||
def viz(self):
|
||||
d = json.loads(self.params)
|
||||
viz = viz_types[self.viz_type](
|
||||
self.datasource,
|
||||
form_data=d)
|
||||
return viz
|
||||
|
||||
@property
|
||||
def datasource_id(self):
|
||||
datasource = self.datasource
|
||||
return datasource.id if datasource else None
|
||||
|
||||
@property
|
||||
def slice_url(self):
|
||||
try:
|
||||
d = json.loads(self.params)
|
||||
except Exception as e:
|
||||
d = {}
|
||||
from werkzeug.urls import Href
|
||||
href = Href(
|
||||
"/panoramix/datasource/{self.datasource_type}/"
|
||||
"{self.datasource_id}/".format(self=self))
|
||||
return href(d)
|
||||
|
||||
@property
|
||||
def edit_url(self):
|
||||
return "/slicemodelview/edit/{}".format(self.id)
|
||||
|
||||
@property
|
||||
def slice_link(self):
|
||||
url = self.slice_url
|
||||
return '<a href="{url}">{self.slice_name}</a>'.format(**locals())
|
||||
|
||||
@property
|
||||
def js_files(self):
|
||||
from panoramix.viz import viz_types
|
||||
return viz_types[self.viz_type].js_files
|
||||
|
||||
@property
|
||||
def css_files(self):
|
||||
from panoramix.viz import viz_types
|
||||
return viz_types[self.viz_type].css_files
|
||||
|
||||
def get_viz(self):
|
||||
pass
|
||||
|
||||
|
||||
dashboard_slices = Table('dashboard_slices', Model.metadata,
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('dashboard_id', Integer, ForeignKey('dashboards.id')),
|
||||
Column('slice_id', Integer, ForeignKey('slices.id')),
|
||||
)
|
||||
|
||||
|
||||
class Dashboard(Model, AuditMixinNullable):
|
||||
"""A dash to slash"""
|
||||
__tablename__ = 'dashboards'
|
||||
id = Column(Integer, primary_key=True)
|
||||
dashboard_title = Column(String(500))
|
||||
position_json = Column(Text)
|
||||
slices = relationship(
|
||||
'Slice', secondary=dashboard_slices, backref='dashboards')
|
||||
|
||||
def __repr__(self):
|
||||
return self.dashboard_title
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return "/panoramix/dashboard/{}/".format(self.id)
|
||||
|
||||
def dashboard_link(self):
|
||||
return '<a href="{self.url}">{self.dashboard_title}</a>'.format(self=self)
|
||||
|
||||
@property
|
||||
def js_files(self):
|
||||
l = []
|
||||
for o in self.slices:
|
||||
l += [f for f in o.js_files if f not in l]
|
||||
return l
|
||||
|
||||
@property
|
||||
def css_files(self):
|
||||
l = []
|
||||
for o in self.slices:
|
||||
l += o.css_files
|
||||
return list(set(l))
|
||||
|
||||
|
||||
class Queryable(object):
|
||||
@property
|
||||
def column_names(self):
|
||||
@@ -40,13 +169,13 @@ class Queryable(object):
|
||||
def filterable_column_names(self):
|
||||
return sorted([c.column_name for c in self.columns if c.filterable])
|
||||
|
||||
class Database(Model, AuditMixin):
|
||||
|
||||
class Database(Model, AuditMixinNullable):
|
||||
__tablename__ = 'dbs'
|
||||
id = Column(Integer, primary_key=True)
|
||||
database_name = Column(String(255), unique=True)
|
||||
database_name = Column(String(250), unique=True)
|
||||
sqlalchemy_uri = Column(String(1024))
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return self.database_name
|
||||
|
||||
@@ -61,13 +190,13 @@ class Database(Model, AuditMixin):
|
||||
autoload_with=self.get_sqla_engine())
|
||||
|
||||
|
||||
class Table(Model, Queryable, AuditMixin):
|
||||
class Table(Model, Queryable, AuditMixinNullable):
|
||||
type = "table"
|
||||
|
||||
__tablename__ = 'tables'
|
||||
id = Column(Integer, primary_key=True)
|
||||
table_name = Column(String(255), unique=True)
|
||||
main_datetime_column_id = Column(Integer, ForeignKey('table_columns.id'))
|
||||
main_datetime_column = relationship(
|
||||
'TableColumn', foreign_keys=[main_datetime_column_id])
|
||||
table_name = Column(String(250), unique=True)
|
||||
main_dttm_col = Column(String(250))
|
||||
default_endpoint = Column(Text)
|
||||
database_id = Column(Integer, ForeignKey('dbs.id'), nullable=False)
|
||||
database = relationship(
|
||||
@@ -75,13 +204,16 @@ class Table(Model, Queryable, AuditMixin):
|
||||
|
||||
baselink = "tableview"
|
||||
|
||||
def __repr__(self):
|
||||
return self.table_name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.table_name
|
||||
|
||||
@property
|
||||
def table_link(self):
|
||||
url = "/panoramix/table/{}/".format(self.id)
|
||||
url = "/panoramix/datasource/{self.type}/{self.id}/".format(self=self)
|
||||
return '<a href="{url}">{self.table_name}</a>'.format(**locals())
|
||||
|
||||
@property
|
||||
@@ -99,7 +231,9 @@ class Table(Model, Queryable, AuditMixin):
|
||||
limit_spec=None,
|
||||
filter=None,
|
||||
is_timeseries=True,
|
||||
timeseries_limit=15, row_limit=None):
|
||||
timeseries_limit=15,
|
||||
row_limit=None,
|
||||
extras=None):
|
||||
"""
|
||||
Unused, legacy way of querying by building a SQL string without
|
||||
using the sqlalchemy expression API (new approach which supports
|
||||
@@ -114,7 +248,9 @@ class Table(Model, Queryable, AuditMixin):
|
||||
to_dttm_iso = to_dttm.isoformat()
|
||||
|
||||
if metrics:
|
||||
main_metric_expr = [m.expression for m in self.metrics if m.metric_name == metrics[0]][0]
|
||||
main_metric_expr = [
|
||||
m.expression for m in self.metrics
|
||||
if m.metric_name == metrics[0]][0]
|
||||
else:
|
||||
main_metric_expr = "COUNT(*)"
|
||||
|
||||
@@ -149,29 +285,30 @@ class Table(Model, Queryable, AuditMixin):
|
||||
on_clause = " AND ".join(["{g} = __{g}".format(g=g) for g in groupby])
|
||||
limiting_join = ""
|
||||
if timeseries_limit and groupby:
|
||||
inner_select = ", ".join(["{g} as __{g}".format(g=g) for g in inner_groupby_exprs])
|
||||
inner_select = ", ".join([
|
||||
"{g} as __{g}".format(g=g) for g in inner_groupby_exprs])
|
||||
inner_groupby_exprs = ", ".join(inner_groupby_exprs)
|
||||
limiting_join = (
|
||||
"JOIN ( \n"
|
||||
" SELECT {inner_select} \n"
|
||||
" FROM {self.table_name} \n"
|
||||
" WHERE \n"
|
||||
" {where_clause}\n"
|
||||
" GROUP BY {inner_groupby_exprs}\n"
|
||||
" ORDER BY {main_metric_expr} DESC\n"
|
||||
" LIMIT {timeseries_limit}\n"
|
||||
") z ON {on_clause}\n"
|
||||
"JOIN ( \n"
|
||||
" SELECT {inner_select} \n"
|
||||
" FROM {self.table_name} \n"
|
||||
" WHERE \n"
|
||||
" {where_clause}\n"
|
||||
" GROUP BY {inner_groupby_exprs}\n"
|
||||
" ORDER BY {main_metric_expr} DESC\n"
|
||||
" LIMIT {timeseries_limit}\n"
|
||||
") z ON {on_clause}\n"
|
||||
).format(**locals())
|
||||
|
||||
sql = (
|
||||
"SELECT\n"
|
||||
" {select_exprs}\n"
|
||||
"FROM {self.table_name}\n"
|
||||
"{limiting_join}"
|
||||
"WHERE\n"
|
||||
" {where_clause}\n"
|
||||
"GROUP BY\n"
|
||||
" {groupby_exprs}\n"
|
||||
"SELECT\n"
|
||||
" {select_exprs}\n"
|
||||
"FROM {self.table_name}\n"
|
||||
"{limiting_join}"
|
||||
"WHERE\n"
|
||||
" {where_clause}\n"
|
||||
"GROUP BY\n"
|
||||
" {groupby_exprs}\n"
|
||||
).format(**locals())
|
||||
df = read_sql_query(
|
||||
sql=sql,
|
||||
@@ -189,18 +326,23 @@ class Table(Model, Queryable, AuditMixin):
|
||||
limit_spec=None,
|
||||
filter=None,
|
||||
is_timeseries=True,
|
||||
timeseries_limit=15, row_limit=None):
|
||||
timeseries_limit=15, row_limit=None,
|
||||
extras=None):
|
||||
|
||||
qry_start_dttm = datetime.now()
|
||||
if not self.main_dttm_col:
|
||||
raise Exception(
|
||||
"Datetime column not provided as part table configuration")
|
||||
timestamp = literal_column(
|
||||
self.main_datetime_column.column_name).label('timestamp')
|
||||
self.main_dttm_col).label('timestamp')
|
||||
metrics_exprs = [
|
||||
literal_column(m.expression).label(m.metric_name)
|
||||
for m in self.metrics if m.metric_name in metrics]
|
||||
|
||||
if metrics:
|
||||
main_metric_expr = literal_column(
|
||||
[m.expression for m in self.metrics if m.metric_name == metrics[0]][0])
|
||||
main_metric_expr = literal_column([
|
||||
m.expression for m in self.metrics
|
||||
if m.metric_name == metrics[0]][0])
|
||||
else:
|
||||
main_metric_expr = literal_column("COUNT(*)")
|
||||
|
||||
@@ -210,7 +352,8 @@ class Table(Model, Queryable, AuditMixin):
|
||||
if groupby:
|
||||
select_exprs = [literal_column(s) for s in groupby]
|
||||
groupby_exprs = [literal_column(s) for s in groupby]
|
||||
inner_groupby_exprs = [literal_column(s).label('__' + s) for s in groupby]
|
||||
inner_groupby_exprs = [
|
||||
literal_column(s).label('__' + s) for s in groupby]
|
||||
if granularity != "all":
|
||||
select_exprs += [timestamp]
|
||||
groupby_exprs += [timestamp]
|
||||
@@ -231,6 +374,8 @@ class Table(Model, Queryable, AuditMixin):
|
||||
if op == 'not in':
|
||||
cond = ~cond
|
||||
where_clause_and.append(cond)
|
||||
if extras and 'where' in extras:
|
||||
where_clause_and += [text(extras['where'])]
|
||||
qry = qry.where(and_(*where_clause_and))
|
||||
qry = qry.order_by(desc(main_metric_expr))
|
||||
qry = qry.limit(row_limit)
|
||||
@@ -244,7 +389,8 @@ class Table(Model, Queryable, AuditMixin):
|
||||
subq = subq.limit(timeseries_limit)
|
||||
on_clause = []
|
||||
for gb in groupby:
|
||||
on_clause.append(literal_column(gb)==literal_column("__" + gb))
|
||||
on_clause.append(
|
||||
literal_column(gb) == literal_column("__" + gb))
|
||||
|
||||
from_clause = from_clause.join(subq.alias(), and_(*on_clause))
|
||||
|
||||
@@ -260,8 +406,8 @@ class Table(Model, Queryable, AuditMixin):
|
||||
return QueryResult(
|
||||
df=df, duration=datetime.now() - qry_start_dttm, query=sql)
|
||||
|
||||
|
||||
def fetch_metadata(self):
|
||||
table = self.database.get_table(self.table_name)
|
||||
try:
|
||||
table = self.database.get_table(self.table_name)
|
||||
except Exception as e:
|
||||
@@ -283,8 +429,8 @@ class Table(Model, Queryable, AuditMixin):
|
||||
dbcol = (
|
||||
db.session
|
||||
.query(TC)
|
||||
.filter(TC.table==self)
|
||||
.filter(TC.column_name==col.name)
|
||||
.filter(TC.table == self)
|
||||
.filter(TC.column_name == col.name)
|
||||
.first()
|
||||
)
|
||||
db.session.flush()
|
||||
@@ -296,11 +442,13 @@ class Table(Model, Queryable, AuditMixin):
|
||||
str(datatype).startswith('STRING')):
|
||||
dbcol.groupby = True
|
||||
dbcol.filterable = True
|
||||
elif str(datatype).upper() in ('DOUBLE', 'FLOAT', 'INT', 'BIGINT'):
|
||||
dbcol.sum = True
|
||||
db.session.merge(self)
|
||||
self.columns.append(dbcol)
|
||||
|
||||
if not any_date_col and 'date' in datatype.lower():
|
||||
any_date_col = dbcol
|
||||
any_date_col = col.name
|
||||
|
||||
if dbcol.sum:
|
||||
metrics.append(M(
|
||||
@@ -343,34 +491,32 @@ class Table(Model, Queryable, AuditMixin):
|
||||
for metric in metrics:
|
||||
m = (
|
||||
db.session.query(M)
|
||||
.filter(M.metric_name==metric.metric_name)
|
||||
.filter(M.table==self)
|
||||
.filter(M.metric_name == metric.metric_name)
|
||||
.filter(M.table_id == self.id)
|
||||
.first()
|
||||
)
|
||||
metric.table = self
|
||||
metric.table_id = self.id
|
||||
if not m:
|
||||
db.session.add(metric)
|
||||
db.session.commit()
|
||||
if not self.main_datetime_column:
|
||||
self.main_datetime_column = any_date_col
|
||||
if not self.main_dttm_col:
|
||||
self.main_dttm_col = any_date_col
|
||||
|
||||
|
||||
|
||||
|
||||
class SqlMetric(Model, AuditMixin):
|
||||
class SqlMetric(Model, AuditMixinNullable):
|
||||
__tablename__ = 'sql_metrics'
|
||||
id = Column(Integer, primary_key=True)
|
||||
metric_name = Column(String(512))
|
||||
verbose_name = Column(String(1024))
|
||||
metric_type = Column(String(32))
|
||||
table_id = Column(Integer,ForeignKey('tables.id'))
|
||||
table_id = Column(Integer, ForeignKey('tables.id'))
|
||||
table = relationship(
|
||||
'Table', backref='metrics', foreign_keys=[table_id])
|
||||
expression = Column(Text)
|
||||
description = Column(Text)
|
||||
|
||||
|
||||
class TableColumn(Model, AuditMixin):
|
||||
class TableColumn(Model, AuditMixinNullable):
|
||||
__tablename__ = 'table_columns'
|
||||
id = Column(Integer, primary_key=True)
|
||||
table_id = Column(Integer, ForeignKey('tables.id'))
|
||||
@@ -390,10 +536,15 @@ class TableColumn(Model, AuditMixin):
|
||||
def __repr__(self):
|
||||
return self.column_name
|
||||
|
||||
class Cluster(Model, AuditMixin):
|
||||
@property
|
||||
def isnum(self):
|
||||
return self.type in ('LONG', 'DOUBLE', 'FLOAT')
|
||||
|
||||
|
||||
class Cluster(Model, AuditMixinNullable):
|
||||
__tablename__ = 'clusters'
|
||||
id = Column(Integer, primary_key=True)
|
||||
cluster_name = Column(String(255), unique=True)
|
||||
cluster_name = Column(String(250), unique=True)
|
||||
coordinator_host = Column(String(256))
|
||||
coordinator_port = Column(Integer)
|
||||
coordinator_endpoint = Column(String(256))
|
||||
@@ -423,21 +574,23 @@ class Cluster(Model, AuditMixin):
|
||||
|
||||
|
||||
class Datasource(Model, AuditMixin, Queryable):
|
||||
type = "druid"
|
||||
|
||||
baselink = "datasourcemodelview"
|
||||
|
||||
__tablename__ = 'datasources'
|
||||
id = Column(Integer, primary_key=True)
|
||||
datasource_name = Column(String(255), unique=True)
|
||||
datasource_name = Column(String(250), unique=True)
|
||||
is_featured = Column(Boolean, default=False)
|
||||
is_hidden = Column(Boolean, default=False)
|
||||
description = Column(Text)
|
||||
default_endpoint = Column(Text)
|
||||
user_id = Column(Integer, ForeignKey('ab_user.id'))
|
||||
owner = relationship('User', backref='datasources', foreign_keys=[user_id])
|
||||
cluster_name = Column(String(255),
|
||||
ForeignKey('clusters.cluster_name'))
|
||||
cluster = relationship('Cluster', backref='datasources', foreign_keys=[cluster_name])
|
||||
cluster_name = Column(
|
||||
String(250), ForeignKey('clusters.cluster_name'))
|
||||
cluster = relationship(
|
||||
'Cluster', backref='datasources', foreign_keys=[cluster_name])
|
||||
|
||||
@property
|
||||
def metrics_combo(self):
|
||||
@@ -454,7 +607,9 @@ class Datasource(Model, AuditMixin, Queryable):
|
||||
|
||||
@property
|
||||
def datasource_link(self):
|
||||
url = "/panoramix/datasource/{}/".format(self.datasource_name)
|
||||
url = (
|
||||
"/panoramix/datasource/"
|
||||
"{self.type}/{self.id}/").format(self=self)
|
||||
return '<a href="{url}">{self.datasource_name}</a>'.format(**locals())
|
||||
|
||||
def get_metric_obj(self, metric_name):
|
||||
@@ -512,7 +667,6 @@ class Datasource(Model, AuditMixin, Queryable):
|
||||
col_obj.type = cols[col]['type']
|
||||
col_obj.datasource = datasource
|
||||
col_obj.generate_metrics()
|
||||
#session.commit()
|
||||
|
||||
def query(
|
||||
self, groupby, metrics,
|
||||
@@ -522,9 +676,14 @@ class Datasource(Model, AuditMixin, Queryable):
|
||||
filter=None,
|
||||
is_timeseries=True,
|
||||
timeseries_limit=None,
|
||||
row_limit=None):
|
||||
row_limit=None,
|
||||
extras=None):
|
||||
qry_start_dttm = datetime.now()
|
||||
|
||||
# add tzinfo to native datetime with config
|
||||
from_dttm = from_dttm.replace(tzinfo=config.get("DRUID_TZ"))
|
||||
to_dttm = to_dttm.replace(tzinfo=config.get("DRUID_TZ"))
|
||||
|
||||
query_str = ""
|
||||
aggregations = {
|
||||
m.metric_name: m.json_obj
|
||||
@@ -538,25 +697,25 @@ class Datasource(Model, AuditMixin, Queryable):
|
||||
dimensions=groupby,
|
||||
aggregations=aggregations,
|
||||
granularity=granularity,
|
||||
intervals= from_dttm.isoformat() + '/' + to_dttm.isoformat(),
|
||||
intervals=from_dttm.isoformat() + '/' + to_dttm.isoformat(),
|
||||
)
|
||||
filters = None
|
||||
for col, op, eq in filter:
|
||||
cond = None
|
||||
if op == '==':
|
||||
cond = Dimension(col)==eq
|
||||
cond = Dimension(col) == eq
|
||||
elif op == '!=':
|
||||
cond = ~(Dimension(col)==eq)
|
||||
cond = ~(Dimension(col) == eq)
|
||||
elif op in ('in', 'not in'):
|
||||
fields = []
|
||||
splitted = eq.split(',')
|
||||
if len(splitted) > 1:
|
||||
for s in eq.split(','):
|
||||
s = s.strip()
|
||||
fields.append(Filter.build_filter(Dimension(col)==s))
|
||||
fields.append(Filter.build_filter(Dimension(col) == s))
|
||||
cond = Filter(type="or", fields=fields)
|
||||
else:
|
||||
cond = Dimension(col)==eq
|
||||
cond = Dimension(col) == eq
|
||||
if op == 'not in':
|
||||
cond = ~cond
|
||||
if filters:
|
||||
@@ -589,7 +748,7 @@ class Datasource(Model, AuditMixin, Queryable):
|
||||
query_str += json.dumps(client.query_dict, indent=2) + "\n"
|
||||
query_str += "//\nPhase 2 (built based on phase one's results)\n"
|
||||
df = client.export_pandas()
|
||||
if not df is None and not df.empty:
|
||||
if df is not None and not df.empty:
|
||||
dims = qry['dimensions']
|
||||
filters = []
|
||||
for index, row in df.iterrows():
|
||||
@@ -630,7 +789,6 @@ class Datasource(Model, AuditMixin, Queryable):
|
||||
duration=datetime.now() - qry_start_dttm)
|
||||
|
||||
|
||||
#class Metric(Model, AuditMixin):
|
||||
class Metric(Model):
|
||||
__tablename__ = 'metrics'
|
||||
id = Column(Integer, primary_key=True)
|
||||
@@ -638,7 +796,7 @@ class Metric(Model):
|
||||
verbose_name = Column(String(1024))
|
||||
metric_type = Column(String(32))
|
||||
datasource_name = Column(
|
||||
String(256),
|
||||
String(250),
|
||||
ForeignKey('datasources.datasource_name'))
|
||||
datasource = relationship('Datasource', backref='metrics')
|
||||
json = Column(Text)
|
||||
@@ -648,16 +806,16 @@ class Metric(Model):
|
||||
def json_obj(self):
|
||||
try:
|
||||
obj = json.loads(self.json)
|
||||
except Exception as e:
|
||||
except:
|
||||
obj = {}
|
||||
return obj
|
||||
|
||||
|
||||
class Column(Model, AuditMixin):
|
||||
class Column(Model, AuditMixinNullable):
|
||||
__tablename__ = 'columns'
|
||||
id = Column(Integer, primary_key=True)
|
||||
datasource_name = Column(
|
||||
String(256),
|
||||
String(250),
|
||||
ForeignKey('datasources.datasource_name'))
|
||||
datasource = relationship('Datasource', backref='columns')
|
||||
column_name = Column(String(256))
|
||||
@@ -688,11 +846,14 @@ class Column(Model, AuditMixin):
|
||||
json=json.dumps({'type': 'count', 'name': 'count'})
|
||||
))
|
||||
# Somehow we need to reassign this for UDAFs
|
||||
corrected_type = 'DOUBLE' if self.type in ('DOUBLE', 'FLOAT') else self.type
|
||||
if self.type in ('DOUBLE', 'FLOAT'):
|
||||
corrected_type = 'DOUBLE'
|
||||
else:
|
||||
corrected_type = self.type
|
||||
|
||||
if self.sum and self.isnum:
|
||||
mt = corrected_type.lower() + 'Sum'
|
||||
name='sum__' + self.column_name
|
||||
name = 'sum__' + self.column_name
|
||||
metrics.append(Metric(
|
||||
metric_name=name,
|
||||
metric_type='sum',
|
||||
@@ -702,7 +863,7 @@ class Column(Model, AuditMixin):
|
||||
))
|
||||
if self.min and self.isnum:
|
||||
mt = corrected_type.lower() + 'Min'
|
||||
name='min__' + self.column_name
|
||||
name = 'min__' + self.column_name
|
||||
metrics.append(Metric(
|
||||
metric_name=name,
|
||||
metric_type='min',
|
||||
@@ -712,7 +873,7 @@ class Column(Model, AuditMixin):
|
||||
))
|
||||
if self.max and self.isnum:
|
||||
mt = corrected_type.lower() + 'Max'
|
||||
name='max__' + self.column_name
|
||||
name = 'max__' + self.column_name
|
||||
metrics.append(Metric(
|
||||
metric_name=name,
|
||||
metric_type='max',
|
||||
@@ -722,7 +883,7 @@ class Column(Model, AuditMixin):
|
||||
))
|
||||
if self.count_distinct:
|
||||
mt = 'count_distinct'
|
||||
name='count_distinct__' + self.column_name
|
||||
name = 'count_distinct__' + self.column_name
|
||||
metrics.append(Metric(
|
||||
metric_name=name,
|
||||
verbose_name='COUNT(DISTINCT {})'.format(self.column_name),
|
||||
@@ -736,9 +897,9 @@ class Column(Model, AuditMixin):
|
||||
for metric in metrics:
|
||||
m = (
|
||||
session.query(M)
|
||||
.filter(M.metric_name==metric.metric_name)
|
||||
.filter(M.datasource_name==self.datasource_name)
|
||||
.filter(Cluster.cluster_name==self.datasource.cluster_name)
|
||||
.filter(M.metric_name == metric.metric_name)
|
||||
.filter(M.datasource_name == self.datasource_name)
|
||||
.filter(Cluster.cluster_name == self.datasource.cluster_name)
|
||||
.first()
|
||||
)
|
||||
metric.datasource_name = self.datasource_name
|
||||
|
||||
BIN
panoramix/static/bubble.png
Normal file
|
After Width: | Height: | Size: 459 KiB |
BIN
panoramix/static/cardash.jpg
Normal file
|
After Width: | Height: | Size: 236 KiB |
BIN
panoramix/static/cloud.png
Normal file
|
After Width: | Height: | Size: 702 KiB |
387
panoramix/static/d3.layout.cloud.js
Normal file
@@ -0,0 +1,387 @@
|
||||
// Word cloud layout by Jason Davies, http://www.jasondavies.com/word-cloud/
|
||||
// Algorithm due to Jonathan Feinberg, http://static.mrfeinberg.com/bv_ch03.pdf
|
||||
(function() {
|
||||
|
||||
if (typeof define === "function" && define.amd) define(["d3"], cloud);
|
||||
else cloud(this.d3);
|
||||
|
||||
function cloud(d3) {
|
||||
d3.layout.cloud = function cloud() {
|
||||
var size = [256, 256],
|
||||
text = cloudText,
|
||||
font = cloudFont,
|
||||
fontSize = cloudFontSize,
|
||||
fontStyle = cloudFontNormal,
|
||||
fontWeight = cloudFontNormal,
|
||||
rotate = cloudRotate,
|
||||
padding = cloudPadding,
|
||||
spiral = archimedeanSpiral,
|
||||
words = [],
|
||||
timeInterval = Infinity,
|
||||
event = d3.dispatch("word", "end"),
|
||||
timer = null,
|
||||
random = Math.random,
|
||||
cloud = {};
|
||||
|
||||
cloud.start = function() {
|
||||
var board = zeroArray((size[0] >> 5) * size[1]),
|
||||
bounds = null,
|
||||
n = words.length,
|
||||
i = -1,
|
||||
tags = [],
|
||||
data = words.map(function(d, i) {
|
||||
d.text = text.call(this, d, i);
|
||||
d.font = font.call(this, d, i);
|
||||
d.style = fontStyle.call(this, d, i);
|
||||
d.weight = fontWeight.call(this, d, i);
|
||||
d.rotate = rotate.call(this, d, i);
|
||||
d.size = ~~fontSize.call(this, d, i);
|
||||
d.padding = padding.call(this, d, i);
|
||||
return d;
|
||||
}).sort(function(a, b) { return b.size - a.size; });
|
||||
|
||||
if (timer) clearInterval(timer);
|
||||
timer = setInterval(step, 0);
|
||||
step();
|
||||
|
||||
return cloud;
|
||||
|
||||
function step() {
|
||||
var start = Date.now();
|
||||
while (Date.now() - start < timeInterval && ++i < n && timer) {
|
||||
var d = data[i];
|
||||
d.x = (size[0] * (random() + .5)) >> 1;
|
||||
d.y = (size[1] * (random() + .5)) >> 1;
|
||||
cloudSprite(d, data, i);
|
||||
if (d.hasText && place(board, d, bounds)) {
|
||||
tags.push(d);
|
||||
event.word(d);
|
||||
if (bounds) cloudBounds(bounds, d);
|
||||
else bounds = [{x: d.x + d.x0, y: d.y + d.y0}, {x: d.x + d.x1, y: d.y + d.y1}];
|
||||
// Temporary hack
|
||||
d.x -= size[0] >> 1;
|
||||
d.y -= size[1] >> 1;
|
||||
}
|
||||
}
|
||||
if (i >= n) {
|
||||
cloud.stop();
|
||||
event.end(tags, bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cloud.stop = function() {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
return cloud;
|
||||
};
|
||||
|
||||
function place(board, tag, bounds) {
|
||||
var perimeter = [{x: 0, y: 0}, {x: size[0], y: size[1]}],
|
||||
startX = tag.x,
|
||||
startY = tag.y,
|
||||
maxDelta = Math.sqrt(size[0] * size[0] + size[1] * size[1]),
|
||||
s = spiral(size),
|
||||
dt = random() < .5 ? 1 : -1,
|
||||
t = -dt,
|
||||
dxdy,
|
||||
dx,
|
||||
dy;
|
||||
|
||||
while (dxdy = s(t += dt)) {
|
||||
dx = ~~dxdy[0];
|
||||
dy = ~~dxdy[1];
|
||||
|
||||
if (Math.min(Math.abs(dx), Math.abs(dy)) >= maxDelta) break;
|
||||
|
||||
tag.x = startX + dx;
|
||||
tag.y = startY + dy;
|
||||
|
||||
if (tag.x + tag.x0 < 0 || tag.y + tag.y0 < 0 ||
|
||||
tag.x + tag.x1 > size[0] || tag.y + tag.y1 > size[1]) continue;
|
||||
// TODO only check for collisions within current bounds.
|
||||
if (!bounds || !cloudCollide(tag, board, size[0])) {
|
||||
if (!bounds || collideRects(tag, bounds)) {
|
||||
var sprite = tag.sprite,
|
||||
w = tag.width >> 5,
|
||||
sw = size[0] >> 5,
|
||||
lx = tag.x - (w << 4),
|
||||
sx = lx & 0x7f,
|
||||
msx = 32 - sx,
|
||||
h = tag.y1 - tag.y0,
|
||||
x = (tag.y + tag.y0) * sw + (lx >> 5),
|
||||
last;
|
||||
for (var j = 0; j < h; j++) {
|
||||
last = 0;
|
||||
for (var i = 0; i <= w; i++) {
|
||||
board[x + i] |= (last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0);
|
||||
}
|
||||
x += sw;
|
||||
}
|
||||
delete tag.sprite;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
cloud.timeInterval = function(_) {
|
||||
return arguments.length ? (timeInterval = _ == null ? Infinity : _, cloud) : timeInterval;
|
||||
};
|
||||
|
||||
cloud.words = function(_) {
|
||||
return arguments.length ? (words = _, cloud) : words;
|
||||
};
|
||||
|
||||
cloud.size = function(_) {
|
||||
return arguments.length ? (size = [+_[0], +_[1]], cloud) : size;
|
||||
};
|
||||
|
||||
cloud.font = function(_) {
|
||||
return arguments.length ? (font = d3.functor(_), cloud) : font;
|
||||
};
|
||||
|
||||
cloud.fontStyle = function(_) {
|
||||
return arguments.length ? (fontStyle = d3.functor(_), cloud) : fontStyle;
|
||||
};
|
||||
|
||||
cloud.fontWeight = function(_) {
|
||||
return arguments.length ? (fontWeight = d3.functor(_), cloud) : fontWeight;
|
||||
};
|
||||
|
||||
cloud.rotate = function(_) {
|
||||
return arguments.length ? (rotate = d3.functor(_), cloud) : rotate;
|
||||
};
|
||||
|
||||
cloud.text = function(_) {
|
||||
return arguments.length ? (text = d3.functor(_), cloud) : text;
|
||||
};
|
||||
|
||||
cloud.spiral = function(_) {
|
||||
return arguments.length ? (spiral = spirals[_] || _, cloud) : spiral;
|
||||
};
|
||||
|
||||
cloud.fontSize = function(_) {
|
||||
return arguments.length ? (fontSize = d3.functor(_), cloud) : fontSize;
|
||||
};
|
||||
|
||||
cloud.padding = function(_) {
|
||||
return arguments.length ? (padding = d3.functor(_), cloud) : padding;
|
||||
};
|
||||
|
||||
cloud.random = function(_) {
|
||||
return arguments.length ? (random = _, cloud) : random;
|
||||
};
|
||||
|
||||
return d3.rebind(cloud, event, "on");
|
||||
};
|
||||
|
||||
function cloudText(d) {
|
||||
return d.text;
|
||||
}
|
||||
|
||||
function cloudFont() {
|
||||
return "serif";
|
||||
}
|
||||
|
||||
function cloudFontNormal() {
|
||||
return "normal";
|
||||
}
|
||||
|
||||
function cloudFontSize(d) {
|
||||
return Math.sqrt(d.value);
|
||||
}
|
||||
|
||||
function cloudRotate() {
|
||||
return (~~(Math.random() * 6) - 3) * 30;
|
||||
}
|
||||
|
||||
function cloudPadding() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Fetches a monochrome sprite bitmap for the specified text.
|
||||
// Load in batches for speed.
|
||||
function cloudSprite(d, data, di) {
|
||||
if (d.sprite) return;
|
||||
c.clearRect(0, 0, (cw << 5) / ratio, ch / ratio);
|
||||
var x = 0,
|
||||
y = 0,
|
||||
maxh = 0,
|
||||
n = data.length;
|
||||
--di;
|
||||
while (++di < n) {
|
||||
d = data[di];
|
||||
c.save();
|
||||
c.font = d.style + " " + d.weight + " " + ~~((d.size + 1) / ratio) + "px " + d.font;
|
||||
var w = c.measureText(d.text + "m").width * ratio,
|
||||
h = d.size << 1;
|
||||
if (d.rotate) {
|
||||
var sr = Math.sin(d.rotate * cloudRadians),
|
||||
cr = Math.cos(d.rotate * cloudRadians),
|
||||
wcr = w * cr,
|
||||
wsr = w * sr,
|
||||
hcr = h * cr,
|
||||
hsr = h * sr;
|
||||
w = (Math.max(Math.abs(wcr + hsr), Math.abs(wcr - hsr)) + 0x1f) >> 5 << 5;
|
||||
h = ~~Math.max(Math.abs(wsr + hcr), Math.abs(wsr - hcr));
|
||||
} else {
|
||||
w = (w + 0x1f) >> 5 << 5;
|
||||
}
|
||||
if (h > maxh) maxh = h;
|
||||
if (x + w >= (cw << 5)) {
|
||||
x = 0;
|
||||
y += maxh;
|
||||
maxh = 0;
|
||||
}
|
||||
if (y + h >= ch) break;
|
||||
c.translate((x + (w >> 1)) / ratio, (y + (h >> 1)) / ratio);
|
||||
if (d.rotate) c.rotate(d.rotate * cloudRadians);
|
||||
c.fillText(d.text, 0, 0);
|
||||
if (d.padding) c.lineWidth = 2 * d.padding, c.strokeText(d.text, 0, 0);
|
||||
c.restore();
|
||||
d.width = w;
|
||||
d.height = h;
|
||||
d.xoff = x;
|
||||
d.yoff = y;
|
||||
d.x1 = w >> 1;
|
||||
d.y1 = h >> 1;
|
||||
d.x0 = -d.x1;
|
||||
d.y0 = -d.y1;
|
||||
d.hasText = true;
|
||||
x += w;
|
||||
}
|
||||
var pixels = c.getImageData(0, 0, (cw << 5) / ratio, ch / ratio).data,
|
||||
sprite = [];
|
||||
while (--di >= 0) {
|
||||
d = data[di];
|
||||
if (!d.hasText) continue;
|
||||
var w = d.width,
|
||||
w32 = w >> 5,
|
||||
h = d.y1 - d.y0;
|
||||
// Zero the buffer
|
||||
for (var i = 0; i < h * w32; i++) sprite[i] = 0;
|
||||
x = d.xoff;
|
||||
if (x == null) return;
|
||||
y = d.yoff;
|
||||
var seen = 0,
|
||||
seenRow = -1;
|
||||
for (var j = 0; j < h; j++) {
|
||||
for (var i = 0; i < w; i++) {
|
||||
var k = w32 * j + (i >> 5),
|
||||
m = pixels[((y + j) * (cw << 5) + (x + i)) << 2] ? 1 << (31 - (i % 32)) : 0;
|
||||
sprite[k] |= m;
|
||||
seen |= m;
|
||||
}
|
||||
if (seen) seenRow = j;
|
||||
else {
|
||||
d.y0++;
|
||||
h--;
|
||||
j--;
|
||||
y++;
|
||||
}
|
||||
}
|
||||
d.y1 = d.y0 + seenRow;
|
||||
d.sprite = sprite.slice(0, (d.y1 - d.y0) * w32);
|
||||
}
|
||||
}
|
||||
|
||||
// Use mask-based collision detection.
|
||||
function cloudCollide(tag, board, sw) {
|
||||
sw >>= 5;
|
||||
var sprite = tag.sprite,
|
||||
w = tag.width >> 5,
|
||||
lx = tag.x - (w << 4),
|
||||
sx = lx & 0x7f,
|
||||
msx = 32 - sx,
|
||||
h = tag.y1 - tag.y0,
|
||||
x = (tag.y + tag.y0) * sw + (lx >> 5),
|
||||
last;
|
||||
for (var j = 0; j < h; j++) {
|
||||
last = 0;
|
||||
for (var i = 0; i <= w; i++) {
|
||||
if (((last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0))
|
||||
& board[x + i]) return true;
|
||||
}
|
||||
x += sw;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function cloudBounds(bounds, d) {
|
||||
var b0 = bounds[0],
|
||||
b1 = bounds[1];
|
||||
if (d.x + d.x0 < b0.x) b0.x = d.x + d.x0;
|
||||
if (d.y + d.y0 < b0.y) b0.y = d.y + d.y0;
|
||||
if (d.x + d.x1 > b1.x) b1.x = d.x + d.x1;
|
||||
if (d.y + d.y1 > b1.y) b1.y = d.y + d.y1;
|
||||
}
|
||||
|
||||
function collideRects(a, b) {
|
||||
return a.x + a.x1 > b[0].x && a.x + a.x0 < b[1].x && a.y + a.y1 > b[0].y && a.y + a.y0 < b[1].y;
|
||||
}
|
||||
|
||||
function archimedeanSpiral(size) {
|
||||
var e = size[0] / size[1];
|
||||
return function(t) {
|
||||
return [e * (t *= .1) * Math.cos(t), t * Math.sin(t)];
|
||||
};
|
||||
}
|
||||
|
||||
function rectangularSpiral(size) {
|
||||
var dy = 4,
|
||||
dx = dy * size[0] / size[1],
|
||||
x = 0,
|
||||
y = 0;
|
||||
return function(t) {
|
||||
var sign = t < 0 ? -1 : 1;
|
||||
// See triangular numbers: T_n = n * (n + 1) / 2.
|
||||
switch ((Math.sqrt(1 + 4 * sign * t) - sign) & 3) {
|
||||
case 0: x += dx; break;
|
||||
case 1: y += dy; break;
|
||||
case 2: x -= dx; break;
|
||||
default: y -= dy; break;
|
||||
}
|
||||
return [x, y];
|
||||
};
|
||||
}
|
||||
|
||||
// TODO reuse arrays?
|
||||
function zeroArray(n) {
|
||||
var a = [],
|
||||
i = -1;
|
||||
while (++i < n) a[i] = 0;
|
||||
return a;
|
||||
}
|
||||
|
||||
var cloudRadians = Math.PI / 180,
|
||||
cw = 1 << 11 >> 5,
|
||||
ch = 1 << 11,
|
||||
canvas,
|
||||
ratio = 1;
|
||||
|
||||
if (typeof document !== "undefined") {
|
||||
canvas = document.createElement("canvas");
|
||||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
ratio = Math.sqrt(canvas.getContext("2d").getImageData(0, 0, 1, 1).data.length >> 2);
|
||||
canvas.width = (cw << 5) / ratio;
|
||||
canvas.height = ch / ratio;
|
||||
} else {
|
||||
// Attempt to use node-canvas.
|
||||
canvas = new Canvas(cw << 5, ch);
|
||||
}
|
||||
|
||||
var c = canvas.getContext("2d"),
|
||||
spirals = {
|
||||
archimedean: archimedeanSpiral,
|
||||
rectangular: rectangularSpiral
|
||||
};
|
||||
c.fillStyle = c.strokeStyle = "red";
|
||||
c.textAlign = "center";
|
||||
}
|
||||
|
||||
})();
|
||||
BIN
panoramix/static/dash.png
Normal file
|
After Width: | Height: | Size: 328 KiB |
BIN
panoramix/static/gallery.jpg
Normal file
|
After Width: | Height: | Size: 93 KiB |
2
panoramix/static/jquery.gridster.min.css
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/*! gridster.js - v0.5.6 - 2014-09-25 - * http://gridster.net/ - Copyright (c) 2014 ducksboard; Licensed MIT */
|
||||
.gridster{position:relative}.gridster>*{margin:0 auto;-webkit-transition:height .4s,width .4s;-moz-transition:height .4s,width .4s;-o-transition:height .4s,width .4s;-ms-transition:height .4s,width .4s;transition:height .4s,width .4s}.gridster .gs-w{z-index:2;position:absolute}.ready .gs-w:not(.preview-holder){-webkit-transition:opacity .3s,left .3s,top .3s;-moz-transition:opacity .3s,left .3s,top .3s;-o-transition:opacity .3s,left .3s,top .3s;transition:opacity .3s,left .3s,top .3s}.ready .gs-w:not(.preview-holder),.ready .resize-preview-holder{-webkit-transition:opacity .3s,left .3s,top .3s,width .3s,height .3s;-moz-transition:opacity .3s,left .3s,top .3s,width .3s,height .3s;-o-transition:opacity .3s,left .3s,top .3s,width .3s,height .3s;transition:opacity .3s,left .3s,top .3s,width .3s,height .3s}.gridster .preview-holder{z-index:1;position:absolute;background-color:#fff;border-color:#fff;opacity:.3}.gridster .player-revert{z-index:10!important;-webkit-transition:left .3s,top .3s!important;-moz-transition:left .3s,top .3s!important;-o-transition:left .3s,top .3s!important;transition:left .3s,top .3s!important}.gridster .dragging,.gridster .resizing{z-index:10!important;-webkit-transition:all 0s!important;-moz-transition:all 0s!important;-o-transition:all 0s!important;transition:all 0s!important}.gs-resize-handle{position:absolute;z-index:1}.gs-resize-handle-both{width:20px;height:20px;bottom:-8px;right:-8px;background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pg08IS0tIEdlbmVyYXRvcjogQWRvYmUgRmlyZXdvcmtzIENTNiwgRXhwb3J0IFNWRyBFeHRlbnNpb24gYnkgQWFyb24gQmVhbGwgKGh0dHA6Ly9maXJld29ya3MuYWJlYWxsLmNvbSkgLiBWZXJzaW9uOiAwLjYuMSAgLS0+DTwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DTxzdmcgaWQ9IlVudGl0bGVkLVBhZ2UlMjAxIiB2aWV3Qm94PSIwIDAgNiA2IiBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjojZmZmZmZmMDAiIHZlcnNpb249IjEuMSINCXhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiDQl4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjZweCIgaGVpZ2h0PSI2cHgiDT4NCTxnIG9wYWNpdHk9IjAuMzAyIj4NCQk8cGF0aCBkPSJNIDYgNiBMIDAgNiBMIDAgNC4yIEwgNCA0LjIgTCA0LjIgNC4yIEwgNC4yIDAgTCA2IDAgTCA2IDYgTCA2IDYgWiIgZmlsbD0iIzAwMDAwMCIvPg0JPC9nPg08L3N2Zz4=);background-position:top left;background-repeat:no-repeat;cursor:se-resize;z-index:20}.gs-resize-handle-x{top:0;bottom:13px;right:-5px;width:10px;cursor:e-resize}.gs-resize-handle-y{left:0;right:13px;bottom:-5px;height:10px;cursor:s-resize}.gs-w:hover .gs-resize-handle,.resizing .gs-resize-handle{opacity:1}.gs-resize-handle,.gs-w.dragging .gs-resize-handle{opacity:0}.gs-resize-disabled .gs-resize-handle{display:none!important}[data-max-sizex="1"] .gs-resize-handle-x,[data-max-sizey="1"] .gs-resize-handle-y,[data-max-sizey="1"][data-max-sizex="1"] .gs-resize-handle{display:none!important}
|
||||
2
panoramix/static/jquery.gridster.with-extras.min.js
vendored
Normal file
BIN
panoramix/static/loading.gif
Normal file
|
After Width: | Height: | Size: 16 KiB |
643
panoramix/static/nv.d3.css
Normal file
@@ -0,0 +1,643 @@
|
||||
/* nvd3 version 1.8.1 (https://github.com/novus/nvd3) 2015-06-15 */
|
||||
.nvd3 .nv-axis {
|
||||
pointer-events:none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nvd3 .nv-axis path {
|
||||
fill: none;
|
||||
stroke: #000;
|
||||
stroke-opacity: .75;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.nvd3 .nv-axis path.domain {
|
||||
stroke-opacity: .75;
|
||||
}
|
||||
|
||||
.nvd3 .nv-axis.nv-x path.domain {
|
||||
stroke-opacity: 0;
|
||||
}
|
||||
|
||||
.nvd3 .nv-axis line {
|
||||
fill: none;
|
||||
stroke: #e5e5e5;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.nvd3 .nv-axis .zero line,
|
||||
/*this selector may not be necessary*/ .nvd3 .nv-axis line.zero {
|
||||
stroke-opacity: .75;
|
||||
}
|
||||
|
||||
.nvd3 .nv-axis .nv-axisMaxMin text {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nvd3 .x .nv-axis .nv-axisMaxMin text,
|
||||
.nvd3 .x2 .nv-axis .nv-axisMaxMin text,
|
||||
.nvd3 .x3 .nv-axis .nv-axisMaxMin text {
|
||||
text-anchor: middle
|
||||
}
|
||||
|
||||
.nvd3 .nv-axis.nv-disabled {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.nvd3 .nv-bars rect {
|
||||
fill-opacity: .75;
|
||||
|
||||
transition: fill-opacity 250ms linear;
|
||||
-moz-transition: fill-opacity 250ms linear;
|
||||
-webkit-transition: fill-opacity 250ms linear;
|
||||
}
|
||||
|
||||
.nvd3 .nv-bars rect.hover {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
|
||||
.nvd3 .nv-bars .hover rect {
|
||||
fill: lightblue;
|
||||
}
|
||||
|
||||
.nvd3 .nv-bars text {
|
||||
fill: rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
.nvd3 .nv-bars .hover text {
|
||||
fill: rgba(0,0,0,1);
|
||||
}
|
||||
|
||||
.nvd3 .nv-multibar .nv-groups rect,
|
||||
.nvd3 .nv-multibarHorizontal .nv-groups rect,
|
||||
.nvd3 .nv-discretebar .nv-groups rect {
|
||||
stroke-opacity: 0;
|
||||
|
||||
transition: fill-opacity 250ms linear;
|
||||
-moz-transition: fill-opacity 250ms linear;
|
||||
-webkit-transition: fill-opacity 250ms linear;
|
||||
}
|
||||
|
||||
.nvd3 .nv-multibar .nv-groups rect:hover,
|
||||
.nvd3 .nv-multibarHorizontal .nv-groups rect:hover,
|
||||
.nvd3 .nv-candlestickBar .nv-ticks rect:hover,
|
||||
.nvd3 .nv-discretebar .nv-groups rect:hover {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
|
||||
.nvd3 .nv-discretebar .nv-groups text,
|
||||
.nvd3 .nv-multibarHorizontal .nv-groups text {
|
||||
font-weight: bold;
|
||||
fill: rgba(0,0,0,1);
|
||||
stroke: rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
/* boxplot CSS */
|
||||
.nvd3 .nv-boxplot circle {
|
||||
fill-opacity: 0.5;
|
||||
}
|
||||
|
||||
.nvd3 .nv-boxplot circle:hover {
|
||||
stroke: #AAA;
|
||||
}
|
||||
|
||||
.nvd3 .nv-boxplot rect:hover {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
|
||||
.nvd3 line.nv-boxplot-median {
|
||||
stroke: black;
|
||||
}
|
||||
|
||||
.nv-boxplot-tick:hover {
|
||||
stroke-width: 2.5px;
|
||||
}
|
||||
/* bullet */
|
||||
.nvd3.nv-bullet { font: 10px sans-serif; }
|
||||
.nvd3.nv-bullet .nv-measure { fill-opacity: .8; }
|
||||
.nvd3.nv-bullet .nv-measure:hover { fill-opacity: 1; }
|
||||
.nvd3.nv-bullet .nv-marker { stroke: #000; stroke-width: 2px; }
|
||||
.nvd3.nv-bullet .nv-markerTriangle { stroke: #000; fill: #fff; stroke-width: 1.5px; }
|
||||
.nvd3.nv-bullet .nv-tick line { stroke: #666; stroke-width: .5px; }
|
||||
.nvd3.nv-bullet .nv-range.nv-s0 { fill: #eee; }
|
||||
.nvd3.nv-bullet .nv-range.nv-s1 { fill: #ddd; }
|
||||
.nvd3.nv-bullet .nv-range.nv-s2 { fill: #ccc; }
|
||||
.nvd3.nv-bullet .nv-title { font-size: 14px; font-weight: bold; }
|
||||
.nvd3.nv-bullet .nv-subtitle { fill: #999; }
|
||||
|
||||
|
||||
.nvd3.nv-bullet .nv-range {
|
||||
fill: #bababa;
|
||||
fill-opacity: .4;
|
||||
}
|
||||
.nvd3.nv-bullet .nv-range:hover {
|
||||
fill-opacity: .7;
|
||||
}
|
||||
|
||||
.nvd3.nv-candlestickBar .nv-ticks .nv-tick {
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.nvd3.nv-candlestickBar .nv-ticks .nv-tick.hover {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.nvd3.nv-candlestickBar .nv-ticks .nv-tick.positive rect {
|
||||
stroke: #2ca02c;
|
||||
fill: #2ca02c;
|
||||
}
|
||||
|
||||
.nvd3.nv-candlestickBar .nv-ticks .nv-tick.negative rect {
|
||||
stroke: #d62728;
|
||||
fill: #d62728;
|
||||
}
|
||||
|
||||
.with-transitions .nv-candlestickBar .nv-ticks .nv-tick {
|
||||
transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
|
||||
-moz-transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
|
||||
-webkit-transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
|
||||
|
||||
}
|
||||
|
||||
.nvd3.nv-candlestickBar .nv-ticks line {
|
||||
stroke: #333;
|
||||
}
|
||||
|
||||
|
||||
.nvd3 .nv-legend .nv-disabled rect {
|
||||
/*fill-opacity: 0;*/
|
||||
}
|
||||
|
||||
.nvd3 .nv-check-box .nv-box {
|
||||
fill-opacity:0;
|
||||
stroke-width:2;
|
||||
}
|
||||
|
||||
.nvd3 .nv-check-box .nv-check {
|
||||
fill-opacity:0;
|
||||
stroke-width:4;
|
||||
}
|
||||
|
||||
.nvd3 .nv-series.nv-disabled .nv-check-box .nv-check {
|
||||
fill-opacity:0;
|
||||
stroke-opacity:0;
|
||||
}
|
||||
|
||||
.nvd3 .nv-controlsWrap .nv-legend .nv-check-box .nv-check {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* line plus bar */
|
||||
.nvd3.nv-linePlusBar .nv-bar rect {
|
||||
fill-opacity: .75;
|
||||
}
|
||||
|
||||
.nvd3.nv-linePlusBar .nv-bar rect:hover {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
.nvd3 .nv-groups path.nv-line {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.nvd3 .nv-groups path.nv-area {
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.nvd3.nv-line .nvd3.nv-scatter .nv-groups .nv-point {
|
||||
fill-opacity: 0;
|
||||
stroke-opacity: 0;
|
||||
}
|
||||
|
||||
.nvd3.nv-scatter.nv-single-point .nv-groups .nv-point {
|
||||
fill-opacity: .5 !important;
|
||||
stroke-opacity: .5 !important;
|
||||
}
|
||||
|
||||
|
||||
.with-transitions .nvd3 .nv-groups .nv-point {
|
||||
transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
|
||||
-moz-transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
|
||||
-webkit-transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
|
||||
|
||||
}
|
||||
|
||||
.nvd3.nv-scatter .nv-groups .nv-point.hover,
|
||||
.nvd3 .nv-groups .nv-point.hover {
|
||||
stroke-width: 7px;
|
||||
fill-opacity: .95 !important;
|
||||
stroke-opacity: .95 !important;
|
||||
}
|
||||
|
||||
|
||||
.nvd3 .nv-point-paths path {
|
||||
stroke: #aaa;
|
||||
stroke-opacity: 0;
|
||||
fill: #eee;
|
||||
fill-opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.nvd3 .nv-indexLine {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
/********************
|
||||
* SVG CSS
|
||||
*/
|
||||
|
||||
/********************
|
||||
Default CSS for an svg element nvd3 used
|
||||
*/
|
||||
svg.nvd3-svg {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
display: block;
|
||||
width:100%;
|
||||
height:100%;
|
||||
}
|
||||
|
||||
/********************
|
||||
Box shadow and border radius styling
|
||||
*/
|
||||
.nvtooltip.with-3d-shadow, .with-3d-shadow .nvtooltip {
|
||||
-moz-box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
||||
-webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
||||
box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
||||
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
|
||||
.nvd3 text {
|
||||
font: normal 12px Arial;
|
||||
}
|
||||
|
||||
.nvd3 .title {
|
||||
font: bold 14px Arial;
|
||||
}
|
||||
|
||||
.nvd3 .nv-background {
|
||||
fill: white;
|
||||
fill-opacity: 0;
|
||||
}
|
||||
|
||||
.nvd3.nv-noData {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
/**********
|
||||
* Brush
|
||||
*/
|
||||
|
||||
.nv-brush .extent {
|
||||
fill-opacity: .125;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.nv-brush .resize path {
|
||||
fill: #eee;
|
||||
stroke: #666;
|
||||
}
|
||||
|
||||
|
||||
/**********
|
||||
* Legend
|
||||
*/
|
||||
|
||||
.nvd3 .nv-legend .nv-series {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nvd3 .nv-legend .nv-disabled circle {
|
||||
fill-opacity: 0;
|
||||
}
|
||||
|
||||
/* focus */
|
||||
.nvd3 .nv-brush .extent {
|
||||
fill-opacity: 0 !important;
|
||||
}
|
||||
|
||||
.nvd3 .nv-brushBackground rect {
|
||||
stroke: #000;
|
||||
stroke-width: .4;
|
||||
fill: #fff;
|
||||
fill-opacity: .7;
|
||||
}
|
||||
|
||||
|
||||
.nvd3.nv-ohlcBar .nv-ticks .nv-tick {
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.hover {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.positive {
|
||||
stroke: #2ca02c;
|
||||
}
|
||||
|
||||
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.negative {
|
||||
stroke: #d62728;
|
||||
}
|
||||
|
||||
|
||||
.nvd3 .background path {
|
||||
fill: none;
|
||||
stroke: #EEE;
|
||||
stroke-opacity: .4;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.nvd3 .foreground path {
|
||||
fill: none;
|
||||
stroke-opacity: .7;
|
||||
}
|
||||
|
||||
.nvd3 .nv-parallelCoordinates-brush .extent
|
||||
{
|
||||
fill: #fff;
|
||||
fill-opacity: .6;
|
||||
stroke: gray;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.nvd3 .nv-parallelCoordinates .hover {
|
||||
fill-opacity: 1;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
|
||||
|
||||
.nvd3 .missingValuesline line {
|
||||
fill: none;
|
||||
stroke: black;
|
||||
stroke-width: 1;
|
||||
stroke-opacity: 1;
|
||||
stroke-dasharray: 5, 5;
|
||||
}
|
||||
.nvd3.nv-pie path {
|
||||
stroke-opacity: 0;
|
||||
transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
|
||||
-moz-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
|
||||
-webkit-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
|
||||
|
||||
}
|
||||
|
||||
.nvd3.nv-pie .nv-pie-title {
|
||||
font-size: 24px;
|
||||
fill: rgba(19, 196, 249, 0.59);
|
||||
}
|
||||
|
||||
.nvd3.nv-pie .nv-slice text {
|
||||
stroke: #000;
|
||||
stroke-width: 0;
|
||||
}
|
||||
|
||||
.nvd3.nv-pie path {
|
||||
stroke: #fff;
|
||||
stroke-width: 1px;
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
|
||||
.nvd3.nv-pie .hover path {
|
||||
fill-opacity: .7;
|
||||
}
|
||||
.nvd3.nv-pie .nv-label {
|
||||
pointer-events: none;
|
||||
}
|
||||
.nvd3.nv-pie .nv-label rect {
|
||||
fill-opacity: 0;
|
||||
stroke-opacity: 0;
|
||||
}
|
||||
|
||||
/* scatter */
|
||||
.nvd3 .nv-groups .nv-point.hover {
|
||||
stroke-width: 20px;
|
||||
stroke-opacity: .5;
|
||||
}
|
||||
|
||||
.nvd3 .nv-scatter .nv-point.hover {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
.nv-noninteractive {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nv-distx, .nv-disty {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* sparkline */
|
||||
.nvd3.nv-sparkline path {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.nvd3.nv-sparklineplus g.nv-hoverValue {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nvd3.nv-sparklineplus .nv-hoverValue line {
|
||||
stroke: #333;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.nvd3.nv-sparklineplus,
|
||||
.nvd3.nv-sparklineplus g {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.nvd3 .nv-hoverArea {
|
||||
fill-opacity: 0;
|
||||
stroke-opacity: 0;
|
||||
}
|
||||
|
||||
.nvd3.nv-sparklineplus .nv-xValue,
|
||||
.nvd3.nv-sparklineplus .nv-yValue {
|
||||
stroke-width: 0;
|
||||
font-size: .9em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.nvd3.nv-sparklineplus .nv-yValue {
|
||||
stroke: #f66;
|
||||
}
|
||||
|
||||
.nvd3.nv-sparklineplus .nv-maxValue {
|
||||
stroke: #2ca02c;
|
||||
fill: #2ca02c;
|
||||
}
|
||||
|
||||
.nvd3.nv-sparklineplus .nv-minValue {
|
||||
stroke: #d62728;
|
||||
fill: #d62728;
|
||||
}
|
||||
|
||||
.nvd3.nv-sparklineplus .nv-currentValue {
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
/* stacked area */
|
||||
.nvd3.nv-stackedarea path.nv-area {
|
||||
fill-opacity: .7;
|
||||
stroke-opacity: 0;
|
||||
transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
|
||||
-moz-transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
|
||||
-webkit-transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
|
||||
}
|
||||
|
||||
.nvd3.nv-stackedarea path.nv-area.hover {
|
||||
fill-opacity: .9;
|
||||
}
|
||||
|
||||
|
||||
.nvd3.nv-stackedarea .nv-groups .nv-point {
|
||||
stroke-opacity: 0;
|
||||
fill-opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
.nvtooltip {
|
||||
position: absolute;
|
||||
background-color: rgba(255,255,255,1.0);
|
||||
color: rgba(0,0,0,1.0);
|
||||
padding: 1px;
|
||||
border: 1px solid rgba(0,0,0,.2);
|
||||
z-index: 10000;
|
||||
display: block;
|
||||
|
||||
font-family: Arial;
|
||||
font-size: 11px;
|
||||
text-align: left;
|
||||
pointer-events: none;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.nvtooltip {
|
||||
background: rgba(255,255,255, 0.8);
|
||||
border: 1px solid rgba(0,0,0,0.5);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/*Give tooltips that old fade in transition by
|
||||
putting a "with-transitions" class on the container div.
|
||||
*/
|
||||
.nvtooltip.with-transitions, .with-transitions .nvtooltip {
|
||||
transition: opacity 50ms linear;
|
||||
-moz-transition: opacity 50ms linear;
|
||||
-webkit-transition: opacity 50ms linear;
|
||||
|
||||
transition-delay: 200ms;
|
||||
-moz-transition-delay: 200ms;
|
||||
-webkit-transition-delay: 200ms;
|
||||
}
|
||||
|
||||
.nvtooltip.x-nvtooltip,
|
||||
.nvtooltip.y-nvtooltip {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.nvtooltip h3 {
|
||||
margin: 0;
|
||||
padding: 4px 14px;
|
||||
line-height: 18px;
|
||||
font-weight: normal;
|
||||
background-color: rgba(247,247,247,0.75);
|
||||
color: rgba(0,0,0,1.0);
|
||||
text-align: center;
|
||||
|
||||
border-bottom: 1px solid #ebebeb;
|
||||
|
||||
-webkit-border-radius: 5px 5px 0 0;
|
||||
-moz-border-radius: 5px 5px 0 0;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
.nvtooltip p {
|
||||
margin: 0;
|
||||
padding: 5px 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nvtooltip span {
|
||||
display: inline-block;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.nvtooltip table {
|
||||
margin: 6px;
|
||||
border-spacing:0;
|
||||
}
|
||||
|
||||
|
||||
.nvtooltip table td {
|
||||
padding: 2px 9px 2px 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.nvtooltip table td.key {
|
||||
font-weight:normal;
|
||||
}
|
||||
.nvtooltip table td.value {
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nvtooltip table tr.highlight td {
|
||||
padding: 1px 9px 1px 0;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 4px;
|
||||
border-top-style: solid;
|
||||
border-top-width: 4px;
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.nvtooltip table td.legend-color-guide div {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.nvtooltip table td.legend-color-guide div {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
.nvtooltip .footer {
|
||||
padding: 3px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nvtooltip-pending-removal {
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/****
|
||||
Interactive Layer
|
||||
*/
|
||||
.nvd3 .nv-interactiveGuideLine {
|
||||
pointer-events:none;
|
||||
}
|
||||
.nvd3 line.nv-guideline {
|
||||
stroke: #ccc;
|
||||
}
|
||||
8
panoramix/static/nv.d3.min.js
vendored
Normal file
BIN
panoramix/static/servers.jpg
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
panoramix/static/slice.jpg
Normal file
|
After Width: | Height: | Size: 147 KiB |
@@ -2,8 +2,8 @@
|
||||
{% import 'appbuilder/baselib.html' as baselib %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'appbuilder/general/confirm.html' %}
|
||||
{% include 'appbuilder/general/alert.html' %}
|
||||
{% include 'appbuilder/general/confirm.html' %}
|
||||
{% include 'appbuilder/general/alert.html' %}
|
||||
|
||||
{% block navbar %}
|
||||
<header class="top" role="header">
|
||||
@@ -11,6 +11,8 @@
|
||||
</header>
|
||||
{% endblock %}
|
||||
|
||||
{% block uncontained %}{% endblock %}
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
{% block messages %}
|
||||
|
||||
@@ -1,13 +1,148 @@
|
||||
{% extends "appbuilder/base.html" %}
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="jumbotron">
|
||||
<h1>Panoramix</h1>
|
||||
<p>Panoramix is an interactive visualization platform built on top of Druid.io</p>
|
||||
|
||||
{% block head_css %}
|
||||
{{super()}}
|
||||
<style>
|
||||
.carousel img {
|
||||
max-height: 500px;
|
||||
}
|
||||
.carousel {
|
||||
overflow: hidden;
|
||||
}
|
||||
div.carousel-caption{
|
||||
background: rgba(0,0,0,0.5);
|
||||
border-radius: 20px;
|
||||
top: 150px;
|
||||
bottom: auto !important;
|
||||
}
|
||||
.carousel-inner > .item > img {
|
||||
margin: 0 auto;
|
||||
}
|
||||
div.navbar {
|
||||
margin: 0px;
|
||||
}
|
||||
.carousel-indicators li {
|
||||
background-color: #AAA;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.carousel-indicators .active {
|
||||
background-color: #000;
|
||||
border: 5px solid black;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block uncontained %}
|
||||
<div id="carousel" style="height: 500px;" class="carousel slide" data-ride="carousel">
|
||||
<!-- Indicators -->
|
||||
<ol class="carousel-indicators">
|
||||
<li data-target="#carousel" data-slide-to="0" class="active"></li>
|
||||
<li data-target="#carousel" data-slide-to="1"></li>
|
||||
<li data-target="#carousel" data-slide-to="2"></li>
|
||||
<li data-target="#carousel" data-slide-to="3"></li>
|
||||
<li data-target="#carousel" data-slide-to="4"></li>
|
||||
</ol>
|
||||
|
||||
<!-- Wrapper for slides -->
|
||||
<div class="carousel-inner" role="listbox">
|
||||
<div class="item active text-center">
|
||||
<img src="{{ url_for("static", filename="dash.png") }}">
|
||||
<div class="carousel-caption">
|
||||
<div>
|
||||
<h1 style="font-size: 80px;">Panoramix</h1>
|
||||
<p style="font-size: 25px;">
|
||||
an open source data visualization platform
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<img width="250" src="/static/tux_panoramix.png">
|
||||
<div class="item">
|
||||
<img src="{{ url_for("static", filename="bubble.png") }}">
|
||||
<div class="carousel-caption">
|
||||
<h1>Explore your data
|
||||
</h1>
|
||||
<p>
|
||||
Intuitively navigate your data while slicing, dicing, and
|
||||
visualizing through a rich set of widgets
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src="{{ url_for("static", filename="dash.png") }}">
|
||||
<div class="carousel-caption">
|
||||
<h1>Create and share dashboards</h1>
|
||||
<p>Assemble many data visualization "slices" into a rich collection</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img src="{{ url_for("static", filename="cloud.png") }}">
|
||||
<div class="carousel-caption">
|
||||
<h1>Extend</h1>
|
||||
<p>Join the community and take part in extending the widget library</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<img style="max-height: none;" src="{{ url_for("static", filename="servers.jpg") }}">
|
||||
<div class="carousel-caption">
|
||||
<h1>Connect</h1>
|
||||
<p>
|
||||
Access data from MySql, Presto.db, Postgres, RedShift, Oracle, MsSql,
|
||||
SQLite, and more through the SqlAlchemy integration. You can also
|
||||
query realtime data blazingly fast out of Druid.io
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Controls -->
|
||||
<div>
|
||||
<a class="left carousel-control" href="#carousel" role="button" data-slide="prev">
|
||||
<span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span>
|
||||
<span class="sr-only">Previous</span>
|
||||
</a>
|
||||
<a class="right carousel-control" href="#carousel" role="button" data-slide="next">
|
||||
<span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
|
||||
<span class="sr-only">Next</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
<div class="container">
|
||||
<div class="row text-center">
|
||||
<div class="col-lg-4">
|
||||
<img class="img-circle" src="{{ url_for('static', filename='cardash.jpg') }}" width="140" height="140">
|
||||
<h2>Dashboards</h2>
|
||||
<p>Browse the dashboards list</p>
|
||||
<p><a class="btn btn-default" href="/dashboardmodelview/list/" role="button">
|
||||
<i class="fa fa-dashboard"></i>
|
||||
</a></p>
|
||||
</div><!-- /.col-lg-4 -->
|
||||
<div class="col-lg-4">
|
||||
<img class="img-circle" src="{{ url_for('static', filename='slice.jpg') }}" width="140" height="140">
|
||||
<h2>Slices</h2>
|
||||
<p>"Slices" are individual views into a single dataset</p>
|
||||
<p><a class="btn btn-default" href="/slicemodelview/list/" role="button">
|
||||
<i class="fa fa-line-chart"></i>
|
||||
</a></p>
|
||||
</div><!-- /.col-lg-4 -->
|
||||
<div class="col-lg-4">
|
||||
<img class="img-circle" src="{{ url_for('static', filename='gallery.jpg') }}" alt="Generic placeholder image" width="140" height="140">
|
||||
<h2>Gallery</h2>
|
||||
<p>Navigate through the growing set of visualizations</p>
|
||||
<p><a class="btn btn-default" href="#" onclick="alert('Not ready yet!');" role="button">
|
||||
<i class="fa fa-picture-o"></i>
|
||||
</a></p>
|
||||
</div><!-- /.col-lg-4 -->
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block tail_js %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
$( document ).ready(function() {
|
||||
$('#carousel').carousel();
|
||||
console.log($('#carousel'));
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
194
panoramix/templates/panoramix/dashboard.html
Normal file
@@ -0,0 +1,194 @@
|
||||
{% extends "panoramix/base.html" %}
|
||||
|
||||
{% block head_css %}
|
||||
{{ super() }}
|
||||
{% for css in dashboard.css_files %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename=css) }}">
|
||||
{% endfor %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename="jquery.gridster.min.css") }}">
|
||||
{% for slice in dashboard.slices %}
|
||||
{% set viz = slice.viz %}
|
||||
{% import viz.template as viz_macros %}
|
||||
{{ viz_macros.viz_css(viz) }}
|
||||
{% endfor %}
|
||||
<style>
|
||||
a i{
|
||||
cursor: pointer;
|
||||
}
|
||||
i.drag{
|
||||
cursor: move; !important
|
||||
}
|
||||
.gridster .preview-holder {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
background-color: #AAA;
|
||||
border-color: #AAA;
|
||||
opacity: 0.3;
|
||||
}
|
||||
.gridster li {
|
||||
list-style-type: none;
|
||||
border: 1px solid gray;
|
||||
overflow: hidden;
|
||||
box-shadow: 2px 2px 2px #AAA;
|
||||
border-radius: 5px;
|
||||
background-color: white;
|
||||
//overflow: auto;
|
||||
}
|
||||
.gridster .dragging,
|
||||
.gridster .resizing {
|
||||
opacity: 0.5;
|
||||
}
|
||||
img.loading {
|
||||
width: 20px;
|
||||
margin: 5px;
|
||||
}
|
||||
.title {
|
||||
text-align: center;
|
||||
}
|
||||
.slice_title {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
padding: 5px;
|
||||
}
|
||||
div.gridster {
|
||||
visibility: hidden
|
||||
}
|
||||
div.slice_content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
table.widget_header {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
}
|
||||
td.icons {
|
||||
width: 50px;
|
||||
}
|
||||
td.icons nobr {
|
||||
display: none;
|
||||
}
|
||||
div.header {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content_fluid %}
|
||||
<div class="title">
|
||||
<div class="row">
|
||||
<div class="col-md-1 text-left"></div>
|
||||
<div class="col-md-10 text-middle">
|
||||
<h2>
|
||||
{{ dashboard.dashboard_title }}
|
||||
<a id="savedash"><i class="fa fa-save"></i></a>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-md-1 text-right">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gridster content_fluid">
|
||||
<ul>
|
||||
{% for slice in dashboard.slices %}
|
||||
{% set pos = pos_dict.get(slice.id, {}) %}
|
||||
{% set viz = slice.viz %}
|
||||
{% import viz.template as viz_macros %}
|
||||
<li
|
||||
id="slice_{{ slice.id }}"
|
||||
slice_id="{{ slice.id }}"
|
||||
data-row="{{ pos.row or 1 }}"
|
||||
data-col="{{ pos.col or loop.index }}"
|
||||
data-sizex="{{ pos.size_x or 4 }}"
|
||||
data-sizey="{{ pos.size_y or 4 }}">
|
||||
<table class="widget_header">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="icons">
|
||||
<nobr class="icons">
|
||||
<a><i class="fa fa-arrows drag"></i></a>
|
||||
<a class="refresh"><i class="fa fa-refresh"></i></a>
|
||||
</nobr>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-center header"><nobr>{{ slice.slice_name }}</nobr></div>
|
||||
</td>
|
||||
<td class="icons text-right">
|
||||
<nobr>
|
||||
<a href="{{ slice.slice_url }}"><i class="fa fa-play"></i></a>
|
||||
<a href="{{ slice.edit_url }}"><i class="fa fa-gear"></i></a>
|
||||
<a class="closewidget"><i class="fa fa-close"></i></a>
|
||||
</br>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{ viz_macros.viz_html(viz) }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block tail %}
|
||||
{{ super() }}
|
||||
{% for js in dashboard.js_files %}
|
||||
<script src="{{ url_for('static', filename=js) }}"></script>
|
||||
{% endfor %}
|
||||
<script src="{{ url_for("static", filename="jquery.gridster.with-extras.min.js") }}"></script>
|
||||
<script>
|
||||
f = d3.format(".4s");
|
||||
</script>
|
||||
<script>
|
||||
$( document ).ready(function() {
|
||||
var gridster = $(".gridster ul").gridster({
|
||||
widget_margins: [5, 5],
|
||||
widget_base_dimensions: [100, 100],
|
||||
draggable: {
|
||||
handle: '.drag',
|
||||
},
|
||||
resize: {
|
||||
enabled: true,
|
||||
stop: function(e, ui, $widget) {
|
||||
$widget.find("a.refresh").click();
|
||||
}
|
||||
},
|
||||
serialize_params:function($w, wgd) {
|
||||
return {
|
||||
slice_id: $($w).attr('slice_id'),
|
||||
col: wgd.col,
|
||||
row: wgd.row,
|
||||
size_x: wgd.size_x,
|
||||
size_y: wgd.size_y
|
||||
};
|
||||
},
|
||||
}).data('gridster');
|
||||
$("div.gridster").css('visibility', 'visible');
|
||||
$("#savedash").click(function(){
|
||||
var data = gridster.serialize();
|
||||
console.log(data);
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: '/panoramix/save_dash/{{ dashboard.id }}/',
|
||||
data: {data: JSON.stringify(data)},
|
||||
success: function(){console.log('Sucess!')},
|
||||
});
|
||||
});
|
||||
$("a.closewidget").click(function(){
|
||||
var li = $(this).parents("li");
|
||||
gridster.remove_widget(li);
|
||||
});
|
||||
$("table.widget_header").mouseover(function(){
|
||||
$(this).find("td.icons nobr").show();
|
||||
});
|
||||
$("table.widget_header").mouseout(function(){
|
||||
$(this).find("td.icons nobr").hide();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% for slice in dashboard.slices %}
|
||||
{% set viz = slice.viz %}
|
||||
{% import viz.template as viz_macros %}
|
||||
{{ viz_macros.viz_js(viz) }}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -2,34 +2,39 @@
|
||||
{% block head_css %}
|
||||
{{super()}}
|
||||
<style>
|
||||
.select2-container-multi .select2-choices {
|
||||
height: 70px;
|
||||
overflow: auto;
|
||||
}
|
||||
.no-gutter > [class*='col-'] {
|
||||
.select2-container-multi .select2-choices {
|
||||
height: 70px;
|
||||
overflow: auto;
|
||||
}
|
||||
.no-gutter > [class*='col-'] {
|
||||
padding-right:0;
|
||||
padding-left:0;
|
||||
}
|
||||
form div.form-control {
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
form input.form-control {
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
.tooltip-inner {
|
||||
max-width: 350px;
|
||||
//width: 350px;
|
||||
}
|
||||
}
|
||||
form div.form-control {
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
form input.form-control {
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
.tooltip-inner {
|
||||
max-width: 350px;
|
||||
//width: 350px;
|
||||
}
|
||||
img.loading {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content_fluid %}
|
||||
{% set datasource = viz.datasource %}
|
||||
{% set form = viz.form %}
|
||||
<div class="container-fluid">
|
||||
<div class="col-md-3">
|
||||
<h3>
|
||||
{{ datasource.name }}
|
||||
{% if datasource.description %}
|
||||
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="bottom" title="{{ datasource.description }}"></i>
|
||||
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="bottom" title="{{ datasource.description }}"></i>
|
||||
{% endif %}
|
||||
<a href="/{{ datasource.baselink }}/edit/{{ datasource.id }}">
|
||||
<i class="fa fa-edit"></i>
|
||||
@@ -38,85 +43,97 @@ form input.form-control {
|
||||
|
||||
<hr>
|
||||
<form id="query" method="GET" style="display: none;">
|
||||
<div>{{ form.viz_type.label }}: {{ form.viz_type(class_="form-control select2") }}</div>
|
||||
{% if 'metrics' not in viz.hidden_fields %}
|
||||
<div>{{ form.metrics.label }}: {{ form.metrics(class_="form-control select2") }}</div>
|
||||
{% endif %}
|
||||
{% if 'granularity' not in viz.hidden_fields %}
|
||||
<div>{{ form.granularity.label }}
|
||||
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="right"
|
||||
title="Supports natural language time as in '10 seconds', '1 day' or '1 week'"
|
||||
id="blah"></i>
|
||||
{{ form.granularity(class_="form-control select2_free_granularity") }}</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
<div class="col-xs-6">{{ form.since.label }}
|
||||
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="right"
|
||||
title="Supports natural language time as in '1 day ago', '28 days' or '3 years'"
|
||||
id="blah"></i>
|
||||
{{ form.since(class_="form-control select2_free_since") }}</div>
|
||||
<div class="col-xs-6">{{ form.until.label }}
|
||||
{{ form.until(class_="form-control select2_free_until") }}</div>
|
||||
{% for fieldname in form.field_order %}
|
||||
{% if not fieldname.__iter__ %}
|
||||
<div>
|
||||
{% set field = form.get_field(fieldname)%}
|
||||
<div>
|
||||
{{ field.label }}
|
||||
{% if field.description %}
|
||||
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="right"
|
||||
title="{{ field.description }}"></i>
|
||||
{% endif %}:
|
||||
</div>
|
||||
<div>
|
||||
{{ field(class_=form.field_css_classes(field.name)) }}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
{% for name in fieldname %}
|
||||
<div class="col-xs-{{ (12 / fieldname|length) | int }}">
|
||||
{% set field = form.get_field(name)%}
|
||||
{{ field.label }}
|
||||
{% if field.description %}
|
||||
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="right"
|
||||
title="{{ field.description }}"></i>
|
||||
{% endif %}:
|
||||
{{ field(class_=form.field_css_classes(field.name)) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% if 'groupby' not in viz.hidden_fields %}
|
||||
<div>{{ form.groupby.label }}: {{ form.groupby(class_="form-control select2") }}</div>
|
||||
{% endif %}
|
||||
{% block extra_fields %}{% endblock %}
|
||||
{% endfor %}
|
||||
<hr>
|
||||
<h4>Filters</h4>
|
||||
<div id="flt0" style="display: none;">
|
||||
<span class="">{{ form.flt_col_0(class_="form-control inc") }}</span>
|
||||
<div class="row">
|
||||
<span class="col col-sm-4">{{ form.flt_op_0(class_="form-control inc") }}</span>
|
||||
<span class="col col-sm-6">{{ form.flt_eq_0(class_="form-control inc") }}</span>
|
||||
<button type="button" class="btn btn-sm remove" aria-label="Delete filter">
|
||||
<span class="glyphicon glyphicon-minus" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
<hr style="margin: 5px 0px;"/>
|
||||
<div id="flt0" style="display: none;">
|
||||
<span class="">{{ form.flt_col_0(class_="form-control inc") }}</span>
|
||||
<div class="row">
|
||||
<span class="col col-sm-4">{{ form.flt_op_0(class_="form-control inc") }}</span>
|
||||
<span class="col col-sm-6">{{ form.flt_eq_0(class_="form-control inc") }}</span>
|
||||
<button type="button" class="btn btn-sm remove" aria-label="Delete filter">
|
||||
<span class="glyphicon glyphicon-minus" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
<hr style="margin: 5px 0px;"/>
|
||||
</div>
|
||||
<div id="filters"></div>
|
||||
<button type="button" id="plus" class="btn btn-sm" aria-label="Add a filter">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
</button>
|
||||
<hr>
|
||||
<button type="button" class="btn btn-primary" id="druidify">Druidify!</button>
|
||||
<button type="button" class="btn btn-default" id="bookmark">Bookmark</button>
|
||||
<button type="button" class="btn btn-primary" id="druidify">
|
||||
<i class="fa fa-bolt"></i>
|
||||
Druidify!
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" id="save">
|
||||
<i class="fa fa-save"></i>
|
||||
Save as Slice
|
||||
</button>
|
||||
<hr style="margin-bottom: 0px;">
|
||||
<img src="{{ url_for("static", filename="tux_panoramix.png") }}" width=250>
|
||||
<input type="hidden" id="slice_name" name="slice_name" value="TEST">
|
||||
<input type="hidden" id="action" name="action" value="">
|
||||
<input type="hidden" name="datasource_name" value="{{ datasource.name }}">
|
||||
<input type="hidden" name="datasource_id" value="{{ datasource.id }}">
|
||||
<input type="hidden" name="datasource_type" value="{{ datasource.type }}">
|
||||
</form><br>
|
||||
</div>
|
||||
|
||||
<div class="col-md-9">
|
||||
<h3>{{ viz.verbose_name }}
|
||||
{% if results %}
|
||||
<span class="label label-success">
|
||||
{{ "{0:0.4f}".format(results.duration.total_seconds()) }} s
|
||||
</span>
|
||||
<span class="label label-info btn"
|
||||
data-toggle="modal" data-target="#query_modal">query</span>
|
||||
{% endif %}
|
||||
{% if False %}
|
||||
<span class="label label-success">
|
||||
{{ "{0:0.4f}".format(results.duration.total_seconds()) }} s
|
||||
</span>
|
||||
<span class="label label-info btn"
|
||||
data-toggle="modal" data-target="#query_modal">query</span>
|
||||
{% endif %}
|
||||
</h3>
|
||||
<hr/>
|
||||
{% block viz %}
|
||||
{% if error_msg %}
|
||||
<span class="alert alert-danger">{{ error_msg }}</span>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
<div class="viz" style="height: 700px;">
|
||||
{% block viz_html %}
|
||||
{% if viz.error_msg %}
|
||||
<div class="alert alert-danger">{{ viz.error_msg }}</div>
|
||||
{% endif %}
|
||||
{% if viz.warning_msg %}
|
||||
<div class="alert alert-warning">{{ viz.warning_msg }}</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% if debug %}
|
||||
<h3>Results</h3>
|
||||
<pre>
|
||||
{{ results }}
|
||||
</pre>
|
||||
|
||||
<h3>Latest Segment Metadata</h3>
|
||||
<pre>
|
||||
{{ latest_metadata }}
|
||||
</pre>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="query_modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
|
||||
@@ -127,7 +144,7 @@ form input.form-control {
|
||||
<h4 class="modal-title" id="myModalLabel">Query</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre>{{ results.query }}</pre>
|
||||
<pre>{{ '' }}</pre>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
@@ -139,32 +156,30 @@ form input.form-control {
|
||||
|
||||
{% block tail_js %}
|
||||
{{ super() }}
|
||||
<script src="{{ url_for("static", filename="d3.min.js") }}"></script>
|
||||
<script>
|
||||
$( document ).ready(function() {
|
||||
f = d3.format(".4s");
|
||||
function getParam(name) {
|
||||
$( document ).ready(function() {
|
||||
function getParam(name) {
|
||||
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
|
||||
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
|
||||
results = regex.exec(location.search);
|
||||
results = regex.exec(location.search);
|
||||
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
|
||||
}
|
||||
}
|
||||
|
||||
$(".select2").select2();
|
||||
$("form").slideDown("slow");
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
$(".select2").select2();
|
||||
$("form").slideDown("slow");
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
|
||||
function set_filters(){
|
||||
for (var i=1; i<10; i++){
|
||||
var eq = getParam("flt_eq_" + i);
|
||||
if (eq !=''){
|
||||
add_filter(i);
|
||||
function set_filters(){
|
||||
for (var i=1; i<10; i++){
|
||||
var eq = getParam("flt_eq_" + i);
|
||||
if (eq !=''){
|
||||
add_filter(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
set_filters();
|
||||
set_filters();
|
||||
|
||||
function add_filter(i) {
|
||||
function add_filter(i) {
|
||||
cp = $("#flt0").clone();
|
||||
$(cp).appendTo("#filters");
|
||||
$(cp).slideDown("slow");
|
||||
@@ -178,81 +193,91 @@ $( document ).ready(function() {
|
||||
$(cp).find('.remove').click(function() {
|
||||
$(this).parent().parent().slideUp("slow", function(){$(this).remove()});
|
||||
});
|
||||
}
|
||||
$("#plus").click(add_filter);
|
||||
$("#bookmark").click(function () {alert("Not implemented yet...");})
|
||||
add_filter();
|
||||
$("#druidify").click(function () {
|
||||
var i = 1;
|
||||
}
|
||||
|
||||
// removing empty filters
|
||||
$("#filters > div").each(function(){
|
||||
if ($(this).find("#flt_eq_0").val() == '')
|
||||
function druidify(){
|
||||
var i = 1;
|
||||
|
||||
// removing empty filters
|
||||
$("#filters > div").each(function(){
|
||||
if ($(this).find("#flt_eq_0").val() == '')
|
||||
$(this).slideUp();
|
||||
});
|
||||
});
|
||||
|
||||
// Assigning the right id to form elements in filters
|
||||
$("#filters > div").each(function(){
|
||||
$(this).attr("id", function(){return "flt_" + i;})
|
||||
$(this).find("#flt_col_0")
|
||||
// Assigning the right id to form elements in filters
|
||||
$("#filters > div").each(function(){
|
||||
$(this).attr("id", function(){return "flt_" + i;})
|
||||
$(this).find("#flt_col_0")
|
||||
.attr("id", function(){return "flt_col_" + i;})
|
||||
.attr("name", function(){return "flt_col_" + i;});
|
||||
$(this).find("#flt_op_0")
|
||||
$(this).find("#flt_op_0")
|
||||
.attr("id", function(){return "flt_op_" + i;})
|
||||
.attr("name", function(){return "flt_op_" + i;});
|
||||
$(this).find("#flt_eq_0")
|
||||
$(this).find("#flt_eq_0")
|
||||
.attr("id", function(){return "flt_eq_" + i;})
|
||||
.attr("name", function(){return "flt_eq_" + i;});
|
||||
i++;
|
||||
});
|
||||
$("#query").submit();
|
||||
});
|
||||
i++;
|
||||
});
|
||||
$("#query").submit();
|
||||
}
|
||||
|
||||
function create_choices (term, data) {
|
||||
$("#plus").click(add_filter);
|
||||
$("#save").click(function () {
|
||||
var slice_name = prompt("Name your slice!");
|
||||
if (slice_name != "" && slice_name != null) {
|
||||
$("#slice_name").val(slice_name);
|
||||
$("#action").val("save");
|
||||
druidify();
|
||||
}
|
||||
})
|
||||
add_filter();
|
||||
$("#druidify").click(druidify);
|
||||
|
||||
function create_choices (term, data) {
|
||||
if ($(data).filter(function() {
|
||||
return this.text.localeCompare(term)===0;
|
||||
return this.text.localeCompare(term)===0;
|
||||
}).length===0)
|
||||
{return {id:term, text:term};}
|
||||
}
|
||||
$(".select2_free_since").select2({
|
||||
}
|
||||
$(".select2_free_since").select2({
|
||||
createSearchChoice: create_choices,
|
||||
multiple: false,
|
||||
data: [
|
||||
{id: '-1 hour', text: '-1 hour'},
|
||||
{id: '-12 hours', text: '-12 hours'},
|
||||
{id: '-1 day', text: '-1 day'},
|
||||
{id: '-7 days', text: '-7 days'},
|
||||
{id: '-28 days', text: '-28 days'},
|
||||
{id: '-90 days', text: '-90 days'},
|
||||
{id: '{{ form.data.since }}', text: '{{ form.data.since }}'},
|
||||
{id: '-1 hour', text: '-1 hour'},
|
||||
{id: '-12 hours', text: '-12 hours'},
|
||||
{id: '-1 day', text: '-1 day'},
|
||||
{id: '-7 days', text: '-7 days'},
|
||||
{id: '-28 days', text: '-28 days'},
|
||||
{id: '-90 days', text: '-90 days'},
|
||||
{id: '{{ viz.form.data.since }}', text: '{{ viz.form.data.since }}'},
|
||||
]
|
||||
});
|
||||
$(".select2_free_until").select2({
|
||||
});
|
||||
$(".select2_free_until").select2({
|
||||
createSearchChoice: create_choices,
|
||||
multiple: false,
|
||||
data: [
|
||||
{id: '{{ form.data.until }}', text: '{{ form.data.until }}'},
|
||||
{id: 'now', text: 'now'},
|
||||
{id: '-1 day', text: '-1 day'},
|
||||
{id: '-7 days', text: '-7 days'},
|
||||
{id: '-28 days', text: '-28 days'},
|
||||
{id: '-90 days', text: '-90 days'},
|
||||
{id: '{{ viz.form.data.until }}', text: '{{ viz.form.data.until }}'},
|
||||
{id: 'now', text: 'now'},
|
||||
{id: '-1 day', text: '-1 day'},
|
||||
{id: '-7 days', text: '-7 days'},
|
||||
{id: '-28 days', text: '-28 days'},
|
||||
{id: '-90 days', text: '-90 days'},
|
||||
]
|
||||
});
|
||||
$(".select2_free_granularity").select2({
|
||||
});
|
||||
$(".select2_free_granularity").select2({
|
||||
createSearchChoice: create_choices,
|
||||
multiple: false,
|
||||
data: [
|
||||
{id: '{{ form.data.granularity }}', text: '{{ form.data.granularity }}'},
|
||||
{id: 'all', text: 'all'},
|
||||
{id: '5 seconds', text: '5 seconds'},
|
||||
{id: '30 seconds', text: '30 seconds'},
|
||||
{id: '1 minute', text: '1 minute'},
|
||||
{id: '5 minutes', text: '5 minutes'},
|
||||
{id: '1 day', text: '1 day'},
|
||||
{id: '7 days', text: '7 days'},
|
||||
{id: '{{ viz.form.data.granularity }}', text: '{{ viz.form.data.granularity }}'},
|
||||
{id: 'all', text: 'all'},
|
||||
{id: '5 seconds', text: '5 seconds'},
|
||||
{id: '30 seconds', text: '30 seconds'},
|
||||
{id: '1 minute', text: '1 minute'},
|
||||
{id: '5 minutes', text: '5 minutes'},
|
||||
{id: '1 day', text: '1 day'},
|
||||
{id: '7 days', text: '7 days'},
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
7
panoramix/templates/panoramix/models/database/add.html
Normal file
@@ -0,0 +1,7 @@
|
||||
{% extends "appbuilder/general/model/add.html" %}
|
||||
|
||||
{% import "panoramix/models/database/macros.html" as macros %}
|
||||
{% block tail_js %}
|
||||
{{ super() }}
|
||||
{{ macros.testconn() }}
|
||||
{% endblock %}
|
||||
7
panoramix/templates/panoramix/models/database/edit.html
Normal file
@@ -0,0 +1,7 @@
|
||||
{% extends "appbuilder/general/model/edit.html" %}
|
||||
|
||||
{% import "panoramix/models/database/macros.html" as macros %}
|
||||
{% block tail_js %}
|
||||
{{ super() }}
|
||||
{{ macros.testconn() }}
|
||||
{% endblock %}
|
||||
19
panoramix/templates/panoramix/models/database/macros.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% macro testconn() %}
|
||||
<script>
|
||||
$("#sqlalchemy_uri").parent()
|
||||
.append('<button id="testconn" class="btn">Test Connection</button>');
|
||||
$("#testconn").click(function() {
|
||||
var url = "/panoramix/testconn";
|
||||
$.ajax({
|
||||
method: "POST",
|
||||
url: url,
|
||||
data: { uri: $("#sqlalchemy_uri").val() }
|
||||
}).done(function() {
|
||||
alert("success");
|
||||
}).fail(function(error) {
|
||||
alert("ERROR: " + error.responseText);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
{% endmacro %}
|
||||
41
panoramix/templates/panoramix/viz.html
Normal file
@@ -0,0 +1,41 @@
|
||||
{% import viz.template as viz_macros %}
|
||||
|
||||
{% if viz.args.get("json") == "true" %}
|
||||
{{ viz.get_json() }}
|
||||
{% else %}
|
||||
{% if viz.args.get("standalone") == "true" %}
|
||||
{% extends 'panoramix/viz_standalone.html' %}
|
||||
{% else %}
|
||||
{% extends 'panoramix/datasource.html' %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% block viz_html %}
|
||||
{{ viz_macros.viz_html(viz) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block head_css %}
|
||||
{{super()}}
|
||||
{% if viz.args.get("skip_libs") != "true" %}
|
||||
{% for css in viz.css_files %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename=css) }}">
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{{ viz_macros.viz_css(viz) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block tail %}
|
||||
{{super()}}
|
||||
{% if viz.args.get("skip_libs") != "true" %}
|
||||
<script src="{{ url_for("static", filename="d3.min.js") }}"></script>
|
||||
<script>
|
||||
f = d3.format(".4s");
|
||||
</script>
|
||||
{% for js in viz.js_files %}
|
||||
<script src="{{ url_for('static', filename=js) }}"></script>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{{ viz_macros.viz_js(viz) }}
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
199
panoramix/templates/panoramix/viz_bignumber.html
Normal file
@@ -0,0 +1,199 @@
|
||||
{% macro viz_html(viz) %}
|
||||
<div id="{{ viz.token }}" style="height: 100%;">
|
||||
<img src="{{ url_for("static", filename="loading.gif") }}" class="loading">
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro viz_js(viz) %}
|
||||
<script>
|
||||
|
||||
$( document ).ready(function() {
|
||||
|
||||
var div = d3.select("#{{ viz.token }}");
|
||||
var render = function(){
|
||||
url = "/";
|
||||
var url = "{{ viz.get_url(json="true")|safe }}";
|
||||
d3.json(url, function(error, json){
|
||||
div.html("");
|
||||
//Define the percentage bounds that define color from red to green
|
||||
if(error != null){
|
||||
var err = '<div class="alert alert-danger">' + error.responseText + '</div>';
|
||||
div.html(err);
|
||||
return '';
|
||||
}
|
||||
var color_range = [-1,1];
|
||||
var compare_pos = -23
|
||||
var target_url = 'd3js.org';
|
||||
|
||||
var f = d3.format('.3s');
|
||||
var fp = d3.format('+.1%');
|
||||
var xy = div.node().getBoundingClientRect();
|
||||
var width = xy.width;
|
||||
var height = xy.height-30;
|
||||
var svg = div.append('svg');
|
||||
svg.attr("width", width);
|
||||
svg.attr("height", height);
|
||||
data = example_data();
|
||||
data = json.data;
|
||||
var compare_suffix = ' ' + json.compare_suffix;
|
||||
var v_compare = null;
|
||||
var v = data[data.length -1][1];
|
||||
if (json.compare_lag >0){
|
||||
pos = data.length - (json.compare_lag+1);
|
||||
if(pos >=0){
|
||||
v_compare = (v / data[pos][1])-1;
|
||||
}
|
||||
}
|
||||
var date_ext = d3.extent(data, function(d){return d[0]});
|
||||
var value_ext = d3.extent(data, function(d){return d[1]});
|
||||
|
||||
var margin=20;
|
||||
var scale_x = d3.time.scale.utc().domain(date_ext).range([margin, width-margin]);
|
||||
var scale_y = d3.scale.linear().domain(value_ext).range([height-(margin),margin]);
|
||||
var colorRange = [d3.hsl(0,1,0.3), d3.hsl(120, 1, 0.3)];
|
||||
var scale_color = d3.scale
|
||||
.linear().domain(color_range)
|
||||
.interpolate(d3.interpolateHsl)
|
||||
.range(colorRange).clamp(true);
|
||||
var line = d3.svg.line()
|
||||
.x(function(d) { return scale_x(d[0])})
|
||||
.y(function(d) { return scale_y(d[1])})
|
||||
.interpolate("basis");
|
||||
|
||||
//Drawing trend line
|
||||
var g = svg.append('g');
|
||||
var path = g.append('path')
|
||||
.attr('d', function(d){return line(data);})
|
||||
.attr('stroke-width', 5)
|
||||
.attr('opacity', 0.5)
|
||||
.attr('fill', "none")
|
||||
.attr('stroke-linecap',"round")
|
||||
.attr('stroke', "grey");
|
||||
|
||||
var g = svg.append('g')
|
||||
.attr('class', 'digits')
|
||||
.attr('opacity', 1);
|
||||
|
||||
var y = height/2;
|
||||
if(v_compare != null)
|
||||
y = (height/8) * 3;
|
||||
|
||||
//Printing big number
|
||||
g.append('text')
|
||||
.attr('x', width/2)
|
||||
.attr('y', y)
|
||||
.attr('class', 'big')
|
||||
.attr('alignment-baseline', 'middle')
|
||||
.attr('id', 'bigNumber')
|
||||
.style('font-weight', 'bold')
|
||||
.style('cursor', 'pointer')
|
||||
.text(f(v))
|
||||
.style('font-size', d3.min([height, width])/3.5)
|
||||
.attr('fill','white');
|
||||
|
||||
var c = scale_color(v_compare);
|
||||
|
||||
//Printing compare %
|
||||
if(v_compare != null){
|
||||
g.append('text')
|
||||
.attr('x', width/2)
|
||||
.attr('y', (height/16) *12)
|
||||
.text(fp(v_compare) + compare_suffix)
|
||||
.style('font-size', d3.min([height, width])/8)
|
||||
.style('text-anchor', 'middle')
|
||||
.attr('fill', c)
|
||||
.attr('stroke', c);
|
||||
}
|
||||
|
||||
var g_axis = svg.append('g').attr('class', 'axis').attr('opacity',0);
|
||||
var g = g_axis.append('g');
|
||||
var x_axis = d3.svg.axis()
|
||||
.scale(scale_x)
|
||||
.orient('bottom')
|
||||
//.tickFormat(d3.time.format('%I%p'))
|
||||
.ticks(4);
|
||||
g.call(x_axis);
|
||||
g.attr('transform', 'translate(0,'+ (height-margin) +')');
|
||||
|
||||
var g = g_axis.append('g').attr('transform', 'translate('+(width-margin)+',0)');
|
||||
var y_axis = d3.svg.axis()
|
||||
.scale(scale_y)
|
||||
.orient('left')
|
||||
.tickFormat(d3.format('.3s'))
|
||||
.tickValues(value_ext);
|
||||
g.call(y_axis);
|
||||
g.selectAll('text')
|
||||
.style('text-anchor','end')
|
||||
.attr('y','-5')
|
||||
.attr('x','1');
|
||||
|
||||
g.selectAll("text")
|
||||
.style('font-size','10px');
|
||||
|
||||
/*
|
||||
g_axis.selectAll('path.domain')
|
||||
.attr('stroke-width:1px;');
|
||||
*/
|
||||
|
||||
div.on('mouseover', function(d){
|
||||
var div = d3.select(this);
|
||||
div.select('path').transition().duration(500).attr('opacity', 1)
|
||||
.style('stroke-width', '2px');
|
||||
div.select('g.digits').transition().duration(500).attr('opacity', 0.1);
|
||||
div.select('g.axis').transition().duration(500).attr('opacity', 1);
|
||||
})
|
||||
.on('mouseout', function(d){
|
||||
var div = d3.select(this);
|
||||
div.select('path').transition().duration(500).attr('opacity', 0.5)
|
||||
.style('stroke-width', '5px');
|
||||
div.select('g.digits').transition().duration(500).attr('opacity', 1);
|
||||
div.select('g.axis').transition().duration(500).attr('opacity', 0);
|
||||
});
|
||||
});
|
||||
};
|
||||
render();
|
||||
$(div).parent().find("a.refresh").click(render);
|
||||
});
|
||||
|
||||
example_data = function(){
|
||||
//Building a random growing trend
|
||||
var rnd = d3.random.normal(5000000, 500000);
|
||||
var data = [];
|
||||
for(i=0; i<24; i++){
|
||||
data.push([
|
||||
new Date(2015, 1, i, 1, 0, 0, 0),
|
||||
rnd() + (i*(rnd()/40))
|
||||
]);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
</script>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro viz_css(viz) %}
|
||||
<style>
|
||||
g.axis text{
|
||||
font-size:10px;
|
||||
font-weight:normal;
|
||||
color: gray;
|
||||
fill: gray;
|
||||
text-anchor:middle;
|
||||
alignment-baseline: middle;
|
||||
font-weight: none;
|
||||
}
|
||||
text.big{
|
||||
stroke: black;
|
||||
text-anchor:middle;
|
||||
fill: black;
|
||||
}
|
||||
g.tick line {
|
||||
stroke-width: 1px;
|
||||
stroke: grey;
|
||||
}
|
||||
.domain {
|
||||
fill: none;
|
||||
stroke: black;
|
||||
stroke-width; 1;
|
||||
}
|
||||
</style>
|
||||
{% endmacro %}
|
||||
@@ -1,64 +1,48 @@
|
||||
{% extends "panoramix/datasource.html" %}
|
||||
{% block viz %}
|
||||
{{ super() }}
|
||||
<div id="chart"></div>
|
||||
{% endblock %}
|
||||
{% macro viz_html(viz) %}
|
||||
<div id="{{ viz.token }}" style="height:100%; width: 100%">
|
||||
<img src="{{ url_for("static", filename="loading.gif") }}" class="loading">
|
||||
<div class="chart" style="height:100%; width: 100%"></div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% block extra_fields %}
|
||||
{% if form.compare %}
|
||||
<div>{{ form.compare.label }}: {{ form.compare(class_="form-control") }}</div>
|
||||
{% endif %}
|
||||
{% if form.rolling_type %}
|
||||
<div class="row">
|
||||
<span class="col col-sm-5">{{ form.rolling_type.label }}: {{ form.rolling_type(class_="form-control select2") }}</span>
|
||||
<span class="col col-sm-4">{{ form.rolling_periods.label }}: {{ form.rolling_periods(class_="form-control") }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.limit %}
|
||||
<div>{{ form.limit.label }}: {{ form.limit(class_="form-control select2") }}</div>
|
||||
{% endif %}
|
||||
{% if form.series %}
|
||||
<div>{{ form.series.label }}: {{ form.series(class_="form-control select2") }}</div>
|
||||
{% endif %}
|
||||
{% if form.entity %}
|
||||
<div>{{ form.entity.label }}: {{ form.entity(class_="form-control select2") }}</div>
|
||||
{% endif %}
|
||||
{% if form.size %}
|
||||
<div>{{ form.size.label }}: {{ form.size(class_="form-control select2") }}</div>
|
||||
{% endif %}
|
||||
{% if form.x %}
|
||||
<div>{{ form.x.label }}: {{ form.x(class_="form-control select2") }}</div>
|
||||
{% endif %}
|
||||
{% if form.y %}
|
||||
<div>{{ form.y.label }}: {{ form.y(class_="form-control select2") }}</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block tail %}
|
||||
{{ super() }}
|
||||
{% if viz.stockchart %}
|
||||
<script src="{{ url_for("static", filename="highstock.js") }}"></script>
|
||||
{% else %}
|
||||
<script src="{{ url_for("static", filename="highcharts.js") }}"></script>
|
||||
{% endif %}
|
||||
<script src="{{ url_for("static", filename="highcharts-more.js") }}"></script>
|
||||
{% macro viz_js(viz) %}
|
||||
<script>
|
||||
$( document ).ready(function() {
|
||||
$( document ).ready(function() {
|
||||
Highcharts.setOptions({
|
||||
colors: [
|
||||
"#FF5A5F", "#007A87", "#7B0051", "#00D1C1", "#8CE071", "#FFB400",
|
||||
"#FFAA91", "#B4A76C", "#9CA299", "#565A5C"
|
||||
],
|
||||
global: {
|
||||
useUTC: false
|
||||
},
|
||||
colors: [
|
||||
"#FF5A5F", "#007A87", "#7B0051", "#00D1C1", "#8CE071", "#FFB400",
|
||||
"#FFAA91", "#B4A76C", "#9CA299", "#565A5C"
|
||||
],
|
||||
global: {
|
||||
useUTC: false
|
||||
},
|
||||
});
|
||||
$("#viz_type").click(function(){
|
||||
$("#queryform").submit();
|
||||
})
|
||||
{% if chart_js %}
|
||||
{{ chart_js|safe }}
|
||||
{% endif %}
|
||||
});
|
||||
var token = $("#{{ viz.token }}");
|
||||
var loading = $("#{{ viz.token }}").find("img.loading");
|
||||
var chart = $("#{{ viz.token }}").find("div.chart");
|
||||
var refresh = function(){
|
||||
chart.hide();
|
||||
loading.show();
|
||||
var url = "{{ viz.get_url(json="true")|safe }}";
|
||||
$.getJSON(url, function(data){
|
||||
chart.width(token.width());
|
||||
chart.height(token.height()-40);
|
||||
chart.highcharts('{{ viz.chart_call }}', data);
|
||||
chart.show();
|
||||
token.find("img.loading").hide();
|
||||
})
|
||||
.fail(function(xhr) {
|
||||
var err = '<div class="alert alert-danger">' + xhr.responseText + '</div>';
|
||||
loading.hide();
|
||||
chart.show();
|
||||
chart.html(err);
|
||||
});
|
||||
};
|
||||
refresh();
|
||||
token.parent().find("a.refresh").click(refresh);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro viz_css(viz) %}
|
||||
{% endmacro %}
|
||||
|
||||
9
panoramix/templates/panoramix/viz_markup.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{% macro viz_html(viz) %}
|
||||
<div style="padding: 10px;overflow: auto; height: 100%;">{{ viz.rendered()|safe }}</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro viz_js(viz) %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro viz_css(viz) %}
|
||||
{% endmacro %}
|
||||
99
panoramix/templates/panoramix/viz_nvd3.html
Normal file
@@ -0,0 +1,99 @@
|
||||
{% macro viz_html(viz) %}
|
||||
<div id="{{ viz.token }}" style="height:100%; width: 100%">
|
||||
<img src="{{ url_for("static", filename="loading.gif") }}" class="loading">
|
||||
<div class="chart" style="height:100%; width: 100%"></div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro viz_js(viz) %}
|
||||
<script>
|
||||
$( document ).ready(function() {
|
||||
function UTC(dttm){
|
||||
return v = new Date(dttm.getUTCFullYear(), dttm.getUTCMonth(), dttm.getUTCDate(), dttm.getUTCHours(), dttm.getUTCMinutes(), dttm.getUTCSeconds());
|
||||
}
|
||||
var tickMultiFormat = d3.time.format.multi([
|
||||
[".%L", function(d) { return d.getMilliseconds(); }],
|
||||
[":%S", function(d) { return d.getSeconds(); }],
|
||||
["%I:%M", function(d) { return d.getMinutes(); }],
|
||||
["%I %p", function(d) { return d.getHours(); }],
|
||||
["%a %d", function(d) { return d.getDay() && d.getDate() != 1; }],
|
||||
["%b %d", function(d) { return d.getDate() != 1; }],
|
||||
["%B", function(d) { return d.getMonth(); }],
|
||||
["%Y", function() { return true; }]
|
||||
]);
|
||||
colors = [
|
||||
"#FF5A5F", "#007A87", "#7B0051", "#00D1C1", "#8CE071", "#FFB400",
|
||||
"#FFAA91", "#B4A76C", "#9CA299", "#565A5C"
|
||||
];
|
||||
var token = d3.select("#{{ viz.token }}");
|
||||
var jtoken = $("#{{ viz.token }}");
|
||||
var loading = $("#{{ viz.token }}").find("img.loading");
|
||||
var chart = $("#{{ viz.token }}").find("div.chart");
|
||||
var refresh = function(){
|
||||
chart.hide();
|
||||
loading.show();
|
||||
var url = "{{ viz.get_url(json="true")|safe }}";
|
||||
$.getJSON(url, function(data){
|
||||
nv.addGraph(function() {
|
||||
{% if viz.chart_type == 'nvd3_line' %}
|
||||
{% if viz.args.show_brush == 'y' %}
|
||||
var chart = nv.models.lineWithFocusChart()
|
||||
var xext = chart.xAxis.scale().domain();
|
||||
chart
|
||||
.x2Axis
|
||||
.tickFormat(function (d) {return tickMultiFormat(UTC(new Date(d))); })
|
||||
.tickValues([]);
|
||||
{% elif viz.chart_type == 'nvd3_line' %}
|
||||
var chart = nv.models.lineChart()
|
||||
{% endif %}
|
||||
|
||||
chart.xScale(d3.time.scale());
|
||||
chart.xAxis
|
||||
.showMaxMin(false)
|
||||
.tickFormat(function (d) {return tickMultiFormat(new Date(d)); });
|
||||
chart.showLegend({{ "{}".format(viz.args.show_legend=='y')|lower }});
|
||||
|
||||
{% elif viz.chart_type == 'nvd3_bar' %}
|
||||
var chart = nv.models.multiBarChart()
|
||||
.showControls(true) //Allow user to switch between 'Grouped' and 'Stacked' mode.
|
||||
.groupSpacing(0.1); //Distance between each group of bars.
|
||||
|
||||
chart.xAxis
|
||||
.showMaxMin(false)
|
||||
.tickFormat(function (d) {return tickMultiFormat(UTC(new Date(d))); });
|
||||
{% endif %}
|
||||
chart.yAxis.tickFormat(d3.format('.3s'));
|
||||
{% if viz.chart_type == "nvd3_line" and viz.args.rich_tooltip == 'y' %}
|
||||
chart.useInteractiveGuideline(true);
|
||||
{% endif %}
|
||||
{% if viz.args.y_axis_zero == 'y' %}
|
||||
chart.forceY([0, 1]);
|
||||
{% elif viz.args.y_log_scale == 'y' %}
|
||||
chart.yScale(d3.scale.log());
|
||||
{% endif %}
|
||||
|
||||
|
||||
token.select('.chart').append("svg")
|
||||
.datum(data)
|
||||
.transition().duration(500)
|
||||
.call(chart);
|
||||
|
||||
return chart;
|
||||
});
|
||||
chart.show();
|
||||
loading.hide();
|
||||
}).fail(function(xhr) {
|
||||
var err = '<div class="alert alert-danger">' + xhr.responseText + '</div>';
|
||||
loading.hide();
|
||||
chart.show();
|
||||
chart.html(err);
|
||||
});
|
||||
};
|
||||
refresh();
|
||||
jtoken.parent().find("a.refresh").click(refresh);
|
||||
});
|
||||
</script>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro viz_css(viz) %}
|
||||
{% endmacro %}
|
||||
13
panoramix/templates/panoramix/viz_standalone.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<html>
|
||||
<head>
|
||||
{% if viz.args.get("skip_libs") != "true" %}
|
||||
{% block head %}
|
||||
<script src="{{url_for('appbuilder.static',filename='js/jquery-latest.js')}}"></script>
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
{% block tail %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% block viz_html %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,55 +1,62 @@
|
||||
{% extends "panoramix/datasource.html" %}
|
||||
|
||||
{% block head_css %}
|
||||
{{super()}}
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='dataTables.bootstrap.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block viz %}
|
||||
{{ super() }}
|
||||
{% if not error_msg %}
|
||||
<table class="dataframe table table-striped table-bordered table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
{% for col in df.columns if not col.endswith('__perc') %}
|
||||
<th>{{ col }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in df.to_dict(orient="records") %}
|
||||
<tr>
|
||||
{% for col in df.columns if not col.endswith('__perc') %}
|
||||
{% if col + '__perc' in df.columns %}
|
||||
<td style="background-image: linear-gradient(to right, lightgrey, lightgrey {{ row[col+'__perc'] }}%, rgba(0,0,0,0) {{ row[col+'__perc'] }}%">{{ row[col] }}</td>
|
||||
{% else %}
|
||||
<td>{{ row[col] }}</td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% macro viz_html(viz) %}
|
||||
{% if viz.args.get("async") == "true" %}
|
||||
{% set df = viz.get_df() %}
|
||||
<table class="dataframe table table-striped table-bordered table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
{% for col in df.columns if not col.endswith('__perc') %}
|
||||
<th>{{ col }}</th>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in df.to_dict(orient="records") %}
|
||||
<tr>
|
||||
{% for col in df.columns if not col.endswith('__perc') %}
|
||||
{% if col + '__perc' in df.columns %}
|
||||
<td style="background-image: linear-gradient(to right, lightgrey, lightgrey {{ row[col+'__perc'] }}%, rgba(0,0,0,0) {{ row[col+'__perc'] }}%">
|
||||
{{ row[col] }}
|
||||
</td>
|
||||
{% else %}
|
||||
<td>{{ row[col] }}</td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div id="{{ viz.token }}" style="display: none;overflow: auto; height: 100%;"></div>
|
||||
<img src="{{ url_for("static", filename="loading.gif") }}" class="loading">
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% block extra_fields %}
|
||||
<div>{{ form.row_limit.label }}: {{ form.row_limit(class_="form-control select2") }}</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block tail %}
|
||||
{{ super() }}
|
||||
<script src="{{ url_for('static', filename='jquery.dataTables.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='dataTables.bootstrap.js') }}"></script>
|
||||
|
||||
<script>
|
||||
//$('table').css('background-color', 'white');
|
||||
$(document).ready(function() {
|
||||
var table = $('table').DataTable({
|
||||
paging: false,
|
||||
{% macro viz_js(viz) %}
|
||||
{% if viz.args.get("async") != "true" %}
|
||||
<script>
|
||||
$( document ).ready(function() {
|
||||
var url = "{{ viz.get_url(async="true", standalone="true", skip_libs="true")|safe }}";
|
||||
var token = $("#{{ viz.token }}");
|
||||
token.load(url, function(response, status, xhr){
|
||||
if(status=="error"){
|
||||
var err = '<div class="alert alert-danger">' + xhr.responseText + '</div>';
|
||||
token.html(err);
|
||||
token.show();
|
||||
}
|
||||
else{
|
||||
var table = token.find('table').DataTable({
|
||||
paging: false,
|
||||
searching: false,
|
||||
});
|
||||
table.column('-1').order( 'desc' ).draw();
|
||||
}
|
||||
token.show();
|
||||
token.parent().find("img.loading").hide();
|
||||
});
|
||||
table.column('-1').order( 'desc' ).draw();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro viz_css(viz) %}
|
||||
{% endmacro %}
|
||||
|
||||
76
panoramix/templates/panoramix/viz_word_cloud.html
Normal file
@@ -0,0 +1,76 @@
|
||||
{% macro viz_html(viz) %}
|
||||
<div id="{{ viz.token }}" style="height: 100%;">
|
||||
<img src="{{ url_for("static", filename="loading.gif") }}" class="loading">
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro viz_js(viz) %}
|
||||
<script>
|
||||
$( document ).ready(function() {
|
||||
var token = d3.select("#{{ viz.token }}");
|
||||
function refresh() {
|
||||
var range = [
|
||||
{{ viz.args.get('size_from') or 20 }},
|
||||
{{ viz.args.get('size_to') or 100 }}
|
||||
];
|
||||
var rotation = "{{ viz.args.get("rotation", "random") }}";
|
||||
if (rotation == "square")
|
||||
var f_rotation = function() { return ~~(Math.random() * 2) * 90; };
|
||||
else if (rotation == "flat")
|
||||
var f_rotation = function() { return 0};
|
||||
else
|
||||
var f_rotation = function() { return (~~(Math.random() * 6) - 3) * 30;};
|
||||
|
||||
var url = "{{ viz.get_url(json="true")|safe }}";
|
||||
var box = token.node().getBoundingClientRect();
|
||||
var size = [box.width, box.height-25];
|
||||
d3.json(url, function(error, data){
|
||||
if(error != null){
|
||||
var err = '<div class="alert alert-danger">' + error.responseText + '</div>';
|
||||
token.html(err);
|
||||
return '';
|
||||
}
|
||||
scale = d3.scale.linear()
|
||||
.range(range)
|
||||
.domain(d3.extent(data, function(d) { return d.size; }));
|
||||
var fill = d3.scale.category20();
|
||||
var layout = d3.layout.cloud()
|
||||
.size(size)
|
||||
.words(data)
|
||||
.padding(5)
|
||||
.rotate(f_rotation)
|
||||
.font("serif")
|
||||
.fontSize(function(d) { return scale(d.size); })
|
||||
.on("end", draw);
|
||||
layout.start();
|
||||
function draw(words) {
|
||||
token.selectAll("*").remove();
|
||||
|
||||
token.append("svg")
|
||||
.attr("width", layout.size()[0])
|
||||
.attr("height", layout.size()[1])
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + layout.size()[0] / 2 + "," + layout.size()[1] / 2 + ")")
|
||||
.selectAll("text")
|
||||
.data(words)
|
||||
.enter().append("text")
|
||||
.style("font-size", function(d) { return d.size + "px"; })
|
||||
.style("font-family", "Impact")
|
||||
.style("fill", function(d, i) { return fill(i); })
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("transform", function(d) {
|
||||
return "translate(" + [d.x, d.y] + ") rotate(" + d.rotate + ")";
|
||||
})
|
||||
.text(function(d) { return d.text; });
|
||||
}
|
||||
});
|
||||
}
|
||||
refresh();
|
||||
jtoken = $(token);
|
||||
jtoken.parent().find("a.refresh").click(refresh);
|
||||
});
|
||||
</script>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro viz_css(viz) %}
|
||||
{% endmacro %}
|
||||
@@ -1,5 +1,36 @@
|
||||
from datetime import datetime
|
||||
import hashlib
|
||||
from sqlalchemy.types import TypeDecorator, TEXT
|
||||
import json
|
||||
import parsedatetime
|
||||
import functools
|
||||
|
||||
|
||||
class memoized(object):
|
||||
"""Decorator that caches a function's return value each time it is called.
|
||||
If called later with the same arguments, the cached value is returned, and
|
||||
not re-evaluated.
|
||||
"""
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
self.cache = {}
|
||||
def __call__(self, *args):
|
||||
try:
|
||||
return self.cache[args]
|
||||
except KeyError:
|
||||
value = self.func(*args)
|
||||
self.cache[args] = value
|
||||
return value
|
||||
except TypeError:
|
||||
# uncachable -- for instance, passing a list as an argument.
|
||||
# Better to not cache than to blow up entirely.
|
||||
return self.func(*args)
|
||||
def __repr__(self):
|
||||
"""Return the function's docstring."""
|
||||
return self.func.__doc__
|
||||
def __get__(self, obj, objtype):
|
||||
"""Support instance methods."""
|
||||
return functools.partial(self.__call__, obj)
|
||||
|
||||
|
||||
def parse_human_datetime(s):
|
||||
@@ -33,3 +64,34 @@ def parse_human_timedelta(s):
|
||||
d = datetime(
|
||||
d.tm_year, d.tm_mon, d.tm_mday, d.tm_hour, d.tm_min, d.tm_sec)
|
||||
return d - dttm
|
||||
|
||||
|
||||
|
||||
class JSONEncodedDict(TypeDecorator):
|
||||
"""Represents an immutable structure as a json-encoded string."""
|
||||
impl = TEXT
|
||||
def process_bind_param(self, value, dialect):
|
||||
if value is not None:
|
||||
value = json.dumps(value)
|
||||
|
||||
return value
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
if value is not None:
|
||||
value = json.loads(value)
|
||||
return value
|
||||
|
||||
def color(s):
|
||||
"""
|
||||
Get a consistent color from the same string using a hash function
|
||||
|
||||
>>> color("foo")
|
||||
'#FF5A5F'
|
||||
"""
|
||||
colors = [
|
||||
"#FF5A5F", "#007A87", "#7B0051", "#00D1C1", "#8CE071", "#FFB400",
|
||||
"#FFAA91", "#B4A76C", "#9CA299", "#565A5C"
|
||||
]
|
||||
h = hashlib.md5(s)
|
||||
i = int(h.hexdigest(), 16)
|
||||
return colors[i % len(colors)]
|
||||
|
||||
@@ -3,34 +3,45 @@ import json
|
||||
import logging
|
||||
|
||||
from flask import request, redirect, flash, Response
|
||||
from flask.ext.appbuilder.models.sqla.interface import SQLAInterface
|
||||
from flask.ext.appbuilder import ModelView, CompactCRUDMixin, BaseView, expose
|
||||
from flask.ext.appbuilder.actions import action
|
||||
from flask.ext.appbuilder.models.sqla.interface import SQLAInterface
|
||||
from flask.ext.appbuilder.security.decorators import has_access
|
||||
from pydruid.client import doublesum
|
||||
from sqlalchemy import create_engine
|
||||
from wtforms.validators import ValidationError
|
||||
from flask.ext.appbuilder.actions import action
|
||||
|
||||
from panoramix import appbuilder, db, models, viz, utils, app
|
||||
|
||||
config = app.config
|
||||
|
||||
|
||||
def validate_json(form, field):
|
||||
try:
|
||||
json.loads(field.data)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
raise ValidationError("Json isn't valid")
|
||||
|
||||
|
||||
class DeleteMixin(object):
|
||||
@action("muldelete", "Delete", "Delete all Really?", "fa-trash", single=False)
|
||||
@action(
|
||||
"muldelete", "Delete", "Delete all Really?", "fa-trash", single=False)
|
||||
def muldelete(self, items):
|
||||
self.datamodel.delete_all(items)
|
||||
self.update_redirect()
|
||||
return redirect(self.get_redirect())
|
||||
|
||||
|
||||
class TableColumnInlineView(CompactCRUDMixin, ModelView):
|
||||
class PanoramixModelView(ModelView):
|
||||
page_size = 100
|
||||
|
||||
|
||||
class TableColumnInlineView(CompactCRUDMixin, PanoramixModelView):
|
||||
datamodel = SQLAInterface(models.TableColumn)
|
||||
can_delete = False
|
||||
edit_columns = [
|
||||
'column_name', 'description', 'table', 'groupby', 'filterable',
|
||||
'column_name', 'description', 'groupby', 'filterable', 'table',
|
||||
'count_distinct', 'sum', 'min', 'max']
|
||||
list_columns = [
|
||||
'column_name', 'type', 'groupby', 'filterable', 'count_distinct',
|
||||
@@ -39,7 +50,7 @@ class TableColumnInlineView(CompactCRUDMixin, ModelView):
|
||||
appbuilder.add_view_no_menu(TableColumnInlineView)
|
||||
|
||||
|
||||
class ColumnInlineView(CompactCRUDMixin, ModelView):
|
||||
class ColumnInlineView(CompactCRUDMixin, PanoramixModelView):
|
||||
datamodel = SQLAInterface(models.Column)
|
||||
edit_columns = [
|
||||
'column_name', 'description', 'datasource', 'groupby',
|
||||
@@ -53,25 +64,23 @@ class ColumnInlineView(CompactCRUDMixin, ModelView):
|
||||
def post_update(self, col):
|
||||
col.generate_metrics()
|
||||
|
||||
def post_update(self, col):
|
||||
col.generate_metrics()
|
||||
|
||||
appbuilder.add_view_no_menu(ColumnInlineView)
|
||||
|
||||
class SqlMetricInlineView(CompactCRUDMixin, ModelView):
|
||||
|
||||
class SqlMetricInlineView(CompactCRUDMixin, PanoramixModelView):
|
||||
datamodel = SQLAInterface(models.SqlMetric)
|
||||
list_columns = ['metric_name', 'verbose_name', 'metric_type' ]
|
||||
list_columns = ['metric_name', 'verbose_name', 'metric_type']
|
||||
edit_columns = [
|
||||
'metric_name', 'description', 'verbose_name', 'metric_type',
|
||||
'table', 'expression']
|
||||
'expression', 'table']
|
||||
add_columns = edit_columns
|
||||
page_size = 100
|
||||
appbuilder.add_view_no_menu(SqlMetricInlineView)
|
||||
|
||||
|
||||
class MetricInlineView(CompactCRUDMixin, ModelView):
|
||||
class MetricInlineView(CompactCRUDMixin, PanoramixModelView):
|
||||
datamodel = SQLAInterface(models.Metric)
|
||||
list_columns = ['metric_name', 'verbose_name', 'metric_type' ]
|
||||
list_columns = ['metric_name', 'verbose_name', 'metric_type']
|
||||
edit_columns = [
|
||||
'metric_name', 'description', 'verbose_name', 'metric_type',
|
||||
'datasource', 'json']
|
||||
@@ -84,7 +93,53 @@ class MetricInlineView(CompactCRUDMixin, ModelView):
|
||||
appbuilder.add_view_no_menu(MetricInlineView)
|
||||
|
||||
|
||||
class ClusterModelView(ModelView, DeleteMixin):
|
||||
class DatabaseView(PanoramixModelView, DeleteMixin):
|
||||
datamodel = SQLAInterface(models.Database)
|
||||
list_columns = ['database_name', 'created_by', 'created_on']
|
||||
add_columns = ['database_name', 'sqlalchemy_uri']
|
||||
edit_columns = add_columns
|
||||
add_template = "panoramix/models/database/add.html"
|
||||
edit_template = "panoramix/models/database/edit.html"
|
||||
description_columns = {
|
||||
'sqlalchemy_uri': (
|
||||
"Refer to the SqlAlchemy docs for more information on how "
|
||||
"to structure your URI here: "
|
||||
"http://docs.sqlalchemy.org/en/rel_1_0/core/engines.html")
|
||||
}
|
||||
|
||||
appbuilder.add_view(
|
||||
DatabaseView,
|
||||
"Databases",
|
||||
icon="fa-database",
|
||||
category="Sources",
|
||||
category_icon='fa-database',)
|
||||
|
||||
|
||||
class TableView(PanoramixModelView, DeleteMixin):
|
||||
datamodel = SQLAInterface(models.Table)
|
||||
list_columns = ['table_link', 'database']
|
||||
add_columns = ['table_name', 'database', 'default_endpoint']
|
||||
edit_columns = [
|
||||
'table_name', 'database', 'main_dttm_col', 'default_endpoint']
|
||||
related_views = [TableColumnInlineView, SqlMetricInlineView]
|
||||
|
||||
def post_add(self, table):
|
||||
table.fetch_metadata()
|
||||
|
||||
def post_update(self, table):
|
||||
table.fetch_metadata()
|
||||
|
||||
appbuilder.add_view(
|
||||
TableView,
|
||||
"Tables",
|
||||
category="Sources",
|
||||
icon='fa-table',)
|
||||
|
||||
|
||||
appbuilder.add_separator("Sources")
|
||||
|
||||
|
||||
class ClusterModelView(PanoramixModelView, DeleteMixin):
|
||||
datamodel = SQLAInterface(models.Cluster)
|
||||
add_columns = [
|
||||
'cluster_name',
|
||||
@@ -97,45 +152,45 @@ class ClusterModelView(ModelView, DeleteMixin):
|
||||
appbuilder.add_view(
|
||||
ClusterModelView,
|
||||
"Druid Clusters",
|
||||
icon="fa-server",
|
||||
category="Admin",
|
||||
category_icon='fa-cogs',)
|
||||
icon="fa-cubes",
|
||||
category="Sources",
|
||||
category_icon='fa-database',)
|
||||
|
||||
|
||||
class DatabaseView(ModelView, DeleteMixin):
|
||||
datamodel = SQLAInterface(models.Database)
|
||||
list_columns = ['database_name']
|
||||
add_columns = ['database_name', 'sqlalchemy_uri']
|
||||
edit_columns = add_columns
|
||||
class SliceModelView(PanoramixModelView, DeleteMixin):
|
||||
datamodel = SQLAInterface(models.Slice)
|
||||
can_add = False
|
||||
list_columns = [
|
||||
'slice_link', 'viz_type', 'datasource_type',
|
||||
'datasource', 'created_by']
|
||||
edit_columns = [
|
||||
'slice_name', 'viz_type', 'druid_datasource',
|
||||
'table', 'dashboards', 'params']
|
||||
|
||||
appbuilder.add_view(
|
||||
DatabaseView,
|
||||
"Databases",
|
||||
icon="fa-database",
|
||||
category="Admin",
|
||||
category_icon='fa-cogs',)
|
||||
SliceModelView,
|
||||
"Slices",
|
||||
icon="fa-bar-chart",
|
||||
category="",
|
||||
category_icon='',)
|
||||
|
||||
|
||||
class TableView(ModelView, DeleteMixin):
|
||||
datamodel = SQLAInterface(models.Table)
|
||||
list_columns = ['table_link', 'database']
|
||||
add_columns = ['table_name', 'database', 'default_endpoint']
|
||||
edit_columns = ['table_name', 'database', 'main_datetime_column', 'default_endpoint']
|
||||
related_views = [TableColumnInlineView, SqlMetricInlineView]
|
||||
class DashboardModelView(PanoramixModelView, DeleteMixin):
|
||||
datamodel = SQLAInterface(models.Dashboard)
|
||||
list_columns = ['dashboard_link', 'created_by']
|
||||
edit_columns = ['dashboard_title', 'slices', 'position_json']
|
||||
add_columns = edit_columns
|
||||
|
||||
def post_add(self, table):
|
||||
table.fetch_metadata()
|
||||
|
||||
def post_update(self, table):
|
||||
table.fetch_metadata()
|
||||
|
||||
appbuilder.add_view(
|
||||
TableView,
|
||||
"Tables",
|
||||
icon='fa-table',)
|
||||
DashboardModelView,
|
||||
"Dashboards",
|
||||
icon="fa-dashboard",
|
||||
category="",
|
||||
category_icon='',)
|
||||
|
||||
|
||||
class DatasourceModelView(ModelView, DeleteMixin):
|
||||
class DatasourceModelView(PanoramixModelView, DeleteMixin):
|
||||
datamodel = SQLAInterface(models.Datasource)
|
||||
list_columns = [
|
||||
'datasource_link', 'cluster', 'owner', 'is_featured', 'is_hidden']
|
||||
@@ -156,6 +211,7 @@ class DatasourceModelView(ModelView, DeleteMixin):
|
||||
appbuilder.add_view(
|
||||
DatasourceModelView,
|
||||
"Druid Datasources",
|
||||
category="Sources",
|
||||
icon="fa-cube")
|
||||
|
||||
|
||||
@@ -171,60 +227,141 @@ def ping():
|
||||
|
||||
class Panoramix(BaseView):
|
||||
@has_access
|
||||
@expose("/table/<table_id>/")
|
||||
def table(self, table_id):
|
||||
@expose("/datasource/<datasource_type>/<datasource_id>/")
|
||||
def datasource(self, datasource_type, datasource_id):
|
||||
action = request.args.get('action')
|
||||
if action == 'save':
|
||||
session = db.session()
|
||||
d = request.args.to_dict(flat=False)
|
||||
del d['action']
|
||||
as_list = ('metrics', 'groupby')
|
||||
for k in d:
|
||||
v = d.get(k)
|
||||
if k in as_list and not isinstance(v, list):
|
||||
d[k] = [v] if v else []
|
||||
if k not in as_list and isinstance(v, list):
|
||||
d[k] = v[0]
|
||||
|
||||
table = (
|
||||
db.session
|
||||
.query(models.Table)
|
||||
.filter_by(id=table_id)
|
||||
.first()
|
||||
)
|
||||
table_id = druid_datasource_id = None
|
||||
datasource_type = request.args.get('datasource_type')
|
||||
if datasource_type in ('datasource', 'druid'):
|
||||
druid_datasource_id = request.args.get('datasource_id')
|
||||
elif datasource_type == 'table':
|
||||
table_id = request.args.get('datasource_id')
|
||||
|
||||
slice_name = request.args.get('slice_name')
|
||||
|
||||
obj = models.Slice(
|
||||
params=json.dumps(d, indent=4, sort_keys=True),
|
||||
viz_type=request.args.get('viz_type'),
|
||||
datasource_name=request.args.get('datasource_name'),
|
||||
druid_datasource_id=druid_datasource_id,
|
||||
table_id=table_id,
|
||||
datasource_type=datasource_type,
|
||||
slice_name=slice_name,
|
||||
)
|
||||
session.add(obj)
|
||||
session.commit()
|
||||
flash("Slice <{}> has been added to the pie".format(slice_name), "info")
|
||||
redirect(obj.slice_url)
|
||||
|
||||
if datasource_type == "table":
|
||||
datasource = (
|
||||
db.session
|
||||
.query(models.Table)
|
||||
.filter_by(id=datasource_id)
|
||||
.first()
|
||||
)
|
||||
else:
|
||||
datasource = (
|
||||
db.session
|
||||
.query(models.Datasource)
|
||||
.filter_by(id=datasource_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not datasource:
|
||||
flash("The datasource seem to have been deleted", "alert")
|
||||
viz_type = request.args.get("viz_type")
|
||||
if not viz_type and table.default_endpoint:
|
||||
return redirect(table.default_endpoint)
|
||||
if not viz_type:
|
||||
viz_type = "table"
|
||||
obj = viz.viz_types[viz_type](
|
||||
table,
|
||||
form_data=request.args, view=self)
|
||||
if request.args.get("json"):
|
||||
return Response(
|
||||
json.dumps(obj.get_query(), indent=4),
|
||||
status=200,
|
||||
mimetype="application/json")
|
||||
if not hasattr(obj, 'df') or obj.df is None or obj.df.empty:
|
||||
pass
|
||||
#return obj.render_no_data()
|
||||
return obj.render()
|
||||
|
||||
@has_access
|
||||
@expose("/datasource/<datasource_name>/")
|
||||
def datasource(self, datasource_name):
|
||||
viz_type = request.args.get("viz_type")
|
||||
|
||||
datasource = (
|
||||
db.session
|
||||
.query(models.Datasource)
|
||||
.filter_by(datasource_name=datasource_name)
|
||||
.first()
|
||||
)
|
||||
if not viz_type and datasource.default_endpoint:
|
||||
return redirect(datasource.default_endpoint)
|
||||
if not viz_type:
|
||||
viz_type = "table"
|
||||
obj = viz.viz_types[viz_type](
|
||||
datasource,
|
||||
form_data=request.args, view=self)
|
||||
if request.args.get("json"):
|
||||
form_data=request.args)
|
||||
if request.args.get("json") == "true":
|
||||
try:
|
||||
payload = obj.get_json()
|
||||
status=200
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
payload = str(e)
|
||||
status=500
|
||||
return Response(
|
||||
json.dumps(obj.get_query(), indent=4),
|
||||
status=200,
|
||||
payload,
|
||||
status=status,
|
||||
mimetype="application/json")
|
||||
if not hasattr(obj, 'df') or obj.df is None or obj.df.empty:
|
||||
return obj.render_no_data()
|
||||
else:
|
||||
try:
|
||||
resp = self.render_template("panoramix/viz.html", viz=obj)
|
||||
except Exception as e:
|
||||
if config.get("DEBUG"):
|
||||
raise(e)
|
||||
return Response(
|
||||
str(e),
|
||||
status=500,
|
||||
mimetype="application/json")
|
||||
return resp
|
||||
|
||||
return obj.render()
|
||||
@has_access
|
||||
@expose("/save_dash/<dashboard_id>/", methods=['GET', 'POST'])
|
||||
def save_dash(self, dashboard_id):
|
||||
data = json.loads(request.form.get('data'))
|
||||
slice_ids = [int(d['slice_id']) for d in data]
|
||||
print slice_ids
|
||||
session = db.session()
|
||||
Dash = models.Dashboard
|
||||
dash = session.query(Dash).filter_by(id=dashboard_id).first()
|
||||
dash.slices = [o for o in dash.slices if o.id in slice_ids]
|
||||
print dash.slices
|
||||
dash.position_json = json.dumps(data, indent=4)
|
||||
session.merge(dash)
|
||||
session.commit()
|
||||
session.close()
|
||||
return "SUCCESS"
|
||||
|
||||
@has_access
|
||||
@expose("/testconn", methods=["POST"])
|
||||
def testconn(self):
|
||||
try:
|
||||
uri = request.form.get('uri')
|
||||
db = create_engine(uri)
|
||||
db.connect()
|
||||
return "SUCCESS"
|
||||
except Exception as e:
|
||||
return Response(
|
||||
str(e),
|
||||
status=500,
|
||||
mimetype="application/json")
|
||||
|
||||
@has_access
|
||||
@expose("/dashboard/<id_>/")
|
||||
def dashboard(self, id_):
|
||||
session = db.session()
|
||||
dashboard = (
|
||||
session
|
||||
.query(models.Dashboard)
|
||||
.filter(models.Dashboard.id == id_)
|
||||
.first()
|
||||
)
|
||||
pos_dict = {}
|
||||
if dashboard.position_json:
|
||||
pos_dict = {
|
||||
int(o['slice_id']):o for o in json.loads(dashboard.position_json)}
|
||||
return self.render_template(
|
||||
"panoramix/dashboard.html", dashboard=dashboard,
|
||||
pos_dict=pos_dict)
|
||||
|
||||
@has_access
|
||||
@expose("/refresh_datasources/")
|
||||
@@ -259,9 +396,6 @@ appbuilder.add_view_no_menu(Panoramix)
|
||||
appbuilder.add_link(
|
||||
"Refresh Druid Metadata",
|
||||
href='/panoramix/refresh_datasources/',
|
||||
category='Admin',
|
||||
category_icon='fa-cogs',
|
||||
category='Sources',
|
||||
category_icon='fa-database',
|
||||
icon="fa-cog")
|
||||
|
||||
#models.Metric.__table__.drop(db.engine)
|
||||
db.create_all()
|
||||
|
||||
540
panoramix/viz.py
@@ -1,99 +1,88 @@
|
||||
from datetime import datetime
|
||||
from flask import flash, request
|
||||
import pandas as pd
|
||||
from collections import OrderedDict
|
||||
from panoramix import utils
|
||||
from panoramix.highchart import Highchart, HighchartBubble
|
||||
from wtforms import Form, SelectMultipleField, SelectField, TextField
|
||||
import config
|
||||
import logging
|
||||
from datetime import datetime
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from flask import flash
|
||||
from markdown import markdown
|
||||
from pandas.io.json import dumps
|
||||
from werkzeug.datastructures import MultiDict
|
||||
from werkzeug.urls import Href
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from panoramix import app, utils
|
||||
from panoramix.highchart import Highchart, HighchartBubble
|
||||
from panoramix.forms import form_factory
|
||||
|
||||
config = app.config
|
||||
|
||||
CHART_ARGS = {
|
||||
'height': 700,
|
||||
'title': None,
|
||||
'target_div': 'chart',
|
||||
}
|
||||
|
||||
|
||||
class OmgWtForm(Form):
|
||||
field_order = (
|
||||
'viz_type', 'granularity', 'since', 'group_by', 'limit')
|
||||
def fields(self):
|
||||
fields = []
|
||||
for field in self.field_order:
|
||||
if hasattr(self, field):
|
||||
obj = getattr(self, field)
|
||||
if isinstance(obj, Field):
|
||||
fields.append(getattr(self, field))
|
||||
return fields
|
||||
|
||||
|
||||
def form_factory(datasource, form_args=None, extra_fields_dict=None):
|
||||
extra_fields_dict = extra_fields_dict or {}
|
||||
|
||||
if form_args:
|
||||
limit = form_args.get("limit")
|
||||
try:
|
||||
limit = int(limit)
|
||||
if limit not in limits:
|
||||
limits.append(limit)
|
||||
limits = sorted(limits)
|
||||
except:
|
||||
pass
|
||||
|
||||
class QueryForm(OmgWtForm):
|
||||
viz_type = SelectField(
|
||||
'Viz',
|
||||
choices=[(k, v.verbose_name) for k, v in viz_types.items()])
|
||||
metrics = SelectMultipleField('Metrics', choices=datasource.metrics_combo)
|
||||
groupby = SelectMultipleField(
|
||||
'Group by', choices=[
|
||||
(s, s) for s in datasource.groupby_column_names])
|
||||
granularity = TextField('Time Granularity', default="one day")
|
||||
since = TextField('Since', default="one day ago")
|
||||
until = TextField('Until', default="now")
|
||||
for i in range(10):
|
||||
setattr(QueryForm, 'flt_col_' + str(i), SelectField(
|
||||
'Filter 1', choices=[(s, s) for s in datasource.filterable_column_names]))
|
||||
setattr(QueryForm, 'flt_op_' + str(i), SelectField(
|
||||
'Filter 1', choices=[(m, m) for m in ['in', 'not in']]))
|
||||
setattr(QueryForm, 'flt_eq_' + str(i), TextField("Super"))
|
||||
for k, v in extra_fields_dict.items():
|
||||
setattr(QueryForm, k, v)
|
||||
return QueryForm
|
||||
|
||||
|
||||
class BaseViz(object):
|
||||
verbose_name = "Base Viz"
|
||||
template = "panoramix/datasource.html"
|
||||
hidden_fields = []
|
||||
def __init__(self, datasource, form_data, view):
|
||||
self.datasource = datasource
|
||||
self.form_class = self.form_class()
|
||||
self.view = view
|
||||
self.form_data = form_data
|
||||
self.metrics = form_data.getlist('metrics') or ['count']
|
||||
self.groupby = form_data.getlist('groupby') or []
|
||||
template = None
|
||||
form_fields = [
|
||||
'viz_type', 'metrics', 'groupby', 'granularity',
|
||||
('since', 'until')]
|
||||
js_files = []
|
||||
css_files = []
|
||||
|
||||
def __init__(self, datasource, form_data):
|
||||
self.datasource = datasource
|
||||
if isinstance(form_data, MultiDict):
|
||||
self.args = form_data.to_dict(flat=False)
|
||||
else:
|
||||
self.args = form_data
|
||||
self.form_data = form_data
|
||||
self.token = self.args.get('token', 'token_' + uuid.uuid4().hex[:8])
|
||||
|
||||
as_list = ('metrics', 'groupby')
|
||||
for k, v in self.args.items():
|
||||
if k in as_list and not isinstance(v, list):
|
||||
self.args[k] = [v]
|
||||
elif k not in as_list and isinstance(v, list) and v:
|
||||
self.args[k] = v[0]
|
||||
|
||||
self.metrics = self.args.get('metrics') or ['count']
|
||||
self.groupby = self.args.get('groupby') or []
|
||||
|
||||
def get_url(self, **kwargs):
|
||||
d = self.args.copy()
|
||||
if 'action' in d:
|
||||
del d['action']
|
||||
d.update(kwargs)
|
||||
href = Href(
|
||||
'/panoramix/datasource/{self.datasource.type}/'
|
||||
'{self.datasource.id}/'.format(**locals()))
|
||||
return href(d)
|
||||
|
||||
def get_df(self):
|
||||
self.error_msg = ""
|
||||
self.results = None
|
||||
try:
|
||||
self.results = self.bake_query()
|
||||
self.df = self.results.df
|
||||
if self.df is not None:
|
||||
if 'timestamp' in self.df.columns:
|
||||
self.df.timestamp = pd.to_datetime(self.df.timestamp)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
self.error_msg = str(e)
|
||||
|
||||
self.results = self.bake_query()
|
||||
df = self.results.df
|
||||
if df is None or df.empty:
|
||||
raise Exception("No data, review your incantations!")
|
||||
else:
|
||||
if 'timestamp' in df.columns:
|
||||
df.timestamp = pd.to_datetime(df.timestamp)
|
||||
return df
|
||||
|
||||
@property
|
||||
def form(self):
|
||||
return self.form_class(self.form_data)
|
||||
|
||||
@property
|
||||
def form_class(self):
|
||||
return form_factory(self.datasource, request.args)
|
||||
return form_factory(self)
|
||||
|
||||
def query_filters(self):
|
||||
args = self.form_data
|
||||
args = self.args
|
||||
# Building filters
|
||||
filters = []
|
||||
for i in range(1, 10):
|
||||
@@ -111,17 +100,16 @@ class BaseViz(object):
|
||||
"""
|
||||
Building a query object
|
||||
"""
|
||||
ds = self.datasource
|
||||
args = self.form_data
|
||||
groupby = args.getlist("groupby") or []
|
||||
metrics = args.getlist("metrics") or ['count']
|
||||
args = self.args
|
||||
groupby = args.get("groupby") or []
|
||||
metrics = args.get("metrics") or ['count']
|
||||
granularity = args.get("granularity", "1 day")
|
||||
if granularity != "all":
|
||||
granularity = utils.parse_human_timedelta(
|
||||
granularity).total_seconds() * 1000
|
||||
limit = int(args.get("limit", 0))
|
||||
row_limit = int(
|
||||
args.get("row_limit", config.ROW_LIMIT))
|
||||
args.get("row_limit", config.get("ROW_LIMIT")))
|
||||
since = args.get("since", "1 year ago")
|
||||
from_dttm = utils.parse_human_datetime(since)
|
||||
if from_dttm > datetime.now():
|
||||
@@ -130,7 +118,13 @@ class BaseViz(object):
|
||||
to_dttm = utils.parse_human_datetime(until)
|
||||
if from_dttm >= to_dttm:
|
||||
flash("The date range doesn't seem right.", "danger")
|
||||
from_dttm = to_dttm # Making them identicial to not raise
|
||||
from_dttm = to_dttm # Making them identical to not raise
|
||||
|
||||
# extras are used to query elements specific to a datasource type
|
||||
# for instance the extra where clause that applies only to Tables
|
||||
extras = {
|
||||
'where': args.get("where", '')
|
||||
}
|
||||
d = {
|
||||
'granularity': granularity,
|
||||
'from_dttm': from_dttm,
|
||||
@@ -141,24 +135,17 @@ class BaseViz(object):
|
||||
'row_limit': row_limit,
|
||||
'filter': self.query_filters(),
|
||||
'timeseries_limit': limit,
|
||||
'extras': extras,
|
||||
}
|
||||
return d
|
||||
|
||||
def render_no_data(self):
|
||||
self.template = "panoramix/no_data.html"
|
||||
return BaseViz.render(self)
|
||||
|
||||
def render(self, *args, **kwargs):
|
||||
form = self.form_class(self.form_data)
|
||||
return self.view.render_template(
|
||||
self.template, form=form, viz=self, datasource=self.datasource,
|
||||
results=self.results,
|
||||
*args, **kwargs)
|
||||
|
||||
|
||||
class TableViz(BaseViz):
|
||||
verbose_name = "Table View"
|
||||
template = 'panoramix/viz_table.html'
|
||||
form_fields = BaseViz.form_fields + ['row_limit']
|
||||
css_files = ['dataTables.bootstrap.css']
|
||||
js_files = ['jquery.dataTables.min.js', 'dataTables.bootstrap.js']
|
||||
|
||||
def query_obj(self):
|
||||
d = super(TableViz, self).query_obj()
|
||||
@@ -166,79 +153,102 @@ class TableViz(BaseViz):
|
||||
d['timeseries_limit'] = None
|
||||
return d
|
||||
|
||||
def render(self):
|
||||
if self.error_msg:
|
||||
return super(TableViz, self).render(error_msg=self.error_msg)
|
||||
def get_df(self):
|
||||
df = super(TableViz, self).get_df()
|
||||
if (
|
||||
self.form_data.get("granularity") == "all" and
|
||||
'timestamp' in df):
|
||||
del df['timestamp']
|
||||
for m in self.metrics:
|
||||
df[m + '__perc'] = np.rint((df[m] / np.max(df[m])) * 100)
|
||||
return df
|
||||
|
||||
df = self.df
|
||||
row_limit = request.args.get("row_limit")
|
||||
if df is None or df.empty:
|
||||
return super(TableViz, self).render(error_msg="No data.")
|
||||
else:
|
||||
if self.form_data.get("granularity") == "all" and 'timestamp' in df:
|
||||
del df['timestamp']
|
||||
for m in self.metrics:
|
||||
import numpy as np
|
||||
df[m + '__perc'] = np.rint((df[m] / np.max(df[m])) * 100)
|
||||
return super(TableViz, self).render(df=df)
|
||||
|
||||
def form_class(self):
|
||||
limits = [10, 50, 100, 500, 1000, 5000, 10000]
|
||||
return form_factory(self.datasource, request.args,
|
||||
extra_fields_dict={
|
||||
'row_limit':
|
||||
SelectField('Row limit', choices=[(s, s) for s in limits])
|
||||
})
|
||||
class MarkupViz(BaseViz):
|
||||
verbose_name = "Markup Widget"
|
||||
template = 'panoramix/viz_markup.html'
|
||||
form_fields = ['viz_type', 'markup_type', 'code']
|
||||
|
||||
def rendered(self):
|
||||
markup_type = self.form_data.get("markup_type")
|
||||
code = self.form_data.get("code", '')
|
||||
if markup_type == "markdown":
|
||||
return markdown(code)
|
||||
elif markup_type == "html":
|
||||
return code
|
||||
|
||||
|
||||
class WordCloudViz(BaseViz):
|
||||
"""
|
||||
Integration with the nice library at:
|
||||
https://github.com/jasondavies/d3-cloud
|
||||
"""
|
||||
verbose_name = "Word Cloud"
|
||||
template = 'panoramix/viz_word_cloud.html'
|
||||
form_fields = [
|
||||
'viz_type',
|
||||
('since', 'until'),
|
||||
'groupby', 'metric', 'limit',
|
||||
('size_from', 'size_to'),
|
||||
'rotation',
|
||||
]
|
||||
js_files = ['d3.layout.cloud.js']
|
||||
|
||||
def query_obj(self):
|
||||
d = super(WordCloudViz, self).query_obj()
|
||||
d['granularity'] = 'all'
|
||||
metric = self.args.get('metric')
|
||||
if not metric:
|
||||
raise Exception("Pick a metric!")
|
||||
d['metrics'] = [self.args.get('metric')]
|
||||
d['groupby'] = [d['groupby'][0]]
|
||||
return d
|
||||
|
||||
def get_json(self):
|
||||
df = self.get_df()
|
||||
df.columns = ['text', 'size']
|
||||
return df.to_json(orient="records")
|
||||
|
||||
|
||||
class NVD3Viz(BaseViz):
|
||||
verbose_name = "Base NVD3 Viz"
|
||||
template = 'panoramix/viz_nvd3.html'
|
||||
chart_kind = 'line'
|
||||
js_files = ['nv.d3.min.js']
|
||||
css_files = ['nv.d3.css']
|
||||
|
||||
|
||||
class HighchartsViz(BaseViz):
|
||||
verbose_name = "Base Highcharts Viz"
|
||||
template = 'panoramix/viz_highcharts.html'
|
||||
chart_kind = 'line'
|
||||
chart_call = "Chart"
|
||||
stacked = False
|
||||
chart_type = 'not_stock'
|
||||
compare = False
|
||||
js_files = ['highstock.js']
|
||||
|
||||
|
||||
class BubbleViz(HighchartsViz):
|
||||
verbose_name = "Bubble Chart"
|
||||
chart_type = 'bubble'
|
||||
hidden_fields = ['granularity', 'metrics', 'groupby']
|
||||
|
||||
def form_class(self):
|
||||
datasource = self.datasource
|
||||
limits = [0, 5, 10, 25, 50, 100, 500]
|
||||
return form_factory(self.datasource, request.args,
|
||||
extra_fields_dict={
|
||||
#'compare': TextField('Period Compare',),
|
||||
'series': SelectField(
|
||||
'Series', choices=[
|
||||
(s, s) for s in datasource.groupby_column_names]),
|
||||
'entity': SelectField(
|
||||
'Entity', choices=[
|
||||
(s, s) for s in datasource.groupby_column_names]),
|
||||
'x': SelectField(
|
||||
'X Axis', choices=datasource.metrics_combo),
|
||||
'y': SelectField(
|
||||
'Y Axis', choices=datasource.metrics_combo),
|
||||
'size': SelectField(
|
||||
'Bubble Size', choices=datasource.metrics_combo),
|
||||
'limit': SelectField(
|
||||
'Limit', choices=[(s, s) for s in limits]),
|
||||
})
|
||||
form_fields = [
|
||||
'viz_type', 'since', 'until',
|
||||
'series', 'entity', 'x', 'y', 'size', 'limit']
|
||||
js_files = ['highstock.js', 'highcharts-more.js']
|
||||
|
||||
def query_obj(self):
|
||||
args = self.form_data
|
||||
d = super(BubbleViz, self).query_obj()
|
||||
d['granularity'] = 'all'
|
||||
d['groupby'] = list({
|
||||
request.args.get('series'),
|
||||
request.args.get('entity')
|
||||
})
|
||||
self.x_metric = request.args.get('x')
|
||||
self.y_metric = request.args.get('y')
|
||||
self.z_metric = request.args.get('size')
|
||||
self.entity = request.args.get('entity')
|
||||
self.series = request.args.get('series')
|
||||
args.get('series'),
|
||||
args.get('entity')
|
||||
})
|
||||
self.x_metric = args.get('x')
|
||||
self.y_metric = args.get('y')
|
||||
self.z_metric = args.get('size')
|
||||
self.entity = args.get('entity')
|
||||
self.series = args.get('series')
|
||||
d['metrics'] = [
|
||||
self.z_metric,
|
||||
self.x_metric,
|
||||
@@ -248,45 +258,83 @@ class BubbleViz(HighchartsViz):
|
||||
raise Exception("Pick a metric for x, y and size")
|
||||
return d
|
||||
|
||||
def render(self):
|
||||
metrics = self.metrics
|
||||
def get_df(self):
|
||||
df = super(BubbleViz, self).get_df()
|
||||
df = df.fillna(0)
|
||||
df['x'] = df[[self.x_metric]]
|
||||
df['y'] = df[[self.y_metric]]
|
||||
df['z'] = df[[self.z_metric]]
|
||||
df['name'] = df[[self.entity]]
|
||||
df['group'] = df[[self.series]]
|
||||
return df
|
||||
|
||||
if not self.error_msg:
|
||||
df = self.df.fillna(0)
|
||||
df['x'] = df[[self.x_metric]]
|
||||
df['y'] = df[[self.y_metric]]
|
||||
df['z'] = df[[self.z_metric]]
|
||||
df['name'] = df[[self.entity]]
|
||||
df['group'] = df[[self.series]]
|
||||
chart = HighchartBubble(df)
|
||||
return super(BubbleViz, self).render(chart_js=chart.javascript_cmd)
|
||||
else:
|
||||
return super(BubbleViz, self).render(error_msg=self.error_msg)
|
||||
def get_json(self):
|
||||
df = self.get_df()
|
||||
chart = HighchartBubble(df)
|
||||
return chart.json
|
||||
|
||||
class BigNumberViz(BaseViz):
|
||||
verbose_name = "Big Number"
|
||||
template = 'panoramix/viz_bignumber.html'
|
||||
js_files = ['d3.min.js']
|
||||
form_fields = [
|
||||
'viz_type',
|
||||
'granularity',
|
||||
('since', 'until'),
|
||||
'metric',
|
||||
'compare_lag',
|
||||
'compare_suffix',
|
||||
#('rolling_type', 'rolling_periods'),
|
||||
]
|
||||
|
||||
def query_obj(self):
|
||||
d = super(BigNumberViz, self).query_obj()
|
||||
metric = self.args.get('metric')
|
||||
if not metric:
|
||||
raise Exception("Pick a metric!")
|
||||
d['metrics'] = [self.args.get('metric')]
|
||||
return d
|
||||
|
||||
def get_json(self):
|
||||
args = self.args
|
||||
df = self.get_df()
|
||||
df = df.sort(columns=df.columns[0])
|
||||
df['timestamp'] = df[[0]].astype(np.int64) // 10**9
|
||||
compare_lag = args.get("compare_lag", "")
|
||||
compare_lag = int(compare_lag) if compare_lag.isdigit() else 0
|
||||
d = {
|
||||
'data': df.values.tolist(),
|
||||
'compare_lag': compare_lag,
|
||||
'compare_suffix': args.get('compare_suffix', ''),
|
||||
}
|
||||
return json.dumps(d)
|
||||
|
||||
|
||||
class TimeSeriesViz(HighchartsViz):
|
||||
verbose_name = "Time Series - Line Chart"
|
||||
chart_type = "spline"
|
||||
stockchart = True
|
||||
chart_call = "StockChart"
|
||||
sort_legend_y = True
|
||||
js_files = ['highstock.js', 'highcharts-more.js']
|
||||
form_fields = [
|
||||
'viz_type',
|
||||
'granularity', ('since', 'until'),
|
||||
'metrics',
|
||||
'groupby', 'limit',
|
||||
('rolling_type', 'rolling_periods'),
|
||||
]
|
||||
|
||||
def render(self):
|
||||
if request.args.get("granularity") == "all":
|
||||
self.error_msg = (
|
||||
"You have to select a time granularity for this view")
|
||||
return super(TimeSeriesViz, self).render(error_msg=self.error_msg)
|
||||
|
||||
def get_df(self):
|
||||
args = self.args
|
||||
df = super(TimeSeriesViz, self).get_df()
|
||||
metrics = self.metrics
|
||||
df = self.df
|
||||
df = df.pivot_table(
|
||||
index="timestamp",
|
||||
columns=self.groupby,
|
||||
values=metrics,)
|
||||
|
||||
rolling_periods = request.args.get("rolling_periods")
|
||||
limit = request.args.get("limit")
|
||||
rolling_type = request.args.get("rolling_type")
|
||||
rolling_periods = args.get("rolling_periods")
|
||||
rolling_type = args.get("rolling_type")
|
||||
if rolling_periods and rolling_type:
|
||||
if rolling_type == 'mean':
|
||||
df = pd.rolling_mean(df, int(rolling_periods))
|
||||
@@ -294,47 +342,108 @@ class TimeSeriesViz(HighchartsViz):
|
||||
df = pd.rolling_std(df, int(rolling_periods))
|
||||
elif rolling_type == 'sum':
|
||||
df = pd.rolling_sum(df, int(rolling_periods))
|
||||
return df
|
||||
|
||||
def get_json(self):
|
||||
df = self.get_df()
|
||||
chart = Highchart(
|
||||
df,
|
||||
compare=self.compare,
|
||||
chart_type=self.chart_type,
|
||||
stacked=self.stacked,
|
||||
stockchart=self.stockchart,
|
||||
sort_legend_y=self.sort_legend_y,
|
||||
**CHART_ARGS)
|
||||
return super(TimeSeriesViz, self).render(chart_js=chart.javascript_cmd)
|
||||
return chart.json
|
||||
|
||||
def form_class(self):
|
||||
limits = [0, 5, 10, 25, 50, 100, 500]
|
||||
return form_factory(self.datasource, request.args,
|
||||
extra_fields_dict={
|
||||
#'compare': TextField('Period Compare',),
|
||||
'rolling_type': SelectField(
|
||||
'Rolling',
|
||||
choices=[(s, s) for s in ['mean', 'sum', 'std']]),
|
||||
'rolling_periods': TextField('Periods',),
|
||||
'limit': SelectField(
|
||||
'Series limit', choices=[(s, s) for s in limits])
|
||||
})
|
||||
|
||||
def bake_query(self):
|
||||
"""
|
||||
Doing a 2 phase query where we limit the number of series.
|
||||
"""
|
||||
return self.datasource.query(**self.query_obj())
|
||||
class NVD3TimeSeriesViz(NVD3Viz):
|
||||
verbose_name = "NVD3 - Time Series - Line Chart"
|
||||
chart_type = "nvd3_line"
|
||||
form_fields = [
|
||||
'viz_type',
|
||||
'granularity', ('since', 'until'),
|
||||
'metrics',
|
||||
'groupby', 'limit',
|
||||
('rolling_type', 'rolling_periods'),
|
||||
('show_brush', 'show_legend'),
|
||||
('rich_tooltip', 'y_axis_zero'),
|
||||
'y_log_scale',
|
||||
]
|
||||
|
||||
def get_df(self):
|
||||
args = self.args
|
||||
df = super(NVD3TimeSeriesViz, self).get_df()
|
||||
metrics = self.metrics
|
||||
df = df.pivot_table(
|
||||
index="timestamp",
|
||||
columns=self.groupby,
|
||||
values=metrics,)
|
||||
|
||||
rolling_periods = args.get("rolling_periods")
|
||||
rolling_type = args.get("rolling_type")
|
||||
if rolling_periods and rolling_type:
|
||||
if rolling_type == 'mean':
|
||||
df = pd.rolling_mean(df, int(rolling_periods))
|
||||
elif rolling_type == 'std':
|
||||
df = pd.rolling_std(df, int(rolling_periods))
|
||||
elif rolling_type == 'sum':
|
||||
df = pd.rolling_sum(df, int(rolling_periods))
|
||||
return df
|
||||
|
||||
def get_json(self):
|
||||
df = self.get_df()
|
||||
series = df.to_dict('series')
|
||||
datas = []
|
||||
for name, ys in series.items():
|
||||
if df[name].dtype.kind not in "biufc":
|
||||
continue
|
||||
|
||||
df.tz_localize(None)
|
||||
df.index.tz_localize(None)
|
||||
df['timestamp'] = pd.to_datetime(df.index, utc=False)
|
||||
if isinstance(name, basestring):
|
||||
series_title = name
|
||||
elif len(self.metrics) > 1:
|
||||
series_title = ", ".join(name)
|
||||
else:
|
||||
series_title = ", ".join(name[1:])
|
||||
d = {
|
||||
"key": series_title,
|
||||
"color": utils.color(series_title),
|
||||
"values": [
|
||||
{'x': ds, 'y': ys[i]}
|
||||
for i, ds in enumerate(df.timestamp)]
|
||||
}
|
||||
datas.append(d)
|
||||
return dumps(datas)
|
||||
|
||||
|
||||
class NVD3TimeSeriesBarViz(NVD3TimeSeriesViz):
|
||||
verbose_name = "NVD3 - Time Series - Bar Chart"
|
||||
chart_type = "nvd3_bar"
|
||||
form_fields = [
|
||||
'viz_type',
|
||||
'granularity', ('since', 'until'),
|
||||
'metrics',
|
||||
'groupby', 'limit',
|
||||
('rolling_type', 'rolling_periods'),
|
||||
'show_legend',
|
||||
]
|
||||
|
||||
|
||||
class TimeSeriesCompareViz(TimeSeriesViz):
|
||||
verbose_name = "Time Series - Percent Change"
|
||||
compare = 'percent'
|
||||
|
||||
|
||||
class TimeSeriesCompareValueViz(TimeSeriesViz):
|
||||
verbose_name = "Time Series - Value Change"
|
||||
compare = 'value'
|
||||
|
||||
|
||||
class TimeSeriesAreaViz(TimeSeriesViz):
|
||||
verbose_name = "Time Series - Stacked Area Chart"
|
||||
stacked=True
|
||||
stacked = True
|
||||
chart_type = "area"
|
||||
|
||||
|
||||
@@ -349,50 +458,49 @@ class TimeSeriesStackedBarViz(TimeSeriesViz):
|
||||
stacked = True
|
||||
|
||||
|
||||
class DistributionBarViz(HighchartsViz):
|
||||
verbose_name = "Distribution - Bar Chart"
|
||||
chart_type = "column"
|
||||
|
||||
def query_obj(self):
|
||||
d = super(DistributionBarViz, self).query_obj()
|
||||
d['granularity'] = "all"
|
||||
return d
|
||||
|
||||
def render(self):
|
||||
df = self.df
|
||||
df = df.pivot_table(
|
||||
index=self.groupby,
|
||||
values=self.metrics)
|
||||
df = df.sort(self.metrics[0], ascending=False)
|
||||
chart = Highchart(
|
||||
df, chart_type=self.chart_type, **CHART_ARGS)
|
||||
return super(DistributionBarViz, self).render(
|
||||
chart_js=chart.javascript_cmd)
|
||||
|
||||
|
||||
class DistributionPieViz(HighchartsViz):
|
||||
verbose_name = "Distribution - Pie Chart"
|
||||
chart_type = "pie"
|
||||
js_files = ['highstock.js']
|
||||
form_fields = [
|
||||
'viz_type', 'metrics', 'groupby',
|
||||
('since', 'until'), 'limit']
|
||||
|
||||
def query_obj(self):
|
||||
d = super(DistributionPieViz, self).query_obj()
|
||||
d['granularity'] = "all"
|
||||
d['is_timeseries'] = False
|
||||
return d
|
||||
|
||||
def render(self):
|
||||
df = self.df
|
||||
def get_df(self):
|
||||
df = super(DistributionPieViz, self).get_df()
|
||||
df = df.pivot_table(
|
||||
index=self.groupby,
|
||||
values=[self.metrics[0]])
|
||||
df = df.sort(self.metrics[0], ascending=False)
|
||||
return df
|
||||
|
||||
def get_json(self):
|
||||
df = self.get_df()
|
||||
chart = Highchart(
|
||||
df, chart_type=self.chart_type, **CHART_ARGS)
|
||||
return super(DistributionPieViz, self).render(
|
||||
chart_js=chart.javascript_cmd)
|
||||
self.chart_js = chart.javascript_cmd
|
||||
return chart.json
|
||||
|
||||
|
||||
class DistributionBarViz(DistributionPieViz):
|
||||
verbose_name = "Distribution - Bar Chart"
|
||||
chart_type = "column"
|
||||
|
||||
|
||||
viz_types = OrderedDict([
|
||||
['table', TableViz],
|
||||
['nvd3_line', NVD3TimeSeriesViz],
|
||||
['nvd3_bar', NVD3TimeSeriesBarViz],
|
||||
['line', TimeSeriesViz],
|
||||
['big_number', BigNumberViz],
|
||||
['compare', TimeSeriesCompareViz],
|
||||
['compare_value', TimeSeriesCompareValueViz],
|
||||
['area', TimeSeriesAreaViz],
|
||||
@@ -401,4 +509,6 @@ viz_types = OrderedDict([
|
||||
['dist_bar', DistributionBarViz],
|
||||
['pie', DistributionPieViz],
|
||||
['bubble', BubbleViz],
|
||||
['markup', MarkupViz],
|
||||
['word_cloud', WordCloudViz],
|
||||
])
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
coverage
|
||||
flask
|
||||
flask-alembic
|
||||
flask-appbuilder
|
||||
flask-migrate
|
||||
flask-testing
|
||||
gunicorn
|
||||
markdown
|
||||
mysql-python
|
||||
nose
|
||||
pandas
|
||||
parsedatetime
|
||||
pydruid
|
||||
|
||||
4
run_tests.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
rm /tmp/panoramix_unittests.db
|
||||
export PANORAMIX_CONFIG=tests.panoramix_test_config
|
||||
panoramix/bin/panoramix db upgrade
|
||||
nosetests tests/core_tests.py --with-coverage --cover-package=panoramix
|
||||
27
setup.py
@@ -1,6 +1,6 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
version = '0.2.0'
|
||||
version = '0.4.0'
|
||||
|
||||
setup(
|
||||
name='panoramix',
|
||||
@@ -9,20 +9,25 @@ setup(
|
||||
"and druid.io"),
|
||||
version=version,
|
||||
packages=find_packages(),
|
||||
package_data={'': ['panoramix/alembic.ini']},
|
||||
package_data={'': [
|
||||
'panoramix/migrations/alembic.ini',
|
||||
'panoramix/data/birth_names.csv.gz',
|
||||
]},
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
scripts=['panoramix/bin/panoramix'],
|
||||
install_requires=[
|
||||
'flask-appbuilder>=1.4.5',
|
||||
'flask-alembic>=1.2.1',
|
||||
'gunicorn>=19.3.0',
|
||||
'pandas>=0.16.2',
|
||||
'pydruid>=0.2.2',
|
||||
'parsedatetime>=1.5',
|
||||
'python-dateutil>=2.4.2',
|
||||
'requests>=2.7.0',
|
||||
'sqlparse>=0.1.16',
|
||||
'flask-appbuilder>=1.4.5, <2.0.0',
|
||||
'flask-login==0.2.11',
|
||||
'flask-migrate>=1.5.1, <2.0.0',
|
||||
'flask-script>=2.0.5, <3.0.0',
|
||||
'gunicorn>=19.3.0, <20.0.0',
|
||||
'pandas>=0.16.2, <1.0.0',
|
||||
'parsedatetime>=1.5, <2.0.0',
|
||||
'pydruid>=0.2.2, <0.3',
|
||||
'python-dateutil>=2.4.2, <3.0.0',
|
||||
'requests>=2.7.0, <3.0.0',
|
||||
'sqlparse>=0.1.16, <0.2.0',
|
||||
],
|
||||
author='Maxime Beauchemin',
|
||||
author_email='maximebeauchemin@gmail.com',
|
||||
|
||||
6487
static/bootstrap-theme.css
vendored
|
Before Width: | Height: | Size: 24 KiB |
207
static/main.css
@@ -1,207 +0,0 @@
|
||||
body { padding-top: 70px; }
|
||||
a.navbar-brand span {
|
||||
color: white;
|
||||
}
|
||||
nav{
|
||||
-webkit-box-shadow: 0px 3px 3px #AAA;
|
||||
-moz-box-shadow: 0px 3px 3px #AAA;
|
||||
box-shadow: 0px 3px 3px #AAA;
|
||||
z-index:999;
|
||||
}
|
||||
a.navbar-brand {
|
||||
cursor: default;
|
||||
}
|
||||
td>span.glyphicon{
|
||||
padding-left: 3px;
|
||||
padding-top: 3px;
|
||||
}
|
||||
button.btn {
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
div.rich_doc {
|
||||
padding: 5px 10px;
|
||||
border: 1px solid #dddddd;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
span.status_square {
|
||||
width:10px;
|
||||
height:10px;
|
||||
border:1px solid grey;
|
||||
display:inline-block;
|
||||
padding-left: 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
div.squares{
|
||||
float:right;
|
||||
font-size: 1;
|
||||
}
|
||||
div.task_row{
|
||||
}
|
||||
span.success{
|
||||
background-color:green;
|
||||
}
|
||||
span.up_for_retry{
|
||||
background-color:yellow;
|
||||
}
|
||||
span.started{
|
||||
background-color:lime;
|
||||
}
|
||||
span.error{
|
||||
background-color:red;
|
||||
}
|
||||
span.queued{
|
||||
background-color:gray;
|
||||
}
|
||||
.tooltip-inner {
|
||||
text-align:left !important;
|
||||
font-size: 12px;
|
||||
}
|
||||
input#execution_date {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
table.highlighttable{
|
||||
width: 100%;
|
||||
table-layout:fixed;
|
||||
}
|
||||
div.linenodiv {
|
||||
padding-right: 1px !important;
|
||||
}
|
||||
.linenos {
|
||||
width: 50px;
|
||||
border: none;
|
||||
}
|
||||
div.linenodiv pre {
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
color: #AAA;
|
||||
background-color: #FCFCFC;
|
||||
text-align:right;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
word-wrap: normal;
|
||||
white-space: pre;
|
||||
}
|
||||
pre code {
|
||||
overflow-wrap: normal;
|
||||
white-space: pre;
|
||||
}
|
||||
input, select {
|
||||
margin: 0px;
|
||||
}
|
||||
.code {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
#sql {
|
||||
border: 1px solid #CCC;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.ace_editor div {
|
||||
font: inherit!important
|
||||
}
|
||||
#ace_container {
|
||||
margin: 10px 0px;
|
||||
}
|
||||
#sql_ace {
|
||||
visibility: hidden;
|
||||
}
|
||||
.no-wrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
div.form-inline{
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
body div.panel {
|
||||
padding: 0px;
|
||||
}
|
||||
.blur {
|
||||
filter:url(#blur-effect-1);
|
||||
}
|
||||
div.legend_item {
|
||||
-moz-border-radius: 5px/5px;
|
||||
-webkit-border-radius: 5px 5px;
|
||||
border-radius: 5px/5px;
|
||||
float:right;
|
||||
margin: 0px 10px 0px 0px;
|
||||
padding:0px 5px;
|
||||
border:solid 2px grey;
|
||||
font-size: 12px;
|
||||
}
|
||||
div.legend_circle{
|
||||
-moz-border-radius: 10px/10px;
|
||||
-webkit-border-radius: 10px 10px;
|
||||
border-radius: 10px/10px;
|
||||
width:15px;
|
||||
height:15px;
|
||||
border:1px solid grey;
|
||||
float:left;
|
||||
margin-top: 2px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
div.square {
|
||||
width:12px;
|
||||
height:12px;
|
||||
float: right;
|
||||
margin-top: 2px;
|
||||
border:1px solid black;
|
||||
}
|
||||
.btn:active, .btn.active {
|
||||
box-shadow: inset 0 6px 6px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.hll { background-color: #ffffcc }
|
||||
.c { color: #408080; font-style: italic } /* Comment */
|
||||
.err { border: 1px solid #FF0000 } /* Error */
|
||||
.k { color: #008000; font-weight: bold } /* Keyword */
|
||||
.o { color: #666666 } /* Operator */
|
||||
.cm { color: #408080; font-style: italic } /* Comment.Multiline */
|
||||
.cp { color: #BC7A00 } /* Comment.Preproc */
|
||||
.c1 { color: #408080; font-style: italic } /* Comment.Single */
|
||||
.cs { color: #408080; font-style: italic } /* Comment.Special */
|
||||
.gd { color: #A00000 } /* Generic.Deleted */
|
||||
.ge { font-style: italic } /* Generic.Emph */
|
||||
.gr { color: #FF0000 } /* Generic.Error */
|
||||
.gh { color: #000080; font-weight: bold } /* Generic.Heading */
|
||||
.gi { color: #00A000 } /* Generic.Inserted */
|
||||
.go { color: #888888 } /* Generic.Output */
|
||||
.gp { color: #000080; font-weight: bold } /* Generic.Prompt */
|
||||
.gs { font-weight: bold } /* Generic.Strong */
|
||||
.gu { color: #800080; font-weight: bold } /* Generic.Subheading */
|
||||
.gt { color: #0044DD } /* Generic.Traceback */
|
||||
.kc { color: #008000; font-weight: bold } /* Keyword.Constant */
|
||||
.kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
|
||||
.kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
|
||||
.kp { color: #008000 } /* Keyword.Pseudo */
|
||||
.kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
|
||||
.kt { color: #B00040 } /* Keyword.Type */
|
||||
.m { color: #666666 } /* Literal.Number */
|
||||
.s { color: #BA2121 } /* Literal.String */
|
||||
.na { color: #7D9029 } /* Name.Attribute */
|
||||
.nb { color: #008000 } /* Name.Builtin */
|
||||
.nc { color: #0000FF; font-weight: bold } /* Name.Class */
|
||||
.no { color: #880000 } /* Name.Constant */
|
||||
.nd { color: #AA22FF } /* Name.Decorator */
|
||||
.ni { color: #999999; font-weight: bold } /* Name.Entity */
|
||||
.ne { color: #D2413A; font-weight: bold } /* Name.Exception */
|
||||
.nf { color: #0000FF } /* Name.Function */
|
||||
.nl { color: #A0A000 } /* Name.Label */
|
||||
.nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
|
||||
.nt { color: #008000; font-weight: bold } /* Name.Tag */
|
||||
.nv { color: #19177C } /* Name.Variable */
|
||||
.ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
|
||||
.w { color: #bbbbbb } /* Text.Whitespace */
|
||||
.mb { color: #666666 } /* Literal.Number.Bin */
|
||||
.mf { color: #666666 } /* Literal.Number.Float */
|
||||
.mh { color: #666666 } /* Literal.Number.Hex */
|
||||
.mi { color: #666666 } /* Literal.Number.Integer */
|
||||
.mo { color: #666666 } /* Literal.Number.Oct */
|
||||
.sb { color: #BA2121 } /* Literal.String.Backtick */
|
||||
.sc { color: #BA2121 } /* Literal.String.Char */
|
||||
.sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
|
||||
.s2 { color: #BA2121 } /* Literal.String.Double */
|
||||
@@ -1,495 +0,0 @@
|
||||
/*! Select2 Bootstrap 3 CSS v1.4.6 | MIT License | github.com/t0m/select2-bootstrap-css */
|
||||
/**
|
||||
* Reset Bootstrap 3 .form-control styles which - if applied to the
|
||||
* original <select>-element the Select2-plugin may be run against -
|
||||
* are copied to the .select2-container.
|
||||
*
|
||||
* 1. Overwrite .select2-container's original display:inline-block
|
||||
* with Bootstrap 3's default for .form-control, display:block;
|
||||
* courtesy of @juristr (@see https://github.com/fk/select2-bootstrap-css/pull/1)
|
||||
*/
|
||||
.select2-container.form-control {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
display: block;
|
||||
/* 1 */
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust Select2 inputs to fit Bootstrap 3 default .form-control appearance.
|
||||
*/
|
||||
.select2-container .select2-choices .select2-search-field input,
|
||||
.select2-container .select2-choice,
|
||||
.select2-container .select2-choices {
|
||||
background: none;
|
||||
padding: 0;
|
||||
border-color: #cccccc;
|
||||
border-radius: 4px;
|
||||
color: #555555;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
background-color: white;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.select2-search input {
|
||||
border-color: #cccccc;
|
||||
border-radius: 4px;
|
||||
color: #555555;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
background-color: white;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.select2-container .select2-choices .select2-search-field input {
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust Select2 input heights to match the Bootstrap default.
|
||||
*/
|
||||
.select2-container .select2-choice {
|
||||
height: 34px;
|
||||
line-height: 1.42857;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address Multi Select2's height which - depending on how many elements have been selected -
|
||||
* may grown higher than their initial size.
|
||||
*/
|
||||
.select2-container.select2-container-multi.form-control {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address Bootstrap 3 control sizing classes
|
||||
* @see http://getbootstrap.com/css/#forms-control-sizes
|
||||
*/
|
||||
.select2-container.input-sm .select2-choice,
|
||||
.input-group-sm .select2-container .select2-choice {
|
||||
height: 30px;
|
||||
line-height: 1.5;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.select2-container.input-lg .select2-choice,
|
||||
.input-group-lg .select2-container .select2-choice {
|
||||
height: 46px;
|
||||
line-height: 1.33333;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.select2-container-multi .select2-choices .select2-search-field input {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.select2-container-multi.input-sm .select2-choices .select2-search-field input,
|
||||
.input-group-sm .select2-container-multi .select2-choices .select2-search-field input {
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.select2-container-multi.input-lg .select2-choices .select2-search-field input,
|
||||
.input-group-lg .select2-container-multi .select2-choices .select2-search-field input {
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust height and line-height for .select2-search-field amd multi-select Select2 widgets.
|
||||
*
|
||||
* 1. Class repetition to address missing .select2-chosen in Select2 < 3.3.2.
|
||||
*/
|
||||
.select2-container-multi .select2-choices .select2-search-field input {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.select2-chosen,
|
||||
.select2-choice > span:first-child,
|
||||
.select2-container .select2-choices .select2-search-field input {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.input-sm .select2-chosen,
|
||||
.input-group-sm .select2-chosen,
|
||||
.input-sm .select2-choice > span:first-child,
|
||||
.input-group-sm .select2-choice > span:first-child,
|
||||
.input-sm .select2-choices .select2-search-field input,
|
||||
.input-group-sm .select2-choices .select2-search-field input {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.input-lg .select2-chosen,
|
||||
.input-group-lg .select2-chosen,
|
||||
.input-lg .select2-choice > span:first-child,
|
||||
.input-group-lg .select2-choice > span:first-child,
|
||||
.input-lg .select2-choices .select2-search-field input,
|
||||
.input-group-lg .select2-choices .select2-search-field input {
|
||||
padding: 10px 16px;
|
||||
}
|
||||
|
||||
.select2-container-multi .select2-choices .select2-search-choice {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.select2-container-multi.input-sm .select2-choices .select2-search-choice,
|
||||
.input-group-sm .select2-container-multi .select2-choices .select2-search-choice {
|
||||
margin-top: 3px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.select2-container-multi.input-lg .select2-choices .select2-search-choice,
|
||||
.input-group-lg .select2-container-multi .select2-choices .select2-search-choice {
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust the single Select2's dropdown arrow button appearance.
|
||||
*
|
||||
* 1. For Select2 v.3.3.2.
|
||||
*/
|
||||
.select2-container .select2-choice .select2-arrow,
|
||||
.select2-container .select2-choice div {
|
||||
border-left: none;
|
||||
background: none;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
||||
}
|
||||
|
||||
.select2-dropdown-open .select2-choice .select2-arrow,
|
||||
.select2-dropdown-open .select2-choice div {
|
||||
border-left-color: transparent;
|
||||
background: none;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust the dropdown arrow button icon position for the single-select Select2 elements
|
||||
* to make it line up vertically now that we increased the height of .select2-container.
|
||||
*
|
||||
* 1. Class repetition to address missing .select2-chosen in Select2 v.3.3.2.
|
||||
*/
|
||||
.select2-container .select2-choice .select2-arrow b,
|
||||
.select2-container .select2-choice div b {
|
||||
background-position: 0 3px;
|
||||
}
|
||||
|
||||
.select2-dropdown-open .select2-choice .select2-arrow b,
|
||||
.select2-dropdown-open .select2-choice div b {
|
||||
background-position: -18px 3px;
|
||||
}
|
||||
|
||||
.select2-container.input-sm .select2-choice .select2-arrow b,
|
||||
.input-group-sm .select2-container .select2-choice .select2-arrow b,
|
||||
.select2-container.input-sm .select2-choice div b,
|
||||
.input-group-sm .select2-container .select2-choice div b {
|
||||
background-position: 0 1px;
|
||||
}
|
||||
|
||||
.select2-dropdown-open.input-sm .select2-choice .select2-arrow b,
|
||||
.input-group-sm .select2-dropdown-open .select2-choice .select2-arrow b,
|
||||
.select2-dropdown-open.input-sm .select2-choice div b,
|
||||
.input-group-sm .select2-dropdown-open .select2-choice div b {
|
||||
background-position: -18px 1px;
|
||||
}
|
||||
|
||||
.select2-container.input-lg .select2-choice .select2-arrow b,
|
||||
.input-group-lg .select2-container .select2-choice .select2-arrow b,
|
||||
.select2-container.input-lg .select2-choice div b,
|
||||
.input-group-lg .select2-container .select2-choice div b {
|
||||
background-position: 0 9px;
|
||||
}
|
||||
|
||||
.select2-dropdown-open.input-lg .select2-choice .select2-arrow b,
|
||||
.input-group-lg .select2-dropdown-open .select2-choice .select2-arrow b,
|
||||
.select2-dropdown-open.input-lg .select2-choice div b,
|
||||
.input-group-lg .select2-dropdown-open .select2-choice div b {
|
||||
background-position: -18px 9px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address Bootstrap's validation states and change Select2's border colors and focus states.
|
||||
* Apply .has-warning, .has-danger or .has-succes to #select2-drop to match Bootstraps' colors.
|
||||
*/
|
||||
.has-warning .select2-choice,
|
||||
.has-warning .select2-choices {
|
||||
border-color: #8a6d3b;
|
||||
}
|
||||
.has-warning .select2-container-active .select2-choice,
|
||||
.has-warning .select2-container-multi.select2-container-active .select2-choices {
|
||||
border-color: #66512c;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
|
||||
}
|
||||
.has-warning.select2-drop-active {
|
||||
border-color: #66512c;
|
||||
}
|
||||
.has-warning.select2-drop-active.select2-drop.select2-drop-above {
|
||||
border-top-color: #66512c;
|
||||
}
|
||||
|
||||
.has-error .select2-choice,
|
||||
.has-error .select2-choices {
|
||||
border-color: #a94442;
|
||||
}
|
||||
.has-error .select2-container-active .select2-choice,
|
||||
.has-error .select2-container-multi.select2-container-active .select2-choices {
|
||||
border-color: #843534;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
|
||||
}
|
||||
.has-error.select2-drop-active {
|
||||
border-color: #843534;
|
||||
}
|
||||
.has-error.select2-drop-active.select2-drop.select2-drop-above {
|
||||
border-top-color: #843534;
|
||||
}
|
||||
|
||||
.has-success .select2-choice,
|
||||
.has-success .select2-choices {
|
||||
border-color: #3c763d;
|
||||
}
|
||||
.has-success .select2-container-active .select2-choice,
|
||||
.has-success .select2-container-multi.select2-container-active .select2-choices {
|
||||
border-color: #2b542c;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
|
||||
}
|
||||
.has-success.select2-drop-active {
|
||||
border-color: #2b542c;
|
||||
}
|
||||
.has-success.select2-drop-active.select2-drop.select2-drop-above {
|
||||
border-top-color: #2b542c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make Select2's active-styles - applied to .select2-container when the widget receives focus -
|
||||
* fit Bootstrap 3's .form-element:focus appearance.
|
||||
*/
|
||||
.select2-container-active .select2-choice,
|
||||
.select2-container-multi.select2-container-active .select2-choices {
|
||||
border-color: #66afe9;
|
||||
outline: none;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
|
||||
-webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
|
||||
-o-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
|
||||
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
|
||||
}
|
||||
|
||||
.select2-drop-active {
|
||||
border-color: #66afe9;
|
||||
}
|
||||
|
||||
.select2-drop-auto-width,
|
||||
.select2-drop.select2-drop-above.select2-drop-active {
|
||||
border-top-color: #66afe9;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select2 widgets in Bootstrap Input Groups
|
||||
*
|
||||
* When Select2 widgets are combined with other elements using Bootstrap 3's
|
||||
* "Input Group" component, we don't want specific edges of the Select2 container
|
||||
* to have a border-radius.
|
||||
*
|
||||
* In Bootstrap 2, input groups required a markup where these style adjustments
|
||||
* could be bound to a CSS-class identifying if the additional elements are appended,
|
||||
* prepended or both.
|
||||
*
|
||||
* Bootstrap 3 doesn't rely on these classes anymore, so we have to use our own.
|
||||
* Use .select2-bootstrap-prepend and .select2-bootstrap-append on a Bootstrap 3 .input-group
|
||||
* to let the contained Select2 widget know which edges should not be rounded as they are
|
||||
* directly followed by another element.
|
||||
*
|
||||
* @see http://getbootstrap.com/components/#input-groups
|
||||
*/
|
||||
.input-group.select2-bootstrap-prepend [class^="select2-choice"] {
|
||||
border-bottom-left-radius: 0 !important;
|
||||
border-top-left-radius: 0 !important;
|
||||
}
|
||||
|
||||
.input-group.select2-bootstrap-append [class^="select2-choice"] {
|
||||
border-bottom-right-radius: 0 !important;
|
||||
border-top-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
.select2-dropdown-open [class^="select2-choice"] {
|
||||
border-bottom-right-radius: 0 !important;
|
||||
border-bottom-left-radius: 0 !important;
|
||||
}
|
||||
|
||||
.select2-dropdown-open.select2-drop-above [class^="select2-choice"] {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-top-left-radius: 0 !important;
|
||||
border-bottom-right-radius: 4px !important;
|
||||
border-bottom-left-radius: 4px !important;
|
||||
background: white;
|
||||
filter: none;
|
||||
}
|
||||
.input-group.select2-bootstrap-prepend .select2-dropdown-open.select2-drop-above [class^="select2-choice"] {
|
||||
border-bottom-left-radius: 0 !important;
|
||||
border-top-left-radius: 0 !important;
|
||||
}
|
||||
.input-group.select2-bootstrap-append .select2-dropdown-open.select2-drop-above [class^="select2-choice"] {
|
||||
border-bottom-right-radius: 0 !important;
|
||||
border-top-right-radius: 0 !important;
|
||||
}
|
||||
.input-group.input-group-sm.select2-bootstrap-prepend .select2-dropdown-open.select2-drop-above [class^="select2-choice"] {
|
||||
border-bottom-right-radius: 3px !important;
|
||||
}
|
||||
.input-group.input-group-lg.select2-bootstrap-prepend .select2-dropdown-open.select2-drop-above [class^="select2-choice"] {
|
||||
border-bottom-right-radius: 6px !important;
|
||||
}
|
||||
.input-group.input-group-sm.select2-bootstrap-append .select2-dropdown-open.select2-drop-above [class^="select2-choice"] {
|
||||
border-bottom-left-radius: 3px !important;
|
||||
}
|
||||
.input-group.input-group-lg.select2-bootstrap-append .select2-dropdown-open.select2-drop-above [class^="select2-choice"] {
|
||||
border-bottom-left-radius: 6px !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust Select2's choices hover and selected styles to match Bootstrap 3's default dropdown styles.
|
||||
*/
|
||||
.select2-results .select2-highlighted {
|
||||
color: white;
|
||||
background-color: #337ab7;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust alignment of Bootstrap 3 buttons in Bootstrap 3 Input Groups to address
|
||||
* Multi Select2's height which - depending on how many elements have been selected -
|
||||
* may grown higher than their initial size.
|
||||
*/
|
||||
.select2-bootstrap-append .select2-container-multiple,
|
||||
.select2-bootstrap-append .input-group-btn,
|
||||
.select2-bootstrap-append .input-group-btn .btn,
|
||||
.select2-bootstrap-prepend .select2-container-multiple,
|
||||
.select2-bootstrap-prepend .input-group-btn,
|
||||
.select2-bootstrap-prepend .input-group-btn .btn {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make Multi Select2's choices match Bootstrap 3's default button styles.
|
||||
*/
|
||||
.select2-container-multi .select2-choices .select2-search-choice {
|
||||
color: #555555;
|
||||
background: white;
|
||||
border-color: #cccccc;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.select2-container-multi .select2-choices .select2-search-choice-focus {
|
||||
background: #ebebeb;
|
||||
border-color: #adadad;
|
||||
color: #333333;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address Multi Select2's choice close-button vertical alignment.
|
||||
*/
|
||||
.select2-search-choice-close {
|
||||
margin-top: -7px;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust the single Select2's clear button position (used to reset the select box
|
||||
* back to the placeholder value and visible once a selection is made
|
||||
* activated by Select2's "allowClear" option).
|
||||
*/
|
||||
.select2-container .select2-choice abbr {
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust "no results" and "selection limit" messages to make use
|
||||
* of Bootstrap 3's default "Alert" style.
|
||||
*
|
||||
* @see http://getbootstrap.com/components/#alerts-default
|
||||
*/
|
||||
.select2-results .select2-no-results,
|
||||
.select2-results .select2-searching,
|
||||
.select2-results .select2-selection-limit {
|
||||
background-color: #fcf8e3;
|
||||
color: #8a6d3b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address disabled Select2 styles.
|
||||
*
|
||||
* 1. For Select2 v.3.3.2.
|
||||
* 2. Revert border-left:0 inherited from Select2's CSS to prevent the arrow
|
||||
* from jumping when switching from disabled to enabled state and vice versa.
|
||||
*/
|
||||
.select2-container.select2-container-disabled .select2-choice,
|
||||
.select2-container.select2-container-disabled .select2-choices {
|
||||
cursor: not-allowed;
|
||||
background-color: #eeeeee;
|
||||
border-color: #cccccc;
|
||||
}
|
||||
.select2-container.select2-container-disabled .select2-choice .select2-arrow,
|
||||
.select2-container.select2-container-disabled .select2-choice div,
|
||||
.select2-container.select2-container-disabled .select2-choices .select2-arrow,
|
||||
.select2-container.select2-container-disabled .select2-choices div {
|
||||
background-color: transparent;
|
||||
border-left: 1px solid transparent;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Address Select2's loading indicator position - which should not stick
|
||||
* to the right edge of Select2's search input.
|
||||
*
|
||||
* 1. in .select2-search input
|
||||
* 2. in Multi Select2's .select2-search-field input
|
||||
* 3. in the status-message of infinite-scroll with remote data (@see http://ivaynberg.github.io/select2/#infinite)
|
||||
*
|
||||
* These styles alter Select2's default background-position of 100%
|
||||
* and supply the new background-position syntax to browsers which support it:
|
||||
*
|
||||
* 1. Android, Safari < 6/Mobile, IE<9: change to a relative background-position of 99%
|
||||
* 2. Chrome 25+, Firefox 13+, IE 9+, Opera 10.5+: use the new CSS3-background-position syntax
|
||||
*
|
||||
* @see http://www.w3.org/TR/css3-background/#background-position
|
||||
*
|
||||
* @todo Since both Select2 and Bootstrap 3 only support IE8 and above,
|
||||
* we could use the :after-pseudo-element to display the loading indicator.
|
||||
* Alternatively, we could supply an altered loading indicator image which already
|
||||
* contains an offset to the right.
|
||||
*/
|
||||
.select2-search input.select2-active,
|
||||
.select2-container-multi .select2-choices .select2-search-field input.select2-active,
|
||||
.select2-more-results.select2-active {
|
||||
background-position: 99%;
|
||||
/* 4 */
|
||||
background-position: right 4px center;
|
||||
/* 5 */
|
||||
}
|
||||
|
||||
/**
|
||||
* To support Select2 pre v3.4.2 in combination with Bootstrap v3.2.0,
|
||||
* ensure that .select2-offscreen width, height and position can not be overwritten.
|
||||
*
|
||||
* This adresses changes in Bootstrap somewhere after the initial v3.0.0 which -
|
||||
* in combination with Select2's pre-v3.4.2 CSS missing the "!important" after
|
||||
* the following rules - allow Bootstrap to overwrite the latter, which results in
|
||||
* the original <select> element Select2 is replacing not be properly being hidden
|
||||
* when used in a "Bootstrap Input Group with Addon".
|
||||
**/
|
||||
.select2-offscreen,
|
||||
.select2-offscreen:focus {
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
position: absolute !important;
|
||||
}
|
||||
1
static/select2.min.css
vendored
2
static/select2.min.js
vendored
BIN
static/serpe.jpg
|
Before Width: | Height: | Size: 121 KiB |
0
tests/__init__.py
Normal file
42
tests/core_tests.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import imp
|
||||
import os
|
||||
import unittest
|
||||
import urllib2
|
||||
os.environ['PANORAMIX_CONFIG'] = 'tests.panoramix_test_config'
|
||||
from flask.ext.testing import LiveServerTestCase, TestCase
|
||||
|
||||
from panoramix import app, db, models
|
||||
BASE_DIR = app.config.get("BASE_DIR")
|
||||
cli = imp.load_source('cli', BASE_DIR + "/bin/panoramix")
|
||||
|
||||
|
||||
class LiveTest(TestCase):
|
||||
|
||||
def create_app(self):
|
||||
app.config['LIVESERVER_PORT'] = 8873
|
||||
app.config['TESTING'] = True
|
||||
return app
|
||||
|
||||
def setUp(self):
|
||||
print BASE_DIR
|
||||
|
||||
def test_load_examples(self):
|
||||
cli.load_examples(sample=True)
|
||||
|
||||
def test_slices(self):
|
||||
Slc = models.Slice
|
||||
for slc in db.session.query(Slc).all():
|
||||
self.client.get(slc.slice_url)
|
||||
viz = slc.viz
|
||||
if hasattr(viz, 'get_json'):
|
||||
self.client.get(viz.get_json())
|
||||
|
||||
def test_dashboard(self):
|
||||
for dash in db.session.query(models.Dashboard).all():
|
||||
self.client.get(dash.url)
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
6
tests/panoramix_test_config.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from panoramix.config import *
|
||||
|
||||
AUTH_USER_REGISTRATION_ROLE = 'alpha'
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/panoramix_unittests.db'
|
||||
DEBUG = True
|
||||
PANORAMIX_WEBSERVER_PORT = 8081
|
||||