mirror of https://github.com/apache/superset.git
New viz: sunbursts
This commit is contained in:
parent
d526711f1e
commit
f5b4a0290e
|
@ -76,7 +76,11 @@ class FormFactory(object):
|
|||
'metric': SelectField(
|
||||
'Metric', choices=datasource.metrics_combo,
|
||||
default=default_metric,
|
||||
description="One or many metrics to display"),
|
||||
description="Chose the metric"),
|
||||
'secondary_metric': SelectField(
|
||||
'Color Metric', choices=datasource.metrics_combo,
|
||||
default=default_metric,
|
||||
description="A metric to use for color"),
|
||||
'groupby': BetterSelectMultipleField(
|
||||
'Group by',
|
||||
choices=self.choicify(datasource.groupby_column_names),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
function viz_nvd3(data_attribute) {
|
||||
var token_name = data_attribute['token'];
|
||||
var token = d3.select('#' + token_name);
|
||||
var json_callback = data_attribute['json_endpoint'];
|
||||
var chart = undefined;
|
||||
|
||||
|
@ -25,7 +26,6 @@ function viz_nvd3(data_attribute) {
|
|||
"#FF5A5F", "#007A87", "#7B0051", "#00D1C1", "#8CE071", "#FFB400",
|
||||
"#FFAA91", "#B4A76C", "#9CA299", "#565A5C"
|
||||
];
|
||||
var token = d3.select('#' + token_name);
|
||||
var jtoken = $('#' + token_name);
|
||||
var loading = $('#' + token_name).find("img.loading");
|
||||
var chart_div = $('#' + token_name).find("div.chart");
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
#sidebar {
|
||||
float: right;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
text.middle{
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
#sequence {
|
||||
}
|
||||
|
||||
#legend {
|
||||
padding: 10px 0 0 3px;
|
||||
}
|
||||
|
||||
#sequence text, #legend text {
|
||||
font-weight: 600;
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
#chart {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
div.token {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#chart path {
|
||||
stroke: #fff;
|
||||
}
|
||||
|
||||
#percentage {
|
||||
font-size: 2.5em;
|
||||
}
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
/*
|
||||
Modified from http://bl.ocks.org/kerryrodden/7090426
|
||||
*/
|
||||
|
||||
function viz_sunburst(data_attribute) {
|
||||
var token = d3.select('#' + data_attribute.token);
|
||||
var render = function() {
|
||||
// Breadcrumb dimensions: width, height, spacing, width of tip/tail.
|
||||
var b = {
|
||||
w: 100, h: 30, s: 3, t: 10
|
||||
};
|
||||
var colorScale;
|
||||
|
||||
// Total size of all segments; we set this later, after loading the data.
|
||||
var totalSize = 0;
|
||||
var div = token.select("#chart");
|
||||
var xy = div.node().getBoundingClientRect();
|
||||
var width = xy.width;
|
||||
var height = xy.height - 25;
|
||||
var radius = Math.min(width, height) / 2;
|
||||
|
||||
var vis = div.append("svg:svg")
|
||||
.attr("width", width)
|
||||
.attr("height", height)
|
||||
.append("svg:g")
|
||||
.attr("id", "container")
|
||||
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
|
||||
|
||||
var gMiddleText = vis.append("svg:g").attr("id", "gMiddleText");
|
||||
|
||||
var partition = d3.layout.partition()
|
||||
.size([2 * Math.PI, radius * radius])
|
||||
.value(function(d) { return d.m1; });
|
||||
|
||||
var arc = d3.svg.arc()
|
||||
.startAngle(function(d) { return d.x; })
|
||||
.endAngle(function(d) { return d.x + d.dx; })
|
||||
.innerRadius(function(d) { return Math.sqrt(d.y); })
|
||||
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy); });
|
||||
|
||||
var ext;
|
||||
d3.json(data_attribute.json_endpoint, function(json){
|
||||
var tree = buildHierarchy(json.data);
|
||||
createVisualization(tree);
|
||||
token.select("img.loading").remove();
|
||||
});
|
||||
|
||||
// Main function to draw and set up the visualization, once we have the data.
|
||||
function createVisualization(json) {
|
||||
|
||||
// Bounding circle underneath the sunburst, to make it easier to detect
|
||||
// when the mouse leaves the parent g.
|
||||
vis.append("svg:circle")
|
||||
.attr("r", radius)
|
||||
.style("opacity", 0);
|
||||
|
||||
// For efficiency, filter nodes to keep only those large enough to see.
|
||||
var nodes = partition.nodes(json)
|
||||
.filter(function(d) {
|
||||
return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
|
||||
});
|
||||
ext = d3.extent(nodes, function(d){return d.m2 / d.m1;});
|
||||
|
||||
colorScale = d3.scale.linear()
|
||||
.domain([ext[0], ext[0] + ((ext[1] - ext[0]) / 2), ext[1]])
|
||||
.range(["#00D1C1", "white","#FFB400"]);
|
||||
|
||||
var path = vis.data([json]).selectAll("path")
|
||||
.data(nodes)
|
||||
.enter().append("svg:path")
|
||||
.attr("display", function(d) { return d.depth ? null : "none"; })
|
||||
.attr("d", arc)
|
||||
.attr("fill-rule", "evenodd")
|
||||
.style("stroke", "grey")
|
||||
.style("stroke-width", "1px")
|
||||
.style("fill", function(d) { return colorScale(d.m2/d.m1); })
|
||||
.style("opacity", 1)
|
||||
.on("mouseenter", mouseenter);
|
||||
|
||||
|
||||
// Add the mouseleave handler to the bounding circle.
|
||||
token.select("#container").on("mouseleave", mouseleave);
|
||||
|
||||
// Get total size of the tree = value of root node from partition.
|
||||
totalSize = path.node().__data__.value;
|
||||
};
|
||||
f = d3.format(".3s");
|
||||
fp = d3.format(".3p");
|
||||
// Fade all but the current sequence, and show it in the breadcrumb trail.
|
||||
function mouseenter(d) {
|
||||
|
||||
var percentage = (d.m1 / totalSize).toPrecision(3);
|
||||
var percentageString = fp(percentage);
|
||||
|
||||
gMiddleText.selectAll("*").remove();
|
||||
gMiddleText.append("text")
|
||||
.classed("middle", true)
|
||||
.style("font-size", "50px")
|
||||
.text(percentageString);
|
||||
gMiddleText.append("text")
|
||||
.classed("middle", true)
|
||||
.style("font-size", "20px")
|
||||
.attr("y", "25")
|
||||
.text("m1: " + f(d.m1) + " | m2: " + f(d.m2));
|
||||
gMiddleText.append("text")
|
||||
.classed("middle", true)
|
||||
.style("font-size", "15px")
|
||||
.attr("y", "50")
|
||||
.text("m2/m1: " + fp(d.m2/d.m1));
|
||||
|
||||
var sequenceArray = getAncestors(d);
|
||||
updateBreadcrumbs(sequenceArray, percentageString);
|
||||
|
||||
// Fade all the segments.
|
||||
token.selectAll("path")
|
||||
.style("stroke-width", "1px")
|
||||
.style("opacity", 0.3);
|
||||
|
||||
// Then highlight only those that are an ancestor of the current segment.
|
||||
token.selectAll("path")
|
||||
.filter(function(node) {
|
||||
return (sequenceArray.indexOf(node) >= 0);
|
||||
})
|
||||
.style("opacity", 1)
|
||||
.style("stroke", "black")
|
||||
.style("stroke-width", "2px");
|
||||
}
|
||||
|
||||
// Restore everything to full opacity when moving off the visualization.
|
||||
function mouseleave(d) {
|
||||
|
||||
// Hide the breadcrumb trail
|
||||
token.select("#trail")
|
||||
.style("visibility", "hidden");
|
||||
gMiddleText.selectAll("*").remove();
|
||||
|
||||
// Deactivate all segments during transition.
|
||||
token.selectAll("path").on("mouseenter", null);
|
||||
//gMiddleText.selectAll("*").remove();
|
||||
|
||||
// Transition each segment to full opacity and then reactivate it.
|
||||
token.selectAll("path")
|
||||
.transition()
|
||||
.duration(200)
|
||||
.style("opacity", 1)
|
||||
.style("stroke", "grey")
|
||||
.style("stroke-width", "1px")
|
||||
.each("end", function() {
|
||||
d3.select(this).on("mouseenter", mouseenter);
|
||||
});
|
||||
}
|
||||
|
||||
// Given a node in a partition layout, return an array of all of its ancestor
|
||||
// nodes, highest first, but excluding the root.
|
||||
function getAncestors(node) {
|
||||
var path = [];
|
||||
var current = node;
|
||||
while (current.parent) {
|
||||
path.unshift(current);
|
||||
current = current.parent;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
// Generate a string that describes the points of a breadcrumb polygon.
|
||||
function breadcrumbPoints(d, i) {
|
||||
var points = [];
|
||||
points.push("0,0");
|
||||
points.push(b.w + ",0");
|
||||
points.push(b.w + b.t + "," + (b.h / 2));
|
||||
points.push(b.w + "," + b.h);
|
||||
points.push("0," + b.h);
|
||||
if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
|
||||
points.push(b.t + "," + (b.h / 2));
|
||||
}
|
||||
return points.join(" ");
|
||||
}
|
||||
|
||||
// Update the breadcrumb trail to show the current sequence and percentage.
|
||||
function updateBreadcrumbs(nodeArray, percentageString) {
|
||||
l = [];
|
||||
for(var i=0; i<nodeArray.length; i++){
|
||||
l.push(nodeArray[i].name)
|
||||
}
|
||||
s = l.join(' > ')
|
||||
gMiddleText.append("text").text(s).classed("middle", true)
|
||||
.attr("y", -75);
|
||||
}
|
||||
|
||||
function buildHierarchy(rows) {
|
||||
var root = {"name": "root", "children": []};
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var row = rows[i];
|
||||
var m1 = +row[row.length-2];
|
||||
var m2 = +row[row.length-1];
|
||||
var parts = row.slice(0, row.length-2);
|
||||
if (isNaN(m1)) { // e.g. if this is a header row
|
||||
continue;
|
||||
}
|
||||
var currentNode = root;
|
||||
for (var j = 0; j < parts.length; j++) {
|
||||
var children = currentNode["children"];
|
||||
var nodeName = parts[j];
|
||||
var childNode;
|
||||
if (j + 1 < parts.length) {
|
||||
// Not yet at the end of the sequence; move down the tree.
|
||||
var foundChild = false;
|
||||
for (var k = 0; k < children.length; k++) {
|
||||
if (children[k]["name"] == nodeName) {
|
||||
childNode = children[k];
|
||||
foundChild = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If we don't already have a child node for this branch, create it.
|
||||
if (!foundChild) {
|
||||
childNode = {"name": nodeName, "children": []};
|
||||
children.push(childNode);
|
||||
}
|
||||
currentNode = childNode;
|
||||
} else {
|
||||
// Reached the end of the sequence; create a leaf node.
|
||||
childNode = {"name": nodeName, "m1": m1, 'm2': m2};
|
||||
children.push(childNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
function recurse(node){
|
||||
if (node.children){
|
||||
var m1 = 0;
|
||||
var m2 = 0;
|
||||
for (var i=0; i<node.children.length; i++){
|
||||
sums = recurse(node.children[i]);
|
||||
m1 += sums[0];
|
||||
m2 += sums[1];
|
||||
}
|
||||
node['m1'] = m1;
|
||||
node['m2'] = m2;
|
||||
}
|
||||
return [node['m1'], node['m2']]
|
||||
}
|
||||
recurse(root);
|
||||
return root;
|
||||
};
|
||||
}
|
||||
return {
|
||||
render: render,
|
||||
resize: render,
|
||||
};
|
||||
}
|
||||
px.registerWidget('sunburst', viz_sunburst);
|
|
@ -0,0 +1,14 @@
|
|||
{% macro viz_html(viz) %}
|
||||
<div id="{{ viz.token }}" class="token">
|
||||
<div id="chart">
|
||||
<img src="{{ url_for("static", filename="img/loading.gif") }}" class="loading">
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro viz_js(viz) %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro viz_css(viz) %}
|
||||
{% endmacro %}
|
||||
|
|
@ -784,6 +784,44 @@ class DistributionBarViz(DistributionPieViz):
|
|||
})
|
||||
|
||||
|
||||
class SunburstViz(BaseViz):
|
||||
viz_type = "sunburst"
|
||||
verbose_name = "Sunburst"
|
||||
is_timeseries = False
|
||||
template = 'panoramix/viz_sunburst.html'
|
||||
js_files = [
|
||||
'lib/d3.min.js',
|
||||
'widgets/viz_sunburst.js']
|
||||
css_files = ['widgets/viz_sunburst.css']
|
||||
fieldsets = (
|
||||
{
|
||||
'label': None,
|
||||
'fields': (
|
||||
'viz_type',
|
||||
('since', 'until'),
|
||||
'groupby',
|
||||
'metric', 'secondary_metric',
|
||||
'limit',
|
||||
)
|
||||
},)
|
||||
|
||||
def get_df(self):
|
||||
df = super(SunburstViz, self).get_df()
|
||||
return df
|
||||
|
||||
def get_json_data(self):
|
||||
df = self.get_df()
|
||||
# if m1 == m2 dupplicate the metric column
|
||||
if self.form_data['metric'] == self.form_data['secondary_metric']:
|
||||
df['dup'] = df[df.columns[-1]]
|
||||
return df.to_json(orient="values")
|
||||
|
||||
def query_obj(self):
|
||||
qry = super(SunburstViz, self).query_obj()
|
||||
qry['metrics'] = [
|
||||
self.form_data['metric'], self.form_data['secondary_metric']]
|
||||
return qry
|
||||
|
||||
viz_types_list = [
|
||||
TableViz,
|
||||
PivotTableViz,
|
||||
|
@ -797,6 +835,7 @@ viz_types_list = [
|
|||
MarkupViz,
|
||||
WordCloudViz,
|
||||
BigNumberViz,
|
||||
SunburstViz,
|
||||
]
|
||||
# This dict is used to
|
||||
viz_types = OrderedDict([(v.viz_type, v) for v in viz_types_list])
|
||||
|
|
Loading…
Reference in New Issue