Improvements

This commit is contained in:
Maxime 2015-07-21 18:56:05 +00:00
parent bd1d8eb242
commit c6dca0f27d
14 changed files with 137 additions and 101 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
*.pyc *.pyc
*.db *.db
tmp tmp
local_config

View File

@ -1,8 +1,5 @@
# TODO # TODO
* STOCK CHART + compare time ranges * compare time ranges
* Save a chart
* Datasource + Owner
* Column description
* Label * Label
* CSV * CSV
* Bookmarks / url shortener * Save / bookmark / url shortener

BIN
app.db

Binary file not shown.

View File

@ -19,20 +19,5 @@ class MyIndexView(IndexView):
appbuilder = AppBuilder( appbuilder = AppBuilder(
app, db.session, base_template='panoramix/base.html', app, db.session, base_template='panoramix/base.html',
indexview=MyIndexView) indexview=MyIndexView)
#appbuilder.app_name = 'Panoramix'
"""
from sqlalchemy.engine import Engine
from sqlalchemy import event
#Only include this for SQLLite constraints
@event.listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
# Will force sqllite contraint foreign keys
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
"""
from app import views from app import views

View File

@ -1,6 +1,7 @@
from flask.ext.appbuilder import Model from flask.ext.appbuilder import Model
from datetime import datetime, timedelta from datetime import datetime, timedelta
from flask.ext.appbuilder.models.mixins import AuditMixin, FileColumn, ImageColumn from flask.ext.appbuilder.models.mixins import AuditMixin, FileColumn, ImageColumn
from flask.ext.appbuilder.security.sqla.models import User
from sqlalchemy import Column, Integer, String, ForeignKey, Text, Boolean from sqlalchemy import Column, Integer, String, ForeignKey, Text, Boolean
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from app import db, utils from app import db, utils
@ -18,6 +19,9 @@ class Datasource(Model, AuditMixin):
is_hidden = Column(Boolean, default=False) is_hidden = Column(Boolean, default=False)
description = Column(Text) description = Column(Text)
default_endpoint = Column(Text) default_endpoint = Column(Text)
user_id = Column(Integer,
ForeignKey('ab_user.id'))
owner = relationship('User', backref='datasources', foreign_keys=[user_id])
@property @property
def metrics_combo(self): def metrics_combo(self):
@ -111,6 +115,7 @@ class Metric(Model):
ForeignKey('datasources.datasource_name')) ForeignKey('datasources.datasource_name'))
datasource = relationship('Datasource', backref='metrics') datasource = relationship('Datasource', backref='metrics')
json = Column(Text) json = Column(Text)
description = Column(Text)
@property @property
def json_obj(self): def json_obj(self):
@ -132,6 +137,7 @@ class Column(Model, AuditMixin):
max = Column(Boolean, default=False) max = Column(Boolean, default=False)
min = Column(Boolean, default=False) min = Column(Boolean, default=False)
filterable = Column(Boolean, default=False) filterable = Column(Boolean, default=False)
description = Column(Text)
def __repr__(self): def __repr__(self):
return self.column_name return self.column_name
@ -150,8 +156,6 @@ class Column(Model, AuditMixin):
json=json.dumps({ json=json.dumps({
'type': 'count', 'name': 'count'}) 'type': 'count', 'name': 'count'})
)) ))
if self.datasource.datasource_name == 'platform' and self.column_name=='subject_id':
print((self.column_name, self.type, self.isnum))
if self.sum and self.isnum: if self.sum and self.isnum:
mt = self.type.lower() + 'Sum' mt = self.type.lower() + 'Sum'

View File

@ -11,7 +11,7 @@
</header> </header>
{% endblock %} {% endblock %}
<div class="container-fluid"> <div class="container">
<div class="row"> <div class="row">
{% block messages %} {% block messages %}
{% include 'appbuilder/flash.html' %} {% include 'appbuilder/flash.html' %}
@ -20,6 +20,10 @@
{% endblock %} {% endblock %}
</div> </div>
</div> </div>
<div class="container-fluid">
{% block content_fluid %}
{% endblock %}
</div>
{% block footer %} {% block footer %}
<footer> <footer>

View File

@ -1,13 +1,13 @@
{% extends "appbuilder/base.html" %} {% extends "appbuilder/base.html" %}
{% block content %} {% block content %}
<div class="jumbotron"> <div class="container">
<div class="container"> <div class="jumbotron">
<h1>Panoramix</h1> <h1>Panoramix</h1>
<p>Panoramix is an interactive visualization platform built on top of Druid.io</p> <p>Panoramix is an interactive visualization platform built on top of Druid.io</p>
</div>
<div class="text-center">
<img width="250" src="/static/tux_panoramix.png">
</div> </div>
</div> </div>
<div class="text-center">
<img width="250" src="/static/tux_panoramix.png">
</div>
{% endblock %} {% endblock %}

View File

@ -13,5 +13,8 @@
box-shadow: 0px 3px 3px #AAA; box-shadow: 0px 3px 3px #AAA;
z-index:999; z-index:999;
} }
.panel.panel-primary {
margin: 10px;
}
</style> </style>
{% endblock %} {% endblock %}

View File

@ -10,12 +10,15 @@
padding-right:0; padding-right:0;
padding-left:0; padding-left:0;
} }
form div.select2-container.form-control { form div.form-control {
margin-bottom: 5px; margin-bottom: 5px !important;
}
form input.form-control {
margin-bottom: 5px !important;
} }
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content_fluid %}
<div class="container-fluid"> <div class="container-fluid">
<div class="col-md-3"> <div class="col-md-3">
<h3> <h3>
@ -36,6 +39,7 @@ form div.select2-container.form-control {
</div> </div>
<div>{{ form.groupby.label }}: {{ form.groupby(class_="form-control select2") }}</div> <div>{{ form.groupby.label }}: {{ form.groupby(class_="form-control select2") }}</div>
<div>{{ form.limit.label }}: {{ form.limit(class_="form-control select2") }}</div> <div>{{ form.limit.label }}: {{ form.limit(class_="form-control select2") }}</div>
{% block extra_fields %}{% endblock %}
<hr> <hr>
<h4>Filters</h4> <h4>Filters</h4>
<div id="flt0" style="display: none;"> <div id="flt0" style="display: none;">

View File

@ -3,6 +3,18 @@
<div id="chart"></div> <div id="chart"></div>
{% endblock %} {% endblock %}
{% block extra_fields %}
{% if form.compare %}
<div>{{ form.compare.label }}: {{ form.compare(class_="form-control") }}</div>
{% endif %}
{% if form.compare %}
<div class="row">
<span class="col col-sm-5">{{ form.rolling_type.label }}: {{ form.rolling_type(class_="form-control select2") }}</span>
<span class="col col-sm-4">{{ form.rolling_periods.label }}: {{ form.rolling_periods(class_="form-control") }}</span>
</div>
{% endif %}
{% endblock %}
{% block tail %} {% block tail %}
{{ super() }} {{ super() }}
{% if viz.chart_type == "stock" %} {% if viz.chart_type == "stock" %}
@ -20,8 +32,6 @@ $( document ).ready(function() {
global: { global: {
useUTC: false useUTC: false
}, },
}); });
$("#viz_type").click(function(){ $("#viz_type").click(function(){
$("#queryform").submit(); $("#queryform").submit();

View File

@ -7,69 +7,14 @@ from flask.ext.appbuilder.models.sqla.interface import SQLAInterface
from flask.ext.appbuilder import ModelView, CompactCRUDMixin, BaseView, expose from flask.ext.appbuilder import ModelView, CompactCRUDMixin, BaseView, expose
from app import appbuilder, db, models, viz, utils from app import appbuilder, db, models, viz, utils
import config import config
from wtforms import Form, SelectMultipleField, SelectField, TextField
from wtforms.fields import Field from wtforms.fields import Field
class OmgWtForm(Form):
field_order = (
'viz_type', 'granularity', 'since', 'group_by', 'limit')
def fields(self):
fields = []
for field in self.field_order:
if hasattr(self, field):
obj = getattr(self, field)
if isinstance(obj, Field):
fields.append(getattr(self, field))
return fields
def form_factory(datasource, form_args=None):
grain = ['all', 'none', 'minute', 'hour', 'day']
limits = [0, 5, 10, 25, 50, 100, 500]
if form_args:
limit = form_args.get("limit")
try:
limit = int(limit)
if limit not in limits:
limits.append(limit)
limits = sorted(limits)
except:
pass
class QueryForm(OmgWtForm):
viz_type = SelectField(
'Viz',
choices=[(k, v.verbose_name) for k, v in viz.viz_types.items()])
metrics = SelectMultipleField('Metrics', choices=datasource.metrics_combo)
groupby = SelectMultipleField(
'Group by', choices=[
(s, s) for s in datasource.groupby_column_names])
#granularity = SelectField(
# 'Time Granularity', choices=[(g, g) for g in grain])
#since = SelectField(
# 'Since', choices=[(s, s) for s in utils.since_l.keys()],
# default="all")
granularity = TextField('Time Granularity', default="one day")
since = TextField('Since', default="one day ago")
until = TextField('Until', default="now")
limit = SelectField(
'Limit', choices=[(s, s) for s in limits])
for i in range(10):
setattr(QueryForm, 'flt_col_' + str(i), SelectField(
'Filter 1', choices=[(s, s) for s in datasource.filterable_column_names]))
setattr(QueryForm, 'flt_op_' + str(i), SelectField(
'Filter 1', choices=[(m, m) for m in ['in', 'not in']]))
setattr(QueryForm, 'flt_eq_' + str(i), TextField("Super"))
return QueryForm
class ColumnInlineView(CompactCRUDMixin, ModelView): class ColumnInlineView(CompactCRUDMixin, ModelView):
datamodel = SQLAInterface(models.Column) datamodel = SQLAInterface(models.Column)
edit_columns = [ edit_columns = [
'column_name', 'datasource', 'groupby', 'count_distinct', 'column_name', 'description', 'datasource', 'groupby',
'sum', 'min', 'max'] 'count_distinct', 'sum', 'min', 'max']
list_columns = [ list_columns = [
'column_name', 'type', 'groupby', 'count_distinct', 'column_name', 'type', 'groupby', 'count_distinct',
'sum', 'min', 'max'] 'sum', 'min', 'max']
@ -81,7 +26,8 @@ class MetricInlineView(CompactCRUDMixin, ModelView):
datamodel = SQLAInterface(models.Metric) datamodel = SQLAInterface(models.Metric)
list_columns = ['metric_name', 'verbose_name', 'metric_type' ] list_columns = ['metric_name', 'verbose_name', 'metric_type' ]
edit_columns = [ edit_columns = [
'metric_name', 'verbose_name', 'metric_type', 'datasource', 'json'] 'metric_name', 'description', 'verbose_name', 'metric_type',
'datasource', 'json']
add_columns = [ add_columns = [
'metric_name', 'verbose_name', 'metric_type', 'datasource', 'json'] 'metric_name', 'verbose_name', 'metric_type', 'datasource', 'json']
appbuilder.add_view_no_menu(MetricInlineView) appbuilder.add_view_no_menu(MetricInlineView)
@ -89,12 +35,13 @@ appbuilder.add_view_no_menu(MetricInlineView)
class DatasourceModelView(ModelView): class DatasourceModelView(ModelView):
datamodel = SQLAInterface(models.Datasource) datamodel = SQLAInterface(models.Datasource)
list_columns = ['datasource_link', 'is_featured', 'is_hidden'] list_columns = ['datasource_link', 'owner', 'is_featured', 'is_hidden']
related_views = [ColumnInlineView, MetricInlineView] related_views = [ColumnInlineView, MetricInlineView]
edit_columns = [ edit_columns = [
'datasource_name', 'description', 'is_featured', 'is_hidden', 'datasource_name', 'description', 'owner', 'is_featured', 'is_hidden',
'default_endpoint'] 'default_endpoint']
page_size = 100 page_size = 100
order_columns = ['datasource_name']
appbuilder.add_view( appbuilder.add_view(
@ -121,7 +68,6 @@ class Panoramix(BaseView):
viz_type = "table" viz_type = "table"
obj = viz.viz_types[viz_type]( obj = viz.viz_types[viz_type](
datasource, datasource,
form_class=form_factory(datasource, request.args),
form_data=request.args, view=self) form_data=request.args, view=self)
if request.args.get("json"): if request.args.get("json"):
return Response( return Response(

View File

@ -1,11 +1,12 @@
from pydruid.utils.filters import Dimension, Filter from pydruid.utils.filters import Dimension, Filter
from datetime import datetime from datetime import datetime
from flask import render_template, flash from flask import render_template, flash, request
import pandas as pd import pandas as pd
from pandas_highcharts.core import serialize from pandas_highcharts.core import serialize
from pydruid.utils import aggregators as agg from pydruid.utils import aggregators as agg
from collections import OrderedDict from collections import OrderedDict
from app import utils from app import utils
from wtforms import Form, SelectMultipleField, SelectField, TextField
import config import config
@ -15,13 +16,63 @@ CHART_ARGS = {
'render_to': 'chart', 'render_to': 'chart',
} }
class OmgWtForm(Form):
field_order = (
'viz_type', 'granularity', 'since', 'group_by', 'limit')
def fields(self):
fields = []
for field in self.field_order:
if hasattr(self, field):
obj = getattr(self, field)
if isinstance(obj, Field):
fields.append(getattr(self, field))
return fields
def form_factory(datasource, form_args=None, extra_fields_dict=None):
extra_fields_dict = extra_fields_dict or {}
limits = [0, 5, 10, 25, 50, 100, 500]
if form_args:
limit = form_args.get("limit")
try:
limit = int(limit)
if limit not in limits:
limits.append(limit)
limits = sorted(limits)
except:
pass
class QueryForm(OmgWtForm):
viz_type = SelectField(
'Viz',
choices=[(k, v.verbose_name) for k, v in viz_types.items()])
metrics = SelectMultipleField('Metrics', choices=datasource.metrics_combo)
groupby = SelectMultipleField(
'Group by', choices=[
(s, s) for s in datasource.groupby_column_names])
granularity = TextField('Time Granularity', default="one day")
since = TextField('Since', default="one day ago")
until = TextField('Until', default="now")
limit = SelectField(
'Limit', choices=[(s, s) for s in limits])
for i in range(10):
setattr(QueryForm, 'flt_col_' + str(i), SelectField(
'Filter 1', choices=[(s, s) for s in datasource.filterable_column_names]))
setattr(QueryForm, 'flt_op_' + str(i), SelectField(
'Filter 1', choices=[(m, m) for m in ['in', 'not in']]))
setattr(QueryForm, 'flt_eq_' + str(i), TextField("Super"))
for k, v in extra_fields_dict.items():
setattr(QueryForm, k, v)
return QueryForm
class BaseViz(object): class BaseViz(object):
verbose_name = "Base Viz" verbose_name = "Base Viz"
template = "panoramix/datasource.html" template = "panoramix/datasource.html"
def __init__(self, datasource, form_class, form_data, view): def __init__(self, datasource, form_data, view):
self.datasource = datasource self.datasource = datasource
self.form_class = form_class self.form_class = self.form_class()
self.form_data = form_data self.form_data = form_data
self.metrics = form_data.getlist('metrics') or ['count'] self.metrics = form_data.getlist('metrics') or ['count']
self.groupby = form_data.getlist('groupby') or [] self.groupby = form_data.getlist('groupby') or []
@ -33,6 +84,9 @@ class BaseViz(object):
self.df_prep() self.df_prep()
self.form_prep() self.form_prep()
def form_class(self):
return form_factory(self.datasource, request.args)
def query_filters(self): def query_filters(self):
args = self.form_data args = self.form_data
# Building filters # Building filters
@ -177,6 +231,12 @@ class TimeSeriesViz(HighchartsViz):
columns=self.groupby, columns=self.groupby,
values=metrics) values=metrics)
rolling_periods = request.args.get("rolling_periods")
rolling_type = request.args.get("rolling_type")
if rolling_periods and rolling_type:
if rolling_type == 'mean':
df = pd.rolling_mean(df, int(rolling_periods))
chart_js = serialize( chart_js = serialize(
df, kind=self.chart_kind, df, kind=self.chart_kind,
viz=self, viz=self,
@ -184,6 +244,16 @@ class TimeSeriesViz(HighchartsViz):
chart_type=self.chart_type, stacked=self.stacked, **CHART_ARGS) chart_type=self.chart_type, stacked=self.stacked, **CHART_ARGS)
return super(TimeSeriesViz, self).render(chart_js=chart_js) return super(TimeSeriesViz, self).render(chart_js=chart_js)
def form_class(self):
return form_factory(self.datasource, request.args,
extra_fields_dict={
'compare': TextField('Period Compare',),
'rolling_type': SelectField(
'Rolling',
choices=[(s, s) for s in ['mean', 'sum', 'std']]),
'rolling_periods': TextField('Periods',),
})
def bake_query(self): def bake_query(self):
""" """
Doing a 2 phase query where we limit the number of series. Doing a 2 phase query where we limit the number of series.

View File

@ -2,16 +2,23 @@ import os
from flask_appbuilder.security.manager import AUTH_OID, AUTH_REMOTE_USER, AUTH_DB, AUTH_LDAP, AUTH_OAUTH from flask_appbuilder.security.manager import AUTH_OID, AUTH_REMOTE_USER, AUTH_DB, AUTH_LDAP, AUTH_OAUTH
basedir = os.path.abspath(os.path.dirname(__file__)) basedir = os.path.abspath(os.path.dirname(__file__))
"""
All configuration in this file can be overridden by providing a local_config
in your PYTHONPATH.
There' a ``from local_config import *`` at the end of this file.
"""
#--------------------------------------------------------- #---------------------------------------------------------
# Panoramix specifix config # Panoramix specifix config
#--------------------------------------------------------- #---------------------------------------------------------
ROW_LIMIT = 5000 ROW_LIMIT = 5000
DRUID_HOST = '10.181.47.80' DRUID_HOST = '0.0.0.0'
DRUID_PORT = 8080 DRUID_PORT = 8080
DRUID_BASE_ENDPOINT = 'druid/v2' DRUID_BASE_ENDPOINT = 'druid/v2'
COORDINATOR_HOST = '10.168.176.249' COORDINATOR_HOST = '0.0.0.0'
COORDINATOR_PORT = '8080' COORDINATOR_PORT = '8080'
COORDINATOR_BASE_ENDPOINT = 'druid/coordinator/v1' COORDINATOR_BASE_ENDPOINT = 'druid/coordinator/v1'
#--------------------------------------------------------- #---------------------------------------------------------
@ -118,3 +125,7 @@ IMG_UPLOAD_URL = '/static/uploads/'
#APP_THEME = "united.css" #APP_THEME = "united.css"
#APP_THEME = "yeti.css" #APP_THEME = "yeti.css"
try:
from local_config import *
except:
pass

View File

@ -1,3 +1,4 @@
flask-alembic
pydruid pydruid
parsedatetime parsedatetime
python-dateutil python-dateutil