mirror of https://github.com/apache/superset.git
Adding Parallel coordinates viz
This commit is contained in:
parent
2885227050
commit
73989f41ac
1
TODO.md
1
TODO.md
|
@ -2,6 +2,7 @@
|
||||||
List of TODO items for Panoramix
|
List of TODO items for Panoramix
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
* Slider form element
|
||||||
* **Dashboard URL filters:** `{dash_url}#fltin__fieldname__value1,value2`
|
* **Dashboard URL filters:** `{dash_url}#fltin__fieldname__value1,value2`
|
||||||
* **Default slice:** choose a default slice for the dataset instead of default endpoint
|
* **Default slice:** choose a default slice for the dataset instead of default endpoint
|
||||||
* **refresh freq**: specifying the refresh frequency of a dashboard and specific slices within it, some randomization would be nice
|
* **refresh freq**: specifying the refresh frequency of a dashboard and specific slices within it, some randomization would be nice
|
||||||
|
|
|
@ -371,6 +371,9 @@ class FormFactory(object):
|
||||||
"Range Filter", default=False,
|
"Range Filter", default=False,
|
||||||
description=(
|
description=(
|
||||||
"Whether to display the time range interactive selector")),
|
"Whether to display the time range interactive selector")),
|
||||||
|
'show_datatable': BetterBooleanField(
|
||||||
|
"Data Table", default=False,
|
||||||
|
description="Whether to display the interactive data table"),
|
||||||
'include_search': BetterBooleanField(
|
'include_search': BetterBooleanField(
|
||||||
"Search Box", default=False,
|
"Search Box", default=False,
|
||||||
description=(
|
description=(
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
.parcoords svg, .parcoords canvas {
|
||||||
|
font-size: 12px;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.parcoords > canvas {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcoords text.label {
|
||||||
|
font: 100%;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcoords rect.background {
|
||||||
|
fill: transparent;
|
||||||
|
}
|
||||||
|
.parcoords rect.background:hover {
|
||||||
|
fill: rgba(120,120,120,0.2);
|
||||||
|
}
|
||||||
|
.parcoords .resize rect {
|
||||||
|
fill: rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.parcoords rect.extent {
|
||||||
|
fill: rgba(255,255,255,0.25);
|
||||||
|
stroke: rgba(0,0,0,0.6);
|
||||||
|
}
|
||||||
|
.parcoords .axis line, .parcoords .axis path {
|
||||||
|
fill: none;
|
||||||
|
stroke: #222;
|
||||||
|
shape-rendering: crispEdges;
|
||||||
|
}
|
||||||
|
.parcoords canvas {
|
||||||
|
opacity: 1;
|
||||||
|
-moz-transition: opacity 0.3s;
|
||||||
|
-webkit-transition: opacity 0.3s;
|
||||||
|
-o-transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
.parcoords canvas.faded {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
.parcoords {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* data table styles */
|
||||||
|
.parcoords .row, .parcoords .header {
|
||||||
|
clear: left; font-size: 12px; line-height: 18px; height: 18px;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
.parcoords .row:nth-child(odd) { background: rgba(0,0,0,0.05); }
|
||||||
|
.parcoords .header { font-weight: bold; }
|
||||||
|
.parcoords .cell { float: left; overflow: hidden; white-space: nowrap; width: 100px; height: 18px; }
|
||||||
|
.parcoords .col-0 { width: 180px; }
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,59 @@
|
||||||
|
// http://bl.ocks.org/3687826
|
||||||
|
d3.divgrid = function(config) {
|
||||||
|
var columns = [];
|
||||||
|
|
||||||
|
var dg = function(selection) {
|
||||||
|
if (columns.length == 0) columns = d3.keys(selection.data()[0][0]);
|
||||||
|
|
||||||
|
// header
|
||||||
|
selection.selectAll(".header")
|
||||||
|
.data([true])
|
||||||
|
.enter().append("div")
|
||||||
|
.attr("class", "header")
|
||||||
|
|
||||||
|
var header = selection.select(".header")
|
||||||
|
.selectAll(".cell")
|
||||||
|
.data(columns);
|
||||||
|
|
||||||
|
header.enter().append("div")
|
||||||
|
.attr("class", function(d,i) { return "col-" + i; })
|
||||||
|
.classed("cell", true)
|
||||||
|
|
||||||
|
selection.selectAll(".header .cell")
|
||||||
|
.text(function(d) { return d; });
|
||||||
|
|
||||||
|
header.exit().remove();
|
||||||
|
|
||||||
|
// rows
|
||||||
|
var rows = selection.selectAll(".row")
|
||||||
|
.data(function(d) { return d; })
|
||||||
|
|
||||||
|
rows.enter().append("div")
|
||||||
|
.attr("class", "row")
|
||||||
|
|
||||||
|
rows.exit().remove();
|
||||||
|
|
||||||
|
var cells = selection.selectAll(".row").selectAll(".cell")
|
||||||
|
.data(function(d) { return columns.map(function(col){return d[col];}) })
|
||||||
|
|
||||||
|
// cells
|
||||||
|
cells.enter().append("div")
|
||||||
|
.attr("class", function(d,i) { return "col-" + i; })
|
||||||
|
.classed("cell", true)
|
||||||
|
|
||||||
|
cells.exit().remove();
|
||||||
|
|
||||||
|
selection.selectAll(".cell")
|
||||||
|
.text(function(d) { return d; });
|
||||||
|
|
||||||
|
return dg;
|
||||||
|
};
|
||||||
|
|
||||||
|
dg.columns = function(_) {
|
||||||
|
if (!arguments.length) return columns;
|
||||||
|
columns = _;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
return dg;
|
||||||
|
};
|
|
@ -0,0 +1,75 @@
|
||||||
|
px.registerViz('para', function(slice) {
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
$('#code').attr('rows', '15')
|
||||||
|
$.getJSON(slice.jsonEndpoint(), function(payload) {
|
||||||
|
var data = payload.data;
|
||||||
|
var fd = payload.form_data;
|
||||||
|
ext = d3.extent(data, function(d){
|
||||||
|
return d[fd.secondary_metric];
|
||||||
|
});
|
||||||
|
ext = [ext[0], (ext[1]-ext[0])/2,ext[1]];
|
||||||
|
var cScale = d3.scale.linear()
|
||||||
|
.domain(ext)
|
||||||
|
.range(['red', 'grey', 'blue'])
|
||||||
|
.interpolate(d3.interpolateLab);
|
||||||
|
var color = function(d){return cScale(d[fd.secondary_metric])};
|
||||||
|
var container = d3.select(slice.selector);
|
||||||
|
if (fd.show_datatable)
|
||||||
|
var eff_height = slice.height() / 2;
|
||||||
|
else
|
||||||
|
var eff_height = slice.height();
|
||||||
|
|
||||||
|
var div = container.append('div')
|
||||||
|
.attr('id', 'parcoords_' + slice.container_id)
|
||||||
|
.style('height', eff_height + 'px')
|
||||||
|
.classed("parcoords", true);
|
||||||
|
var parcoords = d3.parcoords()('#parcoords_' + slice.container_id)
|
||||||
|
.width(slice.width())
|
||||||
|
.color(color)
|
||||||
|
.alpha(0.5)
|
||||||
|
.composite("darken")
|
||||||
|
.height(eff_height)
|
||||||
|
.data(payload.data)
|
||||||
|
.render()
|
||||||
|
.createAxes()
|
||||||
|
.shadows()
|
||||||
|
.reorderable()
|
||||||
|
.brushMode("1D-axes");
|
||||||
|
|
||||||
|
if (fd.show_datatable) {
|
||||||
|
// create data table, row hover highlighting
|
||||||
|
var grid = d3.divgrid();
|
||||||
|
container.append("div")
|
||||||
|
.datum(data.slice(0,10))
|
||||||
|
.attr('id', "grid")
|
||||||
|
.call(grid)
|
||||||
|
.classed("parcoords", true)
|
||||||
|
.selectAll(".row")
|
||||||
|
.on({
|
||||||
|
"mouseover": function(d) { parcoords.highlight([d]) },
|
||||||
|
"mouseout": parcoords.unhighlight
|
||||||
|
});
|
||||||
|
// update data table on brush event
|
||||||
|
parcoords.on("brush", function(d) {
|
||||||
|
d3.select("#grid")
|
||||||
|
.datum(d.slice(0,10))
|
||||||
|
.call(grid)
|
||||||
|
.selectAll(".row")
|
||||||
|
.on({
|
||||||
|
"mouseover": function(d) { parcoords.highlight([d]) },
|
||||||
|
"mouseout": parcoords.unhighlight
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
slice.done();
|
||||||
|
})
|
||||||
|
.fail(function(xhr) {
|
||||||
|
slice.error(xhr.responseText);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
render: refresh,
|
||||||
|
resize: refresh,
|
||||||
|
};
|
||||||
|
});
|
|
@ -1158,6 +1158,44 @@ class IFrameViz(BaseViz):
|
||||||
},)
|
},)
|
||||||
|
|
||||||
|
|
||||||
|
class ParallelCoordinatesViz(BaseViz):
|
||||||
|
viz_type = "para"
|
||||||
|
verbose_name = "Parallel Coordinates"
|
||||||
|
is_timeseries = False
|
||||||
|
js_files = [
|
||||||
|
'lib/para/d3.parcoords.js',
|
||||||
|
'lib/para/divgrid.js',
|
||||||
|
'widgets/viz_para.js']
|
||||||
|
css_files = ['lib/para/d3.parcoords.css']
|
||||||
|
fieldsets = (
|
||||||
|
{
|
||||||
|
'label': None,
|
||||||
|
'fields': (
|
||||||
|
'granularity',
|
||||||
|
('since', 'until'),
|
||||||
|
'series',
|
||||||
|
'metrics',
|
||||||
|
'secondary_metric',
|
||||||
|
'limit',
|
||||||
|
('show_datatable', None),
|
||||||
|
)
|
||||||
|
},)
|
||||||
|
def query_obj(self):
|
||||||
|
d = super(ParallelCoordinatesViz, self).query_obj()
|
||||||
|
fd = self.form_data
|
||||||
|
d['metrics'] = fd.get('metrics')
|
||||||
|
second = fd.get('secondary_metric')
|
||||||
|
if second not in d['metrics']:
|
||||||
|
d['metrics'] += [second]
|
||||||
|
d['groupby'] = [fd.get('series')]
|
||||||
|
return d
|
||||||
|
|
||||||
|
def get_json_data(self):
|
||||||
|
df = self.get_df()
|
||||||
|
df = df[[self.form_data.get('series')] + self.form_data.get('metrics')]
|
||||||
|
return df.to_json(orient="records")
|
||||||
|
|
||||||
|
|
||||||
viz_types_list = [
|
viz_types_list = [
|
||||||
TableViz,
|
TableViz,
|
||||||
PivotTableViz,
|
PivotTableViz,
|
||||||
|
@ -1177,6 +1215,7 @@ viz_types_list = [
|
||||||
WorldMapViz,
|
WorldMapViz,
|
||||||
FilterBoxViz,
|
FilterBoxViz,
|
||||||
IFrameViz,
|
IFrameViz,
|
||||||
|
ParallelCoordinatesViz,
|
||||||
]
|
]
|
||||||
|
|
||||||
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