Merge pull request #94 from mistercrunch/context

Massive js refactor + Dashboard filters
This commit is contained in:
Maxime Beauchemin 2015-12-24 11:57:36 -08:00
commit 9d0de6c8e7
45 changed files with 1023 additions and 12489 deletions

View File

@ -2,6 +2,11 @@
List of TODO items for Panoramix List of TODO items for Panoramix
## Improvments ## Improvments
* Table description is markdown
* Default slice instead of default endpoint
* dashboard controller + filters
* Color hash in JS
* Widget sets ()
* datasource in explore mode could be a dropdown * datasource in explore mode could be a dropdown
* [sql] make "Test Connection" test further * [sql] make "Test Connection" test further
* [druid] Allow for post aggregations (ratios!) * [druid] Allow for post aggregations (ratios!)

View File

@ -1,19 +1,12 @@
#!/usr/bin/env python #!/usr/bin/env python
import csv
from datetime import datetime
import gzip
import json
import os
from subprocess import Popen from subprocess import Popen
from flask.ext.script import Manager from flask.ext.script import Manager
from panoramix import app from panoramix import app
from flask.ext.migrate import MigrateCommand from flask.ext.migrate import MigrateCommand
from panoramix import db from panoramix import db
from flask.ext.appbuilder import Base from panoramix import data, utils
from sqlalchemy import Column, Integer, String, Table, DateTime
from panoramix import models, utils
config = app.config config = app.config
@ -60,314 +53,10 @@ def load_examples(sample):
"""Loads a set of Slices and Dashboards and a supporting dataset """ """Loads a set of Slices and Dashboards and a supporting dataset """
print("Loading examples into {}".format(db)) print("Loading examples into {}".format(db))
print("Loading [World Bank's Health Nutrition and Population Stats]")
BirthNames = Table( data.load_world_bank_health_n_pop()
"birth_names", Base.metadata, print("Loading [Birth names]")
Column("id", Integer, primary_key=True), data.load_birth_names()
Column("state", String(10)),
Column("year", Integer),
Column("name", String(128)),
Column("num", Integer),
Column("ds", DateTime),
Column("gender", String(10)),
Column("sum_boys", Integer),
Column("sum_girls", Integer),
)
try:
BirthNames.drop(db.engine)
except:
pass
BirthNames.create(db.engine)
session = db.session()
filepath = os.path.join(config.get("BASE_DIR"), 'data/birth_names.csv.gz')
with gzip.open(filepath, mode='rt') as f:
bb_csv = csv.reader(f)
for i, (state, year, name, gender, num) in enumerate(bb_csv):
if i == 0 or year < "1965": # jumpy data before 1965
continue
if num == "NA":
num = 0
ds = datetime(int(year), 1, 1)
db.engine.execute(
BirthNames.insert(),
state=state,
year=year,
ds=ds,
name=name, num=num, gender=gender,
sum_boys=num if gender == 'boy' else 0,
sum_girls=num if gender == 'girl' else 0,
)
if i % 1000 == 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.SqlaTable
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=ds&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.TableColumn(column_name="sum_boys", sum=True, type="INTEGER"),
models.TableColumn(column_name="sum_girls", sum=True, type="INTEGER"),
models.TableColumn(column_name="ds", is_dttm=True, type="DATETIME"),
]
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": "ds",
"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="ds",
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 = "Gender by State"
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",
metrics=['sum__sum_girls', 'sum__sum_boys'],
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='ds', rich_tooltip='y', show_legend='y'))
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)
![img](http://monblog.system-linux.net/image/tux/baby-tux_overlord59-tux.png)
"""
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)
slice_name = "Pivot Table"
slc = session.query(Slice).filter_by(slice_name=slice_name).first()
if not slc:
slc = Slice(
slice_name=slice_name,
viz_type='pivot_table',
datasource_type='table',
table=tbl,
params=get_slice_json(
slice_name, viz_type="pivot_table", metrics=['sum__num'],
groupby=['name'], columns=['state']))
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__": if __name__ == "__main__":

View File

@ -0,0 +1,351 @@
from datetime import datetime
import csv
import gzip
import json
import os
from flask.ext.appbuilder import Base
import pandas as pd
from sqlalchemy import Column, String, DateTime, Table, Integer
from panoramix import app, db, models
config = app.config
DATA_FOLDER = os.path.join(config.get("BASE_DIR"), 'data')
def get_or_create_db(session):
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()
return dbobj
def load_world_bank_health_n_pop():
"""
Details on how the data was loaded from
http://data.worldbank.org/data-catalog/health-nutrition-and-population-statistics
DIR = ""
df_country = pd.read_csv(DIR + '/HNP_Country.csv')
df_country.columns = ['country_code'] + list(df_country.columns[1:])
df_country = df_country[['country_code', 'Region']]
df_country.columns = ['country_code', 'region']
df = pd.read_csv(DIR + '/HNP_Data.csv')
del df['Unnamed: 60']
df.columns = ['country_name', 'country_code'] + list(df.columns[2:])
ndf = df.merge(df_country, how='inner')
dims = ('country_name', 'country_code', 'region')
vv = [str(i) for i in range(1960, 2015)]
mdf = pd.melt(ndf, id_vars=dims + ('Indicator Code',), value_vars=vv)
mdf['year'] = mdf.variable + '-01-01'
dims = dims + ('year',)
pdf = mdf.pivot_table(values='value', columns='Indicator Code', index=dims)
pdf = pdf.reset_index()
pdf.to_csv(DIR + '/countries.csv')
pdf.to_json(DIR + '/countries.json', orient='records')
"""
tbl = 'wb_health_population'
with gzip.open(os.path.join(DATA_FOLDER, 'countries.json.gz')) as f:
pdf = pd.read_json(f)
pdf.year = pd.to_datetime(pdf.year)
pdf.to_sql(
tbl,
db.engine,
if_exists='replace',
chunksize=500,
dtype={
'year': DateTime(),
'country_code': String(3),
'country_name': String(255),
'region': String(255),
},
index=False)
print("Creating table reference")
TBL = models.SqlaTable
obj = db.session.query(TBL).filter_by(table_name=tbl).first()
if not obj:
obj = TBL(table_name='wb_health_population')
obj.main_dttm_col = 'year'
obj.database = get_or_create_db(db.session)
models.Table
db.session.add(obj)
db.session.commit()
obj.fetch_metadata()
def load_birth_names():
session = db.session
with gzip.open(os.path.join(DATA_FOLDER, 'birth_names.json.gz')) as f:
pdf = pd.read_json(f)
pdf.ds = pd.to_datetime(pdf.ds, unit='ms')
pdf.to_sql(
'birth_names',
db.engine,
if_exists='replace',
chunksize=500,
dtype={
'ds': DateTime,
'gender': String(16),
'state': String(10),
'name': String(255),
},
index=False)
l = []
print("Done loading table!")
print("-" * 80)
print("Creating table reference")
TBL = models.SqlaTable
obj = db.session.query(TBL).filter_by(table_name='birth_names').first()
if not obj:
obj = TBL(table_name = 'birth_names')
obj.main_dttm_col = 'ds'
obj.database = get_or_create_db(db.session)
models.Table
db.session.add(obj)
db.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": "ds",
"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 = db.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="ds",
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 = "Gender by State"
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",
metrics=['sum__sum_girls', 'sum__sum_boys'],
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='ds', rich_tooltip='y', show_legend='y'))
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)
![img](http://monblog.system-linux.net/image/tux/baby-tux_overlord59-tux.png)
"""
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)
slice_name = "Pivot Table"
slc = session.query(Slice).filter_by(slice_name=slice_name).first()
if not slc:
slc = Slice(
slice_name=slice_name,
viz_type='pivot_table',
datasource_type='table',
table=tbl,
params=get_slice_json(
slice_name, viz_type="pivot_table", metrics=['sum__num'],
groupby=['name'], columns=['state']))
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()

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -32,7 +32,8 @@ class SelectMultipleSortableField(SelectMultipleField):
d[value] = (value, label, selected) d[value] = (value, label, selected)
if self.data: if self.data:
for value in self.data: for value in self.data:
yield d.pop(value) if value:
yield d.pop(value)
while d: while d:
yield d.pop(d.keys()[0]) yield d.pop(d.keys()[0])
@ -402,6 +403,7 @@ class FormFactory(object):
css_classes = field_css_classes css_classes = field_css_classes
standalone = HiddenField() standalone = HiddenField()
async = HiddenField() async = HiddenField()
extra_filters = HiddenField()
json = HiddenField() json = HiddenField()
slice_id = HiddenField() slice_id = HiddenField()
slice_name = HiddenField() slice_name = HiddenField()

View File

@ -1,8 +1,17 @@
from datetime import timedelta from copy import deepcopy, copy
from collections import namedtuple
from datetime import timedelta, datetime
import json
from six import string_types
import sqlparse
import requests
import textwrap
from dateutil.parser import parse from dateutil.parser import parse
from flask import flash from flask import flash
from flask.ext.appbuilder import Model from flask.ext.appbuilder import Model
from flask.ext.appbuilder.models.mixins import AuditMixin from flask.ext.appbuilder.models.mixins import AuditMixin
import pandas as pd
from pandas import read_sql_query from pandas import read_sql_query
from pydruid import client from pydruid import client
from pydruid.utils.filters import Dimension, Filter from pydruid.utils.filters import Dimension, Filter
@ -11,19 +20,11 @@ from sqlalchemy import (
Column, Integer, String, ForeignKey, Text, Boolean, DateTime, Column, Integer, String, ForeignKey, Text, Boolean, DateTime,
Table, create_engine, MetaData, desc, select, and_, func) Table, create_engine, MetaData, desc, select, and_, func)
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.sql import table, literal_column, text from sqlalchemy.sql import table, literal_column, text, column
from sqlalchemy.sql.elements import ColumnClause from sqlalchemy.sql.elements import ColumnClause
from sqlalchemy_utils import EncryptedType from sqlalchemy_utils import EncryptedType
from copy import deepcopy, copy
from collections import namedtuple
from datetime import datetime
import json
import sqlparse
import requests
import textwrap
from six import string_types
from panoramix import app, db, get_session, utils from panoramix import app, db, get_session, utils
from panoramix.viz import viz_types from panoramix.viz import viz_types
@ -85,6 +86,16 @@ class Slice(Model, AuditMixinNullable):
def datasource_id(self): def datasource_id(self):
return self.table_id or self.druid_datasource_id return self.table_id or self.druid_datasource_id
@property
def data(self):
d = self.viz.data
d['slice_id'] = self.id
return d
@property
def json_data(self):
return json.dumps(self.data)
@property @property
def slice_url(self): def slice_url(self):
try: try:
@ -409,9 +420,7 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
groupby_exprs = [] groupby_exprs = []
if groupby: if groupby:
select_exprs = [literal_column(s) for s in groupby]
select_exprs = [] select_exprs = []
groupby_exprs = []
inner_select_exprs = [] inner_select_exprs = []
inner_groupby_exprs = [] inner_groupby_exprs = []
for s in groupby: for s in groupby:
@ -421,8 +430,8 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
outer = ColumnClause(expr, is_literal=True).label(s) outer = ColumnClause(expr, is_literal=True).label(s)
inner = ColumnClause(expr, is_literal=True).label('__' + s) inner = ColumnClause(expr, is_literal=True).label('__' + s)
else: else:
outer = literal_column(s).label(s) outer = column(s).label(s)
inner = literal_column(s).label('__' + s) inner = column(s).label('__' + s)
groupby_exprs.append(outer) groupby_exprs.append(outer)
select_exprs.append(outer) select_exprs.append(outer)
@ -462,7 +471,7 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
cond = ColumnClause( cond = ColumnClause(
col_obj.expression, is_literal=True).in_(values) col_obj.expression, is_literal=True).in_(values)
else: else:
cond = literal_column(col).in_(values) cond = column(col).in_(values)
if op == 'not in': if op == 'not in':
cond = ~cond cond = ~cond
where_clause_and.append(cond) where_clause_and.append(cond)
@ -486,7 +495,7 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
on_clause = [] on_clause = []
for i, gb in enumerate(groupby): for i, gb in enumerate(groupby):
on_clause.append( on_clause.append(
groupby_exprs[i] == literal_column("__" + gb)) groupby_exprs[i] == column("__" + gb))
from_clause = from_clause.join(subq.alias(), and_(*on_clause)) from_clause = from_clause.join(subq.alias(), and_(*on_clause))
@ -499,7 +508,6 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
con=engine con=engine
) )
sql = sqlparse.format(sql, reindent=True) sql = sqlparse.format(sql, reindent=True)
print(sql)
return QueryResult( return QueryResult(
df=df, duration=datetime.now() - qry_start_dttm, query=sql) df=df, duration=datetime.now() - qry_start_dttm, query=sql)
@ -547,33 +555,35 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
if not any_date_col and 'date' in datatype.lower(): if not any_date_col and 'date' in datatype.lower():
any_date_col = col.name any_date_col = col.name
quoted = "{}".format(
column(dbcol.column_name).compile(dialect=db.engine.dialect))
if dbcol.sum: if dbcol.sum:
metrics.append(M( metrics.append(M(
metric_name='sum__' + dbcol.column_name, metric_name='sum__' + dbcol.column_name,
verbose_name='sum__' + dbcol.column_name, verbose_name='sum__' + dbcol.column_name,
metric_type='sum', metric_type='sum',
expression="SUM({})".format(dbcol.column_name) expression="SUM({})".format(quoted)
)) ))
if dbcol.max: if dbcol.max:
metrics.append(M( metrics.append(M(
metric_name='max__' + dbcol.column_name, metric_name='max__' + dbcol.column_name,
verbose_name='max__' + dbcol.column_name, verbose_name='max__' + dbcol.column_name,
metric_type='max', metric_type='max',
expression="MAX({})".format(dbcol.column_name) expression="MAX({})".format(quoted)
)) ))
if dbcol.min: if dbcol.min:
metrics.append(M( metrics.append(M(
metric_name='min__' + dbcol.column_name, metric_name='min__' + dbcol.column_name,
verbose_name='min__' + dbcol.column_name, verbose_name='min__' + dbcol.column_name,
metric_type='min', metric_type='min',
expression="MIN({})".format(dbcol.column_name) expression="MIN({})".format(quoted)
)) ))
if dbcol.count_distinct: if dbcol.count_distinct:
metrics.append(M( metrics.append(M(
metric_name='count_distinct__' + dbcol.column_name, metric_name='count_distinct__' + dbcol.column_name,
verbose_name='count_distinct__' + dbcol.column_name, verbose_name='count_distinct__' + dbcol.column_name,
metric_type='count_distinct', metric_type='count_distinct',
expression="COUNT(DISTINCT {})".format(dbcol.column_name) expression="COUNT(DISTINCT {})".format(quoted)
)) ))
dbcol.type = datatype dbcol.type = datatype
db.session.merge(self) db.session.merge(self)

View File

@ -1,6 +1,10 @@
html>body{ html>body{
margin: 0px; !important margin: 0px; !important
} }
.slice_container {
height: 100%;
}
.container-fluid { .container-fluid {
text-align: left; text-align: left;
} }
@ -132,8 +136,8 @@ legend {
.datasource .tooltip-inner { .datasource .tooltip-inner {
max-width: 350px; max-width: 350px;
} }
.datasource img.loading { img.loading {
width: 30px; width: 40px;
} }
.dashboard a i { .dashboard a i {
@ -181,7 +185,7 @@ legend {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.dashboard table.widget_header { .dashboard table.slice_header {
width: 100%; width: 100%;
height: 20px; height: 20px;
} }
@ -192,7 +196,7 @@ legend {
.dashboard li.widget.pie, .dashboard li.widget.pie,
.dashboard li.widget.dist_bar, .dashboard li.widget.dist_bar,
.dashboard li.widget.sunburst { .dashboard li.widget.sunburst {
overflow: visible; /* This allows elements within these slice typesin a dashboard to overflow */ overflow: visible; /* This allows elements within these widget typesin a dashboard to overflow */
} }
.dashboard div.nvtooltip { .dashboard div.nvtooltip {
z-index: 888; /* this lets tool tips go on top of other slices */ z-index: 888; /* this lets tool tips go on top of other slices */

View File

@ -1,249 +1,339 @@
var timer;
var px = (function() { var px = (function() {
var visualizations = []; var visualizations = {};
var dashboard = undefined;
function registerWidget(name, initializer) { var Slice = function(data, dashboard){
visualizations[name] = initializer; var timer;
}
function makeNullWidget() {
return {
render: function() {},
resize: function() {},
};
}
function initializeWidget(data) {
var token = $('#' + data.token); var token = $('#' + data.token);
var container_id = data.token + '_con';
var selector = '#' + container_id;
var container = $(selector);
var slice_id = data.slice_id;
var name = data['viz_name']; var name = data['viz_name'];
var initializer = visualizations[name];
var user_defined_widget = initializer ? initializer(data) : makeNullWidget();
var dttm = 0; var dttm = 0;
var timer; var timer;
var stopwatch = function () { var stopwatch = function () {
dttm += 10; dttm += 10;
$('#timer').text(Math.round(dttm/10)/100 + " sec"); $('#timer').text(Math.round(dttm/10)/100 + " sec");
} }
var controler = { var qrystr = '';
slice = {
jsonEndpoint: function() {
var parser = document.createElement('a');
parser.href = data.json_endpoint;
// Shallow copy
if (dashboard !== undefined){
qrystr = parser.search + "&extra_filters=" + JSON.stringify(dashboard.filters);
}
else if ($('#query').length == 0){
qrystr = parser.search;
}
else {
qrystr = '?' + $('#query').serialize();
}
var endpoint = parser.pathname + qrystr + "&json=true";
return endpoint;
},
done: function (data) { done: function (data) {
clearInterval(timer); clearInterval(timer);
token.find("img.loading").hide(); token.find("img.loading").hide()
container.show();
if(data !== undefined) if(data !== undefined)
$("#query_container").html(data.query); $("#query_container").html(data.query);
$('#timer').removeClass('btn-warning'); $('#timer').removeClass('btn-warning');
$('span.query').removeClass('disabled');
$('#timer').addClass('btn-success'); $('#timer').addClass('btn-success');
$('span.query').removeClass('disabled');
}, },
error: function (msg) { error: function (msg) {
clearInterval(timer); clearInterval(timer);
token.find("img.loading").hide(); token.find("img.loading").hide();
var err = '<div class="alert alert-danger">' + msg + '</div>'; var err = '<div class="alert alert-danger">' + msg + '</div>';
token.html(err); container.html(err);
container.show();
$('#timer').removeClass('btn-warning'); $('#timer').removeClass('btn-warning');
$('span.query').removeClass('disabled'); $('span.query').removeClass('disabled');
$('#timer').addClass('btn-danger'); $('#timer').addClass('btn-danger');
} },
}; data: data,
widget = { container: container,
container_id: container_id,
selector: selector,
render: function() { render: function() {
token.find("img.loading").show();
container.hide();
container.html('');
dttm = 0;
timer = setInterval(stopwatch, 10); timer = setInterval(stopwatch, 10);
user_defined_widget.render(controler); $('#timer').removeClass('btn-danger btn-success');
$('#timer').addClass('btn-warning');
viz.render();
console.log(slice);
$('#json').click(function(){window.location=slice.jsonEndpoint()});
$('#standalone').click(function(){window.location=slice.data.standalone_endpoint});
$('#csv').click(function(){window.location=slice.data.csv_endpoint});
}, },
resize: function() { resize: function() {
user_defined_widget.resize(); token.find("img.loading").show();
container.hide();
container.html('');
viz.render();
viz.resize();
},
addFilter: function(col, vals) {
if(dashboard !== undefined)
dashboard.addFilter(slice_id, col, vals);
},
clearFilter: function() {
if(dashboard !== undefined)
delete dashboard.clearFilter(slice_id);
}, },
}; };
return widget; var viz = visualizations[data.form_data.viz_type](slice);
slice['viz'] = viz;
return slice;
} }
function initializeDatasourceView() { var Dashboard = function(id){
var dash = {
slices: [],
filters: {},
id: id,
addFilter: function(slice_id, field, values) {
this.filters[slice_id] = [field, values];
this.refreshExcept(slice_id);
},
refreshExcept: function(slice_id) {
this.slices.forEach(function(slice){
if(slice.data.slice_id != slice_id){
slice.render();
}
});
},
clearFilter: function(slice_id) {
delete this.filters[slice_id];
this.refreshExcept(slice_id);
},
getSlice: function(slice_id) {
for(var i=0; i<this.slices.length; i++){
if (this.slices[i].data.slice_id == slice_id)
return this.slices[i];
}
}
}
$('.dashboard li.widget').each(function() {
var data = $(this).data('slice');
var slice = Slice(data, dash);
$(this).find('a.refresh').click(function(){
slice.render();
});
dash.slices.push(slice);
slice.render();
});
dashboard = dash;
return dash;
}
function registerViz(name, initViz) {
visualizations[name] = initViz;
}
function initExploreView() {
function getParam(name) { function getParam(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"), var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search); results = regex.exec(location.search);
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
$(".select2").select2({dropdownAutoWidth : true});
$(".select2Sortable").select2();
$(".select2Sortable").select2Sortable();
$("form").show();
$('[data-toggle="tooltip"]').tooltip({container: 'body'});
function set_filters(){
for (var i = 1; i < 10; i++){
var eq = getParam("flt_eq_" + i);
if (eq != ''){
add_filter(i);
}
} }
}
set_filters();
function add_filter(i) { $(".select2").select2({dropdownAutoWidth : true});
cp = $("#flt0").clone(); $(".select2Sortable").select2();
$(cp).appendTo("#filters"); $(".select2Sortable").select2Sortable();
$(cp).show(); $("form").show();
if (i != undefined){ $('[data-toggle="tooltip"]').tooltip({container: 'body'});
$(cp).find("#flt_eq_0").val(getParam("flt_eq_" + i));
$(cp).find("#flt_op_0").val(getParam("flt_op_" + i));
$(cp).find("#flt_col_0").val(getParam("flt_col_" + i));
}
$(cp).find('select').select2();
$(cp).find('.remove').click(function() {
$(this).parent().parent().remove();
});
}
function druidify(){ function set_filters(){
var i = 1; for (var i = 1; i < 10; i++){
// Assigning the right id to form elements in filters var eq = getParam("flt_eq_" + i);
$("#filters > div").each(function() { if (eq != ''){
$(this).attr("id", function() {return "flt_" + i;}) add_filter(i);
$(this).find("#flt_col_0")
.attr("id", function() {return "flt_col_" + i;})
.attr("name", function() {return "flt_col_" + i;});
$(this).find("#flt_op_0")
.attr("id", function() {return "flt_op_" + i;})
.attr("name", function() {return "flt_op_" + i;});
$(this).find("#flt_eq_0")
.attr("id", function() {return "flt_eq_" + i;})
.attr("name", function() {return "flt_eq_" + i;});
i++;
});
$("#query").submit();
}
$("#plus").click(add_filter);
$("#btn_save").click(function () {
var slice_name = prompt("Name your slice!");
if (slice_name != "" && slice_name != null) {
$("#slice_name").val(slice_name);
$("#action").val("save");
druidify();
}
});
$("#btn_overwrite").click(function () {
var flag = confirm("Overwrite slice [" + $("#slice_name").val() + "] !?");
if (flag) {
$("#action").val("overwrite");
druidify();
}
});
add_filter();
$(".druidify").click(druidify);
function create_choices(term, data) {
var filtered = $(data).filter(function() {
return this.text.localeCompare(term) === 0;
});
if (filtered.length === 0) {
return {id: term, text: term};
}
}
function initSelectionToValue(element, callback) {
callback({id: element.val(), text: element.val()});
}
function list_data(arr) {
var obj = [];
for (var i=0; i<arr.length; i++){
obj.push({id: arr[i], text: arr[i]});
}
return obj;
}
$(".select2_freeform").each(function(){
parent = $(this).parent();
var name = $(this).attr('name');
var l = [];
var selected = '';
for(var i=0; i<this.options.length; i++) {
l.push({id: this.options[i].value, text: this.options[i].text});
if(this.options[i].selected){
selected = this.options[i].value;
} }
}
obj = parent.append(
'<input class="' + $(this).attr('class') + '" name="'+ name +'" type="text" value="' + selected + '">');
$("input[name='" + name +"']")
.select2({
createSearchChoice: create_choices,
initSelection: initSelectionToValue,
multiple: false,
data: l,
});
$(this).remove();
});
}
function initializeDashboardView(dashboard_id) {
var gridster = $(".gridster ul").gridster({
widget_margins: [5, 5],
widget_base_dimensions: [100, 100],
draggable: {
handle: '.drag',
},
resize: {
enabled: true,
stop: function(e, ui, element) {
var widget = $(element).data('widget');
widget.resize();
} }
}, }
serialize_params: function(_w, wgd) { set_filters();
return {
slice_id: $(_w).attr('slice_id'), function add_filter(i) {
col: wgd.col, cp = $("#flt0").clone();
row: wgd.row, $(cp).appendTo("#filters");
size_x: wgd.size_x, $(cp).show();
size_y: wgd.size_y if (i != undefined){
}; $(cp).find("#flt_eq_0").val(getParam("flt_eq_" + i));
}, $(cp).find("#flt_op_0").val(getParam("flt_op_" + i));
}).data('gridster'); $(cp).find("#flt_col_0").val(getParam("flt_col_" + i));
$("div.gridster").css('visibility', 'visible'); }
$("#savedash").click(function() { $(cp).find('select').select2();
var data = { $(cp).find('.remove').click(function() {
positions: gridster.serialize(), $(this).parent().parent().remove();
css: $("#dash_css").val() });
}; }
$.ajax({ function prepForm(){
type: "POST", var i = 1;
url: '/panoramix/save_dash/' + dashboard_id + '/', // Assigning the right id to form elements in filters
data: {'data': JSON.stringify(data)}, $("#filters > div").each(function() {
success: function() {alert("Saved!")}, $(this).attr("id", function() {return "flt_" + i;})
error: function() {alert("Error :(")}, $(this).find("#flt_col_0")
.attr("id", function() {return "flt_col_" + i;})
.attr("name", function() {return "flt_col_" + i;});
$(this).find("#flt_op_0")
.attr("id", function() {return "flt_op_" + i;})
.attr("name", function() {return "flt_op_" + i;});
$(this).find("#flt_eq_0")
.attr("id", function() {return "flt_eq_" + i;})
.attr("name", function() {return "flt_eq_" + i;});
i++;
});
}
function druidify(){
prepForm();
slice.render();
}
$("#plus").click(add_filter);
$("#btn_save").click(function () {
var slice_name = prompt("Name your slice!");
if (slice_name != "" && slice_name != null) {
$("#slice_name").val(slice_name);
prepForm();
$("#action").val("save");
$("#query").submit();
}
}); });
}); $("#btn_overwrite").click(function () {
$("a.closewidget").click(function() { var flag = confirm("Overwrite slice [" + $("#slice_name").val() + "] !?");
var li = $(this).parents("li"); if (flag) {
gridster.remove_widget(li); $("#action").val("overwrite");
}); prepForm();
$("table.widget_header").mouseover(function() { $("#query").submit();
$(this).find("td.icons nobr").show(); }
}); });
$("table.widget_header").mouseout(function() { add_filter();
$(this).find("td.icons nobr").hide(); $(".druidify").click(druidify);
});
$("#dash_css").on("keyup", function(){ function create_choices(term, data) {
css = $(this).val(); var filtered = $(data).filter(function() {
$("#user_style").html(css); return this.text.localeCompare(term) === 0;
}); });
$('li.widget').each(function() { /* this sets the z-index for left side boxes higher. */ if (filtered.length === 0) {
current_row = $(this).attr('data-col'); return {id: term, text: term};
$( this ).css('z-index', 100 - current_row); }
}); }
$("div.chart").each(function() { /* this makes the whole chart fit within the dashboard div */ function initSelectionToValue(element, callback) {
$(this).css('height', '95%'); callback({id: element.val(), text: element.val()});
}); }
} function list_data(arr) {
var obj = [];
for (var i=0; i<arr.length; i++){
obj.push({id: arr[i], text: arr[i]});
}
return obj;
}
$(".select2_freeform").each(function(){
parent = $(this).parent();
var name = $(this).attr('name');
var l = [];
var selected = '';
for(var i=0; i<this.options.length; i++) {
l.push({id: this.options[i].value, text: this.options[i].text});
if(this.options[i].selected){
selected = this.options[i].value;
}
}
obj = parent.append(
'<input class="' + $(this).attr('class') + '" name="'+ name +'" type="text" value="' + selected + '">');
$("input[name='" + name +"']")
.select2({
createSearchChoice: create_choices,
initSelection: initSelectionToValue,
multiple: false,
data: l,
});
$(this).remove();
});
}
function initDashboardView() {
var gridster = $(".gridster ul").gridster({
widget_margins: [5, 5],
widget_base_dimensions: [100, 100],
draggable: {
handle: '.drag',
},
resize: {
enabled: true,
stop: function(e, ui, element) {
var slice_data = $(element).data('slice');
dashboard.getSlice(slice_data.slice_id).resize();
}
},
serialize_params: function(_w, wgd) {
return {
slice_id: $(_w).attr('slice_id'),
col: wgd.col,
row: wgd.row,
size_x: wgd.size_x,
size_y: wgd.size_y
};
},
}).data('gridster');
$("div.gridster").css('visibility', 'visible');
$("#savedash").click(function() {
var data = {
positions: gridster.serialize(),
css: $("#dash_css").val()
};
$.ajax({
type: "POST",
url: '/panoramix/save_dash/' + dashboard.id + '/',
data: {'data': JSON.stringify(data)},
success: function() {alert("Saved!")},
error: function() {alert("Error :(")},
});
});
$("a.closeslice").click(function() {
var li = $(this).parents("li");
gridster.remove_widget(li);
});
$("table.slice_header").mouseover(function() {
$(this).find("td.icons nobr").show();
});
$("table.slice_header").mouseout(function() {
$(this).find("td.icons nobr").hide();
});
$("#dash_css").on("keyup", function(){
css = $(this).val();
$("#user_style").html(css);
});
// this sets the z-index for left side boxes higher
$('li.slice').each(function() {
current_row = $(this).attr('data-col');
$( this ).css('z-index', 100 - current_row);
});
// this makes the whole chart fit within the dashboard div
$("div.chart").each(function() {
$(this).css('height', '95%');
});
}
// Export public functions // Export public functions
return { return {
registerWidget: registerWidget, registerViz: registerViz,
initializeWidget: initializeWidget, Slice: Slice,
initializeDatasourceView: initializeDatasourceView, Dashboard: Dashboard,
initializeDashboardView: initializeDashboardView, initExploreView: initExploreView,
initDashboardView: initDashboardView,
} }
})(); })();

View File

@ -1,15 +1,12 @@
px.registerWidget('big_number', function(data_attribute) { px.registerViz('big_number', function(slice) {
var data_attribute = slice.data;
var div = d3.select(slice.selector);
var token_name = data_attribute['token']; function render() {
var json_callback = data_attribute['json_endpoint']; d3.json(slice.jsonEndpoint(), function(error, payload){
var div = d3.select('#' + token_name);
function render(ctrl) {
d3.json(json_callback, function(error, payload){
//Define the percentage bounds that define color from red to green //Define the percentage bounds that define color from red to green
div.html("");
if (error != null){ if (error != null){
ctrl.error(error.responseText); slice.error(error.responseText);
return ''; return '';
} }
json = payload.data; json = payload.data;
@ -19,9 +16,8 @@ px.registerWidget('big_number', function(data_attribute) {
var f = d3.format('.3s'); var f = d3.format('.3s');
var fp = d3.format('+.1%'); var fp = d3.format('+.1%');
var xy = div.node().getBoundingClientRect(); var width = slice.container.width();
var width = xy.width; var height = slice.container.height() - 30;
var height = xy.height - 30;
var svg = div.append('svg'); var svg = div.append('svg');
svg.attr("width", width); svg.attr("width", width);
svg.attr("height", height); svg.attr("height", height);
@ -135,7 +131,7 @@ px.registerWidget('big_number', function(data_attribute) {
div.select('g.digits').transition().duration(500).attr('opacity', 1); div.select('g.digits').transition().duration(500).attr('opacity', 1);
div.select('g.axis').transition().duration(500).attr('opacity', 0); div.select('g.axis').transition().duration(500).attr('opacity', 0);
}); });
ctrl.done(payload); slice.done(payload);
}); });
}; };

View File

@ -3,9 +3,6 @@
stroke: #000; stroke: #000;
stroke-width: 1.5px; stroke-width: 1.5px;
} }
.directed_force #chart {
height: 100%;
}
.directed_force circle { .directed_force circle {
fill: #ccc; fill: #ccc;

View File

@ -2,25 +2,23 @@
Modified from http://bl.ocks.org/d3noob/5141278 Modified from http://bl.ocks.org/d3noob/5141278
*/ */
function viz_directed_force(data_attribute) { function viz_directed_force(slice) {
var token = d3.select('#' + data_attribute.token); var width = slice.container.width();
var xy = token.select('#chart').node().getBoundingClientRect(); var height = slice.container.height() - 25;
var width = xy.width; var link_length = slice.data.form_data['link_length'];
var height = xy.height - 25; var div = d3.select(slice.selector);
var radius = Math.min(width, height) / 2;
var link_length = data_attribute.form_data['link_length'];
if (link_length === undefined){ if (link_length === undefined){
link_length = 200; link_length = 200;
} }
var charge = data_attribute.form_data['charge']; var charge = slice.data.form_data['charge'];
if (charge === undefined){ if (charge === undefined){
charge = -500; charge = -500;
} }
var render = function(ctrl) { var render = function() {
d3.json(data_attribute.json_endpoint, function(error, json) { d3.json(slice.jsonEndpoint(), function(error, json) {
if (error != null){ if (error != null){
ctrl.error(error.responseText); slice.error(error.responseText);
return ''; return '';
} }
links = json.data; links = json.data;
@ -59,7 +57,7 @@ function viz_directed_force(data_attribute) {
.on("tick", tick) .on("tick", tick)
.start(); .start();
var svg = token.select("#chart").append("svg") var svg = div.append("svg")
.attr("width", width) .attr("width", width)
.attr("height", height); .attr("height", height);
@ -150,7 +148,7 @@ function viz_directed_force(data_attribute) {
.attr("transform", function(d) { .attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")"; }); return "translate(" + d.x + "," + d.y + ")"; });
} }
ctrl.done(json); slice.done(json);
}); });
} }
return { return {
@ -158,4 +156,4 @@ function viz_directed_force(data_attribute) {
resize: render, resize: render,
}; };
} }
px.registerWidget('directed_force', viz_directed_force); px.registerViz('directed_force', viz_directed_force);

View File

@ -1,9 +1,15 @@
px.registerWidget('markup', function(data_attribute) { px.registerViz('markup', function(slice) {
function refresh(ctrl) { function refresh() {
$('#code').attr('rows', '15') $('#code').attr('rows', '15')
ctrl.done(); $.getJSON(slice.jsonEndpoint(), function(payload) {
} slice.container.html(payload.data.html);
slice.done();
})
.fail(function(xhr) {
slice.error(xhr.responseText);
});
};
return { return {
render: refresh, render: refresh,
resize: refresh, resize: refresh,

View File

@ -1,7 +1,4 @@
function viz_nvd3(data_attribute) { function viz_nvd3(slice) {
var token_name = data_attribute['token'];
var token = d3.select('#' + token_name);
var json_callback = data_attribute['json_endpoint'];
var chart = undefined; var chart = undefined;
var data = {}; var data = {};
@ -26,15 +23,13 @@ function viz_nvd3(data_attribute) {
"#FF5A5F", "#007A87", "#7B0051", "#00D1C1", "#8CE071", "#FFB400", "#FF5A5F", "#007A87", "#7B0051", "#00D1C1", "#8CE071", "#FFB400",
"#FFAA91", "#B4A76C", "#9CA299", "#565A5C" "#FFAA91", "#B4A76C", "#9CA299", "#565A5C"
]; ];
var jtoken = $('#' + token_name); var refresh = function() {
var chart_div = $('#' + token_name).find("div.chart"); $.getJSON(slice.jsonEndpoint(), function(payload) {
var refresh = function(ctrl) {
chart_div.hide();
$.getJSON(json_callback, function(payload) {
var data = payload.data; var data = payload.data;
var viz = payload; var viz = payload;
var viz_type = viz.form_data.viz_type; var viz_type = viz.form_data.viz_type;
var fd = viz.form_data;
var f = d3.format('.4s');
nv.addGraph(function() { nv.addGraph(function() {
if (viz_type === 'line') { if (viz_type === 'line') {
if (viz.form_data.show_brush) { if (viz.form_data.show_brush) {
@ -113,10 +108,25 @@ function viz_nvd3(data_attribute) {
chart.yAxis.tickFormat(d3.format('.3p')); chart.yAxis.tickFormat(d3.format('.3p'));
} else if (viz_type === 'bubble') { } else if (viz_type === 'bubble') {
var row = function(col1, col2){
return "<tr><td>" + col1 + "</td><td>" + col2 + "</td></r>"
}
chart = nv.models.scatterChart(); chart = nv.models.scatterChart();
chart.showDistX(true);
chart.showDistY(true);
chart.xAxis.tickFormat(d3.format('.3s')); chart.xAxis.tickFormat(d3.format('.3s'));
chart.yAxis.tickFormat(d3.format('.3s')); chart.yAxis.tickFormat(d3.format('.3s'));
chart.showLegend(viz.form_data.show_legend); chart.showLegend(fd.show_legend);
chart.tooltip.contentGenerator(function (obj) {
p = obj.point;
var s = "<table>"
s += '<tr><td style="color:' + p.color + ';"><strong>' + p[fd.entity] + '</strong> (' + p.group + ')</td></tr>';
s += row(fd.x, f(p.x));
s += row(fd.y, f(p.y));
s += row(fd.size, f(p.size));
s += "</table>";
return s;
});
chart.pointRange([5, 5000]); chart.pointRange([5, 5000]);
} else if (viz_type === 'area') { } else if (viz_type === 'area') {
@ -130,7 +140,7 @@ function viz_nvd3(data_attribute) {
} }
// make space for labels on right // make space for labels on right
chart.height($(".chart").height() - 50).margin({"right": 50}); //chart.height($(".chart").height() - 50).margin({"right": 50});
if ((viz_type === "line" || viz_type === "area") && viz.form_data.rich_tooltip) { if ((viz_type === "line" || viz_type === "area") && viz.form_data.rich_tooltip) {
chart.useInteractiveGuideline(true); chart.useInteractiveGuideline(true);
} }
@ -153,19 +163,17 @@ function viz_nvd3(data_attribute) {
chart.duration(0); chart.duration(0);
token.select('.chart').append("svg") d3.select(slice.selector).append("svg")
.datum(data.chart_data) .datum(data.chart_data)
.transition().duration(500) .transition().duration(500)
.call(chart); .call(chart);
return chart; return chart;
}); });
chart_div.show(); slice.done(data);
ctrl.done(data);
}) })
.fail(function(xhr) { .fail(function(xhr) {
chart_div.show(); slice.error(xhr.responseText);
ctrl.error(xhr.responseText);
}); });
}; };
var resize = function() { var resize = function() {
@ -189,5 +197,5 @@ function viz_nvd3(data_attribute) {
'line', 'line',
'pie', 'pie',
].forEach(function(name) { ].forEach(function(name) {
px.registerWidget(name, viz_nvd3); px.registerViz(name, viz_nvd3);
}); });

View File

@ -1,3 +1,3 @@
li.widget.pivot_table div.token { li.widget.pivot_table div.slice_container {
overflow: auto; overflow: auto;
} }

View File

@ -1,23 +1,20 @@
px.registerWidget('pivot_table', function(data_attribute) { px.registerViz('pivot_table', function(slice) {
var token_name = data_attribute['token']; container = slice.container;
var token = $('#' + token_name); var form_data = slice.data.form_data;
var form_data = data_attribute.form_data;
function refresh(ctrl) { function refresh() {
$.getJSON(data_attribute.json_endpoint, function(json){ $.getJSON(slice.jsonEndpoint(), function(json){
token.html(json.data); container.html(json.data);
if (form_data.groupby.length == 1){ if (form_data.groupby.length == 1){
var table = token.find('table').DataTable({ var table = container.find('table').DataTable({
paging: false, paging: false,
searching: false, searching: false,
}); });
table.column('-1').order( 'desc' ).draw(); table.column('-1').order( 'desc' ).draw();
} }
token.show(); slice.done(json);
ctrl.done(json);
}).fail(function(xhr){ }).fail(function(xhr){
token.show(); slice.error(xhr.responseText);
ctrl.error(xhr.responseText);
}); });
} }
return { return {

View File

@ -1,28 +1,21 @@
#chart { .sankey .node rect {
height: 100%;
}
div.token {
height: 100%;
}
.node rect {
cursor: move; cursor: move;
fill-opacity: .9; fill-opacity: .9;
shape-rendering: crispEdges; shape-rendering: crispEdges;
} }
.node text { .sankey .node text {
pointer-events: none; pointer-events: none;
text-shadow: 0 1px 0 #fff; text-shadow: 0 1px 0 #fff;
} }
.link { .sankey .link {
fill: none; fill: none;
stroke: #000; stroke: #000;
stroke-opacity: .2; stroke-opacity: .2;
} }
.link:hover { .sankey .link:hover {
stroke-opacity: .5; stroke-opacity: .5;
} }

View File

@ -1,12 +1,10 @@
function viz_sankey(data_attribute) { function viz_sankey(slice) {
var token = d3.select('#' + data_attribute.token); var div = d3.select(slice.selector);
var div = token.select("#chart");
var xy = div.node().getBoundingClientRect();
var width = xy.width;
var height = xy.height - 25;
var render = function(ctrl) { var render = function() {
var margin = {top: 1, right: 1, bottom: 6, left: 1}; var width = slice.container.width();
var height = slice.container.height() - 25;
var margin = {top: 5, right: 5, bottom: 5, left: 5};
width = width - margin.left - margin.right; width = width - margin.left - margin.right;
height = height - margin.top - margin.bottom; height = height - margin.top - margin.bottom;
@ -14,7 +12,7 @@ function viz_sankey(data_attribute) {
format = function(d) { return formatNumber(d) + " TWh"; }, format = function(d) { return formatNumber(d) + " TWh"; },
color = d3.scale.category20(); color = d3.scale.category20();
var svg = token.select("#chart").append("svg") var svg = div.append("svg")
.attr("width", width + margin.left + margin.right) .attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom) .attr("height", height + margin.top + margin.bottom)
.append("g") .append("g")
@ -26,9 +24,9 @@ function viz_sankey(data_attribute) {
var path = sankey.link(); var path = sankey.link();
d3.json(data_attribute.json_endpoint, function(error, json) { d3.json(slice.data.json_endpoint, function(error, json) {
if (error != null){ if (error != null){
ctrl.error(error.responseText); slice.error(error.responseText);
return ''; return '';
} }
links = json.data; links = json.data;
@ -95,8 +93,7 @@ function viz_sankey(data_attribute) {
sankey.relayout(); sankey.relayout();
link.attr("d", path); link.attr("d", path);
} }
token.select("img.loading").remove(); slice.done(json);
ctrl.done(json);
}); });
} }
return { return {
@ -104,4 +101,4 @@ function viz_sankey(data_attribute) {
resize: render, resize: render,
}; };
} }
px.registerWidget('sankey', viz_sankey); px.registerViz('sankey', viz_sankey);

View File

@ -1,32 +1,19 @@
#sidebar { .sunburst text.middle{
float: right;
width: 100px;
}
text.middle{
text-anchor: middle; text-anchor: middle;
} }
#sequence { .sunburst #sequence {
} }
#legend { .sunburst #legend {
padding: 10px 0 0 3px; padding: 10px 0 0 3px;
} }
#sequence text, #legend text { .sunburst #sequence text, #legend text {
font-weight: 600; font-weight: 600;
fill: #fff; fill: #fff;
} }
.sunburst #chart {
height: 100%;
}
.sunburst div.token {
height: 100%;
}
.sunburst path { .sunburst path {
stroke: #fff; stroke: #fff;
} }

View File

@ -2,9 +2,9 @@
Modified from http://bl.ocks.org/kerryrodden/7090426 Modified from http://bl.ocks.org/kerryrodden/7090426
*/ */
function viz_sunburst(data_attribute) { function viz_sunburst(slice) {
var token = d3.select('#' + data_attribute.token); var container = d3.select(slice.selector);
var render = function(ctrl) { var render = function() {
// Breadcrumb dimensions: width, height, spacing, width of tip/tail. // Breadcrumb dimensions: width, height, spacing, width of tip/tail.
var b = { var b = {
w: 100, h: 30, s: 3, t: 10 w: 100, h: 30, s: 3, t: 10
@ -13,13 +13,11 @@ function viz_sunburst(data_attribute) {
// Total size of all segments; we set this later, after loading the data. // Total size of all segments; we set this later, after loading the data.
var totalSize = 0; var totalSize = 0;
var div = token.select("#chart"); var width = slice.container.width();
var xy = div.node().getBoundingClientRect(); var height = slice.container.height() - 25;
var width = xy.width;
var height = xy.height - 25;
var radius = Math.min(width, height) / 2; var radius = Math.min(width, height) / 2;
var vis = div.append("svg:svg") var vis = container.append("svg:svg")
.attr("width", width) .attr("width", width)
.attr("height", height) .attr("height", height)
.append("svg:g") .append("svg:g")
@ -39,15 +37,15 @@ function viz_sunburst(data_attribute) {
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy); }); .outerRadius(function(d) { return Math.sqrt(d.y + d.dy); });
var ext; var ext;
d3.json(data_attribute.json_endpoint, function(error, json){ d3.json(slice.data.json_endpoint, function(error, json){
if (error != null){ if (error != null){
ctrl.error(error.responseText); slice.error(error.responseText);
return ''; return '';
} }
var tree = buildHierarchy(json.data); var tree = buildHierarchy(json.data);
createVisualization(tree); createVisualization(tree);
ctrl.done(json); slice.done(json);
}); });
// Main function to draw and set up the visualization, once we have the data. // Main function to draw and set up the visualization, once we have the data.
@ -84,7 +82,7 @@ function viz_sunburst(data_attribute) {
// Add the mouseleave handler to the bounding circle. // Add the mouseleave handler to the bounding circle.
token.select("#container").on("mouseleave", mouseleave); container.select("#container").on("mouseleave", mouseleave);
// Get total size of the tree = value of root node from partition. // Get total size of the tree = value of root node from partition.
totalSize = path.node().__data__.value; totalSize = path.node().__data__.value;
@ -117,12 +115,12 @@ function viz_sunburst(data_attribute) {
updateBreadcrumbs(sequenceArray, percentageString); updateBreadcrumbs(sequenceArray, percentageString);
// Fade all the segments. // Fade all the segments.
token.selectAll("path") container.selectAll("path")
.style("stroke-width", "1px") .style("stroke-width", "1px")
.style("opacity", 0.3); .style("opacity", 0.3);
// Then highlight only those that are an ancestor of the current segment. // Then highlight only those that are an ancestor of the current segment.
token.selectAll("path") container.selectAll("path")
.filter(function(node) { .filter(function(node) {
return (sequenceArray.indexOf(node) >= 0); return (sequenceArray.indexOf(node) >= 0);
}) })
@ -135,16 +133,16 @@ function viz_sunburst(data_attribute) {
function mouseleave(d) { function mouseleave(d) {
// Hide the breadcrumb trail // Hide the breadcrumb trail
token.select("#trail") container.select("#trail")
.style("visibility", "hidden"); .style("visibility", "hidden");
gMiddleText.selectAll("*").remove(); gMiddleText.selectAll("*").remove();
// Deactivate all segments during transition. // Deactivate all segments during transition.
token.selectAll("path").on("mouseenter", null); container.selectAll("path").on("mouseenter", null);
//gMiddleText.selectAll("*").remove(); //gMiddleText.selectAll("*").remove();
// Transition each segment to full opacity and then reactivate it. // Transition each segment to full opacity and then reactivate it.
token.selectAll("path") container.selectAll("path")
.transition() .transition()
.duration(200) .duration(200)
.style("opacity", 1) .style("opacity", 1)
@ -253,4 +251,4 @@ function viz_sunburst(data_attribute) {
resize: render, resize: render,
}; };
} }
px.registerWidget('sunburst', viz_sunburst); px.registerViz('sunburst', viz_sunburst);

View File

@ -1,3 +1,7 @@
li.widget.table div.token { li.widget.table div.token {
overflow: auto; overflow: auto;
} }
td.filtered {
background-color: #005a63;
color: white;
}

View File

@ -1,10 +1,10 @@
px.registerWidget('table', function(data_attribute) { px.registerViz('table', function(slice) {
var data = slice.data;
var form_data = data.form_data;
var token_name = data_attribute['token']; function refresh() {
var token = $('#' + token_name); var f = d3.format('.3s');
$.getJSON(slice.jsonEndpoint(), function(json){
function refresh(ctrl) {
$.getJSON(data_attribute.json_endpoint, function(json){
var data = json.data; var data = json.data;
var metrics = json.form_data.metrics; var metrics = json.form_data.metrics;
function col(c){ function col(c){
@ -18,8 +18,7 @@ px.registerWidget('table', function(data_attribute) {
for (var i=0; i<metrics.length; i++){ for (var i=0; i<metrics.length; i++){
maxes[metrics[i]] = d3.max(col(metrics[i])); maxes[metrics[i]] = d3.max(col(metrics[i]));
} }
var table = d3.select(slice.selector).append('table')
var table = d3.select('#' + token_name).append('table')
.attr('class', 'dataframe table table-striped table-bordered table-condensed table-hover'); .attr('class', 'dataframe table table-striped table-bordered table-condensed table-hover');
table.append('thead').append('tr') table.append('thead').append('tr')
.selectAll('th') .selectAll('th')
@ -32,27 +31,57 @@ px.registerWidget('table', function(data_attribute) {
.append('tr') .append('tr')
.selectAll('td') .selectAll('td')
.data(function(row, i) { .data(function(row, i) {
return data.columns.map(function(c) {return [c, row];}); return data.columns.map(function(c) {
return {col: c, val: row[c], isMetric: metrics.indexOf(c) >=0};
});
}).enter() }).enter()
.append('td') .append('td')
.style('background-image', function(d){ .style('background-image', function(d){
var perc = Math.round((d[1][d[0]] / maxes[d[0]]) * 100); if (d.isMetric){
if (perc !== NaN) var perc = Math.round((d.val / maxes[d.col]) * 100);
return "linear-gradient(to right, lightgrey, lightgrey " + perc + "%, rgba(0,0,0,0) " + perc + "%" return "linear-gradient(to right, lightgrey, lightgrey " + perc + "%, rgba(0,0,0,0) " + perc + "%";
}
}) })
.html(function(d){return d[1][d[0]]}); .attr('data-sort', function(d){
var datatable = token.find('table').DataTable({ if (d.isMetric)
return d.val;
})
.on("click", function(d){
if(!d.isMetric){
var td = d3.select(this);
if (td.classed('filtered')){
slice.clearFilter(d.col, [d.val]);
table.selectAll('.filtered').classed('filtered', false);
} else {
table.selectAll('.filtered').classed('filtered', false);
d3.select(this).classed('filtered', true);
slice.addFilter(d.col, [d.val]);
}
}
})
.style("cursor", function(d){
if(!d.isMetric){
return 'pointer';
}
})
.html(function(d){
if (d.isMetric)
return f(d.val);
else
return d.val;
});
var datatable = slice.container.find('table').DataTable({
paging: false, paging: false,
searching: data_attribute.form_data.include_search, searching: form_data.include_search,
}); });
// Sorting table by main column // Sorting table by main column
if (data_attribute.form_data.metrics.length > 0) { if (form_data.metrics.length > 0) {
var main_metric = data_attribute.form_data.metrics[0]; var main_metric = form_data.metrics[0];
datatable.column(data.columns.indexOf(main_metric)).order( 'desc' ).draw(); datatable.column(data.columns.indexOf(main_metric)).order( 'desc' ).draw();
} }
ctrl.done(json); slice.done(json);
}).fail(function(xhr){ }).fail(function(xhr){
ctrl.error(xhr.responseText); slice.error(xhr.responseText);
}); });
} }

View File

@ -1,13 +1,10 @@
px.registerWidget('word_cloud', function(data_attribute) { px.registerViz('word_cloud', function(slice) {
var slice = slice;
var token_name = data_attribute['token']; var chart = d3.select(slice.selector);
var json_callback = data_attribute['json_endpoint']; function refresh() {
var token = d3.select('#' + token_name); d3.json(slice.jsonEndpoint(), function(error, json) {
function refresh(ctrl) {
d3.json(json_callback, function(error, json) {
if (error != null){ if (error != null){
ctrl.error(error.responseText); slice.error(error.responseText);
return ''; return '';
} }
var data = json.data; var data = json.data;
@ -25,8 +22,7 @@ px.registerWidget('word_cloud', function(data_attribute) {
else { else {
var f_rotation = function() { return (~~(Math.random() * 6) - 3) * 30; }; var f_rotation = function() { return (~~(Math.random() * 6) - 3) * 30; };
} }
var box = token.node().getBoundingClientRect(); var size = [slice.container.width(), slice.container.height() - 25];
var size = [box.width, box.height - 25];
scale = d3.scale.linear() scale = d3.scale.linear()
.range(range) .range(range)
@ -42,9 +38,9 @@ px.registerWidget('word_cloud', function(data_attribute) {
.on("end", draw); .on("end", draw);
layout.start(); layout.start();
function draw(words) { function draw(words) {
token.selectAll("*").remove(); chart.selectAll("*").remove();
token.append("svg") chart.append("svg")
.attr("width", layout.size()[0]) .attr("width", layout.size()[0])
.attr("height", layout.size()[1]) .attr("height", layout.size()[1])
.append("g") .append("g")
@ -61,7 +57,7 @@ px.registerWidget('word_cloud', function(data_attribute) {
}) })
.text(function(d) { return d.text; }); .text(function(d) { return d.text; });
} }
ctrl.done(data); slice.done(data);
}); });
} }
@ -69,5 +65,4 @@ px.registerWidget('word_cloud', function(data_attribute) {
render: refresh, render: refresh,
resize: refresh, resize: refresh,
}; };
}); });

View File

@ -0,0 +1,3 @@
.world_map svg{
background-color: LightSkyBlue;
}

View File

@ -2,26 +2,22 @@
Using the awesome lib at http://datamaps.github.io/ Using the awesome lib at http://datamaps.github.io/
*/ */
function viz_world_map(data_attribute) { function viz_world_map(slice) {
var token = d3.select('#' + data_attribute.token); var render = function() {
var render = function(ctrl) { var container = slice.container;
// Breadcrumb dimensions: width, height, spacing, width of tip/tail. var div = d3.select(slice.selector);
var div = token;
var xy = div.node().getBoundingClientRect();
var width = xy.width;
var height = xy.height - 25;
d3.json(data_attribute.json_endpoint, function(error, json){ d3.json(slice.data.json_endpoint, function(error, json){
if (error != null){ if (error != null){
ctrl.error(error.responseText); slice.error(error.responseText);
return ''; return '';
} }
var ext = d3.extent(json.data, function(d){return d.m1}); var ext = d3.extent(json.data, function(d){return d.m1});
var extRadius = d3.extent(json.data, function(d){return d.m2}); var extRadius = d3.extent(json.data, function(d){return d.m2});
var radiusScale = d3.scale.linear() var radiusScale = d3.scale.linear()
.domain([extRadius[0], extRadius[1]]) .domain([extRadius[0], extRadius[1]])
.range([1, data_attribute.form_data.max_bubble_size]); .range([1, slice.data.form_data.max_bubble_size]);
json.data.forEach(function(d){ json.data.forEach(function(d){
d.radius = radiusScale(d.m2); d.radius = radiusScale(d.m2);
}) })
@ -35,11 +31,12 @@ function viz_world_map(data_attribute) {
d[country.country] = country; d[country.country] = country;
} }
f = d3.format('.3s'); f = d3.format('.3s');
container.show();
var map = new Datamap({ var map = new Datamap({
element: document.getElementById(data_attribute.token), element: slice.container.get(0),
data: json.data, data: json.data,
fills: { fills: {
defaultFill: 'grey' defaultFill: 'transparent'
}, },
geographyConfig: { geographyConfig: {
popupOnHover: true, popupOnHover: true,
@ -75,11 +72,11 @@ function viz_world_map(data_attribute) {
}, },
}); });
map.updateChoropleth(d); map.updateChoropleth(d);
if(data_attribute.form_data.show_bubbles){ if(slice.data.form_data.show_bubbles){
map.bubbles(json.data); map.bubbles(json.data);
token.selectAll("circle.datamaps-bubble").style('fill', '#005a63'); div.selectAll("circle.datamaps-bubble").style('fill', '#005a63');
} }
ctrl.done(json); slice.done(json);
}); });
} }
@ -88,4 +85,4 @@ function viz_world_map(data_attribute) {
resize: render, resize: render,
}; };
} }
px.registerWidget('world_map', viz_world_map); px.registerViz('world_map', viz_world_map);

View File

@ -9,11 +9,6 @@
<style id="user_style" type="text/css"> <style id="user_style" type="text/css">
{{ dashboard.css }} {{ dashboard.css }}
</style> </style>
{% for slice in dashboard.slices %}
{% set viz = slice.viz %}
{% import viz.template as viz_macros %}
{{ viz_macros.viz_css(viz) }}
{% endfor %}
{% endblock %} {% endblock %}
{% block content_fluid %} {% block content_fluid %}
@ -77,17 +72,16 @@ body {
{% for slice in dashboard.slices %} {% for slice in dashboard.slices %}
{% set pos = pos_dict.get(slice.id, {}) %} {% set pos = pos_dict.get(slice.id, {}) %}
{% set viz = slice.viz %} {% set viz = slice.viz %}
{% import viz.template as viz_macros %}
<li <li
id="slice_{{ slice.id }}" id="slice_{{ slice.id }}"
slice_id="{{ slice.id }}" slice_id="{{ slice.id }}"
class="widget {{ slice.viz.viz_type }}" class="widget {{ slice.viz.viz_type }}"
data-widget="{{ slice.viz.get_data_attribute() }}" data-slice="{{ slice.json_data }}"
data-row="{{ pos.row or 1 }}" data-row="{{ pos.row or 1 }}"
data-col="{{ pos.col or loop.index }}" data-col="{{ pos.col or loop.index }}"
data-sizex="{{ pos.size_x or 4 }}" data-sizex="{{ pos.size_x or 4 }}"
data-sizey="{{ pos.size_y or 4 }}"> data-sizey="{{ pos.size_y or 4 }}">
<table class="widget_header"> <table class="slice_header">
<tbody> <tbody>
<tr> <tr>
<td class="icons"> <td class="icons">
@ -103,7 +97,7 @@ body {
<nobr> <nobr>
<a href="{{ slice.slice_url }}"><i class="fa fa-play"></i></a> <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 href="{{ slice.edit_url }}"><i class="fa fa-gear"></i></a>
<a class="closewidget"><i class="fa fa-close"></i></a> <a class="closeslice"><i class="fa fa-close"></i></a>
</br> </br>
</td> </td>
</tr> </tr>
@ -111,7 +105,7 @@ body {
</table> </table>
<div id="{{ viz.token }}" class="token" style="height: 100%;"> <div id="{{ viz.token }}" class="token" style="height: 100%;">
<img src="{{ url_for("static", filename="img/loading.gif") }}" class="loading" alt="loading"> <img src="{{ url_for("static", filename="img/loading.gif") }}" class="loading" alt="loading">
{{ viz_macros.viz_html(viz) }} <div class="slice_container" id="{{ viz.token }}_con" style="height: 100%; width: 100%;"></div>
</div> </div>
</li> </li>
{% endfor %} {% endfor %}
@ -127,19 +121,9 @@ body {
{% endfor %} {% endfor %}
<script src="/static/lib/gridster/jquery.gridster.with-extras.min.js"></script> <script src="/static/lib/gridster/jquery.gridster.with-extras.min.js"></script>
<script> <script>
$(document).ready(px.initializeDashboardView({{ dashboard.id }}));
$(document).ready(function() { $(document).ready(function() {
$('.dashboard .widget').each(function() { px.initDashboardView();
var data = $(this).data('widget'); var dashboard = px.Dashboard({{ dashboard.id }});
var widget = px.initializeWidget(data);
$(this).data('widget', widget);
widget.render();
});
}); });
</script> </script>
{% for slice in dashboard.slices %}
{% set viz = slice.viz %}
{% import viz.template as viz_macros %}
{{ viz_macros.viz_js(viz) }}
{% endfor %}
{% endblock %} {% endblock %}

View File

@ -26,22 +26,29 @@
<span class="btn btn-default notbtn"> <span class="btn btn-default notbtn">
<strong>{{ datasource.full_name }}</strong> <strong>{{ datasource.full_name }}</strong>
{% if datasource.description %} {% if datasource.description %}
<a data-toggle="modal" data-target="#sourceinfo_modal">
<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>
</a>
{% endif %} {% endif %}
<a class="" href="/{{ datasource.baselink }}/edit/{{ datasource.id }}" data-toggle="tooltip" title="Edit datasource"> <a class="" href="/{{ datasource.baselink }}/edit/{{ datasource.id }}" data-toggle="tooltip" title="Edit datasource">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
</a> </a>
</span> </span>
<span>{{ form.get_field("viz_type")(class_="select2") }}</span> <span>{{ form.get_field("viz_type")(class_="select2") }}</span>
<span class="btn btn-info pull-right disabled query"
data-toggle="modal" data-target="#query_modal">query</span>
<span class="btn btn-warning pull-right notbtn" id="timer">0 sec</span>
<div class="btn-group pull-right" role="group"> <div class="btn-group pull-right" role="group">
<span class="btn btn-default disabled"> <span class="btn btn-default" id="standalone" title="Standalone version, use to embed anywhere" data-toggle="tooltip">
<i class="fa fa-file-text"></i> <i class="fa fa-code"></i>
</span> </span>
<span class="btn btn-default" id="csv">.csv</span> <span class="btn btn-default " id="json" title="Export to .json" data-toggle="tooltip">
<span class="btn btn-default" id="json">.json</span> <i class="fa fa-file-code-o"></i>
.json
</span>
<span class="btn btn-default " id="csv" title="Export to .csv format" data-toggle="tooltip">
<i class="fa fa-file-text-o"></i>.csv
</span>
<span class="btn btn-warning notbtn" id="timer">0 sec</span>
<span class="btn btn-info disabled query"
data-toggle="modal" data-target="#query_modal">query</span>
</div> </div>
<hr/> <hr/>
</div> </div>
@ -133,23 +140,15 @@
</div> </div>
<div class="col-md-9"> <div class="col-md-9">
{% block messages %} {% block messages %}{% endblock %}
{% endblock %} {% include 'appbuilder/flash.html' %}
{% include 'appbuilder/flash.html' %}
<div <div
id="{{ viz.token }}" id="{{ viz.token }}"
class="viz widget {{ viz.viz_type }}" class="viz slice {{ viz.viz_type }}"
data-widget="{{ viz.get_data_attribute() }}" data-slice="{{ viz.json_data }}"
style="height: 700px;"> style="height: 700px;">
<img src="{{ url_for("static", filename="img/loading.gif") }}" class="loading" alt="loading"> <img src="{{ url_for("static", filename="img/loading.gif") }}" class="loading" alt="loading">
{% block viz_html %} <div id="{{ viz.token }}_con" class="slice_container" style="height: 100%; width: 100%"></div>
{% 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> </div>
</div> </div>
</div> </div>
@ -176,6 +175,24 @@
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" id="sourceinfo_modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Query</h4>
</div>
<div class="modal-body">
<pre id="query_container">{{ datasource.description }}</pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</form> </form>
</div> </div>
{% endblock %} {% endblock %}
@ -183,15 +200,15 @@
{% block tail_js %} {% block tail_js %}
{{ super() }} {{ super() }}
<script> <script>
$(document).ready(px.initializeDatasourceView); $(document).ready(px.initExploreView);
$(document).ready(function() { $(document).ready(function() {
var data = $('.widget').data('widget'); var data = $('.slice').data('slice');
var widget = px.initializeWidget(data); var slice = px.Slice(data);
$('.widget').data('widget', widget); $('.slice').data('slice', slice);
widget.render(); slice.render();
function get_collapsed_fieldsets(){ function get_collapsed_fieldsets(){
collapsed_fieldsets = $("#collapsed_fieldsets").val() collapsed_fieldsets = $("#collapsed_fieldsets").val();
if (collapsed_fieldsets != undefined && collapsed_fieldsets != "") if (collapsed_fieldsets != undefined && collapsed_fieldsets != "")
collapsed_fieldsets = collapsed_fieldsets.split('||'); collapsed_fieldsets = collapsed_fieldsets.split('||');
else else
@ -242,9 +259,12 @@
$('#csv').click(function () { $('#csv').click(function () {
window.location = '{{ viz.csv_endpoint | safe }}'; window.location = '{{ viz.csv_endpoint | safe }}';
}); });
$('#standalone').click(function () {
window.location = '{{ viz.standalone_endpoint | safe }}';
});
$("#viz_type").change(function() {$("#query").submit();}); $("#viz_type").change(function() {$("#query").submit();});
collapsed_fieldsets = get_collapsed_fieldsets(); collapsed_fieldsets = get_collapsed_fieldsets();
for(var i=0; i<collapsed_fieldsets.length;i++){ for(var i=0; i < collapsed_fieldsets.length; i++){
toggle_fieldset($('legend:contains("' + collapsed_fieldsets[i] + '")'), false); toggle_fieldset($('legend:contains("' + collapsed_fieldsets[i] + '")'), false);
} }
}); });

View File

@ -1,6 +1,8 @@
{% extends "panoramix/base.html" %} {% extends "panoramix/base.html" %}
{% block content %} {% block content %}
<h1><i class='fa fa-star'></i> Featured Datasets </h1> <div class="header">
<h1><i class='fa fa-star'></i> Featured Datasets </h1>
</div>
<hr/> <hr/>
<table class="table table-hover dataTable" id="dataset-table" style="display:None"> <table class="table table-hover dataTable" id="dataset-table" style="display:None">
<thead> <thead>
@ -25,6 +27,7 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<hr/>
{% endblock %} {% endblock %}
{% block head_css %} {% block head_css %}
@ -42,6 +45,7 @@
"bPaginate": false, "bPaginate": false,
"order": [[ 1, "asc" ]] "order": [[ 1, "asc" ]]
}); });
$('#dataset-table_info').remove();
$('#dataset-table').show(); $('#dataset-table').show();
} ); } );
</script> </script>

View File

@ -0,0 +1,29 @@
<html>
<head>
<script src="/static/panoramix.js"></script>
{% block head_css %}
<script src="{{url_for('appbuilder.static',filename='js/jquery-latest.js')}}"></script>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='panoramix.css') }}">
{% endblock %}
</head>
<body>
<div
id="{{ viz.token }}"
class="viz slice {{ viz.viz_type }}"
data-slice="{{ viz.json_data }}"
style="height: 700px;">
<img src="{{ url_for("static", filename="img/loading.gif") }}" class="loading" alt="loading">
<div id="{{ viz.token }}_con" class="slice_container" style="height: 100%; width: 100%"></div>
</div>
<script>
$(document).ready(function() {
var data = $('.slice').data('slice');
var slice = px.Slice(data);
slice.render();
});
</script>
{% block tail %}{% endblock %}
</body>
</html>

View File

@ -1,37 +1,25 @@
{% import viz.template as viz_macros %}
{% if viz.form_data.get("json") == "true" %} {% if viz.form_data.get("json") == "true" %}
{{ viz.get_json() }} {{ viz.get_json() }}
{% else %} {% else %}
{% if viz.request.args.get("standalone") == "true" %} {% if viz.request.args.get("standalone") == "true" %}
{% extends 'panoramix/viz_standalone.html' %} {% extends 'panoramix/standalone.html' %}
{% else %} {% else %}
{% extends 'panoramix/explore.html' %} {% extends 'panoramix/explore.html' %}
{% endif %} {% endif %}
{% block viz_html %}
{{ viz_macros.viz_html(viz) }}
{% endblock %}
{% block head_css %} {% block head_css %}
{{super()}} {{super()}}
{% if viz.request.args.get("skip_libs") != "true" %} {% for css in viz.css_files %}
{% for css in viz.css_files %} <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename=css) }}">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename=css) }}"> {% endfor %}
{% endfor %}
{% endif %}
{{ viz_macros.viz_css(viz) }}
{% endblock %} {% endblock %}
{% block tail %} {% block tail %}
{{super()}} {{super()}}
{% if viz.request.args.get("skip_libs") != "true" %} {% for js in viz.js_files %}
{% for js in viz.js_files %} <script src="{{ url_for('static', filename=js) }}"></script>
<script src="{{ url_for('static', filename=js) }}"></script> {% endfor %}
{% endfor %}
{{ viz_macros.viz_js(viz) }}
{% endif %}
{% endblock %} {% endblock %}
{% endif %} {% endif %}

View File

@ -1,9 +0,0 @@
{% macro viz_html(viz) %}
<div id="{{ viz.token }}" class="viz_bignumber" style="height: 100%;"></div>
{% endmacro %}
{% macro viz_js(viz) %}
{% endmacro %}
{% macro viz_css(viz) %}
{% endmacro %}

View File

@ -1,9 +0,0 @@
{% macro viz_html(viz) %}
<div id="chart"></div>
{% endmacro %}
{% macro viz_js(viz) %}
{% endmacro %}
{% macro viz_css(viz) %}
{% endmacro %}

View File

@ -1,9 +0,0 @@
{% 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 %}

View File

@ -1,9 +0,0 @@
{% macro viz_html(viz) %}
<div class="chart with-3d-shadow with-transitions" style="height:100%; width: 100%"></div>
{% endmacro %}
{% macro viz_js(viz) %}
{% endmacro %}
{% macro viz_css(viz) %}
{% endmacro %}

View File

@ -1,8 +0,0 @@
{% macro viz_html(viz) %}
{% endmacro %}
{% macro viz_js(viz) %}
{% endmacro %}
{% macro viz_css(viz) %}
{% endmacro %}

View File

@ -1,10 +0,0 @@
{% macro viz_html(viz) %}
<div id="chart"></div>
{% endmacro %}
{% macro viz_js(viz) %}
{% endmacro %}
{% macro viz_css(viz) %}
{% endmacro %}

View File

@ -1,13 +0,0 @@
<html>
<head>
{% if viz.request.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>

View File

@ -1,9 +0,0 @@
{% macro viz_html(viz) %}
<div id="chart"></div>
{% endmacro %}
{% macro viz_js(viz) %}
{% endmacro %}
{% macro viz_css(viz) %}
{% endmacro %}

View File

@ -1,37 +0,0 @@
{% macro viz_html(viz) %}
{% if viz.request.args.get("async") == "true" %}
{% set df = viz.get_df() %}
<div class="table">
<table class="dataframe table table-striped table-bordered table-condensed table-hover">
<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>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% endmacro %}
{% macro viz_js(viz) %}
{% endmacro %}
{% macro viz_css(viz) %}
{% endmacro %}

View File

@ -1,9 +0,0 @@
{% macro viz_html(viz) %}
<div id="{{ viz.token }}" style="height: 100%;"></div>
{% endmacro %}
{% macro viz_js(viz) %}
{% endmacro %}
{% macro viz_css(viz) %}
{% endmacro %}

View File

@ -1,8 +0,0 @@
{% macro viz_html(viz) %}
{% endmacro %}
{% macro viz_js(viz) %}
{% endmacro %}
{% macro viz_css(viz) %}
{% endmacro %}

View File

@ -212,3 +212,13 @@ def datetime_f(dttm):
elif now_iso[:4] == dttm[:4]: elif now_iso[:4] == dttm[:4]:
dttm = dttm[5:] dttm = dttm[5:]
return Markup("<nobr>{}</nobr>".format(dttm)) return Markup("<nobr>{}</nobr>".format(dttm))
def json_iso_dttm_ser(obj):
"""
json serializer that deals with dates
usage: json.dumps(object, default=utils.json_ser)
"""
if isinstance(obj, datetime):
obj = obj.isoformat()
return obj

View File

@ -5,7 +5,7 @@ import uuid
from flask import flash, request, Markup from flask import flash, request, Markup
from markdown import markdown from markdown import markdown
from pandas.io.json import dumps from pandas.io.json import dumps, to_json
from werkzeug.datastructures import ImmutableMultiDict from werkzeug.datastructures import ImmutableMultiDict
from werkzeug.urls import Href from werkzeug.urls import Href
import numpy as np import numpy as np
@ -22,7 +22,6 @@ config = app.config
class BaseViz(object): class BaseViz(object):
viz_type = None viz_type = None
verbose_name = "Base Viz" verbose_name = "Base Viz"
template = None
is_timeseries = False is_timeseries = False
fieldsets = ( fieldsets = (
{ {
@ -157,6 +156,14 @@ class BaseViz(object):
eq = form_data.get("flt_eq_" + str(i)) eq = form_data.get("flt_eq_" + str(i))
if col and op and eq: if col and op and eq:
filters.append((col, op, eq)) filters.append((col, op, eq))
# Extra filters (coming from dashboard)
extra_filters = form_data.get('extra_filters', [])
if extra_filters:
extra_filters = json.loads(extra_filters)
for slice_id, (col, vals) in extra_filters.items():
filters += [(col, 'in', ",".join(vals))]
return filters return filters
def query_obj(self): def query_obj(self):
@ -176,7 +183,7 @@ class BaseViz(object):
from_dttm = datetime.now() - (from_dttm-datetime.now()) from_dttm = datetime.now() - (from_dttm-datetime.now())
until = form_data.get("until", "now") until = form_data.get("until", "now")
to_dttm = utils.parse_human_datetime(until) to_dttm = utils.parse_human_datetime(until)
if from_dttm >= to_dttm: if from_dttm > to_dttm:
flash("The date range doesn't seem right.", "danger") flash("The date range doesn't seem right.", "danger")
from_dttm = to_dttm # Making them identical to not raise from_dttm = to_dttm # Making them identical to not raise
@ -205,6 +212,9 @@ class BaseViz(object):
'data': json.loads(self.get_json_data()), 'data': json.loads(self.get_json_data()),
'query': self.query, 'query': self.query,
'form_data': self.form_data, 'form_data': self.form_data,
'json_endpoint': self.json_endpoint,
'csv_endpoint': self.csv_endpoint,
'standalone_endpoint': self.standalone_endpoint,
} }
return json.dumps(payload) return json.dumps(payload)
@ -223,19 +233,27 @@ class BaseViz(object):
def csv_endpoint(self): def csv_endpoint(self):
return self.get_url(csv="true") return self.get_url(csv="true")
def get_data_attribute(self): @property
def standalone_endpoint(self):
return self.get_url(standalone="true")
@property
def data(self):
content = { content = {
'viz_name': self.viz_type, 'viz_name': self.viz_type,
'json_endpoint': self.json_endpoint, 'json_endpoint': self.json_endpoint,
'token': self.token, 'token': self.token,
'form_data': self.form_data, 'form_data': self.form_data,
} }
return dumps(content) return content
@property
def json_data(self):
return dumps(self.data)
class TableViz(BaseViz): class TableViz(BaseViz):
viz_type = "table" viz_type = "table"
verbose_name = "Table View" verbose_name = "Table View"
template = 'panoramix/viz_table.html'
fieldsets = ( fieldsets = (
{ {
'label': None, 'label': None,
@ -293,16 +311,18 @@ class TableViz(BaseViz):
def get_json_data(self): def get_json_data(self):
df = self.get_df() df = self.get_df()
return dumps(dict( return json.dumps(
records=df.to_dict(orient="records"), dict(
columns=df.columns, records=df.to_dict(orient="records"),
)) columns=list(df.columns),
),
default=utils.json_iso_dttm_ser,
)
class PivotTableViz(BaseViz): class PivotTableViz(BaseViz):
viz_type = "pivot_table" viz_type = "pivot_table"
verbose_name = "Pivot Table" verbose_name = "Pivot Table"
template = 'panoramix/viz_pivot_table.html'
css_files = [ css_files = [
'lib/dataTables/dataTables.bootstrap.css', 'lib/dataTables/dataTables.bootstrap.css',
'widgets/viz_pivot_table.css'] 'widgets/viz_pivot_table.css']
@ -373,7 +393,6 @@ class PivotTableViz(BaseViz):
class MarkupViz(BaseViz): class MarkupViz(BaseViz):
viz_type = "markup" viz_type = "markup"
verbose_name = "Markup Widget" verbose_name = "Markup Widget"
template = 'panoramix/viz_markup.html'
js_files = ['widgets/viz_markup.js'] js_files = ['widgets/viz_markup.js']
fieldsets = ( fieldsets = (
{ {
@ -390,6 +409,9 @@ class MarkupViz(BaseViz):
elif markup_type == "html": elif markup_type == "html":
return code return code
def get_json_data(self):
return dumps(dict(html=self.rendered()))
class WordCloudViz(BaseViz): class WordCloudViz(BaseViz):
""" """
@ -398,7 +420,6 @@ class WordCloudViz(BaseViz):
""" """
viz_type = "word_cloud" viz_type = "word_cloud"
verbose_name = "Word Cloud" verbose_name = "Word Cloud"
template = 'panoramix/viz_word_cloud.html'
is_timeseries = False is_timeseries = False
fieldsets = ( fieldsets = (
{ {
@ -435,7 +456,6 @@ class WordCloudViz(BaseViz):
class NVD3Viz(BaseViz): class NVD3Viz(BaseViz):
viz_type = None viz_type = None
verbose_name = "Base NVD3 Viz" verbose_name = "Base NVD3 Viz"
template = 'panoramix/viz_nvd3.html'
is_timeseries = False is_timeseries = False
js_files = [ js_files = [
'lib/d3.min.js', 'lib/d3.min.js',
@ -458,8 +478,8 @@ class BubbleViz(NVD3Viz):
'fields': ( 'fields': (
('since', 'until'), ('since', 'until'),
('series', 'entity'), ('series', 'entity'),
('x', 'y'), 'x', 'y', 'size',
('size', 'limit'), 'limit',
('x_log_scale', 'y_log_scale'), ('x_log_scale', 'y_log_scale'),
('show_legend', None), ('show_legend', None),
) )
@ -517,7 +537,6 @@ class BubbleViz(NVD3Viz):
class BigNumberViz(BaseViz): class BigNumberViz(BaseViz):
viz_type = "big_number" viz_type = "big_number"
verbose_name = "Big Number" verbose_name = "Big Number"
template = 'panoramix/viz_bignumber.html'
is_timeseries = True is_timeseries = True
js_files = [ js_files = [
'lib/d3.min.js', 'lib/d3.min.js',
@ -850,7 +869,6 @@ class SunburstViz(BaseViz):
viz_type = "sunburst" viz_type = "sunburst"
verbose_name = "Sunburst" verbose_name = "Sunburst"
is_timeseries = False is_timeseries = False
template = 'panoramix/viz_sunburst.html'
js_files = [ js_files = [
'lib/d3.min.js', 'lib/d3.min.js',
'widgets/viz_sunburst.js'] 'widgets/viz_sunburst.js']
@ -917,7 +935,6 @@ class SankeyViz(BaseViz):
viz_type = "sankey" viz_type = "sankey"
verbose_name = "Sankey" verbose_name = "Sankey"
is_timeseries = False is_timeseries = False
template = 'panoramix/viz_sankey.html'
js_files = [ js_files = [
'lib/d3.min.js', 'lib/d3.min.js',
'lib/d3-sankey.js', 'lib/d3-sankey.js',
@ -934,10 +951,17 @@ class SankeyViz(BaseViz):
'row_limit', 'row_limit',
) )
},) },)
form_overrides = {} form_overrides = {
'groupby': {
'label': 'Source / Target',
'description': "Choose a source and a target",
},
}
def query_obj(self): def query_obj(self):
qry = super(SankeyViz, self).query_obj() qry = super(SankeyViz, self).query_obj()
if len(qry['groupby']) != 2:
raise Exception("Pick exactly 2 columns as [Source / Target]")
qry['metrics'] = [ qry['metrics'] = [
self.form_data['metric']] self.form_data['metric']]
return qry return qry
@ -953,7 +977,6 @@ class DirectedForceViz(BaseViz):
viz_type = "directed_force" viz_type = "directed_force"
verbose_name = "Directed Force Layout" verbose_name = "Directed Force Layout"
is_timeseries = False is_timeseries = False
template = 'panoramix/viz_directed_force.html'
js_files = [ js_files = [
'lib/d3.min.js', 'lib/d3.min.js',
'widgets/viz_directed_force.js'] 'widgets/viz_directed_force.js']
@ -1000,7 +1023,6 @@ class WorldMapViz(BaseViz):
viz_type = "world_map" viz_type = "world_map"
verbose_name = "World Map" verbose_name = "World Map"
is_timeseries = False is_timeseries = False
template = 'panoramix/viz_world_map.html'
js_files = [ js_files = [
'lib/d3.min.js', 'lib/d3.min.js',
'lib/topojson.min.js', 'lib/topojson.min.js',

View File

@ -2,4 +2,4 @@
rm /tmp/panoramix_unittests.db rm /tmp/panoramix_unittests.db
export PANORAMIX_CONFIG=tests.panoramix_test_config export PANORAMIX_CONFIG=tests.panoramix_test_config
panoramix/bin/panoramix db upgrade panoramix/bin/panoramix db upgrade
nosetests tests/core_tests.py --with-coverage --cover-package=panoramix nosetests tests/core_tests.py --with-coverage --cover-package=panoramix -v