mirror of https://github.com/apache/superset.git
Merge pull request #96 from mistercrunch/filter_box
Adding a filter box widget
This commit is contained in:
commit
3f292e6308
|
@ -16,7 +16,7 @@ It empowers its user to perform **analytics at the speed of thought**.
|
|||
|
||||
Panoramix provides:
|
||||
* A quick way to intuitively visualize datasets
|
||||
* Create and share simple dashboards
|
||||
* Create and share interactive dashboards
|
||||
* A rich set of visualizations to analyze your data, as well as a flexible
|
||||
way to extend the capabilities
|
||||
* An extensible, high granularity security model allowing intricate rules
|
||||
|
|
1
TODO.md
1
TODO.md
|
@ -2,6 +2,7 @@
|
|||
List of TODO items for Panoramix
|
||||
|
||||
## Improvments
|
||||
* Read dashboard filter from URL
|
||||
* Table description is markdown
|
||||
* Animated scatter plots
|
||||
* Filter widget
|
||||
|
|
|
@ -251,6 +251,10 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
|
|||
def __repr__(self):
|
||||
return self.table_name
|
||||
|
||||
@property
|
||||
def description_markeddown(self):
|
||||
return utils.markdown(self.description)
|
||||
|
||||
@property
|
||||
def perm(self):
|
||||
return (
|
||||
|
|
|
@ -2,6 +2,15 @@ html>body{
|
|||
margin: 0px; !important
|
||||
}
|
||||
|
||||
.padded{
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.intable-longtext{
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.slice_container {
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
@ -68,7 +68,6 @@ var px = (function() {
|
|||
$('#timer').removeClass('btn-danger btn-success');
|
||||
$('#timer').addClass('btn-warning');
|
||||
viz.render();
|
||||
console.log(slice);
|
||||
$('#json').click(function(){window.location=slice.jsonEndpoint()});
|
||||
$('#standalone').click(function(){window.location=slice.data.standalone_endpoint});
|
||||
$('#csv').click(function(){window.location=slice.data.csv_endpoint});
|
||||
|
@ -99,9 +98,14 @@ var px = (function() {
|
|||
slices: [],
|
||||
filters: {},
|
||||
id: id,
|
||||
addFilter: function(slice_id, field, values) {
|
||||
this.filters[slice_id] = [field, values];
|
||||
addFilter: function(slice_id, filters) {
|
||||
this.filters[slice_id] = filters;
|
||||
this.refreshExcept(slice_id);
|
||||
console.log(this.filters);
|
||||
},
|
||||
readFilters: function() {
|
||||
// Returns a list of human readable active filters
|
||||
return JSON.stringify(this.filters, null, 4);
|
||||
},
|
||||
refreshExcept: function(slice_id) {
|
||||
this.slices.forEach(function(slice){
|
||||
|
@ -197,6 +201,7 @@ var px = (function() {
|
|||
|
||||
function druidify(){
|
||||
prepForm();
|
||||
$('div.alert').remove();
|
||||
slice.render();
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.select2-highlighted>.filter_box {
|
||||
background-color: transparent;
|
||||
border: 1px dashed black;
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
px.registerViz('filter_box', function(slice) {
|
||||
var slice = slice;
|
||||
var filtersObj = {};
|
||||
d3token = d3.select(slice.selector);
|
||||
|
||||
var fltChanged = function() {
|
||||
filters = []
|
||||
for(flt in filtersObj) {
|
||||
obj = filtersObj[flt];
|
||||
val = obj.val()
|
||||
if(val !== ''){
|
||||
filters.push([flt, val.split(',')]);
|
||||
}
|
||||
}
|
||||
slice.addFilter(filters);
|
||||
}
|
||||
|
||||
var refresh = function() {
|
||||
d3token.selectAll("*").remove();
|
||||
var container = d3token
|
||||
.append('div')
|
||||
.classed('padded', true);
|
||||
$.getJSON(slice.jsonEndpoint(), function(payload) {
|
||||
var maxes = {};
|
||||
for (filter in payload.data){
|
||||
var data = payload.data[filter];
|
||||
maxes[filter] = d3.max(data, function(d){return d.metric});
|
||||
var id = 'fltbox__' + filter;
|
||||
|
||||
var div = container.append('div');
|
||||
div.append("label").text(filter);
|
||||
var sel = div
|
||||
.append('div')
|
||||
.attr('name', filter)
|
||||
.classed('form-control', true)
|
||||
.attr('multiple', '')
|
||||
.attr('id', id);
|
||||
|
||||
filtersObj[filter] = $('#' + id).select2({
|
||||
placeholder: "Select [" + filter + ']',
|
||||
containment: 'parent',
|
||||
dropdownAutoWidth : true,
|
||||
data:data,
|
||||
multiple: true,
|
||||
formatResult: function(result, container, query, escapeMarkup) {
|
||||
var perc = Math.round((result.metric / maxes[result.filter]) * 100);
|
||||
var style = 'padding: 2px 5px;';
|
||||
style += "background-image: ";
|
||||
style += "linear-gradient(to right, lightgrey, lightgrey " + perc + "%, rgba(0,0,0,0) " + perc + "%";
|
||||
|
||||
$(container).attr('style', 'padding: 0px; background: white;');
|
||||
$(container).addClass('filter_box');
|
||||
return '<div style="' + style + '"><span>' + result.text + '</span></div>';
|
||||
},
|
||||
})
|
||||
.on('change', fltChanged);
|
||||
/*
|
||||
.style('background-image', function(d){
|
||||
if (d.isMetric){
|
||||
var perc = Math.round((d.val / maxes[d.col]) * 100);
|
||||
return "linear-gradient(to right, lightgrey, lightgrey " + perc + "%, rgba(0,0,0,0) " + perc + "%";
|
||||
}
|
||||
})
|
||||
*/
|
||||
}
|
||||
slice.done();
|
||||
})
|
||||
.fail(function(xhr) {
|
||||
slice.error(xhr.responseText);
|
||||
});
|
||||
};
|
||||
return {
|
||||
render: refresh,
|
||||
resize: refresh,
|
||||
};
|
||||
});
|
|
@ -55,7 +55,7 @@ px.registerViz('table', function(slice) {
|
|||
} else {
|
||||
table.selectAll('.filtered').classed('filtered', false);
|
||||
d3.select(this).classed('filtered', true);
|
||||
slice.addFilter(d.col, [d.val]);
|
||||
slice.addFilter([[d.col, [d.val]]]);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -54,6 +54,9 @@ body {
|
|||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="btn-group pull-right" role="group" >
|
||||
<button type="button" id="filters" class="btn btn-default" data-toggle="tooltip" title="View the list of active filters">
|
||||
<i class="fa fa-filter"></i>
|
||||
</button>
|
||||
<button type="button" id="css" class="btn btn-default" data-toggle="modal" data-target="#css_modal">
|
||||
<i class="fa fa-code"></i>
|
||||
</button>
|
||||
|
@ -88,6 +91,7 @@ body {
|
|||
<nobr class="icons">
|
||||
<a><i class="fa fa-arrows drag"></i></a>
|
||||
<a class="refresh"><i class="fa fa-refresh"></i></a>
|
||||
<a class="bug" data-slice_id="{{ slice.id }}" data-toggle="tooltip" title="console.log(this.slice);"><i class="fa fa-bug"></i></a>
|
||||
</nobr>
|
||||
</td>
|
||||
<td>
|
||||
|
@ -124,6 +128,12 @@ body {
|
|||
$(document).ready(function() {
|
||||
px.initDashboardView();
|
||||
var dashboard = px.Dashboard({{ dashboard.id }});
|
||||
$('#filters').click( function(){
|
||||
alert(dashboard.readFilters());
|
||||
});
|
||||
$('a.bug').click( function(){
|
||||
console.log(dashboard.getSlice($(this).data('slice_id')));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -182,10 +182,10 @@
|
|||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Query</h4>
|
||||
<h4 class="modal-title">Datasource Description</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre id="query_container">{{ datasource.description }}</pre>
|
||||
{{ datasource.description_markeddown | safe }}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<h1><i class='fa fa-star'></i> Featured Datasets </h1>
|
||||
</div>
|
||||
<hr/>
|
||||
<table class="table table-hover dataTable" id="dataset-table" style="display:None">
|
||||
<table class="table table-hover dataTable table-bordered" id="dataset-table" style="display:None">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Table</th>
|
||||
|
@ -16,9 +16,11 @@
|
|||
<tbody>
|
||||
{% for dataset in featured_datasets %}
|
||||
<tr>
|
||||
<td>
|
||||
<td>
|
||||
<div class="intable-longtext">
|
||||
<h4>{{ dataset.table_name }}</h4>
|
||||
<p>{{ dataset.description }}</p>
|
||||
<p>{{ utils.markdown(dataset.description) | safe }}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td class="small_table">{{ dataset.database }}</td>
|
||||
<td class="small_table">{{ dataset.owner }}</td>
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
from datetime import datetime
|
||||
from dateutil.parser import parse
|
||||
import hashlib
|
||||
from sqlalchemy.types import TypeDecorator, TEXT
|
||||
import json
|
||||
from flask import g, request, Markup
|
||||
import parsedatetime
|
||||
import functools
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
from dateutil.parser import parse
|
||||
from sqlalchemy.types import TypeDecorator, TEXT
|
||||
from flask import g, request, Markup
|
||||
from markdown import markdown as md
|
||||
import parsedatetime
|
||||
|
||||
from panoramix import db
|
||||
|
||||
|
||||
|
@ -222,3 +225,7 @@ def json_iso_dttm_ser(obj):
|
|||
if isinstance(obj, datetime):
|
||||
obj = obj.isoformat()
|
||||
return obj
|
||||
|
||||
|
||||
def markdown(s):
|
||||
return md(s, ['markdown.extensions.tables'])
|
||||
|
|
|
@ -4,7 +4,7 @@ import logging
|
|||
import re
|
||||
import traceback
|
||||
|
||||
from flask import request, redirect, flash, Response, render_template
|
||||
from flask import request, redirect, flash, Response, render_template, Markup
|
||||
from flask.ext.appbuilder import ModelView, CompactCRUDMixin, BaseView, expose
|
||||
from flask.ext.appbuilder.actions import action
|
||||
from flask.ext.appbuilder.models.sqla.interface import SQLAInterface
|
||||
|
@ -153,7 +153,8 @@ class TableView(PanoramixModelView, DeleteMixin):
|
|||
related_views = [TableColumnInlineView, SqlMetricInlineView]
|
||||
base_order = ('changed_on','desc')
|
||||
description_columns = {
|
||||
'offset': "Timezone offset (in hours) for this datasource"
|
||||
'offset': "Timezone offset (in hours) for this datasource",
|
||||
'description': Markup("Supports <a href='https://daringfireball.net/projects/markdown/'>markdown</a>"),
|
||||
}
|
||||
|
||||
def post_add(self, table):
|
||||
|
@ -281,7 +282,8 @@ class DatasourceModelView(PanoramixModelView, DeleteMixin):
|
|||
page_size = 100
|
||||
base_order = ('datasource_name', 'asc')
|
||||
description_columns = {
|
||||
'offset': "Timezone offset (in hours) for this datasource"
|
||||
'offset': "Timezone offset (in hours) for this datasource",
|
||||
'description': Markup("Supports <a href='https://daringfireball.net/projects/markdown/'>markdown</a>"),
|
||||
}
|
||||
|
||||
def post_add(self, datasource):
|
||||
|
@ -564,7 +566,8 @@ class Panoramix(BaseView):
|
|||
featured_datasets = datasets_sqla + datasets_druid
|
||||
return self.render_template(
|
||||
'panoramix/featured_datasets.html',
|
||||
featured_datasets=featured_datasets)
|
||||
featured_datasets=featured_datasets,
|
||||
utils=utils)
|
||||
|
||||
appbuilder.add_view_no_menu(Panoramix)
|
||||
appbuilder.add_link(
|
||||
|
|
|
@ -161,8 +161,11 @@ class BaseViz(object):
|
|||
extra_filters = form_data.get('extra_filters', [])
|
||||
if extra_filters:
|
||||
extra_filters = json.loads(extra_filters)
|
||||
for slice_id, (col, vals) in extra_filters.items():
|
||||
filters += [(col, 'in', ",".join(vals))]
|
||||
for slice_id, slice_filters in extra_filters.items():
|
||||
if slice_filters:
|
||||
for col, vals in slice_filters:
|
||||
if col and vals:
|
||||
filters += [(col, 'in', ",".join(vals))]
|
||||
|
||||
return filters
|
||||
|
||||
|
@ -1105,6 +1108,61 @@ class WorldMapViz(BaseViz):
|
|||
return dumps(d)
|
||||
|
||||
|
||||
class FilterBoxViz(BaseViz):
|
||||
viz_type = "filter_box"
|
||||
verbose_name = "Filters"
|
||||
is_timeseries = False
|
||||
js_files = [
|
||||
'lib/d3.min.js',
|
||||
'widgets/viz_filter_box.js']
|
||||
css_files = [
|
||||
'widgets/viz_filter_box.css']
|
||||
fieldsets = (
|
||||
{
|
||||
'label': None,
|
||||
'fields': (
|
||||
'granularity',
|
||||
('since', 'until'),
|
||||
'groupby',
|
||||
'metric',
|
||||
)
|
||||
},)
|
||||
form_overrides = {
|
||||
'groupby': {
|
||||
'label': 'Filter fields',
|
||||
'description': "The fields you want to filter on",
|
||||
},
|
||||
}
|
||||
def query_obj(self):
|
||||
qry = super(FilterBoxViz, self).query_obj()
|
||||
groupby = self.form_data['groupby']
|
||||
if len(groupby) < 1:
|
||||
raise Exception("Pick at least one filter field")
|
||||
qry['metrics'] = [
|
||||
self.form_data['metric']]
|
||||
return qry
|
||||
|
||||
def get_df(self):
|
||||
qry = self.query_obj()
|
||||
|
||||
filters = [g for g in qry['groupby']]
|
||||
d = {}
|
||||
for flt in filters:
|
||||
qry['groupby'] = [flt]
|
||||
df = super(FilterBoxViz, self).get_df(qry)
|
||||
d[flt] = [
|
||||
{'id': row[0],
|
||||
'text': row[0],
|
||||
'filter': flt,
|
||||
'metric': row[1]}
|
||||
for row in df.itertuples(index=False)]
|
||||
return d
|
||||
|
||||
def get_json_data(self):
|
||||
d = self.get_df()
|
||||
return dumps(d)
|
||||
|
||||
|
||||
viz_types_list = [
|
||||
TableViz,
|
||||
PivotTableViz,
|
||||
|
@ -1122,6 +1180,7 @@ viz_types_list = [
|
|||
DirectedForceViz,
|
||||
SankeyViz,
|
||||
WorldMapViz,
|
||||
FilterBoxViz,
|
||||
]
|
||||
# This dict is used to
|
||||
viz_types = OrderedDict([(v.viz_type, v) for v in viz_types_list])
|
||||
|
|
Loading…
Reference in New Issue