mirror of
https://github.com/apache/superset.git
synced 2024-09-16 02:29:39 -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 ' +
|
'A reference to the [Time] configuration, taking granularity into ' +
|
||||||
'account'),
|
'account'),
|
||||||
};
|
};
|
||||||
|
const sortAxisChoices = [
|
||||||
|
['alpha_asc', 'Alphabetical ascending'],
|
||||||
|
['alpha_desc', 'Alphabetical descending'],
|
||||||
|
['value_asc', 'Value ascending'],
|
||||||
|
['value_desc', 'Value descending'],
|
||||||
|
];
|
||||||
|
|
||||||
const groupByControl = {
|
const groupByControl = {
|
||||||
type: 'SelectControl',
|
type: 'SelectControl',
|
||||||
@ -156,6 +162,22 @@ export const controls = {
|
|||||||
description: '',
|
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: {
|
linear_color_scheme: {
|
||||||
type: 'ColorSchemeControl',
|
type: 'ColorSchemeControl',
|
||||||
label: 'Linear Color Scheme',
|
label: 'Linear Color Scheme',
|
||||||
@ -202,6 +224,7 @@ export const controls = {
|
|||||||
canvas_image_rendering: {
|
canvas_image_rendering: {
|
||||||
type: 'SelectControl',
|
type: 'SelectControl',
|
||||||
label: 'Rendering',
|
label: 'Rendering',
|
||||||
|
renderTrigger: true,
|
||||||
choices: [
|
choices: [
|
||||||
['pixelated', 'pixelated (Sharp)'],
|
['pixelated', 'pixelated (Sharp)'],
|
||||||
['auto', 'auto (Smooth)'],
|
['auto', 'auto (Smooth)'],
|
||||||
@ -236,6 +259,14 @@ export const controls = {
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
show_perc: {
|
||||||
|
type: 'CheckboxControl',
|
||||||
|
label: 'Show percentage',
|
||||||
|
renderTrigger: true,
|
||||||
|
description: 'Whether to include the percentage in the tooltip',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
bar_stacked: {
|
bar_stacked: {
|
||||||
type: 'CheckboxControl',
|
type: 'CheckboxControl',
|
||||||
label: 'Stacked Bars',
|
label: 'Stacked Bars',
|
||||||
|
@ -929,10 +929,10 @@ export const visTypes = {
|
|||||||
label: 'Heatmap',
|
label: 'Heatmap',
|
||||||
controlPanelSections: [
|
controlPanelSections: [
|
||||||
{
|
{
|
||||||
label: 'Axis & Metrics',
|
label: 'Query',
|
||||||
|
expanded: true,
|
||||||
controlSetRows: [
|
controlSetRows: [
|
||||||
['all_columns_x'],
|
['all_columns_x', 'all_columns_y'],
|
||||||
['all_columns_y'],
|
|
||||||
['metric'],
|
['metric'],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -941,9 +941,11 @@ export const visTypes = {
|
|||||||
controlSetRows: [
|
controlSetRows: [
|
||||||
['linear_color_scheme'],
|
['linear_color_scheme'],
|
||||||
['xscale_interval', 'yscale_interval'],
|
['xscale_interval', 'yscale_interval'],
|
||||||
['canvas_image_rendering'],
|
['canvas_image_rendering', 'normalize_across'],
|
||||||
['normalize_across'],
|
|
||||||
['left_margin', 'bottom_margin'],
|
['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: {
|
all_columns_y: {
|
||||||
validators: [v.nonEmpty],
|
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
|
// Returns a linear scaler our of an array of color
|
||||||
if (!Array.isArray(colors)) {
|
if (!Array.isArray(colors)) {
|
||||||
/* eslint no-param-reassign: 0 */
|
/* eslint no-param-reassign: 0 */
|
||||||
colors = spectrums[colors];
|
colors = spectrums[colors];
|
||||||
}
|
}
|
||||||
let ext = [0, 1];
|
let ext = [0, 1];
|
||||||
if (data !== undefined) {
|
if (extents) {
|
||||||
|
ext = extents;
|
||||||
|
}
|
||||||
|
if (data) {
|
||||||
ext = d3.extent(data, accessor);
|
ext = d3.extent(data, accessor);
|
||||||
}
|
}
|
||||||
const chunkSize = (ext[1] - ext[0]) / (colors.length - 1);
|
const chunkSize = (ext[1] - ext[0]) / (colors.length - 1);
|
||||||
const points = colors.map((col, i) => i * chunkSize);
|
const points = colors.map((col, i) => ext[0] + (i * chunkSize));
|
||||||
return d3.scale.linear().domain(points).range(colors);
|
return d3.scale.linear().domain(points).range(colors).clamp(true);
|
||||||
};
|
};
|
||||||
|
@ -49,6 +49,7 @@
|
|||||||
"d3": "^3.5.17",
|
"d3": "^3.5.17",
|
||||||
"d3-cloud": "^1.2.1",
|
"d3-cloud": "^1.2.1",
|
||||||
"d3-sankey": "^0.4.2",
|
"d3-sankey": "^0.4.2",
|
||||||
|
"d3-svg-legend": "^1.x",
|
||||||
"d3-tip": "^0.6.7",
|
"d3-tip": "^0.6.7",
|
||||||
"datamaps": "^0.5.8",
|
"datamaps": "^0.5.8",
|
||||||
"datatables.net-bs": "^1.10.15",
|
"datatables.net-bs": "^1.10.15",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
.heatmap .slice_container {
|
.heatmap {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -28,3 +28,16 @@
|
|||||||
image-rendering: pixelated; /* Awesome future-browsers */
|
image-rendering: pixelated; /* Awesome future-browsers */
|
||||||
-ms-interpolation-mode: nearest-neighbor; /* IE */
|
-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 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 d3tip from 'd3-tip';
|
||||||
|
|
||||||
import { colorScalerFactory } from '../javascripts/modules/colors';
|
import { colorScalerFactory } from '../javascripts/modules/colors';
|
||||||
import '../stylesheets/d3tip.css';
|
import '../stylesheets/d3tip.css';
|
||||||
import './heatmap.css';
|
import './heatmap.css';
|
||||||
|
|
||||||
|
|
||||||
// Inspired from http://bl.ocks.org/mbostock/3074470
|
// Inspired from http://bl.ocks.org/mbostock/3074470
|
||||||
// https://jsfiddle.net/cyril123/h0reyumq/
|
// https://jsfiddle.net/cyril123/h0reyumq/
|
||||||
function heatmapVis(slice, payload) {
|
function heatmapVis(slice, payload) {
|
||||||
// Header for panel in explore v2
|
const data = payload.data.records;
|
||||||
const header = document.getElementById('slice-header');
|
const fd = slice.formData;
|
||||||
const headerHeight = header ? 30 + header.getBoundingClientRect().height : 0;
|
|
||||||
const margin = {
|
const margin = {
|
||||||
top: headerHeight,
|
top: 10,
|
||||||
right: 10,
|
right: 10,
|
||||||
bottom: 35,
|
bottom: 35,
|
||||||
left: 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
|
// Dynamically adjusts based on max x / y category lengths
|
||||||
function adjustMargins() {
|
function adjustMargins() {
|
||||||
const pixelsPerCharX = 4.5; // approx, depends on font size
|
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 longestX = 1;
|
||||||
let longestY = 1;
|
let longestY = 1;
|
||||||
let datum;
|
let datum;
|
||||||
@ -38,6 +37,9 @@ function heatmapVis(slice, payload) {
|
|||||||
|
|
||||||
if (fd.left_margin === 'auto') {
|
if (fd.left_margin === 'auto') {
|
||||||
margin.left = Math.ceil(Math.max(margin.left, pixelsPerCharY * longestY));
|
margin.left = Math.ceil(Math.max(margin.left, pixelsPerCharY * longestY));
|
||||||
|
if (fd.show_legend) {
|
||||||
|
margin.left += 40;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
margin.left = fd.left_margin;
|
margin.left = fd.left_margin;
|
||||||
}
|
}
|
||||||
@ -48,19 +50,29 @@ function heatmapVis(slice, payload) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ordScale(k, rangeBands, reverse = false) {
|
function ordScale(k, rangeBands, sortMethod) {
|
||||||
let domain = {};
|
let domain = {};
|
||||||
$.each(data, function (i, d) {
|
data.forEach((d) => {
|
||||||
domain[d[k]] = true;
|
domain[d[k]] = domain[d[k]] || 0 + d.v;
|
||||||
});
|
});
|
||||||
domain = Object.keys(domain).sort();
|
if (sortMethod === 'alpha_asc') {
|
||||||
if (reverse) {
|
domain = Object.keys(domain).sort();
|
||||||
|
} 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();
|
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).rangeBands(rangeBands);
|
return d3.scale.ordinal().domain(domain).range(d3.range(domain.length));
|
||||||
}
|
}
|
||||||
|
|
||||||
slice.container.html('');
|
slice.container.html('');
|
||||||
@ -74,10 +86,10 @@ function heatmapVis(slice, payload) {
|
|||||||
const hmHeight = height - (margin.bottom + margin.top);
|
const hmHeight = height - (margin.bottom + margin.top);
|
||||||
const fp = d3.format('.3p');
|
const fp = d3.format('.3p');
|
||||||
|
|
||||||
const xScale = ordScale('x');
|
const xScale = ordScale('x', null, fd.sort_x_axis);
|
||||||
const yScale = ordScale('y', undefined, true);
|
const yScale = ordScale('y', null, fd.sort_y_axis);
|
||||||
const xRbScale = ordScale('x', [0, hmWidth]);
|
const xRbScale = ordScale('x', [0, hmWidth], fd.sort_x_axis);
|
||||||
const yRbScale = ordScale('y', [hmHeight, 0]);
|
const yRbScale = ordScale('y', [hmHeight, 0], fd.sort_y_axis);
|
||||||
const X = 0;
|
const X = 0;
|
||||||
const Y = 1;
|
const Y = 1;
|
||||||
const heatmapDim = [xRbScale.domain().length, yRbScale.domain().length];
|
const heatmapDim = [xRbScale.domain().length, yRbScale.domain().length];
|
||||||
@ -102,15 +114,30 @@ function heatmapVis(slice, payload) {
|
|||||||
.style('height', hmHeight + 'px')
|
.style('height', hmHeight + 'px')
|
||||||
.style('image-rendering', fd.canvas_image_rendering)
|
.style('image-rendering', fd.canvas_image_rendering)
|
||||||
.style('left', margin.left + 'px')
|
.style('left', margin.left + 'px')
|
||||||
.style('top', margin.top + headerHeight + 'px')
|
.style('top', margin.top + 'px')
|
||||||
.style('position', 'absolute');
|
.style('position', 'absolute');
|
||||||
|
|
||||||
const svg = container.append('svg')
|
const svg = container.append('svg')
|
||||||
.attr('width', width)
|
.attr('width', width)
|
||||||
.attr('height', height)
|
.attr('height', height)
|
||||||
.style('left', '0px')
|
.style('position', 'relative');
|
||||||
.style('top', headerHeight + 'px')
|
|
||||||
.style('position', 'absolute');
|
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()
|
const tip = d3tip()
|
||||||
.attr('class', 'd3-tip')
|
.attr('class', 'd3-tip')
|
||||||
@ -128,8 +155,10 @@ function heatmapVis(slice, payload) {
|
|||||||
const obj = matrix[m][n];
|
const obj = matrix[m][n];
|
||||||
s += '<div><b>' + fd.all_columns_x + ': </b>' + obj.x + '<div>';
|
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.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>';
|
||||||
s += '<div><b>%: </b>' + fp(obj.perc) + '<div>';
|
if (fd.show_perc) {
|
||||||
|
s += '<div><b>%: </b>' + fp(obj.perc) + '<div>';
|
||||||
|
}
|
||||||
tip.style('display', null);
|
tip.style('display', null);
|
||||||
} else {
|
} else {
|
||||||
// this is a hack to hide the tooltip because we have map it to a single <rect>
|
// 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 imageObj = new Image();
|
||||||
const image = context.createImageData(heatmapDim[0], heatmapDim[1]);
|
const image = context.createImageData(heatmapDim[0], heatmapDim[1]);
|
||||||
const pixs = {};
|
const pixs = {};
|
||||||
$.each(data, function (i, d) {
|
data.forEach((d) => {
|
||||||
const c = d3.rgb(color(d.perc));
|
const c = d3.rgb(color(d.perc));
|
||||||
const x = xScale(d.x);
|
const x = xScale(d.x);
|
||||||
const y = yScale(d.y);
|
const y = yScale(d.y);
|
||||||
|
@ -1479,6 +1479,13 @@ class HeatmapViz(BaseViz):
|
|||||||
df.columns = ['x', 'y', 'v']
|
df.columns = ['x', 'y', 'v']
|
||||||
norm = fd.get('normalize_across')
|
norm = fd.get('normalize_across')
|
||||||
overall = False
|
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':
|
if norm == 'heatmap':
|
||||||
overall = True
|
overall = True
|
||||||
else:
|
else:
|
||||||
@ -1491,10 +1498,11 @@ class HeatmapViz(BaseViz):
|
|||||||
lambda x: (x.v - x.v.min()) / (x.v.max() - x.v.min()))
|
lambda x: (x.v - x.v.min()) / (x.v.max() - x.v.min()))
|
||||||
)
|
)
|
||||||
if overall:
|
if overall:
|
||||||
v = df.v
|
df['perc'] = (df.v - min_) / (max_ - min_)
|
||||||
min_ = v.min()
|
return {
|
||||||
df['perc'] = (v - min_) / (v.max() - min_)
|
'records': df.to_dict(orient="records"),
|
||||||
return df.to_dict(orient="records")
|
'extents': [min_, max_],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class HorizonViz(NVD3TimeSeriesViz):
|
class HorizonViz(NVD3TimeSeriesViz):
|
||||||
|
Loading…
Reference in New Issue
Block a user