mirror of https://github.com/apache/superset.git
Improvements
This commit is contained in:
parent
bd1d8eb242
commit
c6dca0f27d
|
@ -1,3 +1,4 @@
|
|||
*.pyc
|
||||
*.db
|
||||
tmp
|
||||
local_config
|
||||
|
|
7
TODO.md
7
TODO.md
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
||||
|
|
|
@ -13,5 +13,8 @@
|
|||
box-shadow: 0px 3px 3px #AAA;
|
||||
z-index:999;
|
||||
}
|
||||
.panel.panel-primary {
|
||||
margin: 10px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
|
|
@ -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;">
|
||||
|
|
|
@ -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();
|
||||
|
|
68
app/views.py
68
app/views.py
|
@ -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(
|
||||
|
|
76
app/viz.py
76
app/viz.py
|
@ -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.
|
||||
|
|
15
config.py
15
config.py
|
@ -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
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
flask-alembic
|
||||
pydruid
|
||||
parsedatetime
|
||||
python-dateutil
|
||||
|
|
Loading…
Reference in New Issue