Bugfixes+ better docs
This commit is contained in:
parent
2cbe25c6b5
commit
e1b3c7e63b
57
README.md
57
README.md
|
|
@ -4,6 +4,31 @@ Panoramix
|
|||
Panoramix is a data exploration platform designed to be visual, intuitive
|
||||
and interactive.
|
||||
|
||||
|
||||

|
||||
|
||||
Panoramix
|
||||
---------
|
||||
Panoramix's main goal is to make it easy to slice, dice and visualize data.
|
||||
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
|
||||
* 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
|
||||
on who can access which features, and integration with major
|
||||
authentication providers (database, OpenID, LDAP, OAuth & REMOTE_USER
|
||||
through Flask AppBuiler)
|
||||
* A simple semantic layer, allowing to control how data sources are
|
||||
displayed in the UI,
|
||||
by defining which fields should show up in which dropdown and which
|
||||
aggregation and function (metrics) are made available to the user
|
||||
* Deep integration with Druid allows for Panoramix to stay blazing fast while
|
||||
slicing and dicing large, realtime datasets
|
||||
|
||||
|
||||
Buzz Phrases
|
||||
------------
|
||||
|
||||
|
|
@ -12,17 +37,16 @@ Buzz Phrases
|
|||
* Realtime analytics when querying [Druid.io](http://druid.io)
|
||||
* Extentsible to infinity
|
||||
|
||||

|
||||
|
||||
Database Support
|
||||
----------------
|
||||
|
||||
Panoramix was originally designed on to of Druid.io, but quickly broadened
|
||||
Panoramix was originally designed on to of Druid.io, but quickly broadened
|
||||
its scope to support other databases through the use of SqlAlchemy, a Python
|
||||
ORM that is compatible with
|
||||
[many external databases](http://docs.sqlalchemy.org/en/rel_1_0/core/engines.html).
|
||||
ORM that is compatible with
|
||||
[most common databases](http://docs.sqlalchemy.org/en/rel_1_0/core/engines.html).
|
||||
|
||||
What's Druid?
|
||||
|
||||
What is Druid?
|
||||
-------------
|
||||
From their website at http://druid.io
|
||||
|
||||
|
|
@ -33,27 +57,6 @@ and fast data aggregation. Existing Druid deployments have scaled to
|
|||
trillions of events and petabytes of data. Druid is best used to
|
||||
power analytic dashboards and applications.*
|
||||
|
||||
Panoramix
|
||||
---------
|
||||
Panoramix's main goal is to make it easy to slice, dice and visualize data
|
||||
out of Druid. It empowers its user to perform **analytics
|
||||
at the speed of thought**.
|
||||
|
||||
Panoramix started as a hackathon project at Airbnb in while running a POC
|
||||
(proof of concept) on using Druid.
|
||||
|
||||
Panoramix provides:
|
||||
* A way to query intuitively a Druid dataset, allowing for grouping, filtering
|
||||
limiting and defining a time granularity
|
||||
* Many charts and visualization to analyze your data, as well as a flexible
|
||||
way to extend the visualization capabilities
|
||||
* An extensible, high granularity security model allowing intricate rules
|
||||
on who can access which features, and integration with major
|
||||
authentication providers (through Flask AppBuiler)
|
||||
* A simple semantic layer, allowing to control how Druid datasources are
|
||||
displayed in the UI,
|
||||
by defining which fields should show up in which dropdown and which
|
||||
aggregation and function (metrics) are made available to the user
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
|
|
|||
|
|
@ -60,8 +60,8 @@ def load_examples():
|
|||
|
||||
with gzip.open(config.basedir + '/data/birth_names.csv.gz') as f:
|
||||
bb_csv = csv.reader(f)
|
||||
for i, (state, year, name, num, gender) in enumerate(bb_csv):
|
||||
if i == 0 or not name or name=="\xc2\xa0":
|
||||
for i, (state, year, name, gender, num) in enumerate(bb_csv):
|
||||
if i == 0:
|
||||
continue
|
||||
if num == "NA":
|
||||
num = 0
|
||||
|
|
@ -71,8 +71,8 @@ def load_examples():
|
|||
state=state, year=year,
|
||||
ds=ds,
|
||||
name=name, num=num, gender=gender))
|
||||
if i % 1000 == 0:
|
||||
print("{} loaded out of 502619 rows".format(i))
|
||||
if i % 5000 == 0:
|
||||
print("{} loaded out of 82527 rows".format(i))
|
||||
session.commit()
|
||||
session.commit()
|
||||
print("Done loading table!")
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -80,7 +80,10 @@ class Slice(Model, AuditMixinNullable):
|
|||
|
||||
@property
|
||||
def slice_url(self):
|
||||
d = json.loads(self.params)
|
||||
try:
|
||||
d = json.loads(self.params)
|
||||
except Exception as e:
|
||||
d = {}
|
||||
from werkzeug.urls import Href
|
||||
href = Href(
|
||||
"/panoramix/datasource/{self.datasource_type}/"
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@
|
|||
<hr style="margin-bottom: 0px;">
|
||||
<img src="{{ url_for("static", filename="tux_panoramix.png") }}" width=250>
|
||||
<input type="hidden" id="slice_name" name="slice_name" value="TEST">
|
||||
<input type="hidden" id="action" name="action" value="">
|
||||
<input type="hidden" name="datasource_name" value="{{ datasource.name }}">
|
||||
<input type="hidden" name="datasource_id" value="{{ datasource.id }}">
|
||||
<input type="hidden" name="datasource_type" value="{{ datasource.type }}">
|
||||
|
|
@ -193,14 +194,8 @@
|
|||
$(this).parent().parent().slideUp("slow", function(){$(this).remove()});
|
||||
});
|
||||
}
|
||||
$("#plus").click(add_filter);
|
||||
$("#save").click(function () {
|
||||
var slice_name = prompt("Name your slice!");
|
||||
$("#slice_name").val(slice_name);
|
||||
$.get( "/panoramix/save/", $("#query").serialize() );
|
||||
})
|
||||
add_filter();
|
||||
$("#druidify").click(function () {
|
||||
|
||||
function druidify(){
|
||||
var i = 1;
|
||||
|
||||
// removing empty filters
|
||||
|
|
@ -224,7 +219,19 @@
|
|||
i++;
|
||||
});
|
||||
$("#query").submit();
|
||||
});
|
||||
}
|
||||
|
||||
$("#plus").click(add_filter);
|
||||
$("#save").click(function () {
|
||||
var slice_name = prompt("Name your slice!");
|
||||
if (slice_name != "" && slice_name != null) {
|
||||
$("#slice_name").val(slice_name);
|
||||
$("#action").val("save");
|
||||
druidify();
|
||||
}
|
||||
})
|
||||
add_filter();
|
||||
$("#druidify").click(druidify);
|
||||
|
||||
function create_choices (term, data) {
|
||||
if ($(data).filter(function() {
|
||||
|
|
|
|||
|
|
@ -36,14 +36,12 @@ var render = function(){
|
|||
data = example_data();
|
||||
data = json.data;
|
||||
var compare_suffix = ' ' + json.compare_suffix;
|
||||
console.log(data);
|
||||
var v_compare = null;
|
||||
var v = data[data.length -1][1];
|
||||
if (json.compare_lag >0){
|
||||
pos = data.length - (json.compare_lag+1);
|
||||
if(pos >=0)
|
||||
v_compare = 1-(v / data[pos][1]);
|
||||
console.log(v_compare)
|
||||
}
|
||||
var date_ext = d3.extent(data, function(d){return d[0]});
|
||||
var value_ext = d3.extent(data, function(d){return d[1]});
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
{% macro viz_js(viz) %}
|
||||
{% if viz.args.get("async") != "true" %}
|
||||
<script>
|
||||
$( document ).ready(function() {
|
||||
var url = "{{ viz.get_url(async="true", standalone="true", skip_libs="true")|safe }}";
|
||||
console.log(url);
|
||||
var token = $("#{{ viz.token }}");
|
||||
|
|
@ -44,7 +45,7 @@
|
|||
token.show();
|
||||
}
|
||||
else{
|
||||
var table = $('table').DataTable({
|
||||
var table = token.find('table').DataTable({
|
||||
paging: false,
|
||||
searching: false,
|
||||
});
|
||||
|
|
@ -53,6 +54,7 @@
|
|||
token.show();
|
||||
token.parent().find("img.loading").hide();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@ appbuilder.add_view(
|
|||
|
||||
class SliceModelView(ModelView, DeleteMixin):
|
||||
datamodel = SQLAInterface(models.Slice)
|
||||
can_add = False
|
||||
list_columns = ['slice_link', 'viz_type', 'datasource', 'created_by']
|
||||
edit_columns = [
|
||||
'slice_name', 'viz_type', 'druid_datasource', 'table',
|
||||
|
|
@ -211,6 +212,41 @@ class Panoramix(BaseView):
|
|||
@has_access
|
||||
@expose("/datasource/<datasource_type>/<datasource_id>/")
|
||||
def datasource(self, datasource_type, datasource_id):
|
||||
action = request.args.get('action')
|
||||
if action == 'save':
|
||||
session = db.session()
|
||||
d = request.args.to_dict(flat=False)
|
||||
del d['action']
|
||||
as_list = ('metrics', 'groupby')
|
||||
for k in d:
|
||||
v = d.get(k)
|
||||
if k in as_list and not isinstance(v, list):
|
||||
d[k] = [v] if v else []
|
||||
if k not in as_list and isinstance(v, list):
|
||||
d[k] = v[0]
|
||||
|
||||
table_id = druid_datasource_id = None
|
||||
datasource_type = request.args.get('datasource_type')
|
||||
if datasource_type == 'druid':
|
||||
druid_datasource_id = request.args.get('datasource_id')
|
||||
else:
|
||||
table_id = request.args.get('datasource_id')
|
||||
slice_name = request.args.get('slice_name')
|
||||
|
||||
obj = models.Slice(
|
||||
params=json.dumps(d, indent=4, sort_keys=True),
|
||||
viz_type=request.args.get('viz_type'),
|
||||
datasource_name=request.args.get('datasource_name'),
|
||||
druid_datasource_id=druid_datasource_id,
|
||||
table_id=table_id,
|
||||
datasource_type=datasource_type,
|
||||
slice_name=slice_name,
|
||||
)
|
||||
session.add(obj)
|
||||
session.commit()
|
||||
flash("Slice <{}> has been added to the pie".format(slice_name), "info")
|
||||
redirect(obj.slice_url)
|
||||
|
||||
if datasource_type == "table":
|
||||
datasource = (
|
||||
db.session
|
||||
|
|
@ -249,15 +285,13 @@ class Panoramix(BaseView):
|
|||
status=status,
|
||||
mimetype="application/json")
|
||||
else:
|
||||
#try:
|
||||
resp = self.render_template("panoramix/viz.html", viz=obj)
|
||||
'''
|
||||
try:
|
||||
resp = self.render_template("panoramix/viz.html", viz=obj)
|
||||
except Exception as e:
|
||||
return Response(
|
||||
str(e),
|
||||
status=500,
|
||||
mimetype="application/json")
|
||||
'''
|
||||
return resp
|
||||
|
||||
@has_access
|
||||
|
|
@ -279,36 +313,6 @@ class Panoramix(BaseView):
|
|||
|
||||
@has_access
|
||||
@expose("/save/")
|
||||
def save(self):
|
||||
session = db.session()
|
||||
d = request.args.to_dict(flat=False)
|
||||
as_list = ('metrics', 'groupby')
|
||||
for m in as_list:
|
||||
v = d.get(m)
|
||||
if v and not isinstance(d[m], list):
|
||||
d[m] = [d[m]]
|
||||
|
||||
table_id = druid_datasource_id = None
|
||||
datasource_type = request.args.get('datasource_type')
|
||||
if datasource_type == 'druid':
|
||||
druid_datasource_id = request.args.get('datasource_id')
|
||||
else:
|
||||
table_id = request.args.get('datasource_id')
|
||||
|
||||
obj = models.Slice(
|
||||
params=json.dumps(d, indent=4),
|
||||
viz_type=request.args.get('viz_type'),
|
||||
datasource_name=request.args.get('datasource_name'),
|
||||
druid_datasource_id=druid_datasource_id,
|
||||
table_id=table_id,
|
||||
datasource_type=datasource_type,
|
||||
slice_name=request.args.get('slice_name', 'junk'),
|
||||
)
|
||||
session.add(obj)
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
return "super!"
|
||||
|
||||
@has_access
|
||||
@expose("/dashboard/<id_>/")
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ class BaseViz(object):
|
|||
|
||||
def get_url(self, **kwargs):
|
||||
d = self.args.copy()
|
||||
if 'action' in d:
|
||||
del d['action']
|
||||
d.update(kwargs)
|
||||
href = Href(
|
||||
'/panoramix/datasource/{self.datasource.type}/'
|
||||
|
|
@ -335,7 +337,9 @@ class DistributionPieViz(HighchartsViz):
|
|||
verbose_name = "Distribution - Pie Chart"
|
||||
chart_type = "pie"
|
||||
js_files = ['highstock.js']
|
||||
form_fields = BaseViz.form_fields + ['limit']
|
||||
form_fields = [
|
||||
'viz_type', 'metrics', 'groupby',
|
||||
('since', 'until'), 'limit']
|
||||
|
||||
def query_obj(self):
|
||||
d = super(DistributionPieViz, self).query_obj()
|
||||
|
|
|
|||
Loading…
Reference in New Issue