mirror of
https://github.com/apache/superset.git
synced 2024-09-12 16:49:40 -04:00
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 "
|
"The time granularity for the visualization. Note that you "
|
||||||
"can type and use simple natural language as in '10 seconds', "
|
"can type and use simple natural language as in '10 seconds', "
|
||||||
"'1 day' or '56 weeks'")),
|
"'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(
|
'granularity_sqla': SelectField(
|
||||||
'Time Column', default=datasource.main_dttm_col,
|
'Time Column', default=datasource.main_dttm_col,
|
||||||
choices=self.choicify(datasource.dttm_cols),
|
choices=self.choicify(datasource.dttm_cols),
|
||||||
|
@ -139,7 +139,7 @@ legend {
|
|||||||
border-color: #AAA;
|
border-color: #AAA;
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
.dashboard .gridster li {
|
.gridster li.widget{
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
border: 1px solid gray;
|
border: 1px solid gray;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
22
panoramix/static/widgets/viz_directed_force.css
Normal file
22
panoramix/static/widgets/viz_directed_force.css
Normal file
@ -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
panoramix/static/widgets/viz_directed_force.js
Normal file
163
panoramix/static/widgets/viz_directed_force.js
Normal file
@ -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">
|
<div class="container-fluid datasource">
|
||||||
<form id="query" method="GET" style="display: none;">
|
<form id="query" method="GET" style="display: none;">
|
||||||
<div class="header">
|
<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>
|
<i class="fa fa-bolt"></i>
|
||||||
</a>
|
</a>
|
||||||
<span class="btn btn-default notbtn">
|
<span class="btn btn-default notbtn">
|
||||||
|
9
panoramix/templates/panoramix/viz_directed_force.html
Normal file
9
panoramix/templates/panoramix/viz_directed_force.html
Normal file
@ -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']]
|
self.form_data['metric'], self.form_data['secondary_metric']]
|
||||||
return qry
|
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 = [
|
viz_types_list = [
|
||||||
TableViz,
|
TableViz,
|
||||||
PivotTableViz,
|
PivotTableViz,
|
||||||
@ -892,6 +938,7 @@ viz_types_list = [
|
|||||||
WordCloudViz,
|
WordCloudViz,
|
||||||
BigNumberViz,
|
BigNumberViz,
|
||||||
SunburstViz,
|
SunburstViz,
|
||||||
|
DirectedForceViz,
|
||||||
]
|
]
|
||||||
# This dict is used to
|
# This dict is used to
|
||||||
viz_types = OrderedDict([(v.viz_type, v) for v in viz_types_list])
|
viz_types = OrderedDict([(v.viz_type, v) for v in viz_types_list])
|
||||||
|
Loading…
Reference in New Issue
Block a user