From 4bd127071670cfec9a0cba15c205988a82dbb40e Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Wed, 22 Aug 2018 15:27:30 -0700 Subject: [PATCH 1/7] Extract slice and formData --- superset/assets/src/visualizations/heatmap.js | 182 ++++++++++++++---- 1 file changed, 144 insertions(+), 38 deletions(-) diff --git a/superset/assets/src/visualizations/heatmap.js b/superset/assets/src/visualizations/heatmap.js index c26291ffca802..260be0d8e433b 100644 --- a/superset/assets/src/visualizations/heatmap.js +++ b/superset/assets/src/visualizations/heatmap.js @@ -1,4 +1,5 @@ import d3 from 'd3'; +import PropTypes from 'prop-types'; // eslint-disable-next-line no-unused-vars import d3legend from 'd3-svg-legend'; import d3tip from 'd3-tip'; @@ -7,15 +8,72 @@ import { colorScalerFactory } from '../modules/colors'; import '../../stylesheets/d3tip.css'; import './heatmap.css'; +const propTypes = { + data: PropTypes.shape({ + records: PropTypes.array, + }), + 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: rawData, + width, + height, + bottomMargin, + canvasImageRendering, + colorScheme, + columnX, + columnY, + leftMargin, + metric, + normalized, + numberFormat, + showLegend, + showPercentage, + showValues, + sortXAxis, + sortYAxis, + xScaleInterval, + yScaleInterval, + yAxisBounds, + } = props; + + const data = rawData.records; const margin = { top: 10, @@ -23,7 +81,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,27 +89,24 @@ 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]; + const datum = data[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) { + if (showLegend) { margin.left += 40; } } else { - margin.left = fd.left_margin; - } - if (fd.bottom_margin === 'auto') { - margin.bottom = Math.ceil(Math.max(margin.bottom, pixelsPerCharX * longestX)); - } else { - margin.bottom = fd.bottom_margin; + margin.left = leftMargin; } + margin.bottom = (bottomMargin === 'auto') + ? Math.ceil(Math.max(margin.bottom, pixelsPerCharX * longestX)) + : bottomMargin; } function ordScale(k, rangeBands, sortMethod) { @@ -83,28 +138,27 @@ 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() @@ -115,14 +169,14 @@ function heatmapVis(slice, payload) { .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,7 +186,7 @@ function heatmapVis(slice, payload) { .attr('height', height) .style('position', 'relative'); - if (fd.show_values) { + if (showValues) { const cells = svg.selectAll('rect') .data(data) .enter() @@ -150,7 +204,7 @@ function heatmapVis(slice, payload) { .attr('fill', d => d.v >= payload.data.extents[1] / 2 ? 'white' : 'black'); } - if (fd.show_legend) { + if (showLegend) { const colorLegend = d3.legend.color() .labelFormat(valueFormatter) .scale(colorScaler) @@ -177,14 +231,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 += '
' + fd.all_columns_x + ': ' + obj.x + '
'; - s += '
' + fd.all_columns_y + ': ' + obj.y + '
'; - s += '
' + metric + ': ' + valueFormatter(obj.v) + '
'; - if (fd.show_perc) { - s += '
%: ' + fp(fd.normalized ? obj.rank : obj.perc) + '
'; + s += '
' + columnX + ': ' + obj.x + '
'; + s += '
' + columnY + ': ' + obj.y + '
'; + s += '
' + metricLabel + ': ' + valueFormatter(obj.v) + '
'; + if (showPercentage) { + s += '
%: ' + fp(normalized ? obj.rank : obj.perc) + '
'; } tip.style('display', null); } else { @@ -212,7 +266,7 @@ function heatmapVis(slice, payload) { .scale(xRbScale) .tickValues(xRbScale.domain().filter( function (d, i) { - return !(i % (parseInt(fd.xscale_interval, 10))); + return !(i % (xScaleInterval)); })) .orient('bottom'); @@ -220,7 +274,7 @@ function heatmapVis(slice, payload) { .scale(yRbScale) .tickValues(yRbScale.domain().filter( function (d, i) { - return !(i % (parseInt(fd.yscale_interval, 10))); + return !(i % (yScaleInterval)); })) .orient('left'); @@ -247,7 +301,7 @@ function heatmapVis(slice, payload) { const image = context.createImageData(heatmapDim[0], heatmapDim[1]); const pixs = {}; data.forEach((d) => { - const c = d3.rgb(colorScaler(fd.normalized ? d.rank : d.perc)); + 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 +332,56 @@ 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); + + // console.log('formData', formData); + // return; + + 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; From 1af829956440799d76a2bd31b0535992c53095df Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Wed, 22 Aug 2018 15:32:24 -0700 Subject: [PATCH 2/7] Define data shape --- superset/assets/src/visualizations/heatmap.js | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/superset/assets/src/visualizations/heatmap.js b/superset/assets/src/visualizations/heatmap.js index 260be0d8e433b..87423f12a20f3 100644 --- a/superset/assets/src/visualizations/heatmap.js +++ b/superset/assets/src/visualizations/heatmap.js @@ -10,7 +10,14 @@ import './heatmap.css'; const propTypes = { data: PropTypes.shape({ - records: PropTypes.array, + 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, @@ -51,7 +58,7 @@ function Heatmap(element, props) { PropTypes.checkPropTypes(propTypes, props, 'prop', 'Heatmap'); const { - data: rawData, + data, width, height, bottomMargin, @@ -73,7 +80,8 @@ function Heatmap(element, props) { yAxisBounds, } = props; - const data = rawData.records; + const { records, extent } = data; + // const data = rawData.records; const margin = { top: 10, @@ -90,8 +98,8 @@ function Heatmap(element, props) { let longestX = 1; let longestY = 1; - for (let i = 0; i < data.length; i++) { - const 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); } @@ -112,7 +120,7 @@ function Heatmap(element, props) { 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]; }); @@ -188,7 +196,7 @@ function Heatmap(element, props) { if (showValues) { const cells = svg.selectAll('rect') - .data(data) + .data(records) .enter() .append('g') .attr('transform', `translate(${margin.left}, ${margin.top})`); @@ -201,7 +209,7 @@ function Heatmap(element, props) { .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 (showLegend) { @@ -300,7 +308,7 @@ function Heatmap(element, props) { const imageObj = new Image(); const image = context.createImageData(heatmapDim[0], heatmapDim[1]); const pixs = {}; - data.forEach((d) => { + records.forEach((d) => { const c = d3.rgb(colorScaler(normalized ? d.rank : d.perc)); const x = xScale(d.x); const y = yScale(d.y); @@ -357,9 +365,6 @@ function adaptor(slice, payload) { } = formData; const element = document.querySelector(selector); - // console.log('formData', formData); - // return; - return Heatmap(element, { data: payload.data, width: slice.width(), From 24c2bd4817f27fffc9c4d8ec67764a48e0c22e93 Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Wed, 22 Aug 2018 15:39:10 -0700 Subject: [PATCH 3/7] update style --- .../assets/src/visualizations/heatmap.css | 14 +++++--- superset/assets/src/visualizations/heatmap.js | 35 +++++++++---------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/superset/assets/src/visualizations/heatmap.css b/superset/assets/src/visualizations/heatmap.css index 79542e27e57c1..291c2a2cad9e0 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+ */ diff --git a/superset/assets/src/visualizations/heatmap.js b/superset/assets/src/visualizations/heatmap.js index 87423f12a20f3..3bc52a519c999 100644 --- a/superset/assets/src/visualizations/heatmap.js +++ b/superset/assets/src/visualizations/heatmap.js @@ -80,8 +80,7 @@ function Heatmap(element, props) { yAxisBounds, } = props; - const { records, extent } = data; - // const data = rawData.records; + const { records, extents } = data; const margin = { top: 10, @@ -214,17 +213,17 @@ function Heatmap(element, props) { 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(50) + .shapeWidth(10) + .shapeHeight(3) + .labelOffset(2); svg.append('g') - .attr('transform', 'translate(10, 5)') - .call(colorLegend); + .attr('transform', 'translate(10, 5)') + .call(colorLegend); } const tip = d3tip() @@ -258,15 +257,13 @@ function Heatmap(element, props) { }); 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); From bb34eefaf9bc2ab3c5c187d6fb90697171fad837 Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Wed, 22 Aug 2018 15:53:28 -0700 Subject: [PATCH 4/7] organize imports --- superset/assets/src/visualizations/heatmap.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/superset/assets/src/visualizations/heatmap.js b/superset/assets/src/visualizations/heatmap.js index 3bc52a519c999..7bdcd6420e92d 100644 --- a/superset/assets/src/visualizations/heatmap.js +++ b/superset/assets/src/visualizations/heatmap.js @@ -1,7 +1,6 @@ import d3 from 'd3'; import PropTypes from 'prop-types'; -// eslint-disable-next-line no-unused-vars -import d3legend from 'd3-svg-legend'; +import 'd3-svg-legend'; import d3tip from 'd3-tip'; import { colorScalerFactory } from '../modules/colors'; From 382f54c54e6a7cf9518ac89f2eb58964c67fbb95 Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Wed, 22 Aug 2018 16:11:10 -0700 Subject: [PATCH 5/7] fix heatmap axis labels --- .../assets/src/visualizations/heatmap.css | 5 ++++ superset/assets/src/visualizations/heatmap.js | 27 ++++++++++++------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/superset/assets/src/visualizations/heatmap.css b/superset/assets/src/visualizations/heatmap.css index 291c2a2cad9e0..76ecb60352a56 100644 --- a/superset/assets/src/visualizations/heatmap.css +++ b/superset/assets/src/visualizations/heatmap.css @@ -45,3 +45,8 @@ .heatmap .legendCells .cell:last-child text { opacity: 1; } + +.dashboard .heatmap .axis text { + font-size: 10px; + opacity: .75; +} \ No newline at end of file diff --git a/superset/assets/src/visualizations/heatmap.js b/superset/assets/src/visualizations/heatmap.js index 7bdcd6420e92d..962c90a31f294 100644 --- a/superset/assets/src/visualizations/heatmap.js +++ b/superset/assets/src/visualizations/heatmap.js @@ -104,12 +104,14 @@ function Heatmap(element, props) { if (leftMargin === 'auto') { margin.left = Math.ceil(Math.max(margin.left, pixelsPerCharY * longestY)); - if (showLegend) { - margin.left += 40; - } } else { margin.left = leftMargin; } + + if (showLegend) { + margin.right += 40; + } + margin.bottom = (bottomMargin === 'auto') ? Math.ceil(Math.max(margin.bottom, pixelsPerCharX * longestX)) : bottomMargin; @@ -215,13 +217,13 @@ function Heatmap(element, props) { .labelFormat(valueFormatter) .scale(colorScaler) .shapePadding(0) - .cells(50) + .cells(10) .shapeWidth(10) - .shapeHeight(3) - .labelOffset(2); + .shapeHeight(10) + .labelOffset(3); svg.append('g') - .attr('transform', 'translate(10, 5)') + .attr('transform', `translate(${width - 40}, ${margin.top})`) .call(colorLegend); } @@ -268,6 +270,7 @@ function Heatmap(element, props) { const xAxis = d3.svg.axis() .scale(xRbScale) + .outerTickSize(0) .tickValues(xRbScale.domain().filter( function (d, i) { return !(i % (xScaleInterval)); @@ -276,6 +279,7 @@ function Heatmap(element, props) { const yAxis = d3.svg.axis() .scale(yRbScale) + .outerTickSize(0) .tickValues(yRbScale.domain().filter( function (d, i) { return !(i % (yScaleInterval)); @@ -285,10 +289,13 @@ function Heatmap(element, props) { 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') From 93545a4b200872d1dc520a6dee2c9bbad4d4bb48 Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Wed, 22 Aug 2018 16:15:41 -0700 Subject: [PATCH 6/7] add new line --- superset/assets/src/visualizations/heatmap.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset/assets/src/visualizations/heatmap.css b/superset/assets/src/visualizations/heatmap.css index 76ecb60352a56..597a48fd4b9ce 100644 --- a/superset/assets/src/visualizations/heatmap.css +++ b/superset/assets/src/visualizations/heatmap.css @@ -49,4 +49,4 @@ .dashboard .heatmap .axis text { font-size: 10px; opacity: .75; -} \ No newline at end of file +} From 549433a942e009417193f9e2b4d2accb9114eadc Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Wed, 22 Aug 2018 16:52:06 -0700 Subject: [PATCH 7/7] adjust indent --- superset/assets/src/visualizations/heatmap.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/superset/assets/src/visualizations/heatmap.js b/superset/assets/src/visualizations/heatmap.js index 962c90a31f294..3a6b35167899d 100644 --- a/superset/assets/src/visualizations/heatmap.js +++ b/superset/assets/src/visualizations/heatmap.js @@ -170,11 +170,11 @@ function Heatmap(element, props) { 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(element); @@ -302,7 +302,6 @@ function Heatmap(element, props) { .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') .call(yAxis); - const context = canvas.node().getContext('2d'); context.imageSmoothingEnabled = false;