mirror of https://github.com/apache/superset.git
Merge pull request #84 from mistercrunch/force
Adding directed force layout viz
This commit is contained in:
commit
d4588e00ea
|
@ -134,6 +134,34 @@ class FormFactory(object):
|
|||
"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),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
|
@ -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">
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{% macro viz_html(viz) %}
|
||||
<div id="chart"></div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro viz_js(viz) %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro viz_css(viz) %}
|
||||
{% endmacro %}
|
|
@ -878,6 +878,52 @@ class SunburstViz(BaseViz):
|
|||
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,
|
||||
|
@ -892,6 +938,7 @@ viz_types_list = [
|
|||
WordCloudViz,
|
||||
BigNumberViz,
|
||||
SunburstViz,
|
||||
DirectedForceViz,
|
||||
]
|
||||
# This dict is used to
|
||||
viz_types = OrderedDict([(v.viz_type, v) for v in viz_types_list])
|
||||
|
|
Loading…
Reference in New Issue