diff --git a/panoramix/forms.py b/panoramix/forms.py index f287f5ca03..70da120c57 100644 --- a/panoramix/forms.py +++ b/panoramix/forms.py @@ -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), diff --git a/panoramix/static/panoramix.css b/panoramix/static/panoramix.css index 85ce8bece6..ea0753f4dc 100644 --- a/panoramix/static/panoramix.css +++ b/panoramix/static/panoramix.css @@ -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; diff --git a/panoramix/static/widgets/viz_directed_force.css b/panoramix/static/widgets/viz_directed_force.css new file mode 100644 index 0000000000..3787a274a8 --- /dev/null +++ b/panoramix/static/widgets/viz_directed_force.css @@ -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; +} diff --git a/panoramix/static/widgets/viz_directed_force.js b/panoramix/static/widgets/viz_directed_force.js new file mode 100644 index 0000000000..33bb53a119 --- /dev/null +++ b/panoramix/static/widgets/viz_directed_force.js @@ -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 = '
' + error.responseText + '
'; + 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); diff --git a/panoramix/templates/panoramix/explore.html b/panoramix/templates/panoramix/explore.html index 4a11000aef..40e7bb2f23 100644 --- a/panoramix/templates/panoramix/explore.html +++ b/panoramix/templates/panoramix/explore.html @@ -20,7 +20,7 @@