Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding directed force layout viz #84

Merged
merged 1 commit into from
Dec 15, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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])