Skip to content

Commit

Permalink
Panoramix v1 dashboards is up
Browse files Browse the repository at this point in the history
  • Loading branch information
mistercrunch committed Sep 18, 2015
1 parent 521b000 commit f6753af
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 97 deletions.
2 changes: 1 addition & 1 deletion panoramix/highchart.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def serialize_yaxis(self):


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': {
Expand Down
29 changes: 22 additions & 7 deletions panoramix/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
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_, 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
Expand All @@ -22,8 +22,8 @@
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'])

Expand Down Expand Up @@ -52,18 +52,32 @@ def __repr__(self):
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_link(self):
def slice_url(self):
d = json.loads(self.params)
kwargs = "&".join([k + '=' + v for k, v in d.iteritems()])
url = (
from werkzeug.urls import Href
href = Href(
"/panoramix/{self.datasource_type}/"
"{self.datasource_id}/?{kwargs}").format(**locals())
"{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
Expand Down Expand Up @@ -92,6 +106,7 @@ class Dashboard(Model, AuditMixin):
__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')

Expand Down
147 changes: 108 additions & 39 deletions panoramix/templates/panoramix/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,30 @@
{% 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;
Expand All @@ -23,6 +41,9 @@
}
.slice_title {
text-align: center;
font-weight: bold;
font-size: 14px;
padding: 5px;
}
div.gridster {
visibility: hidden
Expand All @@ -35,21 +56,52 @@
{% endblock %}

{% block content_fluid %}
<div class="title"><h2>{{ dashboard.dashboard_title }}</h2></div>
<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 %}
<li
id="slice_{{ slice.id }}"
data-row="1"
data-col="{{ loop.index }}"
data-sizex="2"
data-sizey="2">
<div class="slice_title">
<h5>{{ slice.slice_name }}</h5>
</div>
<div id="slice_content_{{ slice.id }}"><img src="/static/loading.gif" class="loading"></div>
</li>
{% 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>
Expand All @@ -66,32 +118,49 @@ <h5>{{ slice.slice_name }}</h5>
f = d3.format(".4s");
</script>
<script>

$( document ).ready(function() {
$(".gridster ul").gridster({
widget_margins: [5, 5],
widget_base_dimensions: [150, 150],
resize: {enabled: true}
});
var url = "/panoramix/table/2/?flt_col_0=gender&rolling_periods=&datasource_id=2&flt_op_0=in&slice_name=Super%20Slice&viz_type=line&since=50%20years%20ago&groupby=name&metrics=total&limit=25&flt_eq_0=&granularity=one%20day&datasource_name=baby_names&where=&until=now&rolling_type=mean&datasource_type=table&skip_libs=true&standalone=true";
$.ajax({
url: url,
success: function(result){
$("#slice_content_2").html(result);
},
async: true,
});
var url = "/panoramix/table/2/?flt_col_0=gender&datasource_id=2&flt_op_0=in&viz_type=pie&since=50%20years%20ago&until=now&metrics=total&limit=10&granularity=one%20day&datasource_name=baby_names&slice_name=Pie&where=&groupby=name&flt_eq_0=&datasource_type=table&skip_libs=true&standalone=true";
$.ajax({
url: url,
success: function(result){
$("#slice_content_3").html(result);
},
async: true,
});
$("div.gridster").css('visibility', 'visible');
});
$( 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 %}

39 changes: 25 additions & 14 deletions panoramix/templates/panoramix/viz_highcharts.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{% macro viz_html(viz) %}
<div id="{{ viz.token }}" style="height:100%; width:100%;">
<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 %}

Expand All @@ -16,19 +17,29 @@
useUTC: false
},
});
$("#viz_type").click(function(){
$("#queryform").submit();
});
var url = window.location + "&json=true";
console.log(url);
$.getJSON(url, function(data){
console.log(data);
$("#{{ viz.token }}").highcharts('{{ viz.chart_call }}', data);
})
.fail(function(xhr) {
var err = '<div class="alert alert-danger">' + xhr.responseText + '</div>';
$("#{{ viz.token }}").html(err);
});
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>
{% endmacro %}
Expand Down
7 changes: 4 additions & 3 deletions panoramix/templates/panoramix/viz_table.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,16 @@
{% macro viz_js(viz) %}
{% if viz.args.get("async") != "true" %}
<script>
var url = window.location + '&async=true&standalone=true&skip_libs=true';
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();
$("img.loading").hide();
$("#{{ viz.token }} img.loading").hide();
});
</script>
{% endif %}
Expand Down
28 changes: 28 additions & 0 deletions panoramix/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,34 @@
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):
Expand Down
Loading

0 comments on commit f6753af

Please sign in to comment.