Skip to content

Commit

Permalink
Merge pull request #25 from mistercrunch/dash
Browse files Browse the repository at this point in the history
Adding basic dashboarding support!
  • Loading branch information
mistercrunch committed Sep 18, 2015
2 parents 95b0801 + cd09b0d commit c1f28a3
Show file tree
Hide file tree
Showing 16 changed files with 842 additions and 368 deletions.
2 changes: 0 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

12 changes: 8 additions & 4 deletions panoramix/highchart.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -83,7 +87,7 @@ def __init__(
if sort_legend_y:
if 'tooltip' not in chart:
chart['tooltip'] = {
'formatter': "{{TOOLTIP_FORMATTER}}"
#'formatter': "{{TOOLTIP_FORMATTER}}"
}
if self.zoom:
chart["zoomType"] = self.zoom
Expand Down Expand Up @@ -192,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
113 changes: 107 additions & 6 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_
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 All @@ -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):
Expand Down Expand Up @@ -72,6 +167,8 @@ def get_table(self, table_name):


class Table(Model, Queryable, AuditMixin):
type = "table"

__tablename__ = 'tables'
id = Column(Integer, primary_key=True)
table_name = Column(String(255), unique=True)
Expand All @@ -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
Expand Down Expand Up @@ -446,6 +546,7 @@ def refresh_datasources(self):


class Datasource(Model, AuditMixin, Queryable):
type = "datasource"

baselink = "datasourcemodelview"

Expand Down
2 changes: 2 additions & 0 deletions panoramix/static/jquery.gridster.min.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions panoramix/static/jquery.gridster.with-extras.min.js

Large diffs are not rendered by default.

Binary file added panoramix/static/loading.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
166 changes: 166 additions & 0 deletions panoramix/templates/panoramix/dashboard.html
Original file line number Diff line number Diff line change
@@ -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 %}
Loading

0 comments on commit c1f28a3

Please sign in to comment.