Merge pull request #25 from mistercrunch/dash

Adding basic dashboarding support!
This commit is contained in:
Maxime Beauchemin 2015-09-18 13:42:02 -07:00
commit c1f28a3e74
16 changed files with 842 additions and 368 deletions

View File

@ -3,8 +3,6 @@
* DRUID: Allow for post aggregations (ratios!)
* compare time ranges
* csv export out of table view
* Save / bookmark / url shortener
* SQL: Find a way to manage granularity
* Create ~/.panoramix/ to host DB and config, generate default config there
* Add a per-datasource permission

View File

@ -9,12 +9,16 @@ class BaseHighchart(object):
target_div = 'chart'
@property
def javascript_cmd(self):
def json(self):
js = dumps(self.chart)
js = (
return (
js.replace('"{{TOOLTIP_FORMATTER}}"', self.tooltip_formatter)
.replace("\n", " ")
)
@property
def javascript_cmd(self):
js = self.json
if self.stockchart:
return "new Highcharts.StockChart(%s);" % js
return "new Highcharts.Chart(%s);" % js
@ -83,7 +87,7 @@ class Highchart(BaseHighchart):
if sort_legend_y:
if 'tooltip' not in chart:
chart['tooltip'] = {
'formatter': "{{TOOLTIP_FORMATTER}}"
#'formatter': "{{TOOLTIP_FORMATTER}}"
}
if self.zoom:
chart["zoomType"] = self.zoom
@ -192,7 +196,7 @@ class Highchart(BaseHighchart):
class HighchartBubble(BaseHighchart):
def __init__(self, df, target_div='chart', height=800):
def __init__(self, df, target_div=None, height=None):
self.df = df
self.chart = {
'chart': {

View File

@ -8,11 +8,11 @@ from pydruid import client
from pydruid.utils.filters import Dimension, Filter
from sqlalchemy import (
Column, Integer, String, ForeignKey, Text, Boolean, DateTime)
from panoramix.utils import JSONEncodedDict
from sqlalchemy import Table as sqlaTable
from sqlalchemy import create_engine, MetaData, desc, select, and_
from sqlalchemy import create_engine, MetaData, desc, select, and_, Table
from sqlalchemy.orm import relationship
from sqlalchemy.sql import table, literal_column, text
from flask import request
from copy import deepcopy, copy
from collections import namedtuple
@ -22,8 +22,8 @@ import sqlparse
import requests
import textwrap
from panoramix import db, get_session
import config
from panoramix import db, get_session, config, utils
from panoramix.viz import viz_types
QueryResult = namedtuple('namedtuple', ['df', 'query', 'duration'])
@ -32,9 +32,104 @@ class Slice(Model, AuditMixin):
"""A slice is essentially a report or a view on data"""
__tablename__ = 'slices'
id = Column(Integer, primary_key=True)
params = Column(JSONEncodedDict)
datasource = Column(String(250))
slice_name = Column(String(250))
druid_datasource_id = Column(Integer, ForeignKey('datasources.id'))
table_id = Column(Integer, ForeignKey('tables.id'))
datasource_type = Column(String(200))
datasource_name = Column(String(2000))
viz_type = Column(String(250))
params = Column(Text)
table = relationship(
'Table', foreign_keys=[table_id], backref='slices')
druid_datasource = relationship(
'Datasource', foreign_keys=[druid_datasource_id], backref='slices')
def __repr__(self):
return self.slice_name
@property
def datasource(self):
return self.table or self.druid_datasource
@property
@utils.memoized
def viz(self):
d = json.loads(self.params)
viz = viz_types[self.viz_type](
self.datasource,
form_data=d)
return viz
@property
def datasource_id(self):
datasource = self.datasource
return datasource.id if datasource else None
@property
def slice_url(self):
d = json.loads(self.params)
from werkzeug.urls import Href
href = Href(
"/panoramix/{self.datasource_type}/"
"{self.datasource_id}/".format(self=self))
return href(d)
@property
def slice_link(self):
url = self.slice_url
return '<a href="{url}">{self.slice_name}</a>'.format(**locals())
@property
def js_files(self):
from panoramix.viz import viz_types
return viz_types[self.viz_type].js_files
@property
def css_files(self):
from panoramix.viz import viz_types
return viz_types[self.viz_type].css_files
def get_viz(self):
pass
dashboard_slices = Table('dashboard_slices', Model.metadata,
Column('id', Integer, primary_key=True),
Column('dashboard_id', Integer, ForeignKey('dashboards.id')),
Column('slice_id', Integer, ForeignKey('slices.id')),
)
class Dashboard(Model, AuditMixin):
"""A dash to slash"""
__tablename__ = 'dashboards'
id = Column(Integer, primary_key=True)
dashboard_title = Column(String(500))
position_json = Column(Text)
slices = relationship(
'Slice', secondary=dashboard_slices, backref='dashboards')
def __repr__(self):
return self.dashboard_title
def dashboard_link(self):
url = "/panoramix/dashboard/{}/".format(self.id)
return '<a href="{url}">{self.dashboard_title}</a>'.format(**locals())
@property
def js_files(self):
l = []
for o in self.slices:
l += [f for f in o.js_files if f not in l]
return l
@property
def css_files(self):
l = []
for o in self.slices:
l += o.css_files
return list(set(l))
class Queryable(object):
@ -72,6 +167,8 @@ class Database(Model, AuditMixin):
class Table(Model, Queryable, AuditMixin):
type = "table"
__tablename__ = 'tables'
id = Column(Integer, primary_key=True)
table_name = Column(String(255), unique=True)
@ -85,6 +182,9 @@ class Table(Model, Queryable, AuditMixin):
baselink = "tableview"
def __repr__(self):
return self.table_name
@property
def name(self):
return self.table_name
@ -446,6 +546,7 @@ class Cluster(Model, AuditMixin):
class Datasource(Model, AuditMixin, Queryable):
type = "datasource"
baselink = "datasourcemodelview"

View File

@ -0,0 +1,2 @@
/*! gridster.js - v0.5.6 - 2014-09-25 - * http://gridster.net/ - Copyright (c) 2014 ducksboard; Licensed MIT */
.gridster{position:relative}.gridster>*{margin:0 auto;-webkit-transition:height .4s,width .4s;-moz-transition:height .4s,width .4s;-o-transition:height .4s,width .4s;-ms-transition:height .4s,width .4s;transition:height .4s,width .4s}.gridster .gs-w{z-index:2;position:absolute}.ready .gs-w:not(.preview-holder){-webkit-transition:opacity .3s,left .3s,top .3s;-moz-transition:opacity .3s,left .3s,top .3s;-o-transition:opacity .3s,left .3s,top .3s;transition:opacity .3s,left .3s,top .3s}.ready .gs-w:not(.preview-holder),.ready .resize-preview-holder{-webkit-transition:opacity .3s,left .3s,top .3s,width .3s,height .3s;-moz-transition:opacity .3s,left .3s,top .3s,width .3s,height .3s;-o-transition:opacity .3s,left .3s,top .3s,width .3s,height .3s;transition:opacity .3s,left .3s,top .3s,width .3s,height .3s}.gridster .preview-holder{z-index:1;position:absolute;background-color:#fff;border-color:#fff;opacity:.3}.gridster .player-revert{z-index:10!important;-webkit-transition:left .3s,top .3s!important;-moz-transition:left .3s,top .3s!important;-o-transition:left .3s,top .3s!important;transition:left .3s,top .3s!important}.gridster .dragging,.gridster .resizing{z-index:10!important;-webkit-transition:all 0s!important;-moz-transition:all 0s!important;-o-transition:all 0s!important;transition:all 0s!important}.gs-resize-handle{position:absolute;z-index:1}.gs-resize-handle-both{width:20px;height:20px;bottom:-8px;right:-8px;background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pg08IS0tIEdlbmVyYXRvcjogQWRvYmUgRmlyZXdvcmtzIENTNiwgRXhwb3J0IFNWRyBFeHRlbnNpb24gYnkgQWFyb24gQmVhbGwgKGh0dHA6Ly9maXJld29ya3MuYWJlYWxsLmNvbSkgLiBWZXJzaW9uOiAwLjYuMSAgLS0+DTwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DTxzdmcgaWQ9IlVudGl0bGVkLVBhZ2UlMjAxIiB2aWV3Qm94PSIwIDAgNiA2IiBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjojZmZmZmZmMDAiIHZlcnNpb249IjEuMSINCXhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiDQl4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjZweCIgaGVpZ2h0PSI2cHgiDT4NCTxnIG9wYWNpdHk9IjAuMzAyIj4NCQk8cGF0aCBkPSJNIDYgNiBMIDAgNiBMIDAgNC4yIEwgNCA0LjIgTCA0LjIgNC4yIEwgNC4yIDAgTCA2IDAgTCA2IDYgTCA2IDYgWiIgZmlsbD0iIzAwMDAwMCIvPg0JPC9nPg08L3N2Zz4=);background-position:top left;background-repeat:no-repeat;cursor:se-resize;z-index:20}.gs-resize-handle-x{top:0;bottom:13px;right:-5px;width:10px;cursor:e-resize}.gs-resize-handle-y{left:0;right:13px;bottom:-5px;height:10px;cursor:s-resize}.gs-w:hover .gs-resize-handle,.resizing .gs-resize-handle{opacity:1}.gs-resize-handle,.gs-w.dragging .gs-resize-handle{opacity:0}.gs-resize-disabled .gs-resize-handle{display:none!important}[data-max-sizex="1"] .gs-resize-handle-x,[data-max-sizey="1"] .gs-resize-handle-y,[data-max-sizey="1"][data-max-sizex="1"] .gs-resize-handle{display:none!important}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,166 @@
{% extends "panoramix/base.html" %}
{% block head_css %}
{{ super() }}
{% for css in dashboard.css_files %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename=css) }}">
{% endfor %}
<link rel="stylesheet" href="{{ url_for('static', filename="jquery.gridster.min.css") }}">
<style>
a i{
cursor: pointer;
}
i.drag{
cursor: move; !important
}
.gridster .preview-holder {
z-index: 1;
position: absolute;
background-color: #AAA;
border-color: #AAA;
opacity: 0.3;
}
.gridster li {
list-style-type: none;
border: 1px solid gray;
overflow: auto;
box-shadow: 2px 2px 2px #AAA;
border-radius: 5px;
background-color: white;
}
.gridster .dragging,
.gridster .resizing {
opacity: 0.5;
}
img.loading {
width: 20px;
margin: 5px;
}
.title {
text-align: center;
}
.slice_title {
text-align: center;
font-weight: bold;
font-size: 14px;
padding: 5px;
}
div.gridster {
visibility: hidden
}
div.slice_content {
width: 100%;
height: 100%;
}
</style>
{% endblock %}
{% block content_fluid %}
<div class="title">
<div class="row">
<div class="col-md-1 text-left"></div>
<div class="col-md-10 text-middle">
<h2>
{{ dashboard.dashboard_title }}
<a id="savedash"><i class="fa fa-save"></i></a>
</h2>
</div>
<div class="col-md-1 text-right">
</div>
</div>
</div>
<div class="gridster content_fluid">
<ul>
{% for slice in dashboard.slices %}
{% set pos = pos_dict.get(slice.id, {}) %}
{% set viz = slice.viz %}
{% import viz.template as viz_macros %}
<li
id="slice_{{ slice.id }}"
slice_id="{{ slice.id }}"
data-row="{{ pos.row or 1 }}"
data-col="{{ pos.col or loop.index }}"
data-sizex="{{ pos.size_x or 4 }}"
data-sizey="{{ pos.size_y or 4 }}">
<div class="slice_title" style="height: 0px;">
<div class="row">
<div class="col-md-4 text-left">
<a>
<i class="fa fa-arrows drag"></i>
</a>
</div>
<div class="col-md-4 text-middle">
<span>{{ slice.slice_name }}</span>
</div>
<div class="col-md-4 text-right" style="position: relative;">
<a href="{{ slice.slice_url }}"><i class="fa fa-play"></i></a>
<a class="refresh"><i class="fa fa-refresh"></i></a>
<a><i class="fa fa-gear"></i></a>
<a class="closewidget"><i class="fa fa-close"></i></a>
</div>
</div>
</div>
{{ viz_macros.viz_html(viz) }}
</li>
{% endfor %}
</ul>
</div>
{% endblock %}
{% block tail %}
{{ super() }}
{% for js in dashboard.js_files %}
<script src="{{ url_for('static', filename=js) }}"></script>
{% endfor %}
<script src="{{ url_for("static", filename="jquery.gridster.with-extras.min.js") }}"></script>
<script src="{{ url_for("static", filename="d3.min.js") }}"></script>
<script>
f = d3.format(".4s");
</script>
<script>
$( document ).ready(function() {
var gridster = $(".gridster ul").gridster({
widget_margins: [5, 5],
widget_base_dimensions: [100, 100],
draggable: {
handle: '.drag',
},
resize: {
enabled: true,
stop: function(e, ui, $widget) {
$widget.find("a.refresh").click();
}
},
serialize_params:function($w, wgd) {
return {
slice_id: $($w).attr('slice_id'),
col: wgd.col,
row: wgd.row,
size_x: wgd.size_x,
size_y: wgd.size_y
};
},
}).data('gridster');
$("div.gridster").css('visibility', 'visible');
$("#savedash").click(function(){
var data = gridster.serialize();
console.log(data);
$.ajax({
type: "POST",
url: '/panoramix/save_dash/{{ dashboard.id }}/',
data: {data: JSON.stringify(data)},
success: function(){console.log('Sucess!')},
});
});
$("a.closewidget").click(function(){
var li = $(this).parents("li");
gridster.remove_widget(li);
});
});
</script>
{% for slice in dashboard.slices %}
{% set viz = slice.viz %}
{% import viz.template as viz_macros %}
{{ viz_macros.viz_js(viz) }}
{% endfor %}
{% endblock %}

View File

@ -1,36 +1,40 @@
{% extends "panoramix/base.html" %}
{% block head_css %}
{{super()}}
{% set datasource = viz.datasource %}
<style>
.select2-container-multi .select2-choices {
height: 70px;
overflow: auto;
}
.no-gutter > [class*='col-'] {
.select2-container-multi .select2-choices {
height: 70px;
overflow: auto;
}
.no-gutter > [class*='col-'] {
padding-right:0;
padding-left:0;
}
form div.form-control {
margin-bottom: 5px !important;
}
form input.form-control {
margin-bottom: 5px !important;
}
.tooltip-inner {
max-width: 350px;
//width: 350px;
}
}
form div.form-control {
margin-bottom: 5px !important;
}
form input.form-control {
margin-bottom: 5px !important;
}
.tooltip-inner {
max-width: 350px;
//width: 350px;
}
img.loading {
width: 30px;
}
</style>
{% endblock %}
{% block content_fluid %}
{% set datasource = viz.datasource %}
{% set form = viz.form %}
<div class="container-fluid">
<div class="col-md-3">
<h3>
{{ datasource.name }}
{% if datasource.description %}
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="bottom" title="{{ datasource.description }}"></i>
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="bottom" title="{{ datasource.description }}"></i>
{% endif %}
<a href="/{{ datasource.baselink }}/edit/{{ datasource.id }}">
<i class="fa fa-edit"></i>
@ -40,94 +44,95 @@ form input.form-control {
<hr>
<form id="query" method="GET" style="display: none;">
{% for fieldname in form.field_order %}
{% if not fieldname.__iter__ %}
<div>
{% set field = form.get_field(fieldname)%}
<div>
{{ field.label }}
{% if field.description %}
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="right"
title="{{ field.description }}"></i>
{% endif %}:
</div>
<div>
{{ field(class_=form.field_css_classes(field.name)) }}
</div>
</div>
{% else %}
<div class="row">
<div class="form-group">
{% for name in fieldname %}
<div class="col-xs-{{ (12 / fieldname|length) | int }}">
{% set field = form.get_field(name)%}
{{ field.label }}
{% if field.description %}
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="right"
title="{{ field.description }}"></i>
{% endif %}:
{{ field(class_=form.field_css_classes(field.name)) }}
</div>
{% endfor %}
</div>
{% if not fieldname.__iter__ %}
<div>
{% set field = form.get_field(fieldname)%}
<div>
{{ field.label }}
{% if field.description %}
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="right"
title="{{ field.description }}"></i>
{% endif %}:
</div>
<div>
{{ field(class_=form.field_css_classes(field.name)) }}
</div>
</div>
{% else %}
<div class="row">
<div class="form-group">
{% for name in fieldname %}
<div class="col-xs-{{ (12 / fieldname|length) | int }}">
{% set field = form.get_field(name)%}
{{ field.label }}
{% if field.description %}
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="right"
title="{{ field.description }}"></i>
{% endif %}:
{{ field(class_=form.field_css_classes(field.name)) }}
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% endif %}
{% endfor %}
<hr>
<h4>Filters</h4>
<div id="flt0" style="display: none;">
<span class="">{{ form.flt_col_0(class_="form-control inc") }}</span>
<div class="row">
<span class="col col-sm-4">{{ form.flt_op_0(class_="form-control inc") }}</span>
<span class="col col-sm-6">{{ form.flt_eq_0(class_="form-control inc") }}</span>
<button type="button" class="btn btn-sm remove" aria-label="Delete filter">
<span class="glyphicon glyphicon-minus" aria-hidden="true"></span>
</button>
</div>
<hr style="margin: 5px 0px;"/>
<div id="flt0" style="display: none;">
<span class="">{{ form.flt_col_0(class_="form-control inc") }}</span>
<div class="row">
<span class="col col-sm-4">{{ form.flt_op_0(class_="form-control inc") }}</span>
<span class="col col-sm-6">{{ form.flt_eq_0(class_="form-control inc") }}</span>
<button type="button" class="btn btn-sm remove" aria-label="Delete filter">
<span class="glyphicon glyphicon-minus" aria-hidden="true"></span>
</button>
</div>
<hr style="margin: 5px 0px;"/>
</div>
<div id="filters"></div>
<button type="button" id="plus" class="btn btn-sm" aria-label="Add a filter">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
</button>
<hr>
<button type="button" class="btn btn-primary" id="druidify">Druidify!</button>
<button type="button" class="btn btn-default" id="bookmark">Bookmark</button>
<button type="button" class="btn btn-primary" id="druidify">
<i class="fa fa-bolt"></i>
Druidify!
</button>
<button type="button" class="btn btn-default" id="save">
<i class="fa fa-save"></i>
Save as Slice
</button>
<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" name="datasource_name" value="{{ datasource.name }}">
<input type="hidden" name="datasource_id" value="{{ datasource.id }}">
<input type="hidden" name="datasource_type" value="{{ datasource.type }}">
</form><br>
</div>
<div class="col-md-9">
<h3>{{ viz.verbose_name }}
{% if results %}
<span class="label label-success">
{{ "{0:0.4f}".format(results.duration.total_seconds()) }} s
</span>
<span class="label label-info btn"
data-toggle="modal" data-target="#query_modal">query</span>
{% endif %}
{% if False %}
<span class="label label-success">
{{ "{0:0.4f}".format(results.duration.total_seconds()) }} s
</span>
<span class="label label-info btn"
data-toggle="modal" data-target="#query_modal">query</span>
{% endif %}
</h3>
<hr/>
{% block viz %}
{% if viz.error_msg %}
<div class="alert alert-danger">{{ viz.error_msg }}</div>
{% endif %}
{% if viz.warning_msg %}
<div class="alert alert-warning">{{ viz.warning_msg }}</div>
{% endif %}
{% endblock %}
<div class="viz" style="height: 600px;">
{% block viz_html %}
{% if viz.error_msg %}
<div class="alert alert-danger">{{ viz.error_msg }}</div>
{% endif %}
{% if viz.warning_msg %}
<div class="alert alert-warning">{{ viz.warning_msg }}</div>
{% endif %}
{% endblock %}
</div>
{% if debug %}
<h3>Results</h3>
<pre>
{{ results }}
</pre>
<h3>Latest Segment Metadata</h3>
<pre>
{{ latest_metadata }}
</pre>
{% endif %}
</div>
</div>
<div class="modal fade" id="query_modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
@ -138,7 +143,7 @@ form input.form-control {
<h4 class="modal-title" id="myModalLabel">Query</h4>
</div>
<div class="modal-body">
<pre>{{ results.query }}</pre>
<pre>{{ '' }}</pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
@ -150,32 +155,30 @@ form input.form-control {
{% block tail_js %}
{{ super() }}
<script src="{{ url_for("static", filename="d3.min.js") }}"></script>
<script>
$( document ).ready(function() {
f = d3.format(".4s");
function getParam(name) {
$( document ).ready(function() {
function getParam(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search);
results = regex.exec(location.search);
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
}
$(".select2").select2();
$("form").slideDown("slow");
$('[data-toggle="tooltip"]').tooltip();
$(".select2").select2();
$("form").slideDown("slow");
$('[data-toggle="tooltip"]').tooltip();
function set_filters(){
for (var i=1; i<10; i++){
var eq = getParam("flt_eq_" + i);
if (eq !=''){
add_filter(i);
function set_filters(){
for (var i=1; i<10; i++){
var eq = getParam("flt_eq_" + i);
if (eq !=''){
add_filter(i);
}
}
}
}
set_filters();
set_filters();
function add_filter(i) {
function add_filter(i) {
cp = $("#flt0").clone();
$(cp).appendTo("#filters");
$(cp).slideDown("slow");
@ -189,81 +192,85 @@ $( document ).ready(function() {
$(cp).find('.remove').click(function() {
$(this).parent().parent().slideUp("slow", function(){$(this).remove()});
});
}
$("#plus").click(add_filter);
$("#bookmark").click(function () {alert("Not implemented yet...");})
add_filter();
$("#druidify").click(function () {
var i = 1;
}
$("#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 () {
var i = 1;
// removing empty filters
$("#filters > div").each(function(){
if ($(this).find("#flt_eq_0").val() == '')
// removing empty filters
$("#filters > div").each(function(){
if ($(this).find("#flt_eq_0").val() == '')
$(this).slideUp();
});
});
// Assigning the right id to form elements in filters
$("#filters > div").each(function(){
$(this).attr("id", function(){return "flt_" + i;})
$(this).find("#flt_col_0")
// Assigning the right id to form elements in filters
$("#filters > div").each(function(){
$(this).attr("id", function(){return "flt_" + i;})
$(this).find("#flt_col_0")
.attr("id", function(){return "flt_col_" + i;})
.attr("name", function(){return "flt_col_" + i;});
$(this).find("#flt_op_0")
$(this).find("#flt_op_0")
.attr("id", function(){return "flt_op_" + i;})
.attr("name", function(){return "flt_op_" + i;});
$(this).find("#flt_eq_0")
$(this).find("#flt_eq_0")
.attr("id", function(){return "flt_eq_" + i;})
.attr("name", function(){return "flt_eq_" + i;});
i++;
i++;
});
$("#query").submit();
});
$("#query").submit();
});
function create_choices (term, data) {
function create_choices (term, data) {
if ($(data).filter(function() {
return this.text.localeCompare(term)===0;
return this.text.localeCompare(term)===0;
}).length===0)
{return {id:term, text:term};}
}
$(".select2_free_since").select2({
}
$(".select2_free_since").select2({
createSearchChoice: create_choices,
multiple: false,
data: [
{id: '-1 hour', text: '-1 hour'},
{id: '-12 hours', text: '-12 hours'},
{id: '-1 day', text: '-1 day'},
{id: '-7 days', text: '-7 days'},
{id: '-28 days', text: '-28 days'},
{id: '-90 days', text: '-90 days'},
{id: '{{ form.data.since }}', text: '{{ form.data.since }}'},
{id: '-1 hour', text: '-1 hour'},
{id: '-12 hours', text: '-12 hours'},
{id: '-1 day', text: '-1 day'},
{id: '-7 days', text: '-7 days'},
{id: '-28 days', text: '-28 days'},
{id: '-90 days', text: '-90 days'},
{id: '{{ viz.form.data.since }}', text: '{{ viz.form.data.since }}'},
]
});
$(".select2_free_until").select2({
});
$(".select2_free_until").select2({
createSearchChoice: create_choices,
multiple: false,
data: [
{id: '{{ form.data.until }}', text: '{{ form.data.until }}'},
{id: 'now', text: 'now'},
{id: '-1 day', text: '-1 day'},
{id: '-7 days', text: '-7 days'},
{id: '-28 days', text: '-28 days'},
{id: '-90 days', text: '-90 days'},
{id: '{{ viz.form.data.until }}', text: '{{ viz.form.data.until }}'},
{id: 'now', text: 'now'},
{id: '-1 day', text: '-1 day'},
{id: '-7 days', text: '-7 days'},
{id: '-28 days', text: '-28 days'},
{id: '-90 days', text: '-90 days'},
]
});
$(".select2_free_granularity").select2({
});
$(".select2_free_granularity").select2({
createSearchChoice: create_choices,
multiple: false,
data: [
{id: '{{ form.data.granularity }}', text: '{{ form.data.granularity }}'},
{id: 'all', text: 'all'},
{id: '5 seconds', text: '5 seconds'},
{id: '30 seconds', text: '30 seconds'},
{id: '1 minute', text: '1 minute'},
{id: '5 minutes', text: '5 minutes'},
{id: '1 day', text: '1 day'},
{id: '7 days', text: '7 days'},
{id: '{{ viz.form.data.granularity }}', text: '{{ viz.form.data.granularity }}'},
{id: 'all', text: 'all'},
{id: '5 seconds', text: '5 seconds'},
{id: '30 seconds', text: '30 seconds'},
{id: '1 minute', text: '1 minute'},
{id: '5 minutes', text: '5 minutes'},
{id: '1 day', text: '1 day'},
{id: '7 days', text: '7 days'},
]
});
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,41 @@
{% import viz.template as viz_macros %}
{% if viz.args.get("json") == "true" %}
{{ viz.get_json() }}
{% else %}
{% if viz.args.get("standalone") == "true" %}
{% extends 'panoramix/viz_standalone.html' %}
{% else %}
{% extends 'panoramix/datasource.html' %}
{% endif %}
{% block viz_html %}
{{ viz_macros.viz_html(viz) }}
{% endblock %}
{% block head_css %}
{{super()}}
{% if viz.args.get("skip_libs") != "true" %}
{% for css in viz.css_files %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename=css) }}">
{% endfor %}
{% endif %}
{{ viz_macros.viz_css(viz) }}
{% endblock %}
{% block tail %}
{{super()}}
{% if viz.args.get("skip_libs") != "true" %}
<script src="{{ url_for("static", filename="d3.min.js") }}"></script>
<script>
f = d3.format(".4s");
</script>
{% for js in viz.js_files %}
<script src="{{ url_for('static', filename=js) }}"></script>
{% endfor %}
{% endif %}
{{ viz_macros.viz_js(viz) }}
{% endblock %}
{% endif %}

View File

@ -1,34 +1,48 @@
{% extends "panoramix/datasource.html" %}
{% block viz %}
{{ super() }}
<div id="chart"></div>
{% endblock %}
{% macro viz_html(viz) %}
<div id="{{ viz.token }}" style="height:100%; width: 100%">
<img src="{{ url_for("static", filename="loading.gif") }}" class="loading">
<div class="chart" style="height:100%; width: 100%"></div>
</div>
{% endmacro %}
{% block tail %}
{{ super() }}
{% if viz.stockchart %}
<script src="{{ url_for("static", filename="highstock.js") }}"></script>
{% else %}
<script src="{{ url_for("static", filename="highcharts.js") }}"></script>
{% endif %}
<script src="{{ url_for("static", filename="highcharts-more.js") }}"></script>
{% macro viz_js(viz) %}
<script>
$( document ).ready(function() {
$( document ).ready(function() {
Highcharts.setOptions({
colors: [
"#FF5A5F", "#007A87", "#7B0051", "#00D1C1", "#8CE071", "#FFB400",
"#FFAA91", "#B4A76C", "#9CA299", "#565A5C"
],
global: {
useUTC: false
},
colors: [
"#FF5A5F", "#007A87", "#7B0051", "#00D1C1", "#8CE071", "#FFB400",
"#FFAA91", "#B4A76C", "#9CA299", "#565A5C"
],
global: {
useUTC: false
},
});
$("#viz_type").click(function(){
$("#queryform").submit();
})
{% if chart_js %}
{{ chart_js|safe }}
{% endif %}
});
var token = $("#{{ viz.token }}");
var loading = $("#{{ viz.token }}").find("img.loading");
var chart = $("#{{ viz.token }}").find("div.chart");
var refresh = function(){
chart.hide();
loading.show();
var url = "{{ viz.get_url(json="true")|safe }}";
$.getJSON(url, function(data){
chart.width(token.width());
chart.height(token.height()-40);
chart.highcharts('{{ viz.chart_call }}', data);
chart.show();
token.find("img.loading").hide();
})
.fail(function(xhr) {
var err = '<div class="alert alert-danger">' + xhr.responseText + '</div>';
loading.hide();
chart.show();
chart.html(err);
});
};
refresh();
token.parent().find("a.refresh").click(refresh);
});
</script>
{% endblock %}
{% endmacro %}
{% macro viz_css(viz) %}
{% endmacro %}

View File

@ -0,0 +1,13 @@
<html>
<head>
{% if viz.args.get("skip_libs") != "true" %}
{% block head %}
<script src="{{url_for('appbuilder.static',filename='js/jquery-latest.js')}}"></script>
{% endblock %}
{% endif %}
{% block tail %}{% endblock %}
</head>
<body>
{% block viz_html %}{% endblock %}
</body>
</html>

View File

@ -1,51 +1,54 @@
{% extends "panoramix/datasource.html" %}
{% block head_css %}
{{super()}}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='dataTables.bootstrap.css') }}">
{% endblock %}
{% block viz %}
{{ super() }}
{% if not error_msg %}
<table class="dataframe table table-striped table-bordered table-condensed">
<thead>
<tr>
{% for col in df.columns if not col.endswith('__perc') %}
<th>{{ col }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in df.to_dict(orient="records") %}
<tr>
{% for col in df.columns if not col.endswith('__perc') %}
{% if col + '__perc' in df.columns %}
<td style="background-image: linear-gradient(to right, lightgrey, lightgrey {{ row[col+'__perc'] }}%, rgba(0,0,0,0) {{ row[col+'__perc'] }}%">{{ row[col] }}</td>
{% else %}
<td>{{ row[col] }}</td>
{% endif %}
{% endfor %}
</tr>
{% macro viz_html(viz) %}
{% if viz.args.get("async") == "true" %}
{% set df = viz.get_df() %}
<table class="dataframe table table-striped table-bordered table-condensed">
<thead>
<tr>
{% for col in df.columns if not col.endswith('__perc') %}
<th>{{ col }}</th>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endblock %}
</tr>
</thead>
<tbody>
{% for row in df.to_dict(orient="records") %}
<tr>
{% for col in df.columns if not col.endswith('__perc') %}
{% if col + '__perc' in df.columns %}
<td style="background-image: linear-gradient(to right, lightgrey, lightgrey {{ row[col+'__perc'] }}%, rgba(0,0,0,0) {{ row[col+'__perc'] }}%">
{{ row[col] }}
</td>
{% else %}
<td>{{ row[col] }}</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div id="{{ viz.token }}" style="display: none;">
</div>
<img src="{{ url_for("static", filename="loading.gif") }}" class="loading">
{% endif %}
{% endmacro %}
{% block tail %}
{{ super() }}
<script src="{{ url_for('static', filename='jquery.dataTables.min.js') }}"></script>
<script src="{{ url_for('static', filename='dataTables.bootstrap.js') }}"></script>
<script>
//$('table').css('background-color', 'white');
$(document).ready(function() {
var table = $('table').DataTable({
{% macro viz_js(viz) %}
{% if viz.args.get("async") != "true" %}
<script>
var url = "{{ viz.get_url(async="true", standalone="true", skip_libs="true")|safe }}";
console.log(url);
$("#{{ viz.token }}").load(url, function(){
var table = $('table').DataTable({
paging: false,
searching: false,
});
$("#{{ viz.token }}").show();
table.column('-1').order( 'desc' ).draw();
$("#{{ viz.token }} img.loading").hide();
});
table.column('-1').order( 'desc' ).draw();
});
</script>
{% endblock %}
</script>
{% endif %}
{% endmacro %}
{% macro viz_css(viz) %}
{% endmacro %}

View File

@ -2,6 +2,34 @@ from datetime import datetime
from sqlalchemy.types import TypeDecorator, TEXT
import json
import parsedatetime
import functools
class memoized(object):
"""Decorator that caches a function's return value each time it is called.
If called later with the same arguments, the cached value is returned, and
not re-evaluated.
"""
def __init__(self, func):
self.func = func
self.cache = {}
def __call__(self, *args):
try:
return self.cache[args]
except KeyError:
value = self.func(*args)
self.cache[args] = value
return value
except TypeError:
# uncachable -- for instance, passing a list as an argument.
# Better to not cache than to blow up entirely.
return self.func(*args)
def __repr__(self):
"""Return the function's docstring."""
return self.func.__doc__
def __get__(self, obj, objtype):
"""Support instance methods."""
return functools.partial(self.__call__, obj)
def parse_human_datetime(s):

View File

@ -104,6 +104,33 @@ appbuilder.add_view(
category_icon='fa-cogs',)
class SliceModelView(ModelView, DeleteMixin):
datamodel = SQLAInterface(models.Slice)
list_columns = ['slice_link', 'viz_type', 'datasource', 'created_by']
appbuilder.add_view(
SliceModelView,
"Slices",
icon="fa-bar-chart",
category="",
category_icon='',)
class DashboardModelView(ModelView, DeleteMixin):
datamodel = SQLAInterface(models.Dashboard)
list_columns = ['dashboard_link', 'created_by']
edit_columns = ['dashboard_title', 'slices', 'position_json']
add_columns = edit_columns
appbuilder.add_view(
DashboardModelView,
"Dashboards",
icon="fa-dashboard",
category="",
category_icon='',)
class DatabaseView(ModelView, DeleteMixin):
datamodel = SQLAInterface(models.Database)
list_columns = ['database_name']
@ -159,7 +186,7 @@ class DatasourceModelView(ModelView, DeleteMixin):
appbuilder.add_view(
DatasourceModelView,
"Druid Datasources",
icon="fa-cube")
icon="fa-cubes")
@app.route('/health')
@ -176,13 +203,14 @@ class Panoramix(BaseView):
@has_access
@expose("/table/<table_id>/")
def table(self, table_id):
table = (
db.session
.query(models.Table)
.filter_by(id=table_id)
.first()
)
if not table:
flash("The table seem to have been deleted", "alert")
viz_type = request.args.get("viz_type")
if not viz_type and table.default_endpoint:
return redirect(table.default_endpoint)
@ -190,19 +218,43 @@ class Panoramix(BaseView):
viz_type = "table"
obj = viz.viz_types[viz_type](
table,
form_data=request.args, view=self)
if request.args.get("json"):
form_data=request.args)
if request.args.get("json") == "true":
try:
payload = obj.get_json()
status=200
except Exception as e:
logging.exception(e)
payload = str(e)
status=500
return Response(
json.dumps(obj.get_query(), indent=4),
status=200,
payload,
status=status,
mimetype="application/json")
return obj.check_and_render()
else:
return self.render_template("panoramix/viz.html", viz=obj)
@has_access
@expose("/save_dash/<dashboard_id>/", methods=['GET', 'POST'])
def save_dash(self, dashboard_id):
data = json.loads(request.form.get('data'))
slice_ids = [int(d['slice_id']) for d in data]
print slice_ids
session = db.session()
Dash = models.Dashboard
dash = session.query(Dash).filter_by(id=dashboard_id).first()
dash.slices = [o for o in dash.slices if o.id in slice_ids]
print dash.slices
dash.position_json = json.dumps(data, indent=4)
session.merge(dash)
session.commit()
session.close()
return "SUCCESS"
@has_access
@expose("/datasource/<datasource_name>/")
def datasource(self, datasource_name):
viz_type = request.args.get("viz_type")
datasource = (
db.session
.query(models.Datasource)
@ -215,7 +267,7 @@ class Panoramix(BaseView):
viz_type = "table"
obj = viz.viz_types[viz_type](
datasource,
form_data=request.args, view=self)
form_data=request.args)
if request.args.get("json"):
return Response(
json.dumps(obj.get_query(), indent=4),
@ -226,6 +278,55 @@ class Panoramix(BaseView):
return obj.check_and_render()
@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_>/")
def dashboard(self, id_):
session = db.session()
dashboard = (
session
.query(models.Dashboard)
.filter(models.Dashboard.id == id_)
.first()
)
pos_dict = {
int(o['slice_id']):o for o in json.loads(dashboard.position_json)}
return self.render_template(
"panoramix/dashboard.html", dashboard=dashboard,
pos_dict=pos_dict)
@has_access
@expose("/refresh_datasources/")
def refresh_datasources(self):

View File

@ -1,55 +1,83 @@
from datetime import datetime
from flask import flash, request
import pandas as pd
from collections import OrderedDict
import config
import logging
import numpy as np
from datetime import datetime
from urllib import urlencode
import uuid
from panoramix import utils
from flask import flash
from werkzeug.datastructures import MultiDict
from werkzeug.urls import Href
import numpy as np
import pandas as pd
from panoramix import utils, config
from panoramix.highchart import Highchart, HighchartBubble
from panoramix.forms import form_factory
CHART_ARGS = {
'height': 700,
'title': None,
'target_div': 'chart',
}
class BaseViz(object):
verbose_name = "Base Viz"
template = "panoramix/datasource.html"
template = None
hidden_fields = []
form_fields = [
'viz_type', 'metrics', 'groupby', 'granularity',
('since', 'until')]
js_files = []
css_files = []
def __init__(self, datasource, form_data, view):
def __init__(self, datasource, form_data):
self.datasource = datasource
self.form_class = self.form_class()
self.view = view
if isinstance(form_data, MultiDict):
self.args = form_data.to_dict(flat=True)
else:
self.args = form_data
self.form_data = form_data
self.metrics = form_data.getlist('metrics') or ['count']
self.groupby = form_data.getlist('groupby') or []
self.token = self.args.get('token', 'token_' + uuid.uuid4().hex[:8])
as_list = ('metrics', 'groupby')
d = self.args
for m in as_list:
if m in d and d[m] and not isinstance(d[m], list):
d[m] = [d[m]]
self.metrics = self.args.get('metrics') or ['count']
self.groupby = self.args.get('groupby') or []
def get_url(self, **kwargs):
d = self.args.copy()
d.update(kwargs)
href = Href('/panoramix/table/2/')
return href(d)
def get_df(self):
self.error_msg = ""
self.results = None
try:
self.results = self.bake_query()
self.df = self.results.df
if self.df is not None:
if 'timestamp' in self.df.columns:
self.df.timestamp = pd.to_datetime(self.df.timestamp)
#try:
self.results = self.bake_query()
df = self.results.df
if df is not None:
if 'timestamp' in df.columns:
df.timestamp = pd.to_datetime(df.timestamp)
return df
'''
except Exception as e:
logging.exception(e)
self.error_msg = str(e)
'''
@property
def form(self):
return self.form_class(self.form_data)
@property
def form_class(self):
return form_factory(self)
def query_filters(self):
args = self.form_data
args = self.args
# Building filters
filters = []
for i in range(1, 10):
@ -67,9 +95,9 @@ class BaseViz(object):
"""
Building a query object
"""
args = self.form_data
groupby = args.getlist("groupby") or []
metrics = args.getlist("metrics") or ['count']
args = self.args
groupby = args.get("groupby") or []
metrics = args.get("metrics") or ['count']
granularity = args.get("granularity", "1 day")
if granularity != "all":
granularity = utils.parse_human_timedelta(
@ -85,7 +113,7 @@ class BaseViz(object):
to_dttm = utils.parse_human_datetime(until)
if from_dttm >= to_dttm:
flash("The date range doesn't seem right.", "danger")
from_dttm = to_dttm # Making them identicial to not raise
from_dttm = to_dttm # Making them identical to not raise
# extras are used to query elements specific to a datasource type
# for instance the extra where clause that applies only to Tables
@ -106,36 +134,13 @@ class BaseViz(object):
}
return d
def render_no_data(self):
self.template = "panoramix/no_data.html"
return BaseViz.render(self)
def check_and_render(self, *args, **kwards):
if (
hasattr(self, 'df') and
self.df is not None and
len(self.df) == config.ROW_LIMIT):
self.warning_msg = (
"Doh! The system limit of {} rows was reached, "
"showing partial results.").format(config.ROW_LIMIT)
if self.error_msg:
return BaseViz.render(self, error_msg=self.error_msg)
else:
return self.render(*args, **kwards)
def render(self, *args, **kwargs):
form = self.form_class(self.form_data)
return self.view.render_template(
self.template, form=form, viz=self, datasource=self.datasource,
results=self.results,
*args, **kwargs)
class TableViz(BaseViz):
verbose_name = "Table View"
template = 'panoramix/viz_table.html'
form_fields = BaseViz.form_fields + ['row_limit']
css_files = ['dataTables.bootstrap.css']
js_files = ['jquery.dataTables.min.js', 'dataTables.bootstrap.js']
def query_obj(self):
d = super(TableViz, self).query_obj()
@ -143,27 +148,25 @@ class TableViz(BaseViz):
d['timeseries_limit'] = None
return d
def render(self):
df = self.df
if df is None or df.empty:
return super(TableViz, self).render(error_msg="No data.")
else:
if (
self.form_data.get("granularity") == "all" and
'timestamp' in df):
del df['timestamp']
for m in self.metrics:
df[m + '__perc'] = np.rint((df[m] / np.max(df[m])) * 100)
return super(TableViz, self).render(df=df)
def get_df(self):
df = super(TableViz, self).get_df()
if (
self.form_data.get("granularity") == "all" and
'timestamp' in df):
del df['timestamp']
for m in self.metrics:
df[m + '__perc'] = np.rint((df[m] / np.max(df[m])) * 100)
return df
class HighchartsViz(BaseViz):
verbose_name = "Base Highcharts Viz"
template = 'panoramix/viz_highcharts.html'
chart_kind = 'line'
chart_call = "Chart"
stacked = False
chart_type = 'not_stock'
compare = False
js_files = ['highstock.js']
class BubbleViz(HighchartsViz):
@ -173,19 +176,21 @@ class BubbleViz(HighchartsViz):
form_fields = [
'viz_type', 'since', 'until',
'series', 'entity', 'x', 'y', 'size', 'limit']
js_files = ['highstock.js', 'highcharts-more.js']
def query_obj(self):
args = self.form_data
d = super(BubbleViz, self).query_obj()
d['granularity'] = 'all'
d['groupby'] = list({
request.args.get('series'),
request.args.get('entity')
})
self.x_metric = request.args.get('x')
self.y_metric = request.args.get('y')
self.z_metric = request.args.get('size')
self.entity = request.args.get('entity')
self.series = request.args.get('series')
args.get('series'),
args.get('entity')
})
self.x_metric = args.get('x')
self.y_metric = args.get('y')
self.z_metric = args.get('size')
self.entity = args.get('entity')
self.series = args.get('series')
d['metrics'] = [
self.z_metric,
self.x_metric,
@ -195,22 +200,28 @@ class BubbleViz(HighchartsViz):
raise Exception("Pick a metric for x, y and size")
return d
def render(self):
df = self.df.fillna(0)
def get_df(self):
df = super(BubbleViz, self).get_df()
df = df.fillna(0)
df['x'] = df[[self.x_metric]]
df['y'] = df[[self.y_metric]]
df['z'] = df[[self.z_metric]]
df['name'] = df[[self.entity]]
df['group'] = df[[self.series]]
return df
def get_json(self):
df = self.get_df()
chart = HighchartBubble(df)
return super(BubbleViz, self).render(chart_js=chart.javascript_cmd)
return chart.json
class TimeSeriesViz(HighchartsViz):
verbose_name = "Time Series - Line Chart"
chart_type = "spline"
stockchart = True
chart_call = "StockChart"
sort_legend_y = True
js_files = ['highstock.js', 'highcharts-more.js']
form_fields = [
'viz_type',
'granularity', ('since', 'until'),
@ -219,21 +230,17 @@ class TimeSeriesViz(HighchartsViz):
('rolling_type', 'rolling_periods'),
]
def render(self):
if request.args.get("granularity") == "all":
self.error_msg = (
"You have to select a time granularity for this view")
return super(TimeSeriesViz, self).render(error_msg=self.error_msg)
def get_df(self):
args = self.args
df = super(TimeSeriesViz, self).get_df()
metrics = self.metrics
df = self.df
df = df.pivot_table(
index="timestamp",
columns=self.groupby,
values=metrics,)
rolling_periods = request.args.get("rolling_periods")
rolling_type = request.args.get("rolling_type")
rolling_periods = args.get("rolling_periods")
rolling_type = args.get("rolling_type")
if rolling_periods and rolling_type:
if rolling_type == 'mean':
df = pd.rolling_mean(df, int(rolling_periods))
@ -241,22 +248,18 @@ class TimeSeriesViz(HighchartsViz):
df = pd.rolling_std(df, int(rolling_periods))
elif rolling_type == 'sum':
df = pd.rolling_sum(df, int(rolling_periods))
return df
def get_json(self):
df = self.get_df()
chart = Highchart(
df,
compare=self.compare,
chart_type=self.chart_type,
stacked=self.stacked,
stockchart=self.stockchart,
sort_legend_y=self.sort_legend_y,
**CHART_ARGS)
return super(TimeSeriesViz, self).render(chart_js=chart.javascript_cmd)
def bake_query(self):
"""
Doing a 2 phase query where we limit the number of series.
"""
return self.datasource.query(**self.query_obj())
return chart.json
class TimeSeriesCompareViz(TimeSeriesViz):
@ -286,32 +289,12 @@ class TimeSeriesStackedBarViz(TimeSeriesViz):
stacked = True
class DistributionBarViz(HighchartsViz):
verbose_name = "Distribution - Bar Chart"
chart_type = "column"
form_fields = BaseViz.form_fields + ['limit']
def query_obj(self):
d = super(DistributionBarViz, self).query_obj()
d['granularity'] = "all"
d['is_timeseries'] = False
return d
def render(self):
df = self.df
df = df.pivot_table(
index=self.groupby,
values=self.metrics)
df = df.sort(self.metrics[0], ascending=False)
chart = Highchart(
df, chart_type=self.chart_type, **CHART_ARGS)
return super(DistributionBarViz, self).render(
chart_js=chart.javascript_cmd)
class DistributionPieViz(HighchartsViz):
verbose_name = "Distribution - Pie Chart"
chart_type = "pie"
js_files = ['highstock.js']
form_fields = BaseViz.form_fields + ['limit']
def query_obj(self):
@ -320,16 +303,26 @@ class DistributionPieViz(HighchartsViz):
d['is_timeseries'] = False
return d
def render(self):
df = self.df
def get_df(self):
df = super(DistributionPieViz, self).get_df()
df = df.pivot_table(
index=self.groupby,
values=[self.metrics[0]])
df = df.sort(self.metrics[0], ascending=False)
return df
def get_json(self):
df = self.get_df()
chart = Highchart(
df, chart_type=self.chart_type, **CHART_ARGS)
return super(DistributionPieViz, self).render(
chart_js=chart.javascript_cmd)
self.chart_js = chart.javascript_cmd
return chart.json
class DistributionBarViz(DistributionPieViz):
verbose_name = "Distribution - Bar Chart"
chart_type = "column"
viz_types = OrderedDict([
['table', TableViz],

View File

@ -1,6 +1,6 @@
from setuptools import setup, find_packages
version = '0.2.1'
version = '0.3.0'
setup(
name='panoramix',
@ -17,6 +17,7 @@ setup(
'flask-appbuilder>=1.4.5',
'flask-login==0.2.11',
'flask-migrate>=1.5.1',
'flask-login==0.2.11',
'gunicorn>=19.3.0',
'pandas>=0.16.2',
'pydruid>=0.2.2',