mirror of
https://github.com/apache/superset.git
synced 2024-09-12 08:39:45 -04:00
Heatmap
This commit is contained in:
parent
a2f14b3787
commit
0f58c609d0
@ -142,6 +142,14 @@ class FormFactory(object):
|
||||
'Columns',
|
||||
choices=self.choicify(datasource.column_names),
|
||||
description="Columns to display"),
|
||||
'all_columns_x': SelectField(
|
||||
'X',
|
||||
choices=self.choicify(datasource.column_names),
|
||||
description="Columns to display"),
|
||||
'all_columns_y': SelectField(
|
||||
'Y',
|
||||
choices=self.choicify(datasource.column_names),
|
||||
description="Columns to display"),
|
||||
'granularity': FreeFormSelectField(
|
||||
'Time Granularity', default="one day",
|
||||
choices=self.choicify([
|
||||
|
55
panoramix/static/lib/d3.tip.css
Normal file
55
panoramix/static/lib/d3.tip.css
Normal file
@ -0,0 +1,55 @@
|
||||
.d3-tip {
|
||||
line-height: 1;
|
||||
font-weight: bold;
|
||||
padding: 12px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: #fff;
|
||||
border-radius: 2px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Creates a small triangle extender for the tooltip */
|
||||
.d3-tip:after {
|
||||
box-sizing: border-box;
|
||||
display: inline;
|
||||
font-size: 10px;
|
||||
width: 100%;
|
||||
line-height: 1;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Northward tooltips */
|
||||
.d3-tip.n:after {
|
||||
content: "\25BC";
|
||||
margin: -1px 0 0 0;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Eastward tooltips */
|
||||
.d3-tip.e:after {
|
||||
content: "\25C0";
|
||||
margin: -4px 0 0 0;
|
||||
top: 50%;
|
||||
left: -8px;
|
||||
}
|
||||
|
||||
/* Southward tooltips */
|
||||
.d3-tip.s:after {
|
||||
content: "\25B2";
|
||||
margin: 0 0 1px 0;
|
||||
top: -8px;
|
||||
left: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Westward tooltips */
|
||||
.d3-tip.w:after {
|
||||
content: "\25B6";
|
||||
margin: -4px 0 0 -1px;
|
||||
top: 50%;
|
||||
left: 100%;
|
||||
}
|
324
panoramix/static/lib/d3.tip.js
Normal file
324
panoramix/static/lib/d3.tip.js
Normal file
@ -0,0 +1,324 @@
|
||||
// d3.tip
|
||||
// Copyright (c) 2013 Justin Palmer
|
||||
//
|
||||
// Tooltips for d3.js SVG visualizations
|
||||
|
||||
(function (root, factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module with d3 as a dependency.
|
||||
define(['d3'], factory)
|
||||
} else if (typeof module === 'object' && module.exports) {
|
||||
// CommonJS
|
||||
module.exports = function(d3) {
|
||||
d3.tip = factory(d3)
|
||||
return d3.tip
|
||||
}
|
||||
} else {
|
||||
// Browser global.
|
||||
root.d3.tip = factory(root.d3)
|
||||
}
|
||||
}(this, function (d3) {
|
||||
|
||||
// Public - contructs a new tooltip
|
||||
//
|
||||
// Returns a tip
|
||||
return function() {
|
||||
var direction = d3_tip_direction,
|
||||
offset = d3_tip_offset,
|
||||
html = d3_tip_html,
|
||||
node = initNode(),
|
||||
svg = null,
|
||||
point = null,
|
||||
target = null
|
||||
|
||||
function tip(vis) {
|
||||
svg = getSVGNode(vis)
|
||||
point = svg.createSVGPoint()
|
||||
document.body.appendChild(node)
|
||||
}
|
||||
|
||||
// Public - show the tooltip on the screen
|
||||
//
|
||||
// Returns a tip
|
||||
tip.show = function() {
|
||||
var args = Array.prototype.slice.call(arguments)
|
||||
if(args[args.length - 1] instanceof SVGElement) target = args.pop()
|
||||
|
||||
var content = html.apply(this, args),
|
||||
poffset = offset.apply(this, args),
|
||||
dir = direction.apply(this, args),
|
||||
nodel = getNodeEl(),
|
||||
i = directions.length,
|
||||
coords,
|
||||
scrollTop = document.documentElement.scrollTop || document.body.scrollTop,
|
||||
scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft
|
||||
|
||||
nodel.html(content)
|
||||
.style({ opacity: 1, 'pointer-events': 'all' })
|
||||
|
||||
while(i--) nodel.classed(directions[i], false)
|
||||
coords = direction_callbacks.get(dir).apply(this)
|
||||
nodel.classed(dir, true).style({
|
||||
top: (coords.top + poffset[0]) + scrollTop + 'px',
|
||||
left: (coords.left + poffset[1]) + scrollLeft + 'px'
|
||||
})
|
||||
|
||||
return tip
|
||||
}
|
||||
|
||||
// Public - hide the tooltip
|
||||
//
|
||||
// Returns a tip
|
||||
tip.hide = function() {
|
||||
var nodel = getNodeEl()
|
||||
nodel.style({ opacity: 0, 'pointer-events': 'none' })
|
||||
return tip
|
||||
}
|
||||
|
||||
// Public: Proxy attr calls to the d3 tip container. Sets or gets attribute value.
|
||||
//
|
||||
// n - name of the attribute
|
||||
// v - value of the attribute
|
||||
//
|
||||
// Returns tip or attribute value
|
||||
tip.attr = function(n, v) {
|
||||
if (arguments.length < 2 && typeof n === 'string') {
|
||||
return getNodeEl().attr(n)
|
||||
} else {
|
||||
var args = Array.prototype.slice.call(arguments)
|
||||
d3.selection.prototype.attr.apply(getNodeEl(), args)
|
||||
}
|
||||
|
||||
return tip
|
||||
}
|
||||
|
||||
// Public: Proxy style calls to the d3 tip container. Sets or gets a style value.
|
||||
//
|
||||
// n - name of the property
|
||||
// v - value of the property
|
||||
//
|
||||
// Returns tip or style property value
|
||||
tip.style = function(n, v) {
|
||||
if (arguments.length < 2 && typeof n === 'string') {
|
||||
return getNodeEl().style(n)
|
||||
} else {
|
||||
var args = Array.prototype.slice.call(arguments)
|
||||
d3.selection.prototype.style.apply(getNodeEl(), args)
|
||||
}
|
||||
|
||||
return tip
|
||||
}
|
||||
|
||||
// Public: Set or get the direction of the tooltip
|
||||
//
|
||||
// v - One of n(north), s(south), e(east), or w(west), nw(northwest),
|
||||
// sw(southwest), ne(northeast) or se(southeast)
|
||||
//
|
||||
// Returns tip or direction
|
||||
tip.direction = function(v) {
|
||||
if (!arguments.length) return direction
|
||||
direction = v == null ? v : d3.functor(v)
|
||||
|
||||
return tip
|
||||
}
|
||||
|
||||
// Public: Sets or gets the offset of the tip
|
||||
//
|
||||
// v - Array of [x, y] offset
|
||||
//
|
||||
// Returns offset or
|
||||
tip.offset = function(v) {
|
||||
if (!arguments.length) return offset
|
||||
offset = v == null ? v : d3.functor(v)
|
||||
|
||||
return tip
|
||||
}
|
||||
|
||||
// Public: sets or gets the html value of the tooltip
|
||||
//
|
||||
// v - String value of the tip
|
||||
//
|
||||
// Returns html value or tip
|
||||
tip.html = function(v) {
|
||||
if (!arguments.length) return html
|
||||
html = v == null ? v : d3.functor(v)
|
||||
|
||||
return tip
|
||||
}
|
||||
|
||||
// Public: destroys the tooltip and removes it from the DOM
|
||||
//
|
||||
// Returns a tip
|
||||
tip.destroy = function() {
|
||||
if(node) {
|
||||
getNodeEl().remove();
|
||||
node = null;
|
||||
}
|
||||
return tip;
|
||||
}
|
||||
|
||||
function d3_tip_direction() { return 'n' }
|
||||
function d3_tip_offset() { return [0, 0] }
|
||||
function d3_tip_html() { return ' ' }
|
||||
|
||||
var direction_callbacks = d3.map({
|
||||
n: direction_n,
|
||||
s: direction_s,
|
||||
e: direction_e,
|
||||
w: direction_w,
|
||||
nw: direction_nw,
|
||||
ne: direction_ne,
|
||||
sw: direction_sw,
|
||||
se: direction_se
|
||||
}),
|
||||
|
||||
directions = direction_callbacks.keys()
|
||||
|
||||
function direction_n() {
|
||||
var bbox = getScreenBBox()
|
||||
return {
|
||||
top: bbox.n.y - node.offsetHeight,
|
||||
left: bbox.n.x - node.offsetWidth / 2
|
||||
}
|
||||
}
|
||||
|
||||
function direction_s() {
|
||||
var bbox = getScreenBBox()
|
||||
return {
|
||||
top: bbox.s.y,
|
||||
left: bbox.s.x - node.offsetWidth / 2
|
||||
}
|
||||
}
|
||||
|
||||
function direction_e() {
|
||||
var bbox = getScreenBBox()
|
||||
return {
|
||||
top: bbox.e.y - node.offsetHeight / 2,
|
||||
left: bbox.e.x
|
||||
}
|
||||
}
|
||||
|
||||
function direction_w() {
|
||||
var bbox = getScreenBBox()
|
||||
return {
|
||||
top: bbox.w.y - node.offsetHeight / 2,
|
||||
left: bbox.w.x - node.offsetWidth
|
||||
}
|
||||
}
|
||||
|
||||
function direction_nw() {
|
||||
var bbox = getScreenBBox()
|
||||
return {
|
||||
top: bbox.nw.y - node.offsetHeight,
|
||||
left: bbox.nw.x - node.offsetWidth
|
||||
}
|
||||
}
|
||||
|
||||
function direction_ne() {
|
||||
var bbox = getScreenBBox()
|
||||
return {
|
||||
top: bbox.ne.y - node.offsetHeight,
|
||||
left: bbox.ne.x
|
||||
}
|
||||
}
|
||||
|
||||
function direction_sw() {
|
||||
var bbox = getScreenBBox()
|
||||
return {
|
||||
top: bbox.sw.y,
|
||||
left: bbox.sw.x - node.offsetWidth
|
||||
}
|
||||
}
|
||||
|
||||
function direction_se() {
|
||||
var bbox = getScreenBBox()
|
||||
return {
|
||||
top: bbox.se.y,
|
||||
left: bbox.e.x
|
||||
}
|
||||
}
|
||||
|
||||
function initNode() {
|
||||
var node = d3.select(document.createElement('div'))
|
||||
node.style({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
opacity: 0,
|
||||
'pointer-events': 'none',
|
||||
'box-sizing': 'border-box'
|
||||
})
|
||||
|
||||
return node.node()
|
||||
}
|
||||
|
||||
function getSVGNode(el) {
|
||||
el = el.node()
|
||||
if(el.tagName.toLowerCase() === 'svg')
|
||||
return el
|
||||
|
||||
return el.ownerSVGElement
|
||||
}
|
||||
|
||||
function getNodeEl() {
|
||||
if(node === null) {
|
||||
node = initNode();
|
||||
// re-add node to DOM
|
||||
document.body.appendChild(node);
|
||||
};
|
||||
return d3.select(node);
|
||||
}
|
||||
|
||||
// Private - gets the screen coordinates of a shape
|
||||
//
|
||||
// Given a shape on the screen, will return an SVGPoint for the directions
|
||||
// n(north), s(south), e(east), w(west), ne(northeast), se(southeast), nw(northwest),
|
||||
// sw(southwest).
|
||||
//
|
||||
// +-+-+
|
||||
// | |
|
||||
// + +
|
||||
// | |
|
||||
// +-+-+
|
||||
//
|
||||
// Returns an Object {n, s, e, w, nw, sw, ne, se}
|
||||
function getScreenBBox() {
|
||||
var targetel = target || d3.event.target;
|
||||
|
||||
while ('undefined' === typeof targetel.getScreenCTM && 'undefined' === targetel.parentNode) {
|
||||
targetel = targetel.parentNode;
|
||||
}
|
||||
|
||||
var bbox = {},
|
||||
matrix = targetel.getScreenCTM(),
|
||||
tbbox = targetel.getBBox(),
|
||||
width = tbbox.width,
|
||||
height = tbbox.height,
|
||||
x = tbbox.x,
|
||||
y = tbbox.y
|
||||
|
||||
point.x = x
|
||||
point.y = y
|
||||
bbox.nw = point.matrixTransform(matrix)
|
||||
point.x += width
|
||||
bbox.ne = point.matrixTransform(matrix)
|
||||
point.y += height
|
||||
bbox.se = point.matrixTransform(matrix)
|
||||
point.x -= width
|
||||
bbox.sw = point.matrixTransform(matrix)
|
||||
point.y -= height / 2
|
||||
bbox.w = point.matrixTransform(matrix)
|
||||
point.x += width
|
||||
bbox.e = point.matrixTransform(matrix)
|
||||
point.x -= width / 2
|
||||
point.y -= height / 2
|
||||
bbox.n = point.matrixTransform(matrix)
|
||||
point.y += height
|
||||
bbox.s = point.matrixTransform(matrix)
|
||||
|
||||
return bbox
|
||||
}
|
||||
|
||||
return tip
|
||||
};
|
||||
|
||||
}));
|
@ -12,6 +12,12 @@
|
||||
.heatmap svg {
|
||||
cursor: move;
|
||||
}
|
||||
.heatmap .axis .tick:first-child {
|
||||
display: none;
|
||||
|
||||
.heatmap canvas, .heatmap img {
|
||||
image-rendering: optimizeSpeed; /* Older versions of FF */
|
||||
image-rendering: -moz-crisp-edges; /* FF 6.0+ */
|
||||
image-rendering: -webkit-optimize-contrast; /* Safari */
|
||||
image-rendering: -o-crisp-edges; /* OS X & Windows Opera (12.02+) */
|
||||
image-rendering: pixelated; /* Awesome future-browsers */
|
||||
-ms-interpolation-mode: nearest-neighbor; /* IE */
|
||||
}
|
||||
|
@ -1,25 +1,45 @@
|
||||
// Inspired from http://bl.ocks.org/mbostock/3074470
|
||||
// https://jsfiddle.net/cyril123/h0reyumq/
|
||||
px.registerViz('heatmap', function(slice) {
|
||||
function refresh() {
|
||||
d3.json("https://gist.githubusercontent.com/mbostock/3074470/raw/c028fa03cde541bbd7fdcaa27e61f6332af3b556/heatmap.json", function(error, heatmap) {
|
||||
if (error) {
|
||||
slice.error(error);
|
||||
return;
|
||||
var width = slice.width();
|
||||
var height = slice.height();
|
||||
d3.json(slice.jsonEndpoint(), function(error, payload) {
|
||||
var matrix = {};
|
||||
if (error){
|
||||
slice.error(error.responseText);
|
||||
return '';
|
||||
}
|
||||
var heatmap = payload.data;
|
||||
function ordScale(k, rangeBands, reverse) {
|
||||
if (reverse === undefined)
|
||||
reverse = false;
|
||||
domain = {};
|
||||
$.each(heatmap, function(i, d){
|
||||
domain[d[k]] = true;
|
||||
});
|
||||
domain = Object.keys(domain).sort();
|
||||
if (reverse)
|
||||
domain.reverse();
|
||||
if (rangeBands === undefined) {
|
||||
return d3.scale.ordinal().domain(domain).range(d3.range(domain.length));
|
||||
}
|
||||
else {
|
||||
return d3.scale.ordinal().domain(domain).rangeBands(rangeBands);
|
||||
}
|
||||
}
|
||||
var xScale = ordScale('x');
|
||||
var yScale = ordScale('y', undefined, true);
|
||||
var xRbScale = ordScale('x', [0, width]);
|
||||
var yRbScale = ordScale('y', [height, 0]);
|
||||
var X = 0, Y = 1;
|
||||
var canvasDim = [slice.width(), slice.height()];
|
||||
var canvasAspect = canvasDim[Y] / canvasDim[X];
|
||||
var heatmapDim = [heatmap[X].length, heatmap.length];
|
||||
var heatmapAspect = heatmapDim[Y] / heatmapDim[X];
|
||||
|
||||
if (heatmapAspect < canvasAspect)
|
||||
canvasDim[Y] = canvasDim[X] * heatmapAspect;
|
||||
else
|
||||
canvasDim[X] = canvasDim[Y] / heatmapAspect;
|
||||
var canvasDim = [width, height];
|
||||
var heatmapDim = [xRbScale.domain().length, yRbScale.domain().length];
|
||||
|
||||
ext = d3.extent(heatmap, function(d){return d.v;});
|
||||
var color = d3.scale.linear()
|
||||
.domain([95, 115, 135, 155, 175, 195])
|
||||
.range(["#0a0", "#6c0", "#ee0", "#eb4", "#eb9", "#fff"]);
|
||||
.domain(ext)
|
||||
.range(["#fff", "#000"]);
|
||||
|
||||
var scale = [
|
||||
d3.scale.linear()
|
||||
@ -35,6 +55,7 @@ px.registerViz('heatmap', function(slice) {
|
||||
var canvas = container.append("canvas")
|
||||
.attr("width", heatmapDim[X])
|
||||
.attr("height", heatmapDim[Y])
|
||||
.attr("image-rendering", "pixelated")
|
||||
.style("width", canvasDim[X] + "px")
|
||||
.style("height", canvasDim[Y] + "px")
|
||||
.style("position", "absolute");
|
||||
@ -44,16 +65,27 @@ px.registerViz('heatmap', function(slice) {
|
||||
.attr("height", canvasDim[Y])
|
||||
.style("position", "relative");
|
||||
|
||||
var tip = d3.tip()
|
||||
.attr('class', 'd3-tip')
|
||||
.offset([10, 0])
|
||||
.html(function (d) {
|
||||
var k = d3.mouse(this);
|
||||
var m = Math.floor(scale[X].invert(k[0]))
|
||||
var n = Math.floor(scale[Y].invert(k[1]))
|
||||
return "Intensity Count: " + heatmap[n][m];
|
||||
})
|
||||
svg.call(tip);
|
||||
var tip = d3.tip()
|
||||
.attr('class', 'd3-tip')
|
||||
.offset(function(){
|
||||
var k = d3.mouse(this);
|
||||
var x = k[0] - (width / 2);
|
||||
return [k[1] - 15, x];
|
||||
})
|
||||
.html(function (d) {
|
||||
var k = d3.mouse(this);
|
||||
var m = Math.floor(scale[0].invert(k[0]));
|
||||
var n = Math.floor(scale[1].invert(k[1]));
|
||||
var obj = matrix[m][n];
|
||||
if (obj !== undefined) {
|
||||
var s = "";
|
||||
s += "<div><b>X: </b>" + obj.x + "<div>"
|
||||
s += "<div><b>Y: </b>" + obj.y + "<div>"
|
||||
s += "<div><b>V: </b>" + obj.v + "<div>"
|
||||
return s;
|
||||
}
|
||||
})
|
||||
svg.call(tip);
|
||||
|
||||
var zoom = d3.behavior.zoom()
|
||||
.center(canvasDim.map(
|
||||
@ -73,10 +105,10 @@ px.registerViz('heatmap', function(slice) {
|
||||
|
||||
var axis = [
|
||||
d3.svg.axis()
|
||||
.scale(scale[X])
|
||||
.scale(xRbScale)
|
||||
.orient("top"),
|
||||
d3.svg.axis()
|
||||
.scale(scale[Y])
|
||||
.scale(yRbScale)
|
||||
.orient("right")
|
||||
];
|
||||
|
||||
@ -88,10 +120,11 @@ px.registerViz('heatmap', function(slice) {
|
||||
.attr("class", "y axis")
|
||||
];
|
||||
|
||||
svg.on('mousemove', tip.show); //Added
|
||||
svg.on('mouseout', tip.hide); //Added
|
||||
svg.on('mousemove', tip.show);
|
||||
svg.on('mouseout', tip.hide);
|
||||
|
||||
var context = canvas.node().getContext("2d");
|
||||
context.imageSmoothingEnabled = false;
|
||||
var imageObj;
|
||||
var imageDim;
|
||||
var imageScale;
|
||||
@ -101,17 +134,31 @@ px.registerViz('heatmap', function(slice) {
|
||||
// Compute the pixel colors; scaled by CSS.
|
||||
function createImageObj() {
|
||||
imageObj = new Image();
|
||||
var image = context.createImageData(heatmapDim[X], heatmapDim[Y]);
|
||||
image = context.createImageData(heatmapDim[0], heatmapDim[1]);
|
||||
var pixs = {};
|
||||
$.each(heatmap, function(i, d) {
|
||||
var c = d3.rgb(color(d.v));
|
||||
var x = xScale(d.x);
|
||||
var y = yScale(d.y);
|
||||
pixs[x + (y*xScale.domain().length)] = c;
|
||||
if (matrix[x] === undefined)
|
||||
matrix[x] = {}
|
||||
if (matrix[x][y] === undefined)
|
||||
matrix[x][y] = d;
|
||||
});
|
||||
|
||||
for (var y = 0, p = -1; y < heatmapDim[Y]; ++y) {
|
||||
for (var x = 0; x < heatmapDim[X]; ++x) {
|
||||
//console.log("heatmap x and y :: ",x,y,heatmap[y][x]);
|
||||
var c = d3.rgb(color(heatmap[y][x]));
|
||||
p = -1;
|
||||
for(var i=0; i< heatmapDim[0] * heatmapDim[1]; i++){
|
||||
c = pixs[i];
|
||||
var alpha = 255;
|
||||
if (c === undefined){
|
||||
c = d3.rgb('#F00');
|
||||
alpha = 0;
|
||||
}
|
||||
image.data[++p] = c.r;
|
||||
image.data[++p] = c.g;
|
||||
image.data[++p] = c.b;
|
||||
image.data[++p] = 255;
|
||||
}
|
||||
image.data[++p] = alpha;
|
||||
}
|
||||
context.putImageData(image, 0, 0);
|
||||
imageObj.src = canvas.node().toDataURL();
|
||||
@ -121,7 +168,9 @@ px.registerViz('heatmap', function(slice) {
|
||||
}
|
||||
|
||||
function drawAxes() {
|
||||
axisElement.forEach(function(v, i) {v.call(axis[i])});
|
||||
console.log(scale[0].domain());
|
||||
axisElement[0].call(axis[0]);
|
||||
axisElement[1].call(axis[1]);
|
||||
}
|
||||
|
||||
function zoomEvent() {
|
||||
|
@ -1202,24 +1202,26 @@ class HeatmapViz(BaseViz):
|
||||
'fields': (
|
||||
'granularity',
|
||||
('since', 'until'),
|
||||
'all_columns_x',
|
||||
'all_columns_y',
|
||||
'metric',
|
||||
'x',
|
||||
'y',
|
||||
)
|
||||
},)
|
||||
def query_obj(self):
|
||||
d = super(HeatmapViz, 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')]
|
||||
d['metrics'] = [fd.get('metric')]
|
||||
d['groupby'] = [fd.get('all_columns_x'), fd.get('all_columns_y')]
|
||||
return d
|
||||
|
||||
def get_json_data(self):
|
||||
df = self.get_df()
|
||||
df = df[[self.form_data.get('series')] + self.form_data.get('metrics')]
|
||||
df = df[[
|
||||
self.form_data.get('all_columns_x'),
|
||||
self.form_data.get('all_columns_y'),
|
||||
self.form_data.get('metric')
|
||||
]]
|
||||
df.columns = ['x', 'y', 'v']
|
||||
return df.to_json(orient="records")
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user