diff --git a/panoramix/bin/panoramix b/panoramix/bin/panoramix
index 47b41d562..2f9066aad 100755
--- a/panoramix/bin/panoramix
+++ b/panoramix/bin/panoramix
@@ -116,12 +116,13 @@ def load_examples(sample):
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=one+day&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.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)
@@ -141,7 +142,7 @@ def load_examples(sample):
"flt_col_1": "gender",
"flt_eq_1": "",
"flt_op_1": "in",
- "granularity": "all",
+ "granularity": "ds",
"groupby": [],
"metric": 'sum__num',
"metrics": ["sum__num"],
@@ -194,7 +195,7 @@ def load_examples(sample):
datasource_type='table',
table=tbl,
params=get_slice_json(
- slice_name, viz_type="big_number", granularity="1 day",
+ slice_name, viz_type="big_number", granularity="ds",
compare_lag="5", compare_suffix="over 5Y"))
session.add(slc)
slices.append(slc)
@@ -237,7 +238,7 @@ def load_examples(sample):
table=tbl,
params=get_slice_json(
slice_name, viz_type="line", groupby=['name'],
- granularity='1 day', rich_tooltip='y', show_legend='y'))
+ granularity='ds', rich_tooltip='y', show_legend='y'))
session.add(slc)
slices.append(slc)
diff --git a/panoramix/forms.py b/panoramix/forms.py
index 072103777..5fdeace87 100644
--- a/panoramix/forms.py
+++ b/panoramix/forms.py
@@ -67,6 +67,13 @@ class FormFactory(object):
"The time granularity for the visualization. Note that you "
"can type and use simple natural language as in '10 seconds', "
"'1 day' or '56 weeks'")),
+ 'granularity_sqla': SelectField(
+ 'Time Column', default=datasource.main_dttm_col,
+ choices=self.choicify(datasource.dttm_cols),
+ description=(
+ "The time granularity for the visualization. Note that you "
+ "can define arbitrary expression that return a DATETIME "
+ "column in the table editor")),
'since': TextField(
'Since', default="7 days ago", description=(
"Timestamp from filter. This supports free form typing and "
@@ -217,7 +224,7 @@ class FormFactory(object):
standalone = HiddenField()
async = HiddenField()
json = HiddenField()
- previous_viz_type = HiddenField()
+ previous_viz_type = HiddenField(default=viz.viz_type)
filter_cols = datasource.filterable_column_names or ['']
for i in range(10):
@@ -243,4 +250,11 @@ class FormFactory(object):
if datasource.__class__.__name__ == 'SqlaTable':
QueryForm.field_order += ['where']
setattr(QueryForm, 'where', px_form_fields['where'])
+
+ if 'granularity' in viz.form_fields:
+ setattr(
+ QueryForm,
+ 'granularity', px_form_fields['granularity_sqla'])
+ field_css_classes['granularity'] = ['form-control', 'select2']
+
return QueryForm
diff --git a/panoramix/models.py b/panoramix/models.py
index e7f4ba442..f6018e807 100644
--- a/panoramix/models.py
+++ b/panoramix/models.py
@@ -164,6 +164,10 @@ class Queryable(object):
def column_names(self):
return sorted([c.column_name for c in self.columns])
+ @property
+ def main_dttm_col(self):
+ return "timestamp"
+
@property
def groupby_column_names(self):
return sorted([c.column_name for c in self.columns if c.groupby])
@@ -172,6 +176,10 @@ class Queryable(object):
def filterable_column_names(self):
return sorted([c.column_name for c in self.columns if c.filterable])
+ @property
+ def dttm_cols(self):
+ return []
+
class Database(Model, AuditMixinNullable):
__tablename__ = 'dbs'
@@ -216,6 +224,13 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
"[{self.database}].[{self.table_name}]"
"(id:{self.id})").format(self=self)
+ @property
+ def dttm_cols(self):
+ l = [c.column_name for c in self.columns if c.is_dttm]
+ if self.main_dttm_col not in l:
+ l.append(self.main_dttm_col)
+ return l
+
@property
def name(self):
return self.table_name
@@ -339,13 +354,20 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
inner_from_dttm=None, inner_to_dttm=None,
extras=None):
+ # For backward compatibility
+ if granularity not in self.dttm_cols:
+ granularity = self.main_dttm_col
+
cols = {col.column_name: col for col in self.columns}
qry_start_dttm = datetime.now()
if not self.main_dttm_col:
raise Exception(
"Datetime column not provided as part table configuration")
- timestamp = literal_column(
- self.main_dttm_col).label('timestamp')
+ dttm_expr = cols[granularity].expression
+ if dttm_expr:
+ timestamp = ColumnClause(dttm_expr, is_literal=True).label('timestamp')
+ else:
+ timestamp = literal_column(granularity).label('timestamp')
metrics_exprs = [
literal_column(m.expression).label(m.metric_name)
for m in self.metrics if m.metric_name in metrics]
@@ -381,7 +403,7 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
inner_groupby_exprs.append(inner)
inner_select_exprs.append(inner)
- if granularity != "all":
+ if is_timeseries:
select_exprs += [timestamp]
groupby_exprs += [timestamp]
@@ -562,7 +584,7 @@ class TableColumn(Model, AuditMixinNullable):
table = relationship(
'SqlaTable', backref='columns', foreign_keys=[table_id])
column_name = Column(String(256))
- is_dttm = Column(Boolean, default=True)
+ is_dttm = Column(Boolean, default=False)
is_active = Column(Boolean, default=True)
type = Column(String(32), default='')
groupby = Column(Boolean, default=False)
@@ -740,6 +762,9 @@ class Datasource(Model, AuditMixin, Queryable):
m.metric_name: m.json_obj
for m in self.metrics if m.metric_name in metrics
}
+ if granularity != "all":
+ granularity = utils.parse_human_timedelta(
+ granularity).total_seconds() * 1000
if not isinstance(granularity, basestring):
granularity = {"type": "duration", "duration": granularity}
diff --git a/panoramix/templates/panoramix/datasource.html b/panoramix/templates/panoramix/datasource.html
index 517c93e5f..e6151f17e 100644
--- a/panoramix/templates/panoramix/datasource.html
+++ b/panoramix/templates/panoramix/datasource.html
@@ -88,7 +88,7 @@
- {{ form.previous_viz_type() }}
+
diff --git a/panoramix/views.py b/panoramix/views.py
index 522559c65..d6580ec72 100644
--- a/panoramix/views.py
+++ b/panoramix/views.py
@@ -42,12 +42,18 @@ class TableColumnInlineView(CompactCRUDMixin, PanoramixModelView):
can_delete = False
edit_columns = [
'column_name', 'description', 'groupby', 'filterable', 'table',
- 'count_distinct', 'sum', 'min', 'max', 'expression']
+ 'count_distinct', 'sum', 'min', 'max', 'expression', 'is_dttm']
add_columns = edit_columns
list_columns = [
'column_name', 'type', 'groupby', 'filterable', 'count_distinct',
- 'sum', 'min', 'max']
+ 'sum', 'min', 'max', 'is_dttm']
page_size = 100
+ description_columns = {
+ 'is_dttm': (
+ "Whether to make this column available as a "
+ "[Time Granularity] option, column has to be DATETIME or "
+ "DATETIME-like"),
+ }
appbuilder.add_view_no_menu(TableColumnInlineView)
diff --git a/panoramix/viz.py b/panoramix/viz.py
index 3081b9061..d3ed4ae33 100644
--- a/panoramix/viz.py
+++ b/panoramix/viz.py
@@ -22,9 +22,13 @@ class BaseViz(object):
viz_type = None
verbose_name = "Base Viz"
template = None
+ is_timeseries = False
form_fields = [
- 'viz_type', 'metrics', 'groupby', 'granularity',
- ('since', 'until')]
+ 'viz_type',
+ 'granularity',
+ ('since', 'until'),
+ 'metrics', 'groupby',
+ ]
js_files = []
css_files = []
@@ -37,22 +41,21 @@ class BaseViz(object):
ff = FormFactory(self)
form_class = ff.get_form()
defaults = form_class().data.copy()
+ previous_viz_type = form_data.get('previous_viz_type')
if isinstance(form_data, ImmutableMultiDict):
form = form_class(form_data)
else:
form = form_class(**form_data)
-
data = form.data.copy()
+
if not form.validate():
for k, v in form.errors.items():
if not data.get('json') and not data.get('async'):
flash("{}: {}".format(k, " ".join(v)), 'danger')
- previous_viz_type = form_data.get('previous_viz_type')
- if previous_viz_type in viz_types and previous_viz_type != self.viz_type:
- data = {
- k: form.data[k]
- for k in form_data.keys()
- if k in viz_types[previous_viz_type].flat_form_fields() and k in form.data}
+ data = {
+ k: form.data[k]
+ for k in form_data.keys()
+ if k in form.data}
defaults.update(data)
self.form_data = defaults
@@ -134,10 +137,7 @@ class BaseViz(object):
form_data = self.form_data
groupby = form_data.get("groupby") or []
metrics = form_data.get("metrics") or ['count']
- granularity = form_data.get("granularity", "1 day")
- if granularity != "all":
- granularity = utils.parse_human_timedelta(
- granularity).total_seconds() * 1000
+ granularity = form_data.get("granularity")
limit = int(form_data.get("limit", 0))
row_limit = int(
form_data.get("row_limit", config.get("ROW_LIMIT")))
@@ -160,7 +160,7 @@ class BaseViz(object):
'granularity': granularity,
'from_dttm': from_dttm,
'to_dttm': to_dttm,
- 'is_timeseries': True,
+ 'is_timeseries': self.is_timeseries,
'groupby': groupby,
'metrics': metrics,
'row_limit': row_limit,
@@ -194,6 +194,7 @@ class TableViz(BaseViz):
template = 'panoramix/viz_table.html'
form_fields = BaseViz.form_fields + ['row_limit']
css_files = ['lib/dataTables/dataTables.bootstrap.css']
+ is_timeseries = False
js_files = [
'lib/dataTables/jquery.dataTables.min.js',
'lib/dataTables/dataTables.bootstrap.js']
@@ -220,6 +221,7 @@ class MarkupViz(BaseViz):
verbose_name = "Markup Widget"
template = 'panoramix/viz_markup.html'
form_fields = ['viz_type', 'markup_type', 'code']
+ is_timeseries = False
def rendered(self):
markup_type = self.form_data.get("markup_type")
@@ -238,6 +240,7 @@ class WordCloudViz(BaseViz):
viz_type = "word_cloud"
verbose_name = "Word Cloud"
template = 'panoramix/viz_word_cloud.html'
+ is_timeseries = False
form_fields = [
'viz_type',
('since', 'until'),
@@ -253,7 +256,6 @@ class WordCloudViz(BaseViz):
def query_obj(self):
d = super(WordCloudViz, self).query_obj()
- d['granularity'] = 'all'
metric = self.form_data.get('metric')
if not metric:
raise Exception("Pick a metric!")
@@ -271,6 +273,7 @@ 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',
'lib/nvd3/nv.d3.min.js',
@@ -285,6 +288,7 @@ class NVD3Viz(BaseViz):
class BubbleViz(NVD3Viz):
viz_type = "bubble"
verbose_name = "Bubble Chart"
+ is_timeseries = False
form_fields = [
'viz_type',
('since', 'until'),
@@ -298,7 +302,6 @@ class BubbleViz(NVD3Viz):
def query_obj(self):
form_data = self.form_data
d = super(BubbleViz, self).query_obj()
- d['granularity'] = 'all'
d['groupby'] = list({
form_data.get('series'),
form_data.get('entity')
@@ -349,6 +352,7 @@ 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',
'widgets/viz_bignumber.js',
@@ -400,6 +404,7 @@ class NVD3TimeSeriesViz(NVD3Viz):
viz_type = "line"
verbose_name = "Time Series - Line Chart"
sort_series = False
+ is_timeseries = True
form_fields = [
'viz_type',
'granularity', ('since', 'until'),
@@ -553,16 +558,17 @@ class NVD3TimeSeriesStackedViz(NVD3TimeSeriesViz):
class DistributionPieViz(NVD3Viz):
viz_type = "pie"
verbose_name = "Distribution - NVD3 - Pie Chart"
+ is_timeseries = False
form_fields = [
- 'viz_type', 'metrics', 'groupby',
+ 'viz_type',
('since', 'until'),
+ 'metrics', 'groupby',
'limit',
('donut', 'show_legend'),
]
def query_obj(self):
d = super(DistributionPieViz, self).query_obj()
- d['granularity'] = "all"
d['is_timeseries'] = False
return d
@@ -589,6 +595,7 @@ class DistributionPieViz(NVD3Viz):
class DistributionBarViz(DistributionPieViz):
viz_type = "dist_bar"
verbose_name = "Distribution - Bar Chart"
+ is_timeseries = False
form_fields = [
'viz_type', 'metrics', 'groupby',
('since', 'until'),