Skip to content

Commit

Permalink
Merge pull request #84 from mistercrunch/force
Browse files Browse the repository at this point in the history
Adding directed force layout viz
  • Loading branch information
mistercrunch committed Dec 15, 2015
2 parents f1ceac9 + c563057 commit d4588e0
Show file tree
Hide file tree
Showing 7 changed files with 271 additions and 2 deletions.
28 changes: 28 additions & 0 deletions panoramix/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,34 @@ def __init__(self, viz):
"The time granularity for the visualization. Note that you "
"can type and use simple natural language as in '10 seconds', "
"'1 day' or '56 weeks'")),
'link_length': FreeFormSelectField(
'Link Length', default="200",
choices=self.choicify([
'10',
'25',
'50',
'75',
'100',
'150',
'200',
'250',
]),
description="Link length in the force layout"),
'charge': FreeFormSelectField(
'Charge', default="-500",
choices=self.choicify([
'-50',
'-75',
'-100',
'-150',
'-200',
'-250',
'-500',
'-1000',
'-2500',
'-5000',
]),
description="Charge in the force layout"),
'granularity_sqla': SelectField(
'Time Column', default=datasource.main_dttm_col,
choices=self.choicify(datasource.dttm_cols),
Expand Down
2 changes: 1 addition & 1 deletion panoramix/static/panoramix.css
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ legend {
border-color: #AAA;
opacity: 0.3;
}
.dashboard .gridster li {
.gridster li.widget{
list-style-type: none;
border: 1px solid gray;
overflow: hidden;
Expand Down
22 changes: 22 additions & 0 deletions panoramix/static/widgets/viz_directed_force.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.directed_force path.link {
fill: none;
stroke: #000;
stroke-width: 1.5px;
}
.directed_force #chart {
height: 100%;
}

.directed_force circle {
fill: #ccc;
stroke: #000;
stroke-width: 1.5px;
stroke-opacity: 1;
opacity: 0.75;
}

.directed_force text {
fill: #000;
font: 10px sans-serif;
pointer-events: none;
}
163 changes: 163 additions & 0 deletions panoramix/static/widgets/viz_directed_force.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
Modified from http://bl.ocks.org/d3noob/5141278
*/

function viz_directed_force(data_attribute) {
var token = d3.select('#' + data_attribute.token);
var xy = token.select('#chart').node().getBoundingClientRect();
var width = xy.width;
var height = xy.height - 25;
var radius = Math.min(width, height) / 2;
var link_length = data_attribute.form_data['link_length'];
if (link_length === undefined){
link_length = 200;
}
var charge = data_attribute.form_data['charge'];
if (charge === undefined){
charge = -500;
}
var render = function(done) {
d3.json(data_attribute.json_endpoint, function(error, json) {

if (error != null){
var err = '<div class="alert alert-danger">' + error.responseText + '</div>';
token.html(err);
done();
return '';
}
links = json.data;
var nodes = {};
// Compute the distinct nodes from the links.
links.forEach(function(link) {
link.source = nodes[link.source] ||
(nodes[link.source] = {name: link.source});
link.target = nodes[link.target] ||
(nodes[link.target] = {name: link.target});
link.value = +link.value;
var target_name = link.target.name;
var source_name = link.source.name;
if (nodes[target_name]['total'] === undefined)
nodes[target_name]['total'] = link.value;
if (nodes[source_name]['total'] === undefined)
nodes[source_name]['total'] = 0;
if (nodes[target_name]['max'] === undefined)
nodes[target_name]['max'] = 0;
if (link.value > nodes[target_name]['max'])
nodes[target_name]['max'] = link.value;
if (nodes[target_name]['min'] === undefined)
nodes[target_name]['min'] = 0;
if (link.value > nodes[target_name]['min'])
nodes[target_name]['min'] = link.value;

nodes[target_name]['total'] += link.value;
});

var force = d3.layout.force()
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(link_length)
.charge(charge)
.on("tick", tick)
.start();

var svg = token.select("#chart").append("svg")
.attr("width", width)
.attr("height", height);

// build the arrow.
svg.append("svg:defs").selectAll("marker")
.data(["end"]) // Different link/path types can be defined here
.enter().append("svg:marker") // This section adds in the arrows
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", -1.5)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");

var edgeScale = d3.scale.linear()
.range([0.1, 0.5]);
// add the links and the arrows
var path = svg.append("svg:g").selectAll("path")
.data(force.links())
.enter().append("svg:path")
//.attr("class", function(d) { return "link " + d.type; })
.attr("class", "link")
.style("opacity", function(d){
return edgeScale(d.value/d.target.max);
})
.attr("marker-end", "url(#end)");

// define the nodes
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
.on("mouseenter", function(d){
d3.select(this)
.select("circle")
.transition()
.style('stroke-width', 5);
d3.select(this)
.select("text")
.transition()
.style('font-size', 25);
})
.on("mouseleave", function(d){
d3.select(this)
.select("circle")
.transition()
.style('stroke-width', 1.5);
d3.select(this)
.select("text")
.transition()
.style('font-size', 12);
})
.call(force.drag);

// add the nodes
var ext = d3.extent(d3.values(nodes), function(d){return Math.sqrt(d.total);})
var circleScale = d3.scale.linear()
.domain(ext)
.range([3, 30]);

node.append("circle")
.attr("r", function(d){return circleScale(Math.sqrt(d.total));});

// add the text
node.append("text")
.attr("x", 6)
.attr("dy", ".35em")
.text(function(d) { return d.name; });

// add the curvy lines
function tick() {
path.attr("d", function(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" +
d.source.x + "," +
d.source.y + "A" +
dr + "," + dr + " 0 0,1 " +
d.target.x + "," +
d.target.y;
});

node
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")"; });
}
done(json);
});
}
return {
render: render,
resize: render,
};
}
px.registerWidget('directed_force', viz_directed_force);
2 changes: 1 addition & 1 deletion panoramix/templates/panoramix/explore.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<div class="container-fluid datasource">
<form id="query" method="GET" style="display: none;">
<div class="header">
<a class="btn btn-primary" data-toggle="tooltip" title="Slice!">
<a class="btn btn-primary druidify" data-toggle="tooltip" title="Slice!">
<i class="fa fa-bolt"></i>
</a>
<span class="btn btn-default notbtn">
Expand Down
9 changes: 9 additions & 0 deletions panoramix/templates/panoramix/viz_directed_force.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% macro viz_html(viz) %}
<div id="chart"></div>
{% endmacro %}

{% macro viz_js(viz) %}
{% endmacro %}

{% macro viz_css(viz) %}
{% endmacro %}
47 changes: 47 additions & 0 deletions panoramix/viz.py
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,52 @@ def query_obj(self):
self.form_data['metric'], self.form_data['secondary_metric']]
return qry

class DirectedForceViz(BaseViz):
viz_type = "directed_force"
verbose_name = "Directed Force Layout"
is_timeseries = False
template = 'panoramix/viz_directed_force.html'
js_files = [
'lib/d3.min.js',
'widgets/viz_directed_force.js']
css_files = ['widgets/viz_directed_force.css']
fieldsets = (
{
'label': None,
'fields': (
'granularity',
('since', 'until'),
'groupby',
'metric',
'row_limit',
)
},
{
'label': 'Force Layout',
'fields': (
'link_length',
'charge',
)
},)
form_overrides = {
'groupby': {
'label': 'Source / Target',
'description': "Choose a source and a target",
},
}
def query_obj(self):
qry = super(DirectedForceViz, self).query_obj()
if len(self.form_data['groupby']) != 2:
raise Exception("Pick exactly 2 columns to 'Group By'")
qry['metrics'] = [self.form_data['metric']]
return qry

def get_json_data(self):
df = self.get_df()
df.columns = ['source', 'target', 'value']
d = df.to_dict(orient='records')
return dumps(d)

viz_types_list = [
TableViz,
PivotTableViz,
Expand All @@ -892,6 +938,7 @@ def query_obj(self):
WordCloudViz,
BigNumberViz,
SunburstViz,
DirectedForceViz,
]
# This dict is used to
viz_types = OrderedDict([(v.viz_type, v) for v in viz_types_list])

0 comments on commit d4588e0

Please sign in to comment.