mirror of https://github.com/apache/superset.git
Form factory refactor
This commit is contained in:
parent
6dd81a3e95
commit
1283bc0788
2
TODO.md
2
TODO.md
|
@ -6,3 +6,5 @@
|
||||||
* Save / bookmark / url shortener
|
* Save / bookmark / url shortener
|
||||||
* SQL: Find a way to manage granularity
|
* SQL: Find a way to manage granularity
|
||||||
* Create ~/.panoramix/ to host DB and config, generate default config there
|
* Create ~/.panoramix/ to host DB and config, generate default config there
|
||||||
|
* Add a per-datasource permission
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
from wtforms import Field, Form, SelectMultipleField, SelectField, TextField
|
||||||
|
from flask_appbuilder.fieldwidgets import Select2Widget, Select2ManyWidget
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class OmgWtForm(Form):
|
||||||
|
field_order = tuple()
|
||||||
|
css_classes = dict()
|
||||||
|
@property
|
||||||
|
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 get_field(self, fieldname):
|
||||||
|
return getattr(self, fieldname)
|
||||||
|
|
||||||
|
def field_css_classes(self, fieldname):
|
||||||
|
if fieldname in self.css_classes:
|
||||||
|
return " ".join(self.css_classes[fieldname])
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def form_factory(datasource, viz, form_args=None):
|
||||||
|
from panoramix.viz import viz_types
|
||||||
|
row_limits = [10, 50, 100, 500, 1000, 5000, 10000]
|
||||||
|
series_limits = [0, 5, 10, 25, 50, 100, 500]
|
||||||
|
group_by_choices = [(s, s) for s in datasource.groupby_column_names]
|
||||||
|
# Pool of all the fields that can be used in Panoramix
|
||||||
|
px_form_fields = {
|
||||||
|
'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"),
|
||||||
|
'row_limit':
|
||||||
|
SelectField(
|
||||||
|
'Row limit', choices=[(s, s) for s in row_limits]),
|
||||||
|
'limit':
|
||||||
|
SelectField(
|
||||||
|
'Series limit', choices=[(s, s) for s in series_limits]),
|
||||||
|
'rolling_type': SelectField(
|
||||||
|
'Rolling',
|
||||||
|
choices=[(s, s) for s in ['mean', 'sum', 'std']]),
|
||||||
|
'rolling_periods': TextField('Periods',),
|
||||||
|
'series': SelectField('Series', choices=group_by_choices),
|
||||||
|
'entity': SelectField('Entity', choices=group_by_choices),
|
||||||
|
'x': SelectField('X Axis', choices=datasource.metrics_combo),
|
||||||
|
'y': SelectField('Y Axis', choices=datasource.metrics_combo),
|
||||||
|
'size': SelectField('Bubble Size', choices=datasource.metrics_combo),
|
||||||
|
}
|
||||||
|
field_css_classes = {k: ['form-control'] for k in px_form_fields.keys()}
|
||||||
|
select2 = [
|
||||||
|
'viz_type', 'metrics', 'groupby',
|
||||||
|
'row_limit', 'rolling_type', 'series',
|
||||||
|
'entity', 'x', 'y', 'size',]
|
||||||
|
field_css_classes['since'] += ['select2_free_since']
|
||||||
|
field_css_classes['until'] += ['select2_free_until']
|
||||||
|
field_css_classes['granularity'] += ['select2_free_granularity']
|
||||||
|
for field in select2:
|
||||||
|
field_css_classes[field] += ['select2']
|
||||||
|
|
||||||
|
|
||||||
|
class QueryForm(OmgWtForm):
|
||||||
|
field_order = viz.form_fields
|
||||||
|
css_classes = field_css_classes
|
||||||
|
|
||||||
|
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 ff in viz.form_fields:
|
||||||
|
if isinstance(ff, basestring):
|
||||||
|
ff = [ff]
|
||||||
|
for s in ff:
|
||||||
|
setattr(QueryForm, s, px_form_fields[s])
|
||||||
|
return QueryForm
|
|
@ -38,32 +38,27 @@ form input.form-control {
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<form id="query" method="GET" style="display: none;">
|
<form id="query" method="GET" style="display: none;">
|
||||||
<div>{{ form.viz_type.label }}: {{ form.viz_type(class_="form-control select2") }}</div>
|
{% for fieldname in form.field_order %}
|
||||||
{% if 'metrics' not in viz.hidden_fields %}
|
{% if not fieldname.__iter__ %}
|
||||||
<div>{{ form.metrics.label }}: {{ form.metrics(class_="form-control select2") }}</div>
|
<div>
|
||||||
{% endif %}
|
{% set field = form.get_field(fieldname)%}
|
||||||
{% if 'granularity' not in viz.hidden_fields %}
|
{{ field.label }}:
|
||||||
<div>{{ form.granularity.label }}
|
{{ field(class_=form.field_css_classes(field.name)) }}
|
||||||
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="right"
|
</div>
|
||||||
title="Supports natural language time as in '10 seconds', '1 day' or '1 week'"
|
{% else %}
|
||||||
id="blah"></i>
|
<div class="row">
|
||||||
{{ form.granularity(class_="form-control select2_free_granularity") }}</div>
|
<div class="form-group">
|
||||||
{% endif %}
|
{% for name in fieldname %}
|
||||||
<div class="row">
|
<div class="col-xs-{{ (12 / fieldname|length) | int }}">
|
||||||
<div class="form-group">
|
{% set field = form.get_field(name)%}
|
||||||
<div class="col-xs-6">{{ form.since.label }}
|
{{ field.label }}:
|
||||||
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="right"
|
{{ field(class_=form.field_css_classes(field.name)) }}
|
||||||
title="Supports natural language time as in '1 day ago', '28 days' or '3 years'"
|
</div>
|
||||||
id="blah"></i>
|
{% endfor %}
|
||||||
{{ form.since(class_="form-control select2_free_since") }}</div>
|
</div>
|
||||||
<div class="col-xs-6">{{ form.until.label }}
|
</div>
|
||||||
{{ form.until(class_="form-control select2_free_until") }}</div>
|
{% endif %}
|
||||||
</div>
|
{% endfor %}
|
||||||
</div>
|
|
||||||
{% if 'groupby' not in viz.hidden_fields %}
|
|
||||||
<div>{{ form.groupby.label }}: {{ form.groupby(class_="form-control select2") }}</div>
|
|
||||||
{% endif %}
|
|
||||||
{% block extra_fields %}{% endblock %}
|
|
||||||
<hr>
|
<hr>
|
||||||
<h4>Filters</h4>
|
<h4>Filters</h4>
|
||||||
<div id="flt0" style="display: none;">
|
<div id="flt0" style="display: none;">
|
||||||
|
|
|
@ -4,36 +4,6 @@
|
||||||
<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.rolling_type %}
|
|
||||||
<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 %}
|
|
||||||
{% if form.limit %}
|
|
||||||
<div>{{ form.limit.label }}: {{ form.limit(class_="form-control select2") }}</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if form.series %}
|
|
||||||
<div>{{ form.series.label }}: {{ form.series(class_="form-control select2") }}</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if form.entity %}
|
|
||||||
<div>{{ form.entity.label }}: {{ form.entity(class_="form-control select2") }}</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if form.size %}
|
|
||||||
<div>{{ form.size.label }}: {{ form.size(class_="form-control select2") }}</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if form.x %}
|
|
||||||
<div>{{ form.x.label }}: {{ form.x(class_="form-control select2") }}</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if form.y %}
|
|
||||||
<div>{{ form.y.label }}: {{ form.y(class_="form-control select2") }}</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block tail %}
|
{% block tail %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
{% if viz.stockchart %}
|
{% if viz.stockchart %}
|
||||||
|
|
|
@ -33,10 +33,6 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_fields %}
|
|
||||||
<div>{{ form.row_limit.label }}: {{ form.row_limit(class_="form-control select2") }}</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block tail %}
|
{% block tail %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script src="{{ url_for('static', filename='jquery.dataTables.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='jquery.dataTables.min.js') }}"></script>
|
||||||
|
|
118
panoramix/viz.py
118
panoramix/viz.py
|
@ -2,12 +2,13 @@ from datetime import datetime
|
||||||
from flask import flash, request
|
from flask import flash, request
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from panoramix import utils
|
|
||||||
from panoramix.highchart import Highchart, HighchartBubble
|
|
||||||
from wtforms import Form, SelectMultipleField, SelectField, TextField
|
|
||||||
import config
|
import config
|
||||||
import logging
|
import logging
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from panoramix import utils
|
||||||
|
from panoramix.highchart import Highchart, HighchartBubble
|
||||||
|
from panoramix.forms import form_factory
|
||||||
|
|
||||||
CHART_ARGS = {
|
CHART_ARGS = {
|
||||||
'height': 700,
|
'height': 700,
|
||||||
|
@ -16,58 +17,14 @@ CHART_ARGS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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 {}
|
|
||||||
|
|
||||||
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")
|
|
||||||
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"
|
||||||
hidden_fields = []
|
hidden_fields = []
|
||||||
|
form_fields = [
|
||||||
|
'viz_type', 'metrics', 'groupby', 'granularity',
|
||||||
|
('since', 'until')]
|
||||||
|
|
||||||
def __init__(self, datasource, form_data, view):
|
def __init__(self, datasource, form_data, view):
|
||||||
self.datasource = datasource
|
self.datasource = datasource
|
||||||
self.form_class = self.form_class()
|
self.form_class = self.form_class()
|
||||||
|
@ -90,7 +47,7 @@ class BaseViz(object):
|
||||||
|
|
||||||
|
|
||||||
def form_class(self):
|
def form_class(self):
|
||||||
return form_factory(self.datasource, request.args)
|
return form_factory(self.datasource, self, request.args)
|
||||||
|
|
||||||
def query_filters(self):
|
def query_filters(self):
|
||||||
args = self.form_data
|
args = self.form_data
|
||||||
|
@ -159,6 +116,7 @@ class BaseViz(object):
|
||||||
class TableViz(BaseViz):
|
class TableViz(BaseViz):
|
||||||
verbose_name = "Table View"
|
verbose_name = "Table View"
|
||||||
template = 'panoramix/viz_table.html'
|
template = 'panoramix/viz_table.html'
|
||||||
|
form_fields = BaseViz.form_fields + ['row_limit']
|
||||||
|
|
||||||
def query_obj(self):
|
def query_obj(self):
|
||||||
d = super(TableViz, self).query_obj()
|
d = super(TableViz, self).query_obj()
|
||||||
|
@ -178,17 +136,11 @@ class TableViz(BaseViz):
|
||||||
if self.form_data.get("granularity") == "all" and 'timestamp' in df:
|
if self.form_data.get("granularity") == "all" and 'timestamp' in df:
|
||||||
del df['timestamp']
|
del df['timestamp']
|
||||||
for m in self.metrics:
|
for m in self.metrics:
|
||||||
import numpy as np
|
|
||||||
df[m + '__perc'] = np.rint((df[m] / np.max(df[m])) * 100)
|
df[m + '__perc'] = np.rint((df[m] / np.max(df[m])) * 100)
|
||||||
return super(TableViz, self).render(df=df)
|
return super(TableViz, self).render(df=df)
|
||||||
|
|
||||||
def form_class(self):
|
def form_class(self):
|
||||||
limits = [10, 50, 100, 500, 1000, 5000, 10000]
|
return form_factory(self.datasource, self, request.args)
|
||||||
return form_factory(self.datasource, request.args,
|
|
||||||
extra_fields_dict={
|
|
||||||
'row_limit':
|
|
||||||
SelectField('Row limit', choices=[(s, s) for s in limits])
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class HighchartsViz(BaseViz):
|
class HighchartsViz(BaseViz):
|
||||||
|
@ -204,28 +156,12 @@ class BubbleViz(HighchartsViz):
|
||||||
verbose_name = "Bubble Chart"
|
verbose_name = "Bubble Chart"
|
||||||
chart_type = 'bubble'
|
chart_type = 'bubble'
|
||||||
hidden_fields = ['granularity', 'metrics', 'groupby']
|
hidden_fields = ['granularity', 'metrics', 'groupby']
|
||||||
|
form_fields = [
|
||||||
|
'viz_type', 'since', 'until',
|
||||||
|
'series', 'entity', 'x', 'y', 'size', 'limit']
|
||||||
|
|
||||||
def form_class(self):
|
def form_class(self):
|
||||||
datasource = self.datasource
|
return form_factory(self.datasource, self, request.args)
|
||||||
limits = [0, 5, 10, 25, 50, 100, 500]
|
|
||||||
return form_factory(self.datasource, request.args,
|
|
||||||
extra_fields_dict={
|
|
||||||
#'compare': TextField('Period Compare',),
|
|
||||||
'series': SelectField(
|
|
||||||
'Series', choices=[
|
|
||||||
(s, s) for s in datasource.groupby_column_names]),
|
|
||||||
'entity': SelectField(
|
|
||||||
'Entity', choices=[
|
|
||||||
(s, s) for s in datasource.groupby_column_names]),
|
|
||||||
'x': SelectField(
|
|
||||||
'X Axis', choices=datasource.metrics_combo),
|
|
||||||
'y': SelectField(
|
|
||||||
'Y Axis', choices=datasource.metrics_combo),
|
|
||||||
'size': SelectField(
|
|
||||||
'Bubble Size', choices=datasource.metrics_combo),
|
|
||||||
'limit': SelectField(
|
|
||||||
'Limit', choices=[(s, s) for s in limits]),
|
|
||||||
})
|
|
||||||
|
|
||||||
def query_obj(self):
|
def query_obj(self):
|
||||||
d = super(BubbleViz, self).query_obj()
|
d = super(BubbleViz, self).query_obj()
|
||||||
|
@ -264,12 +200,18 @@ class BubbleViz(HighchartsViz):
|
||||||
return super(BubbleViz, self).render(error_msg=self.error_msg)
|
return super(BubbleViz, self).render(error_msg=self.error_msg)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TimeSeriesViz(HighchartsViz):
|
class TimeSeriesViz(HighchartsViz):
|
||||||
verbose_name = "Time Series - Line Chart"
|
verbose_name = "Time Series - Line Chart"
|
||||||
chart_type = "spline"
|
chart_type = "spline"
|
||||||
stockchart = True
|
stockchart = True
|
||||||
sort_legend_y = True
|
sort_legend_y = True
|
||||||
|
form_fields = [
|
||||||
|
'viz_type',
|
||||||
|
'granularity', ('since', 'until'),
|
||||||
|
'metrics',
|
||||||
|
'groupby', 'limit',
|
||||||
|
('rolling_type', 'rolling_periods'),
|
||||||
|
]
|
||||||
|
|
||||||
def render(self):
|
def render(self):
|
||||||
if request.args.get("granularity") == "all":
|
if request.args.get("granularity") == "all":
|
||||||
|
@ -285,7 +227,6 @@ class TimeSeriesViz(HighchartsViz):
|
||||||
values=metrics,)
|
values=metrics,)
|
||||||
|
|
||||||
rolling_periods = request.args.get("rolling_periods")
|
rolling_periods = request.args.get("rolling_periods")
|
||||||
limit = request.args.get("limit")
|
|
||||||
rolling_type = request.args.get("rolling_type")
|
rolling_type = request.args.get("rolling_type")
|
||||||
if rolling_periods and rolling_type:
|
if rolling_periods and rolling_type:
|
||||||
if rolling_type == 'mean':
|
if rolling_type == 'mean':
|
||||||
|
@ -306,17 +247,7 @@ class TimeSeriesViz(HighchartsViz):
|
||||||
return super(TimeSeriesViz, self).render(chart_js=chart.javascript_cmd)
|
return super(TimeSeriesViz, self).render(chart_js=chart.javascript_cmd)
|
||||||
|
|
||||||
def form_class(self):
|
def form_class(self):
|
||||||
limits = [0, 5, 10, 25, 50, 100, 500]
|
return form_factory(self.datasource, self, request.args)
|
||||||
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',),
|
|
||||||
'limit': SelectField(
|
|
||||||
'Series limit', choices=[(s, s) for s in limits])
|
|
||||||
})
|
|
||||||
|
|
||||||
def bake_query(self):
|
def bake_query(self):
|
||||||
"""
|
"""
|
||||||
|
@ -324,14 +255,17 @@ class TimeSeriesViz(HighchartsViz):
|
||||||
"""
|
"""
|
||||||
return self.datasource.query(**self.query_obj())
|
return self.datasource.query(**self.query_obj())
|
||||||
|
|
||||||
|
|
||||||
class TimeSeriesCompareViz(TimeSeriesViz):
|
class TimeSeriesCompareViz(TimeSeriesViz):
|
||||||
verbose_name = "Time Series - Percent Change"
|
verbose_name = "Time Series - Percent Change"
|
||||||
compare = 'percent'
|
compare = 'percent'
|
||||||
|
|
||||||
|
|
||||||
class TimeSeriesCompareValueViz(TimeSeriesViz):
|
class TimeSeriesCompareValueViz(TimeSeriesViz):
|
||||||
verbose_name = "Time Series - Value Change"
|
verbose_name = "Time Series - Value Change"
|
||||||
compare = 'value'
|
compare = 'value'
|
||||||
|
|
||||||
|
|
||||||
class TimeSeriesAreaViz(TimeSeriesViz):
|
class TimeSeriesAreaViz(TimeSeriesViz):
|
||||||
verbose_name = "Time Series - Stacked Area Chart"
|
verbose_name = "Time Series - Stacked Area Chart"
|
||||||
stacked=True
|
stacked=True
|
||||||
|
|
Loading…
Reference in New Issue