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
*.db
tmp
local_config

View File

@ -1,8 +1,5 @@
# TODO
* STOCK CHART + compare time ranges
* Save a chart
* Datasource + Owner
* Column description
* compare time ranges
* Label
* 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(
app, db.session, base_template='panoramix/base.html',
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

View File

@ -1,6 +1,7 @@
from flask.ext.appbuilder import Model
from datetime import datetime, timedelta
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.orm import relationship
from app import db, utils
@ -18,6 +19,9 @@ class Datasource(Model, AuditMixin):
is_hidden = Column(Boolean, default=False)
description = Column(Text)
default_endpoint = Column(Text)
user_id = Column(Integer,
ForeignKey('ab_user.id'))
owner = relationship('User', backref='datasources', foreign_keys=[user_id])
@property
def metrics_combo(self):
@ -111,6 +115,7 @@ class Metric(Model):
ForeignKey('datasources.datasource_name'))
datasource = relationship('Datasource', backref='metrics')
json = Column(Text)
description = Column(Text)
@property
def json_obj(self):
@ -132,6 +137,7 @@ class Column(Model, AuditMixin):
max = Column(Boolean, default=False)
min = Column(Boolean, default=False)
filterable = Column(Boolean, default=False)
description = Column(Text)
def __repr__(self):
return self.column_name
@ -150,8 +156,6 @@ class Column(Model, AuditMixin):
json=json.dumps({
'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:
mt = self.type.lower() + 'Sum'

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,18 @@
<div id="chart"></div>
{% 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 %}
{{ super() }}
{% if viz.chart_type == "stock" %}
@ -20,8 +32,6 @@ $( document ).ready(function() {
global: {
useUTC: false
},
});
$("#viz_type").click(function(){
$("#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 app import appbuilder, db, models, viz, utils
import config
from wtforms import Form, SelectMultipleField, SelectField, TextField
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):
datamodel = SQLAInterface(models.Column)
edit_columns = [
'column_name', 'datasource', 'groupby', 'count_distinct',
'sum', 'min', 'max']
'column_name', 'description', 'datasource', 'groupby',
'count_distinct', 'sum', 'min', 'max']
list_columns = [
'column_name', 'type', 'groupby', 'count_distinct',
'sum', 'min', 'max']
@ -81,7 +26,8 @@ class MetricInlineView(CompactCRUDMixin, ModelView):
datamodel = SQLAInterface(models.Metric)
list_columns = ['metric_name', 'verbose_name', 'metric_type' ]
edit_columns = [
'metric_name', 'verbose_name', 'metric_type', 'datasource', 'json']
'metric_name', 'description', 'verbose_name', 'metric_type',
'datasource', 'json']
add_columns = [
'metric_name', 'verbose_name', 'metric_type', 'datasource', 'json']
appbuilder.add_view_no_menu(MetricInlineView)
@ -89,12 +35,13 @@ appbuilder.add_view_no_menu(MetricInlineView)
class DatasourceModelView(ModelView):
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]
edit_columns = [
'datasource_name', 'description', 'is_featured', 'is_hidden',
'datasource_name', 'description', 'owner', 'is_featured', 'is_hidden',
'default_endpoint']
page_size = 100
order_columns = ['datasource_name']
appbuilder.add_view(
@ -121,7 +68,6 @@ class Panoramix(BaseView):
viz_type = "table"
obj = viz.viz_types[viz_type](
datasource,
form_class=form_factory(datasource, request.args),
form_data=request.args, view=self)
if request.args.get("json"):
return Response(

View File

@ -1,11 +1,12 @@
from pydruid.utils.filters import Dimension, Filter
from datetime import datetime
from flask import render_template, flash
from flask import render_template, flash, request
import pandas as pd
from pandas_highcharts.core import serialize
from pydruid.utils import aggregators as agg
from collections import OrderedDict
from app import utils
from wtforms import Form, SelectMultipleField, SelectField, TextField
import config
@ -15,13 +16,63 @@ CHART_ARGS = {
'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):
verbose_name = "Base Viz"
template = "panoramix/datasource.html"
def __init__(self, datasource, form_class, form_data, view):
def __init__(self, datasource, form_data, view):
self.datasource = datasource
self.form_class = form_class
self.form_class = self.form_class()
self.form_data = form_data
self.metrics = form_data.getlist('metrics') or ['count']
self.groupby = form_data.getlist('groupby') or []
@ -33,6 +84,9 @@ class BaseViz(object):
self.df_prep()
self.form_prep()
def form_class(self):
return form_factory(self.datasource, request.args)
def query_filters(self):
args = self.form_data
# Building filters
@ -177,6 +231,12 @@ class TimeSeriesViz(HighchartsViz):
columns=self.groupby,
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(
df, kind=self.chart_kind,
viz=self,
@ -184,6 +244,16 @@ class TimeSeriesViz(HighchartsViz):
chart_type=self.chart_type, stacked=self.stacked, **CHART_ARGS)
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):
"""
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
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
#---------------------------------------------------------
ROW_LIMIT = 5000
DRUID_HOST = '10.181.47.80'
DRUID_HOST = '0.0.0.0'
DRUID_PORT = 8080
DRUID_BASE_ENDPOINT = 'druid/v2'
COORDINATOR_HOST = '10.168.176.249'
COORDINATOR_HOST = '0.0.0.0'
COORDINATOR_PORT = '8080'
COORDINATOR_BASE_ENDPOINT = 'druid/coordinator/v1'
#---------------------------------------------------------
@ -118,3 +125,7 @@ IMG_UPLOAD_URL = '/static/uploads/'
#APP_THEME = "united.css"
#APP_THEME = "yeti.css"
try:
from local_config import *
except:
pass

View File

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