Skip to content

Commit

Permalink
Merge pull request #96 from mistercrunch/filter_box
Browse files Browse the repository at this point in the history
Adding a filter box widget
  • Loading branch information
mistercrunch committed Dec 29, 2015
2 parents 5ad4f38 + 37cbf61 commit 3f292e6
Show file tree
Hide file tree
Showing 14 changed files with 199 additions and 19 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions panoramix/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
9 changes: 9 additions & 0 deletions panoramix/static/panoramix.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ html>body{
margin: 0px; !important
}

.padded{
padding: 10px;
}

.intable-longtext{
max-height: 200px;
overflow: auto;
}

.slice_container {
height: 100%;
}
Expand Down
11 changes: 8 additions & 3 deletions panoramix/static/panoramix.js
Original file line number Diff line number Diff line change
Expand Up @@ -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});
Expand Down Expand Up @@ -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){
Expand Down Expand Up @@ -197,6 +201,7 @@ var px = (function() {

function druidify(){
prepForm();
$('div.alert').remove();
slice.render();
}

Expand Down
4 changes: 4 additions & 0 deletions panoramix/static/widgets/viz_filter_box.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.select2-highlighted>.filter_box {
background-color: transparent;
border: 1px dashed black;
}
76 changes: 76 additions & 0 deletions panoramix/static/widgets/viz_filter_box.js
Original file line number Diff line number Diff line change
@@ -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,
};
});
2 changes: 1 addition & 1 deletion panoramix/static/widgets/viz_table.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]]]);
}
}
})
Expand Down
10 changes: 10 additions & 0 deletions panoramix/templates/panoramix/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ <h2>
</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>
Expand Down Expand Up @@ -88,6 +91,7 @@ <h2>
<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>
Expand Down Expand Up @@ -124,6 +128,12 @@ <h2>
$(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 %}
4 changes: 2 additions & 2 deletions panoramix/templates/panoramix/explore.html
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,10 @@ <h4 class="modal-title">Query</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</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>
Expand Down
8 changes: 5 additions & 3 deletions panoramix/templates/panoramix/featured_datasets.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -16,9 +16,11 @@ <h1><i class='fa fa-star'></i> Featured Datasets </h1>
<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>
Expand Down
13 changes: 10 additions & 3 deletions panoramix/utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
from datetime import datetime
from dateutil.parser import parse
import functools
import hashlib
from sqlalchemy.types import TypeDecorator, TEXT
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
import functools

from panoramix import db


Expand Down Expand Up @@ -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'])
11 changes: 7 additions & 4 deletions panoramix/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -564,7 +566,8 @@ def featured_datasets(self):
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(
Expand Down
63 changes: 61 additions & 2 deletions panoramix/viz.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,11 @@ def query_filters(self):
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

Expand Down Expand Up @@ -1105,6 +1108,61 @@ def get_json_data(self):
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,
Expand All @@ -1122,6 +1180,7 @@ def get_json_data(self):
DirectedForceViz,
SankeyViz,
WorldMapViz,
FilterBoxViz,
]
# This dict is used to
viz_types = OrderedDict([(v.viz_type, v) for v in viz_types_list])

0 comments on commit 3f292e6

Please sign in to comment.