mirror of
https://github.com/apache/superset.git
synced 2024-09-06 22:07:34 -04:00
[heatmap] numerous improvements (#3456)
* [heatmap] numerous improvements * flexibility as to how to sort X and Y axis (alpha/value, desc/asc) * option to show a legend * fixed margins, maximize real estate * allowed users to define bounds * Tunning
This commit is contained in:
parent
8223729e1e
commit
49f24d128b
@ -36,6 +36,12 @@ const timeColumnOption = {
|
||||
'A reference to the [Time] configuration, taking granularity into ' +
|
||||
'account'),
|
||||
};
|
||||
const sortAxisChoices = [
|
||||
['alpha_asc', 'Alphabetical ascending'],
|
||||
['alpha_desc', 'Alphabetical descending'],
|
||||
['value_asc', 'Value ascending'],
|
||||
['value_desc', 'Value descending'],
|
||||
];
|
||||
|
||||
const groupByControl = {
|
||||
type: 'SelectControl',
|
||||
@ -156,6 +162,22 @@ export const controls = {
|
||||
description: '',
|
||||
},
|
||||
|
||||
sort_x_axis: {
|
||||
type: 'SelectControl',
|
||||
label: 'Sort X Axis',
|
||||
choices: sortAxisChoices,
|
||||
clearable: false,
|
||||
default: 'alpha_asc',
|
||||
},
|
||||
|
||||
sort_y_axis: {
|
||||
type: 'SelectControl',
|
||||
label: 'Sort Y Axis',
|
||||
choices: sortAxisChoices,
|
||||
clearable: false,
|
||||
default: 'alpha_asc',
|
||||
},
|
||||
|
||||
linear_color_scheme: {
|
||||
type: 'ColorSchemeControl',
|
||||
label: 'Linear Color Scheme',
|
||||
@ -202,6 +224,7 @@ export const controls = {
|
||||
canvas_image_rendering: {
|
||||
type: 'SelectControl',
|
||||
label: 'Rendering',
|
||||
renderTrigger: true,
|
||||
choices: [
|
||||
['pixelated', 'pixelated (Sharp)'],
|
||||
['auto', 'auto (Smooth)'],
|
||||
@ -236,6 +259,14 @@ export const controls = {
|
||||
default: false,
|
||||
},
|
||||
|
||||
show_perc: {
|
||||
type: 'CheckboxControl',
|
||||
label: 'Show percentage',
|
||||
renderTrigger: true,
|
||||
description: 'Whether to include the percentage in the tooltip',
|
||||
default: true,
|
||||
},
|
||||
|
||||
bar_stacked: {
|
||||
type: 'CheckboxControl',
|
||||
label: 'Stacked Bars',
|
||||
|
@ -929,10 +929,10 @@ export const visTypes = {
|
||||
label: 'Heatmap',
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Axis & Metrics',
|
||||
label: 'Query',
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['all_columns_x'],
|
||||
['all_columns_y'],
|
||||
['all_columns_x', 'all_columns_y'],
|
||||
['metric'],
|
||||
],
|
||||
},
|
||||
@ -941,9 +941,11 @@ export const visTypes = {
|
||||
controlSetRows: [
|
||||
['linear_color_scheme'],
|
||||
['xscale_interval', 'yscale_interval'],
|
||||
['canvas_image_rendering'],
|
||||
['normalize_across'],
|
||||
['canvas_image_rendering', 'normalize_across'],
|
||||
['left_margin', 'bottom_margin'],
|
||||
['y_axis_bounds', 'y_axis_format'],
|
||||
['show_legend', 'show_perc'],
|
||||
['sort_x_axis', 'sort_y_axis'],
|
||||
],
|
||||
},
|
||||
],
|
||||
@ -954,6 +956,18 @@ export const visTypes = {
|
||||
all_columns_y: {
|
||||
validators: [v.nonEmpty],
|
||||
},
|
||||
y_axis_bounds: {
|
||||
label: 'Value bounds',
|
||||
renderTrigger: false,
|
||||
description: (
|
||||
'Hard value bounds applied for color coding. Is only relevant ' +
|
||||
'and applied when the normalization is applied against the whole ' +
|
||||
'heatmap.'
|
||||
),
|
||||
},
|
||||
y_axis_format: {
|
||||
label: 'Value Format',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -116,17 +116,20 @@ export const getColorFromScheme = (function () {
|
||||
};
|
||||
}());
|
||||
|
||||
export const colorScalerFactory = function (colors, data, accessor) {
|
||||
export const colorScalerFactory = function (colors, data, accessor, extents) {
|
||||
// Returns a linear scaler our of an array of color
|
||||
if (!Array.isArray(colors)) {
|
||||
/* eslint no-param-reassign: 0 */
|
||||
colors = spectrums[colors];
|
||||
}
|
||||
let ext = [0, 1];
|
||||
if (data !== undefined) {
|
||||
if (extents) {
|
||||
ext = extents;
|
||||
}
|
||||
if (data) {
|
||||
ext = d3.extent(data, accessor);
|
||||
}
|
||||
const chunkSize = (ext[1] - ext[0]) / (colors.length - 1);
|
||||
const points = colors.map((col, i) => i * chunkSize);
|
||||
return d3.scale.linear().domain(points).range(colors);
|
||||
const points = colors.map((col, i) => ext[0] + (i * chunkSize));
|
||||
return d3.scale.linear().domain(points).range(colors).clamp(true);
|
||||
};
|
||||
|
@ -49,6 +49,7 @@
|
||||
"d3": "^3.5.17",
|
||||
"d3-cloud": "^1.2.1",
|
||||
"d3-sankey": "^0.4.2",
|
||||
"d3-svg-legend": "^1.x",
|
||||
"d3-tip": "^0.6.7",
|
||||
"datamaps": "^0.5.8",
|
||||
"datatables.net-bs": "^1.10.15",
|
||||
|
@ -1,4 +1,4 @@
|
||||
.heatmap .slice_container {
|
||||
.heatmap {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@ -28,3 +28,16 @@
|
||||
image-rendering: pixelated; /* Awesome future-browsers */
|
||||
-ms-interpolation-mode: nearest-neighbor; /* IE */
|
||||
}
|
||||
|
||||
.heatmap .legendCells text {
|
||||
font-size: 10px;
|
||||
font-weight: normal;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.heatmap .legendCells .cell:first-child text {
|
||||
opacity: 1;
|
||||
}
|
||||
.heatmap .legendCells .cell:last-child text {
|
||||
opacity: 1;
|
||||
}
|
||||
|
@ -1,31 +1,30 @@
|
||||
import d3 from 'd3';
|
||||
import $ from 'jquery';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import d3legend from 'd3-svg-legend';
|
||||
import d3tip from 'd3-tip';
|
||||
|
||||
import { colorScalerFactory } from '../javascripts/modules/colors';
|
||||
import '../stylesheets/d3tip.css';
|
||||
import './heatmap.css';
|
||||
|
||||
|
||||
// Inspired from http://bl.ocks.org/mbostock/3074470
|
||||
// https://jsfiddle.net/cyril123/h0reyumq/
|
||||
function heatmapVis(slice, payload) {
|
||||
// Header for panel in explore v2
|
||||
const header = document.getElementById('slice-header');
|
||||
const headerHeight = header ? 30 + header.getBoundingClientRect().height : 0;
|
||||
const data = payload.data.records;
|
||||
const fd = slice.formData;
|
||||
|
||||
const margin = {
|
||||
top: headerHeight,
|
||||
top: 10,
|
||||
right: 10,
|
||||
bottom: 35,
|
||||
left: 35,
|
||||
};
|
||||
const valueFormatter = d3.format(fd.y_axis_format);
|
||||
|
||||
const data = payload.data;
|
||||
const fd = slice.formData;
|
||||
// Dynamically adjusts based on max x / y category lengths
|
||||
function adjustMargins() {
|
||||
const pixelsPerCharX = 4.5; // approx, depends on font size
|
||||
const pixelsPerCharY = 10; // approx, depends on font size
|
||||
const pixelsPerCharY = 6; // approx, depends on font size
|
||||
let longestX = 1;
|
||||
let longestY = 1;
|
||||
let datum;
|
||||
@ -38,6 +37,9 @@ function heatmapVis(slice, payload) {
|
||||
|
||||
if (fd.left_margin === 'auto') {
|
||||
margin.left = Math.ceil(Math.max(margin.left, pixelsPerCharY * longestY));
|
||||
if (fd.show_legend) {
|
||||
margin.left += 40;
|
||||
}
|
||||
} else {
|
||||
margin.left = fd.left_margin;
|
||||
}
|
||||
@ -48,20 +50,30 @@ function heatmapVis(slice, payload) {
|
||||
}
|
||||
}
|
||||
|
||||
function ordScale(k, rangeBands, reverse = false) {
|
||||
function ordScale(k, rangeBands, sortMethod) {
|
||||
let domain = {};
|
||||
$.each(data, function (i, d) {
|
||||
domain[d[k]] = true;
|
||||
data.forEach((d) => {
|
||||
domain[d[k]] = domain[d[k]] || 0 + d.v;
|
||||
});
|
||||
if (sortMethod === 'alpha_asc') {
|
||||
domain = Object.keys(domain).sort();
|
||||
if (reverse) {
|
||||
} else if (sortMethod === 'alpha_desc') {
|
||||
domain = Object.keys(domain).sort().reverse();
|
||||
} else if (sortMethod === 'value_desc') {
|
||||
domain = Object.keys(domain).sort((d1, d2) => domain[d2] - domain[d1]);
|
||||
} else if (sortMethod === 'value_asc') {
|
||||
domain = Object.keys(domain).sort((d1, d2) => domain[d1] - domain[d2]);
|
||||
}
|
||||
|
||||
if (k === 'y' && rangeBands) {
|
||||
domain.reverse();
|
||||
}
|
||||
if (rangeBands === undefined) {
|
||||
return d3.scale.ordinal().domain(domain).range(d3.range(domain.length));
|
||||
}
|
||||
|
||||
if (rangeBands) {
|
||||
return d3.scale.ordinal().domain(domain).rangeBands(rangeBands);
|
||||
}
|
||||
return d3.scale.ordinal().domain(domain).range(d3.range(domain.length));
|
||||
}
|
||||
|
||||
slice.container.html('');
|
||||
const matrix = {};
|
||||
@ -74,10 +86,10 @@ function heatmapVis(slice, payload) {
|
||||
const hmHeight = height - (margin.bottom + margin.top);
|
||||
const fp = d3.format('.3p');
|
||||
|
||||
const xScale = ordScale('x');
|
||||
const yScale = ordScale('y', undefined, true);
|
||||
const xRbScale = ordScale('x', [0, hmWidth]);
|
||||
const yRbScale = ordScale('y', [hmHeight, 0]);
|
||||
const xScale = ordScale('x', null, fd.sort_x_axis);
|
||||
const yScale = ordScale('y', null, fd.sort_y_axis);
|
||||
const xRbScale = ordScale('x', [0, hmWidth], fd.sort_x_axis);
|
||||
const yRbScale = ordScale('y', [hmHeight, 0], fd.sort_y_axis);
|
||||
const X = 0;
|
||||
const Y = 1;
|
||||
const heatmapDim = [xRbScale.domain().length, yRbScale.domain().length];
|
||||
@ -102,15 +114,30 @@ function heatmapVis(slice, payload) {
|
||||
.style('height', hmHeight + 'px')
|
||||
.style('image-rendering', fd.canvas_image_rendering)
|
||||
.style('left', margin.left + 'px')
|
||||
.style('top', margin.top + headerHeight + 'px')
|
||||
.style('top', margin.top + 'px')
|
||||
.style('position', 'absolute');
|
||||
|
||||
const svg = container.append('svg')
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.style('left', '0px')
|
||||
.style('top', headerHeight + 'px')
|
||||
.style('position', 'absolute');
|
||||
.style('position', 'relative');
|
||||
|
||||
if (fd.show_legend) {
|
||||
const legendScaler = colorScalerFactory(
|
||||
fd.linear_color_scheme, null, null, payload.data.extents);
|
||||
const colorLegend = d3.legend.color()
|
||||
.labelFormat(valueFormatter)
|
||||
.scale(legendScaler)
|
||||
.shapePadding(0)
|
||||
.cells(50)
|
||||
.shapeWidth(10)
|
||||
.shapeHeight(3)
|
||||
.labelOffset(2);
|
||||
|
||||
svg.append('g')
|
||||
.attr('transform', 'translate(10, 5)')
|
||||
.call(colorLegend);
|
||||
}
|
||||
|
||||
const tip = d3tip()
|
||||
.attr('class', 'd3-tip')
|
||||
@ -128,8 +155,10 @@ function heatmapVis(slice, payload) {
|
||||
const obj = matrix[m][n];
|
||||
s += '<div><b>' + fd.all_columns_x + ': </b>' + obj.x + '<div>';
|
||||
s += '<div><b>' + fd.all_columns_y + ': </b>' + obj.y + '<div>';
|
||||
s += '<div><b>' + fd.metric + ': </b>' + obj.v + '<div>';
|
||||
s += '<div><b>' + fd.metric + ': </b>' + valueFormatter(obj.v) + '<div>';
|
||||
if (fd.show_perc) {
|
||||
s += '<div><b>%: </b>' + fp(obj.perc) + '<div>';
|
||||
}
|
||||
tip.style('display', null);
|
||||
} else {
|
||||
// this is a hack to hide the tooltip because we have map it to a single <rect>
|
||||
@ -190,7 +219,7 @@ function heatmapVis(slice, payload) {
|
||||
const imageObj = new Image();
|
||||
const image = context.createImageData(heatmapDim[0], heatmapDim[1]);
|
||||
const pixs = {};
|
||||
$.each(data, function (i, d) {
|
||||
data.forEach((d) => {
|
||||
const c = d3.rgb(color(d.perc));
|
||||
const x = xScale(d.x);
|
||||
const y = yScale(d.y);
|
||||
|
@ -1479,6 +1479,13 @@ class HeatmapViz(BaseViz):
|
||||
df.columns = ['x', 'y', 'v']
|
||||
norm = fd.get('normalize_across')
|
||||
overall = False
|
||||
max_ = df.v.max()
|
||||
min_ = df.v.min()
|
||||
bounds = fd.get('y_axis_bounds')
|
||||
if bounds and bounds[0]:
|
||||
min_ = bounds[0]
|
||||
if bounds and bounds[1]:
|
||||
max_ = bounds[1]
|
||||
if norm == 'heatmap':
|
||||
overall = True
|
||||
else:
|
||||
@ -1491,10 +1498,11 @@ class HeatmapViz(BaseViz):
|
||||
lambda x: (x.v - x.v.min()) / (x.v.max() - x.v.min()))
|
||||
)
|
||||
if overall:
|
||||
v = df.v
|
||||
min_ = v.min()
|
||||
df['perc'] = (v - min_) / (v.max() - min_)
|
||||
return df.to_dict(orient="records")
|
||||
df['perc'] = (df.v - min_) / (max_ - min_)
|
||||
return {
|
||||
'records': df.to_dict(orient="records"),
|
||||
'extents': [min_, max_],
|
||||
}
|
||||
|
||||
|
||||
class HorizonViz(NVD3TimeSeriesViz):
|
||||
|
Loading…
Reference in New Issue
Block a user