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
## 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
* [sql] make "Test Connection" test further
* [druid] Allow for post aggregations (ratios!)

View File

@ -1,19 +1,12 @@
#!/usr/bin/env python
import csv
from datetime import datetime
import gzip
import json
import os
from subprocess import Popen
from flask.ext.script import Manager
from panoramix import app
from flask.ext.migrate import MigrateCommand
from panoramix import db
from flask.ext.appbuilder import Base
from sqlalchemy import Column, Integer, String, Table, DateTime
from panoramix import models, utils
from panoramix import data, utils
config = app.config
@ -60,314 +53,10 @@ 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", 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()
print("Loading [World Bank's Health Nutrition and Population Stats]")
data.load_world_bank_health_n_pop()
print("Loading [Birth names]")
data.load_birth_names()
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)
if self.data:
for value in self.data:
yield d.pop(value)
if value:
yield d.pop(value)
while d:
yield d.pop(d.keys()[0])
@ -402,6 +403,7 @@ class FormFactory(object):
css_classes = field_css_classes
standalone = HiddenField()
async = HiddenField()
extra_filters = HiddenField()
json = HiddenField()
slice_id = 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 flask import flash
from flask.ext.appbuilder import Model
from flask.ext.appbuilder.models.mixins import AuditMixin
import pandas as pd
from pandas import read_sql_query
from pydruid import client
from pydruid.utils.filters import Dimension, Filter
@ -11,19 +20,11 @@ from sqlalchemy import (
Column, Integer, String, ForeignKey, Text, Boolean, DateTime,
Table, create_engine, MetaData, desc, select, and_, func)
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_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.viz import viz_types
@ -85,6 +86,16 @@ class Slice(Model, AuditMixinNullable):
def datasource_id(self):
return self.table_id or self.druid_datasource_id
@property
def data(self):
d = self.viz.data
d['slice_id'] = self.id
return d
@property
def json_data(self):
return json.dumps(self.data)
@property
def slice_url(self):
try:
@ -409,9 +420,7 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
groupby_exprs = []
if groupby:
select_exprs = [literal_column(s) for s in groupby]
select_exprs = []
groupby_exprs = []
inner_select_exprs = []
inner_groupby_exprs = []
for s in groupby:
@ -421,8 +430,8 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
outer = ColumnClause(expr, is_literal=True).label(s)
inner = ColumnClause(expr, is_literal=True).label('__' + s)
else:
outer = literal_column(s).label(s)
inner = literal_column(s).label('__' + s)
outer = column(s).label(s)
inner = column(s).label('__' + s)
groupby_exprs.append(outer)
select_exprs.append(outer)
@ -462,7 +471,7 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
cond = ColumnClause(
col_obj.expression, is_literal=True).in_(values)
else:
cond = literal_column(col).in_(values)
cond = column(col).in_(values)
if op == 'not in':
cond = ~cond
where_clause_and.append(cond)
@ -486,7 +495,7 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
on_clause = []
for i, gb in enumerate(groupby):
on_clause.append(
groupby_exprs[i] == literal_column("__" + gb))
groupby_exprs[i] == column("__" + gb))
from_clause = from_clause.join(subq.alias(), and_(*on_clause))
@ -499,7 +508,6 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
con=engine
)
sql = sqlparse.format(sql, reindent=True)
print(sql)
return QueryResult(
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():
any_date_col = col.name
quoted = "{}".format(
column(dbcol.column_name).compile(dialect=db.engine.dialect))
if dbcol.sum:
metrics.append(M(
metric_name='sum__' + dbcol.column_name,
verbose_name='sum__' + dbcol.column_name,
metric_type='sum',
expression="SUM({})".format(dbcol.column_name)
expression="SUM({})".format(quoted)
))
if dbcol.max:
metrics.append(M(
metric_name='max__' + dbcol.column_name,
verbose_name='max__' + dbcol.column_name,
metric_type='max',
expression="MAX({})".format(dbcol.column_name)
expression="MAX({})".format(quoted)
))
if dbcol.min:
metrics.append(M(
metric_name='min__' + dbcol.column_name,
verbose_name='min__' + dbcol.column_name,
metric_type='min',
expression="MIN({})".format(dbcol.column_name)
expression="MIN({})".format(quoted)
))
if dbcol.count_distinct:
metrics.append(M(
metric_name='count_distinct__' + dbcol.column_name,
verbose_name='count_distinct__' + dbcol.column_name,
metric_type='count_distinct',
expression="COUNT(DISTINCT {})".format(dbcol.column_name)
expression="COUNT(DISTINCT {})".format(quoted)
))
dbcol.type = datatype
db.session.merge(self)

View File

@ -1,6 +1,10 @@
html>body{
margin: 0px; !important
}
.slice_container {
height: 100%;
}
.container-fluid {
text-align: left;
}
@ -132,8 +136,8 @@ legend {
.datasource .tooltip-inner {
max-width: 350px;
}
.datasource img.loading {
width: 30px;
img.loading {
width: 40px;
}
.dashboard a i {
@ -181,7 +185,7 @@ legend {
width: 100%;
height: 100%;
}
.dashboard table.widget_header {
.dashboard table.slice_header {
width: 100%;
height: 20px;
}
@ -192,7 +196,7 @@ legend {
.dashboard li.widget.pie,
.dashboard li.widget.dist_bar,
.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 {
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 visualizations = [];
var visualizations = {};
var dashboard = undefined;
function registerWidget(name, initializer) {
visualizations[name] = initializer;
}
function makeNullWidget() {
return {
render: function() {},
resize: function() {},
};
}
function initializeWidget(data) {
var Slice = function(data, dashboard){
var timer;
var token = $('#' + data.token);
var container_id = data.token + '_con';
var selector = '#' + container_id;
var container = $(selector);
var slice_id = data.slice_id;
var name = data['viz_name'];
var initializer = visualizations[name];
var user_defined_widget = initializer ? initializer(data) : makeNullWidget();
var dttm = 0;
var timer;
var stopwatch = function () {
dttm += 10;
$('#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) {
clearInterval(timer);
token.find("img.loading").hide();
token.find("img.loading").hide()
container.show();
if(data !== undefined)
$("#query_container").html(data.query);
$('#timer').removeClass('btn-warning');
$('span.query').removeClass('disabled');
$('#timer').addClass('btn-success');
$('span.query').removeClass('disabled');
},
error: function (msg) {
clearInterval(timer);
token.find("img.loading").hide();
var err = '<div class="alert alert-danger">' + msg + '</div>';
token.html(err);
container.html(err);
container.show();
$('#timer').removeClass('btn-warning');
$('span.query').removeClass('disabled');
$('#timer').addClass('btn-danger');
}
};
widget = {
},
data: data,
container: container,
container_id: container_id,
selector: selector,
render: function() {
token.find("img.loading").show();
container.hide();
container.html('');
dttm = 0;
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() {
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) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search);
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) {
cp = $("#flt0").clone();
$(cp).appendTo("#filters");
$(cp).show();
if (i != undefined){
$(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();
});
}
$(".select2").select2({dropdownAutoWidth : true});
$(".select2Sortable").select2();
$(".select2Sortable").select2Sortable();
$("form").show();
$('[data-toggle="tooltip"]').tooltip({container: 'body'});
function druidify(){
var i = 1;
// Assigning the right id to form elements in filters
$("#filters > div").each(function() {
$(this).attr("id", function() {return "flt_" + i;})
$(this).find("#flt_col_0")
.attr("id", function() {return "flt_col_" + i;})
.attr("name", function() {return "flt_col_" + i;});
$(this).find("#flt_op_0")
.attr("id", function() {return "flt_op_" + i;})
.attr("name", function() {return "flt_op_" + i;});
$(this).find("#flt_eq_0")
.attr("id", function() {return "flt_eq_" + i;})
.attr("name", function() {return "flt_eq_" + i;});
i++;
});
$("#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;
function set_filters(){
for (var i = 1; i < 10; i++){
var eq = getParam("flt_eq_" + i);
if (eq != ''){
add_filter(i);
}
}
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) {
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 :(")},
}
set_filters();
function add_filter(i) {
cp = $("#flt0").clone();
$(cp).appendTo("#filters");
$(cp).show();
if (i != undefined){
$(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 prepForm(){
var i = 1;
// Assigning the right id to form elements in filters
$("#filters > div").each(function() {
$(this).attr("id", function() {return "flt_" + i;})
$(this).find("#flt_col_0")
.attr("id", function() {return "flt_col_" + i;})
.attr("name", function() {return "flt_col_" + i;});
$(this).find("#flt_op_0")
.attr("id", function() {return "flt_op_" + i;})
.attr("name", function() {return "flt_op_" + i;});
$(this).find("#flt_eq_0")
.attr("id", function() {return "flt_eq_" + i;})
.attr("name", function() {return "flt_eq_" + i;});
i++;
});
}
function 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();
}
});
});
$("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();
});
$("#dash_css").on("keyup", function(){
css = $(this).val();
$("#user_style").html(css);
});
$('li.widget').each(function() { /* this sets the z-index for left side boxes higher. */
current_row = $(this).attr('data-col');
$( this ).css('z-index', 100 - current_row);
});
$("div.chart").each(function() { /* this makes the whole chart fit within the dashboard div */
$(this).css('height', '95%');
});
}
$("#btn_overwrite").click(function () {
var flag = confirm("Overwrite slice [" + $("#slice_name").val() + "] !?");
if (flag) {
$("#action").val("overwrite");
prepForm();
$("#query").submit();
}
});
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 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
return {
registerWidget: registerWidget,
initializeWidget: initializeWidget,
initializeDatasourceView: initializeDatasourceView,
initializeDashboardView: initializeDashboardView,
registerViz: registerViz,
Slice: Slice,
Dashboard: Dashboard,
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'];
var json_callback = data_attribute['json_endpoint'];
var div = d3.select('#' + token_name);
function render(ctrl) {
d3.json(json_callback, function(error, payload){
function render() {
d3.json(slice.jsonEndpoint(), function(error, payload){
//Define the percentage bounds that define color from red to green
div.html("");
if (error != null){
ctrl.error(error.responseText);
slice.error(error.responseText);
return '';
}
json = payload.data;
@ -19,9 +16,8 @@ px.registerWidget('big_number', function(data_attribute) {
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 width = slice.container.width();
var height = slice.container.height() - 30;
var svg = div.append('svg');
svg.attr("width", width);
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.axis').transition().duration(500).attr('opacity', 0);
});
ctrl.done(payload);
slice.done(payload);
});
};

View File

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

View File

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

View File

@ -1,7 +1,4 @@
function viz_nvd3(data_attribute) {
var token_name = data_attribute['token'];
var token = d3.select('#' + token_name);
var json_callback = data_attribute['json_endpoint'];
function viz_nvd3(slice) {
var chart = undefined;
var data = {};
@ -26,15 +23,13 @@ function viz_nvd3(data_attribute) {
"#FF5A5F", "#007A87", "#7B0051", "#00D1C1", "#8CE071", "#FFB400",
"#FFAA91", "#B4A76C", "#9CA299", "#565A5C"
];
var jtoken = $('#' + token_name);
var chart_div = $('#' + token_name).find("div.chart");
var refresh = function(ctrl) {
chart_div.hide();
$.getJSON(json_callback, function(payload) {
var refresh = function() {
$.getJSON(slice.jsonEndpoint(), function(payload) {
var data = payload.data;
var viz = payload;
var viz_type = viz.form_data.viz_type;
var fd = viz.form_data;
var f = d3.format('.4s');
nv.addGraph(function() {
if (viz_type === 'line') {
if (viz.form_data.show_brush) {
@ -113,10 +108,25 @@ function viz_nvd3(data_attribute) {
chart.yAxis.tickFormat(d3.format('.3p'));
} else if (viz_type === 'bubble') {
var row = function(col1, col2){
return "<tr><td>" + col1 + "</td><td>" + col2 + "</td></r>"
}
chart = nv.models.scatterChart();
chart.showDistX(true);
chart.showDistY(true);
chart.xAxis.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]);
} else if (viz_type === 'area') {
@ -130,7 +140,7 @@ function viz_nvd3(data_attribute) {
}
// 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) {
chart.useInteractiveGuideline(true);
}
@ -153,19 +163,17 @@ function viz_nvd3(data_attribute) {
chart.duration(0);
token.select('.chart').append("svg")
d3.select(slice.selector).append("svg")
.datum(data.chart_data)
.transition().duration(500)
.call(chart);
return chart;
});
chart_div.show();
ctrl.done(data);
slice.done(data);
})
.fail(function(xhr) {
chart_div.show();
ctrl.error(xhr.responseText);
slice.error(xhr.responseText);
});
};
var resize = function() {
@ -189,5 +197,5 @@ function viz_nvd3(data_attribute) {
'line',
'pie',
].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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,10 @@
px.registerWidget('word_cloud', function(data_attribute) {
var token_name = data_attribute['token'];
var json_callback = data_attribute['json_endpoint'];
var token = d3.select('#' + token_name);
function refresh(ctrl) {
d3.json(json_callback, function(error, json) {
px.registerViz('word_cloud', function(slice) {
var slice = slice;
var chart = d3.select(slice.selector);
function refresh() {
d3.json(slice.jsonEndpoint(), function(error, json) {
if (error != null){
ctrl.error(error.responseText);
slice.error(error.responseText);
return '';
}
var data = json.data;
@ -25,8 +22,7 @@ px.registerWidget('word_cloud', function(data_attribute) {
else {
var f_rotation = function() { return (~~(Math.random() * 6) - 3) * 30; };
}
var box = token.node().getBoundingClientRect();
var size = [box.width, box.height - 25];
var size = [slice.container.width(), slice.container.height() - 25];
scale = d3.scale.linear()
.range(range)
@ -42,9 +38,9 @@ px.registerWidget('word_cloud', function(data_attribute) {
.on("end", draw);
layout.start();
function draw(words) {
token.selectAll("*").remove();
chart.selectAll("*").remove();
token.append("svg")
chart.append("svg")
.attr("width", layout.size()[0])
.attr("height", layout.size()[1])
.append("g")
@ -61,7 +57,7 @@ px.registerWidget('word_cloud', function(data_attribute) {
})
.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,
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/
*/
function viz_world_map(data_attribute) {
var token = d3.select('#' + data_attribute.token);
var render = function(ctrl) {
// Breadcrumb dimensions: width, height, spacing, width of tip/tail.
var div = token;
var xy = div.node().getBoundingClientRect();
var width = xy.width;
var height = xy.height - 25;
function viz_world_map(slice) {
var render = function() {
var container = slice.container;
var div = d3.select(slice.selector);
d3.json(data_attribute.json_endpoint, function(error, json){
d3.json(slice.data.json_endpoint, function(error, json){
if (error != null){
ctrl.error(error.responseText);
slice.error(error.responseText);
return '';
}
var ext = d3.extent(json.data, function(d){return d.m1});
var extRadius = d3.extent(json.data, function(d){return d.m2});
var radiusScale = d3.scale.linear()
.domain([extRadius[0], extRadius[1]])
.range([1, data_attribute.form_data.max_bubble_size]);
.range([1, slice.data.form_data.max_bubble_size]);
json.data.forEach(function(d){
d.radius = radiusScale(d.m2);
})
@ -35,11 +31,12 @@ function viz_world_map(data_attribute) {
d[country.country] = country;
}
f = d3.format('.3s');
container.show();
var map = new Datamap({
element: document.getElementById(data_attribute.token),
element: slice.container.get(0),
data: json.data,
fills: {
defaultFill: 'grey'
defaultFill: 'transparent'
},
geographyConfig: {
popupOnHover: true,
@ -75,11 +72,11 @@ function viz_world_map(data_attribute) {
},
});
map.updateChoropleth(d);
if(data_attribute.form_data.show_bubbles){
if(slice.data.form_data.show_bubbles){
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,
};
}
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">
{{ dashboard.css }}
</style>
{% for slice in dashboard.slices %}
{% set viz = slice.viz %}
{% import viz.template as viz_macros %}
{{ viz_macros.viz_css(viz) }}
{% endfor %}
{% endblock %}
{% block content_fluid %}
@ -77,17 +72,16 @@ body {
{% 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 }}"
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-col="{{ pos.col or loop.index }}"
data-sizex="{{ pos.size_x or 4 }}"
data-sizey="{{ pos.size_y or 4 }}">
<table class="widget_header">
<table class="slice_header">
<tbody>
<tr>
<td class="icons">
@ -103,7 +97,7 @@ body {
<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>
<a class="closeslice"><i class="fa fa-close"></i></a>
</br>
</td>
</tr>
@ -111,7 +105,7 @@ body {
</table>
<div id="{{ viz.token }}" class="token" style="height: 100%;">
<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>
</li>
{% endfor %}
@ -127,19 +121,9 @@ body {
{% endfor %}
<script src="/static/lib/gridster/jquery.gridster.with-extras.min.js"></script>
<script>
$(document).ready(px.initializeDashboardView({{ dashboard.id }}));
$(document).ready(function() {
$('.dashboard .widget').each(function() {
var data = $(this).data('widget');
var widget = px.initializeWidget(data);
$(this).data('widget', widget);
widget.render();
});
px.initDashboardView();
var dashboard = px.Dashboard({{ dashboard.id }});
});
</script>
{% for slice in dashboard.slices %}
{% set viz = slice.viz %}
{% import viz.template as viz_macros %}
{{ viz_macros.viz_js(viz) }}
{% endfor %}
{% endblock %}

View File

@ -26,22 +26,29 @@
<span class="btn btn-default notbtn">
<strong>{{ datasource.full_name }}</strong>
{% 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>
</a>
{% endif %}
<a class="" href="/{{ datasource.baselink }}/edit/{{ datasource.id }}" data-toggle="tooltip" title="Edit datasource">
<i class="fa fa-edit"></i>
</a>
</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">
<span class="btn btn-default disabled">
<i class="fa fa-file-text"></i>
<span class="btn btn-default" id="standalone" title="Standalone version, use to embed anywhere" data-toggle="tooltip">
<i class="fa fa-code"></i>
</span>
<span class="btn btn-default" id="csv">.csv</span>
<span class="btn btn-default" id="json">.json</span>
<span class="btn btn-default " id="json" title="Export to .json" data-toggle="tooltip">
<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>
<hr/>
</div>
@ -133,23 +140,15 @@
</div>
<div class="col-md-9">
{% block messages %}
{% endblock %}
{% include 'appbuilder/flash.html' %}
{% block messages %}{% endblock %}
{% include 'appbuilder/flash.html' %}
<div
id="{{ viz.token }}"
class="viz widget {{ viz.viz_type }}"
data-widget="{{ viz.get_data_attribute() }}"
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">
{% 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 id="{{ viz.token }}_con" class="slice_container" style="height: 100%; width: 100%"></div>
</div>
</div>
</div>
@ -176,6 +175,24 @@
</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>
</div>
{% endblock %}
@ -183,15 +200,15 @@
{% block tail_js %}
{{ super() }}
<script>
$(document).ready(px.initializeDatasourceView);
$(document).ready(px.initExploreView);
$(document).ready(function() {
var data = $('.widget').data('widget');
var widget = px.initializeWidget(data);
$('.widget').data('widget', widget);
widget.render();
var data = $('.slice').data('slice');
var slice = px.Slice(data);
$('.slice').data('slice', slice);
slice.render();
function get_collapsed_fieldsets(){
collapsed_fieldsets = $("#collapsed_fieldsets").val()
collapsed_fieldsets = $("#collapsed_fieldsets").val();
if (collapsed_fieldsets != undefined && collapsed_fieldsets != "")
collapsed_fieldsets = collapsed_fieldsets.split('||');
else
@ -242,9 +259,12 @@
$('#csv').click(function () {
window.location = '{{ viz.csv_endpoint | safe }}';
});
$('#standalone').click(function () {
window.location = '{{ viz.standalone_endpoint | safe }}';
});
$("#viz_type").change(function() {$("#query").submit();});
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);
}
});

View File

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

View File

@ -2,4 +2,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
nosetests tests/core_tests.py --with-coverage --cover-package=panoramix -v