You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by cc...@apache.org on 2018/08/28 04:49:57 UTC
[incubator-superset] branch master updated: [SIP-5] Refactor and
update heatmap (#5704)
This is an automated email from the ASF dual-hosted git repository.
ccwilliams pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/master by this push:
new fcf2c75 [SIP-5] Refactor and update heatmap (#5704)
fcf2c75 is described below
commit fcf2c756c0cdebb8aad194ce0df9c6c1ec58deff
Author: Krist Wongsuphasawat <kr...@gmail.com>
AuthorDate: Mon Aug 27 21:49:54 2018 -0700
[SIP-5] Refactor and update heatmap (#5704)
* Extract slice and formData
* Define data shape
* update style
* organize imports
* fix heatmap axis labels
* add new line
* adjust indent
---
superset/assets/src/visualizations/heatmap.css | 19 +-
superset/assets/src/visualizations/heatmap.js | 255 ++++++++++++++++++-------
2 files changed, 198 insertions(+), 76 deletions(-)
diff --git a/superset/assets/src/visualizations/heatmap.css b/superset/assets/src/visualizations/heatmap.css
index 79542e2..597a48f 100644
--- a/superset/assets/src/visualizations/heatmap.css
+++ b/superset/assets/src/visualizations/heatmap.css
@@ -6,20 +6,24 @@
}
.heatmap .axis text {
- font: 10px sans-serif;
+ font: 12px sans-serif;
text-rendering: optimizeLegibility;
+ fill: #555;
+}
+
+.heatmap .background-rect {
+ stroke: #ddd;
+ fill-opacity: 0;
+ pointer-events: all;
}
.heatmap .axis path,
.heatmap .axis line {
fill: none;
- stroke: #000;
+ stroke: #ddd;
shape-rendering: crispEdges;
}
-.heatmap svg {
-}
-
.heatmap canvas, .heatmap img {
image-rendering: optimizeSpeed; /* Older versions of FF */
image-rendering: -moz-crisp-edges; /* FF 6.0+ */
@@ -41,3 +45,8 @@
.heatmap .legendCells .cell:last-child text {
opacity: 1;
}
+
+.dashboard .heatmap .axis text {
+ font-size: 10px;
+ opacity: .75;
+}
diff --git a/superset/assets/src/visualizations/heatmap.js b/superset/assets/src/visualizations/heatmap.js
index c26291f..3a6b351 100644
--- a/superset/assets/src/visualizations/heatmap.js
+++ b/superset/assets/src/visualizations/heatmap.js
@@ -1,21 +1,85 @@
import d3 from 'd3';
-// eslint-disable-next-line no-unused-vars
-import d3legend from 'd3-svg-legend';
+import PropTypes from 'prop-types';
+import 'd3-svg-legend';
import d3tip from 'd3-tip';
import { colorScalerFactory } from '../modules/colors';
import '../../stylesheets/d3tip.css';
import './heatmap.css';
+const propTypes = {
+ data: PropTypes.shape({
+ records: PropTypes.arrayOf(PropTypes.shape({
+ x: PropTypes.string,
+ y: PropTypes.string,
+ v: PropTypes.number,
+ perc: PropTypes.number,
+ rank: PropTypes.number,
+ })),
+ extents: PropTypes.arrayOf(PropTypes.number),
+ }),
+ width: PropTypes.number,
+ height: PropTypes.number,
+ bottomMargin: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number,
+ ]),
+ colorScheme: PropTypes.string,
+ columnX: PropTypes.string,
+ columnY: PropTypes.string,
+ leftMargin: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number,
+ ]),
+ metric: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.object,
+ ]),
+ normalized: PropTypes.bool,
+ numberFormat: PropTypes.string,
+ showLegend: PropTypes.bool,
+ showPercentage: PropTypes.bool,
+ showValues: PropTypes.bool,
+ sortXAxis: PropTypes.string,
+ sortYAxis: PropTypes.string,
+ xScaleInterval: PropTypes.number,
+ yScaleInterval: PropTypes.number,
+ yAxisBounds: PropTypes.arrayOf(PropTypes.number),
+};
+
function cmp(a, b) {
return a > b ? 1 : -1;
}
// Inspired from http://bl.ocks.org/mbostock/3074470
// https://jsfiddle.net/cyril123/h0reyumq/
-function heatmapVis(slice, payload) {
- const data = payload.data.records;
- const fd = slice.formData;
+function Heatmap(element, props) {
+ PropTypes.checkPropTypes(propTypes, props, 'prop', 'Heatmap');
+
+ const {
+ data,
+ width,
+ height,
+ bottomMargin,
+ canvasImageRendering,
+ colorScheme,
+ columnX,
+ columnY,
+ leftMargin,
+ metric,
+ normalized,
+ numberFormat,
+ showLegend,
+ showPercentage,
+ showValues,
+ sortXAxis,
+ sortYAxis,
+ xScaleInterval,
+ yScaleInterval,
+ yAxisBounds,
+ } = props;
+
+ const { records, extents } = data;
const margin = {
top: 10,
@@ -23,7 +87,7 @@ function heatmapVis(slice, payload) {
bottom: 35,
left: 35,
};
- const valueFormatter = d3.format(fd.y_axis_format);
+ const valueFormatter = d3.format(numberFormat);
// Dynamically adjusts based on max x / y category lengths
function adjustMargins() {
@@ -31,33 +95,32 @@ function heatmapVis(slice, payload) {
const pixelsPerCharY = 6; // approx, depends on font size
let longestX = 1;
let longestY = 1;
- let datum;
- for (let i = 0; i < data.length; i++) {
- datum = data[i];
+ for (let i = 0; i < records.length; i++) {
+ const datum = records[i];
longestX = Math.max(longestX, datum.x.toString().length || 1);
longestY = Math.max(longestY, datum.y.toString().length || 1);
}
- if (fd.left_margin === 'auto') {
+ if (leftMargin === 'auto') {
margin.left = Math.ceil(Math.max(margin.left, pixelsPerCharY * longestY));
- if (fd.show_legend) {
- margin.left += 40;
- }
} else {
- margin.left = fd.left_margin;
+ margin.left = leftMargin;
}
- if (fd.bottom_margin === 'auto') {
- margin.bottom = Math.ceil(Math.max(margin.bottom, pixelsPerCharX * longestX));
- } else {
- margin.bottom = fd.bottom_margin;
+
+ if (showLegend) {
+ margin.right += 40;
}
+
+ margin.bottom = (bottomMargin === 'auto')
+ ? Math.ceil(Math.max(margin.bottom, pixelsPerCharX * longestX))
+ : bottomMargin;
}
function ordScale(k, rangeBands, sortMethod) {
let domain = {};
const actualKeys = {}; // hack to preserve type of keys when number
- data.forEach((d) => {
+ records.forEach((d) => {
domain[d[k]] = (domain[d[k]] || 0) + d.v;
actualKeys[d[k]] = d[k];
});
@@ -83,46 +146,45 @@ function heatmapVis(slice, payload) {
return d3.scale.ordinal().domain(domain).range(d3.range(domain.length));
}
- slice.container.html('');
+ // eslint-disable-next-line no-param-reassign
+ element.innerHTML = '';
const matrix = {};
adjustMargins();
- const width = slice.width();
- const height = slice.height();
const hmWidth = width - (margin.left + margin.right);
const hmHeight = height - (margin.bottom + margin.top);
const fp = d3.format('.2%');
- 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 xScale = ordScale('x', null, sortXAxis);
+ const yScale = ordScale('y', null, sortYAxis);
+ const xRbScale = ordScale('x', [0, hmWidth], sortXAxis);
+ const yRbScale = ordScale('y', [hmHeight, 0], sortYAxis);
const X = 0;
const Y = 1;
const heatmapDim = [xRbScale.domain().length, yRbScale.domain().length];
- const minBound = fd.y_axis_bounds[0] || 0;
- const maxBound = fd.y_axis_bounds[1] || 1;
- const colorScaler = colorScalerFactory(fd.linear_color_scheme, null, null, [minBound, maxBound]);
+ const minBound = yAxisBounds[0] || 0;
+ const maxBound = yAxisBounds[1] || 1;
+ const colorScaler = colorScalerFactory(colorScheme, null, null, [minBound, maxBound]);
const scale = [
d3.scale.linear()
- .domain([0, heatmapDim[X]])
- .range([0, hmWidth]),
+ .domain([0, heatmapDim[X]])
+ .range([0, hmWidth]),
d3.scale.linear()
- .domain([0, heatmapDim[Y]])
- .range([0, hmHeight]),
+ .domain([0, heatmapDim[Y]])
+ .range([0, hmHeight]),
];
- const container = d3.select(slice.selector);
+ const container = d3.select(element);
const canvas = container.append('canvas')
.attr('width', heatmapDim[X])
.attr('height', heatmapDim[Y])
.style('width', hmWidth + 'px')
.style('height', hmHeight + 'px')
- .style('image-rendering', fd.canvas_image_rendering)
+ .style('image-rendering', canvasImageRendering)
.style('left', margin.left + 'px')
.style('top', margin.top + 'px')
.style('position', 'absolute');
@@ -132,9 +194,9 @@ function heatmapVis(slice, payload) {
.attr('height', height)
.style('position', 'relative');
- if (fd.show_values) {
+ if (showValues) {
const cells = svg.selectAll('rect')
- .data(data)
+ .data(records)
.enter()
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
@@ -147,22 +209,22 @@ function heatmapVis(slice, payload) {
.attr('dy', '.35em')
.text(d => valueFormatter(d.v))
.attr('font-size', Math.min(yRbScale.rangeBand(), xRbScale.rangeBand()) / 3 + 'px')
- .attr('fill', d => d.v >= payload.data.extents[1] / 2 ? 'white' : 'black');
+ .attr('fill', d => d.v >= extents[1] / 2 ? 'white' : 'black');
}
- if (fd.show_legend) {
+ if (showLegend) {
const colorLegend = d3.legend.color()
- .labelFormat(valueFormatter)
- .scale(colorScaler)
- .shapePadding(0)
- .cells(50)
- .shapeWidth(10)
- .shapeHeight(3)
- .labelOffset(2);
+ .labelFormat(valueFormatter)
+ .scale(colorScaler)
+ .shapePadding(0)
+ .cells(10)
+ .shapeWidth(10)
+ .shapeHeight(10)
+ .labelOffset(3);
svg.append('g')
- .attr('transform', 'translate(10, 5)')
- .call(colorLegend);
+ .attr('transform', `translate(${width - 40}, ${margin.top})`)
+ .call(colorLegend);
}
const tip = d3tip()
@@ -177,14 +239,14 @@ function heatmapVis(slice, payload) {
const k = d3.mouse(this);
const m = Math.floor(scale[0].invert(k[0]));
const n = Math.floor(scale[1].invert(k[1]));
- const metric = typeof fd.metric === 'object' ? fd.metric.label : fd.metric;
+ const metricLabel = typeof metric === 'object' ? metric.label : metric;
if (m in matrix && n in matrix[m]) {
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>' + metric + ': </b>' + valueFormatter(obj.v) + '<div>';
- if (fd.show_perc) {
- s += '<div><b>%: </b>' + fp(fd.normalized ? obj.rank : obj.perc) + '<div>';
+ s += '<div><b>' + columnX + ': </b>' + obj.x + '<div>';
+ s += '<div><b>' + columnY + ': </b>' + obj.y + '<div>';
+ s += '<div><b>' + metricLabel + ': </b>' + valueFormatter(obj.v) + '<div>';
+ if (showPercentage) {
+ s += '<div><b>%: </b>' + fp(normalized ? obj.rank : obj.perc) + '<div>';
}
tip.style('display', null);
} else {
@@ -196,48 +258,50 @@ function heatmapVis(slice, payload) {
});
const rect = svg.append('g')
- .attr('transform', `translate(${margin.left}, ${margin.top})`)
+ .attr('transform', `translate(${margin.left}, ${margin.top})`)
.append('rect')
- .attr('pointer-events', 'all')
- .on('mousemove', tip.show)
- .on('mouseout', tip.hide)
- .style('fill-opacity', 0)
- .attr('stroke', 'black')
- .attr('width', hmWidth)
- .attr('height', hmHeight);
+ .classed('background-rect', true)
+ .on('mousemove', tip.show)
+ .on('mouseout', tip.hide)
+ .attr('width', hmWidth)
+ .attr('height', hmHeight);
rect.call(tip);
const xAxis = d3.svg.axis()
.scale(xRbScale)
+ .outerTickSize(0)
.tickValues(xRbScale.domain().filter(
function (d, i) {
- return !(i % (parseInt(fd.xscale_interval, 10)));
+ return !(i % (xScaleInterval));
}))
.orient('bottom');
const yAxis = d3.svg.axis()
.scale(yRbScale)
+ .outerTickSize(0)
.tickValues(yRbScale.domain().filter(
function (d, i) {
- return !(i % (parseInt(fd.yscale_interval, 10)));
+ return !(i % (yScaleInterval));
}))
.orient('left');
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(' + margin.left + ',' + (margin.top + hmHeight) + ')')
- .call(xAxis)
+ .call(xAxis)
.selectAll('text')
- .style('text-anchor', 'end')
- .attr('transform', 'rotate(-45)');
+ .attr('x', -4)
+ .attr('y', 10)
+ .attr('dy', '0.3em')
+ .style('text-anchor', 'end')
+ .attr('transform', 'rotate(-45)');
svg.append('g')
.attr('class', 'y axis')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.call(yAxis);
-
const context = canvas.node().getContext('2d');
context.imageSmoothingEnabled = false;
@@ -246,8 +310,8 @@ function heatmapVis(slice, payload) {
const imageObj = new Image();
const image = context.createImageData(heatmapDim[0], heatmapDim[1]);
const pixs = {};
- data.forEach((d) => {
- const c = d3.rgb(colorScaler(fd.normalized ? d.rank : d.perc));
+ records.forEach((d) => {
+ const c = d3.rgb(colorScaler(normalized ? d.rank : d.perc));
const x = xScale(d.x);
const y = yScale(d.y);
pixs[x + (y * xScale.domain().length)] = c;
@@ -278,4 +342,53 @@ function heatmapVis(slice, payload) {
createImageObj();
}
-module.exports = heatmapVis;
+Heatmap.propTypes = propTypes;
+
+function adaptor(slice, payload) {
+ const { selector, formData } = slice;
+ const {
+ bottom_margin: bottomMargin,
+ canvas_image_rendering: canvasImageRendering,
+ all_columns_x: columnX,
+ all_columns_y: columnY,
+ linear_color_scheme: colorScheme,
+ left_margin: leftMargin,
+ metric,
+ normalized,
+ show_legend: showLegend,
+ show_perc: showPercentage,
+ show_values: showValues,
+ sort_x_axis: sortXAxis,
+ sort_y_axis: sortYAxis,
+ xscale_interval: xScaleInterval,
+ yscale_interval: yScaleInterval,
+ y_axis_bounds: yAxisBounds,
+ y_axis_format: numberFormat,
+ } = formData;
+ const element = document.querySelector(selector);
+
+ return Heatmap(element, {
+ data: payload.data,
+ width: slice.width(),
+ height: slice.height(),
+ bottomMargin,
+ canvasImageRendering,
+ colorScheme,
+ columnX,
+ columnY,
+ leftMargin,
+ metric,
+ normalized,
+ numberFormat,
+ showLegend,
+ showPercentage,
+ showValues,
+ sortXAxis,
+ sortYAxis,
+ xScaleInterval: parseInt(xScaleInterval, 10),
+ yScaleInterval: parseInt(yScaleInterval, 10),
+ yAxisBounds,
+ });
+}
+
+export default adaptor;