From 6eea4ac005208361ea7425ae6fece7e34e6d6007 Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Mon, 27 Aug 2018 21:49:54 -0700 Subject: [PATCH 1/9] [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 (cherry picked from commit fcf2c756c0cdebb8aad194ce0df9c6c1ec58deff) --- .../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 79542e27e57c1..597a48fd4b9ce 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 c26291ffca802..3a6b35167899d 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 += '
' + 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 { @@ -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; From 619e94558bc95f03401f8885c11c511cf4545b54 Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Mon, 27 Aug 2018 21:50:08 -0700 Subject: [PATCH 2/9] [SIP-5] Refactor sankey (#5701) * extract slice and formdata from sankey * use arrow function * reorganize imports (cherry picked from commit 506cfd14500542d451161c061071fd9075abf65f) --- superset/assets/src/visualizations/sankey.js | 90 ++++++++++++-------- 1 file changed, 56 insertions(+), 34 deletions(-) diff --git a/superset/assets/src/visualizations/sankey.js b/superset/assets/src/visualizations/sankey.js index bb08fb3caab16..29ed2e2ffa18e 100644 --- a/superset/assets/src/visualizations/sankey.js +++ b/superset/assets/src/visualizations/sankey.js @@ -1,28 +1,47 @@ /* eslint-disable no-param-reassign */ import d3 from 'd3'; +import PropTypes from 'prop-types'; +import { sankey as d3Sankey } from 'd3-sankey'; import { getColorFromScheme } from '../modules/colors'; import './sankey.css'; -d3.sankey = require('d3-sankey').sankey; - - -function sankeyVis(slice, payload) { - const div = d3.select(slice.selector); +const propTypes = { + data: PropTypes.arrayOf(PropTypes.shape({ + source: PropTypes.string, + target: PropTypes.string, + value: PropTypes.number, + })), + width: PropTypes.number, + height: PropTypes.number, + colorScheme: PropTypes.string, +}; + +const formatNumber = d3.format(',.2f'); + +function Sankey(element, props) { + PropTypes.checkPropTypes(propTypes, props, 'prop', 'Sankey'); + + const { + data, + width, + height, + colorScheme, + } = props; + + const div = d3.select(element); const margin = { top: 5, right: 5, bottom: 5, left: 5, }; - const width = slice.width() - margin.left - margin.right; - const height = slice.height() - margin.top - margin.bottom; - - const formatNumber = d3.format(',.2f'); + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; div.selectAll('*').remove(); const svg = div.append('svg') - .attr('width', width + margin.left + margin.right) - .attr('height', height + margin.top + margin.bottom) + .attr('width', innerWidth + margin.left + margin.right) + .attr('height', innerHeight + margin.top + margin.bottom) .append('g') .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); @@ -30,16 +49,16 @@ function sankeyVis(slice, payload) { .attr('class', 'sankey-tooltip') .style('opacity', 0); - const sankey = d3.sankey() + const sankey = d3Sankey() .nodeWidth(15) .nodePadding(10) - .size([width, height]); + .size([innerWidth, innerHeight]); const path = sankey.link(); let nodes = {}; // Compute the distinct nodes from the links. - const links = payload.data.map(function (row) { + const links = data.map(function (row) { const link = Object.assign({}, row); link.source = nodes[link.source] || (nodes[link.source] = { name: link.source }); link.target = nodes[link.target] || (nodes[link.target] = { name: link.target }); @@ -120,13 +139,9 @@ function sankeyVis(slice, payload) { .enter() .append('g') .attr('class', 'node') - .attr('transform', function (d) { - return 'translate(' + d.x + ',' + d.y + ')'; - }) + .attr('transform', d => 'translate(' + d.x + ',' + d.y + ')') .call(d3.behavior.drag() - .origin(function (d) { - return d; - }) + .origin(d => d) .on('dragstart', function () { this.parentNode.appendChild(this); }) @@ -138,31 +153,38 @@ function sankeyVis(slice, payload) { .attr('width', sankey.nodeWidth()) .style('fill', function (d) { const name = d.name || 'N/A'; - d.color = getColorFromScheme(name.replace(/ .*/, ''), slice.formData.color_scheme); + d.color = getColorFromScheme(name.replace(/ .*/, ''), colorScheme); return d.color; }) - .style('stroke', function (d) { - return d3.rgb(d.color).darker(2); - }) + .style('stroke', d => d3.rgb(d.color).darker(2)) .on('mouseover', onmouseover) .on('mouseout', onmouseout); node.append('text') .attr('x', -6) - .attr('y', function (d) { - return d.dy / 2; - }) + .attr('y', d => d.dy / 2) .attr('dy', '.35em') .attr('text-anchor', 'end') .attr('transform', null) - .text(function (d) { - return d.name; - }) - .filter(function (d) { - return d.x < width / 2; - }) + .text(d => d.name) + .filter(d => d.x < innerWidth / 2) .attr('x', 6 + sankey.nodeWidth()) .attr('text-anchor', 'start'); } -module.exports = sankeyVis; +Sankey.propTypes = propTypes; + +function adaptor(slice, payload) { + const { selector, formData } = slice; + const { color_scheme: colorScheme } = formData; + const element = document.querySelector(selector); + + return Sankey(element, { + data: payload.data, + width: slice.width(), + height: slice.height(), + colorScheme, + }); +} + +export default adaptor; From 3db6acc8ee94924c873b1ff5e2ab5ea9134096f1 Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Wed, 29 Aug 2018 12:36:48 -0700 Subject: [PATCH 3/9] [SIP-5] Refactor World Map (#5719) * extract slice and formData * update proptypes of data (cherry picked from commit 00cc6e981e946b29d582ab78e74967a2e009deec) --- .../assets/src/visualizations/world_map.js | 92 +++++++++++++------ 1 file changed, 63 insertions(+), 29 deletions(-) diff --git a/superset/assets/src/visualizations/world_map.js b/superset/assets/src/visualizations/world_map.js index e7c1047ca7e9d..6c4948a7dd6c1 100644 --- a/superset/assets/src/visualizations/world_map.js +++ b/superset/assets/src/visualizations/world_map.js @@ -1,51 +1,67 @@ -// JS -const d3 = require('d3'); -const Datamap = require('datamaps'); +import d3 from 'd3'; +import PropTypes from 'prop-types'; +import Datamap from 'datamaps'; +import './world_map.css'; -// CSS -require('./world_map.css'); +const propTypes = { + data: PropTypes.arrayOf(PropTypes.shape({ + country: PropTypes.string, + latitude: PropTypes.number, + longitude: PropTypes.number, + name: PropTypes.string, + m1: PropTypes.number, + m2: PropTypes.number, + })), + height: PropTypes.number, + maxBubbleSize: PropTypes.number, + showBubbles: PropTypes.bool, +}; -function worldMapChart(slice, payload) { - const container = slice.container; - const div = d3.select(slice.selector); +const formatter = d3.format('.3s'); - container.css('height', slice.height()); +function WorldMap(element, props) { + PropTypes.checkPropTypes(propTypes, props, 'prop', 'WorldMap'); + + const { + data, + height, + maxBubbleSize, + showBubbles, + } = props; + + const div = d3.select(element); + + const container = element; + container.style.height = `${height}px`; div.selectAll('*').remove(); - const fd = slice.formData; + // Ignore XXX's to get better normalization - let data = payload.data.filter(d => (d.country && d.country !== 'XXX')); + const filteredData = data.filter(d => (d.country && d.country !== 'XXX')); - const ext = d3.extent(data, function (d) { - return d.m1; - }); - const extRadius = d3.extent(data, function (d) { - return d.m2; - }); + const ext = d3.extent(filteredData, d => d.m1); + const extRadius = d3.extent(filteredData, d => d.m2); const radiusScale = d3.scale.linear() .domain([extRadius[0], extRadius[1]]) - .range([1, fd.max_bubble_size]); + .range([1, maxBubbleSize]); const colorScale = d3.scale.linear() .domain([ext[0], ext[1]]) .range(['#FFF', 'black']); - data = data.map(d => Object.assign({}, d, { + const processedData = filteredData.map(d => ({ + ...d, radius: radiusScale(d.m2), fillColor: colorScale(d.m1), })); const mapData = {}; - data.forEach((d) => { + processedData.forEach((d) => { mapData[d.country] = d; }); - const formatter = d3.format('.3s'); - - container.show(); - const map = new Datamap({ - element: slice.container.get(0), - data, + element, + data: processedData, fills: { defaultFill: '#ddd', }, @@ -85,10 +101,28 @@ function worldMapChart(slice, payload) { map.updateChoropleth(mapData); - if (fd.show_bubbles) { - map.bubbles(data); + if (showBubbles) { + map.bubbles(processedData); div.selectAll('circle.datamaps-bubble').style('fill', '#005a63'); } } -module.exports = worldMapChart; +WorldMap.propTypes = propTypes; + +function adaptor(slice, payload) { + const { selector, formData } = slice; + const { + max_bubble_size: maxBubbleSize, + show_bubbles: showBubbles, + } = formData; + const element = document.querySelector(selector); + + return WorldMap(element, { + data: payload.data, + height: slice.height(), + maxBubbleSize: parseInt(maxBubbleSize, 10), + showBubbles, + }); +} + +export default adaptor; From 67706200704d2716d11eb3c3d88875997a76bb3b Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Wed, 29 Aug 2018 14:56:28 -0700 Subject: [PATCH 4/9] [SIP-5] Refactor rose (#5763) * refactor rose * update proptypes (cherry picked from commit 86f99b0e1e653b046f5c019bbcd3568e84c0b937) --- superset/assets/src/visualizations/rose.js | 89 +++++++++++++++++----- 1 file changed, 70 insertions(+), 19 deletions(-) diff --git a/superset/assets/src/visualizations/rose.js b/superset/assets/src/visualizations/rose.js index a0a545aae028f..875e748b5ef34 100644 --- a/superset/assets/src/visualizations/rose.js +++ b/superset/assets/src/visualizations/rose.js @@ -1,11 +1,27 @@ /* eslint no-use-before-define: ["error", { "functions": false }] */ import d3 from 'd3'; +import PropTypes from 'prop-types'; import nv from 'nvd3'; import { d3TimeFormatPreset } from '../modules/utils'; import { getColorFromScheme } from '../modules/colors'; - import './rose.css'; +const propTypes = { + // Data is an object hashed by numeric value, perhaps timestamp + data: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.shape({ + key: PropTypes.arrayOf(PropTypes.string), + name: PropTypes.arrayOf(PropTypes.string), + time: PropTypes.number, + value: PropTypes.number, + }))), + width: PropTypes.number, + height: PropTypes.number, + dateTimeFormat: PropTypes.string, + numberFormat: PropTypes.string, + useRichTooltip: PropTypes.bool, + useAreaProportions: PropTypes.bool, +}; + function copyArc(d) { return { startAngle: d.startAngle, @@ -22,10 +38,21 @@ function sortValues(a, b) { return b.value - a.value; } -function roseVis(slice, payload) { - const data = payload.data; - const fd = slice.formData; - const div = d3.select(slice.selector); +function Rose(element, props) { + PropTypes.checkPropTypes(propTypes, props, 'prop', 'Rose'); + + const { + data, + width, + height, + colorScheme, + dateTimeFormat, + numberFormat, + useRichTooltip, + useAreaProportions, + } = props; + + const div = d3.select(element); const datum = data; const times = Object.keys(datum) @@ -33,8 +60,8 @@ function roseVis(slice, payload) { .sort((a, b) => a - b); const numGrains = times.length; const numGroups = datum[times[0]].length; - const format = d3.format(fd.number_format); - const timeFormat = d3TimeFormatPreset(fd.date_time_format); + const format = d3.format(numberFormat); + const timeFormat = d3TimeFormatPreset(dateTimeFormat); d3.select('.nvtooltip').remove(); div.selectAll('*').remove(); @@ -43,12 +70,12 @@ function roseVis(slice, payload) { const legend = nv.models.legend(); const tooltip = nv.models.tooltip(); const state = { disabled: datum[times[0]].map(() => false) }; - const color = name => getColorFromScheme(name, fd.color_scheme); + const color = name => getColorFromScheme(name, colorScheme); const svg = div .append('svg') - .attr('width', slice.width()) - .attr('height', slice.height()); + .attr('width', width) + .attr('height', height); const g = svg .append('g') @@ -68,7 +95,7 @@ function roseVis(slice, payload) { function tooltipData(d, i, adatum) { const timeIndex = Math.floor(d.arcId / numGroups); - const series = fd.rich_tooltip ? + const series = useRichTooltip ? adatum[times[timeIndex]] .filter(v => !state.disabled[v.id % numGroups]) .map(v => ({ @@ -85,8 +112,8 @@ function roseVis(slice, payload) { } legend - .width(slice.width()) - .color(d => getColorFromScheme(d.key, fd.color_scheme)); + .width(width) + .color(d => getColorFromScheme(d.key, colorScheme)); legendWrap .datum(legendData(datum)) .call(legend); @@ -96,16 +123,15 @@ function roseVis(slice, payload) { .valueFormatter(format); // Compute max radius, which the largest value will occupy - const width = slice.width(); - const height = slice.height() - legend.height(); + const roseHeight = height - legend.height(); const margin = { top: legend.height() }; const edgeMargin = 35; // space between outermost radius and slice edge - const maxRadius = Math.min(width, height) / 2 - edgeMargin; + const maxRadius = Math.min(width, roseHeight) / 2 - edgeMargin; const labelThreshold = 0.05; const gro = 8; // mouseover radius growth in pixels const mini = 0.075; - const centerTranslate = `translate(${width / 2},${height / 2 + margin.top})`; + const centerTranslate = `translate(${width / 2},${roseHeight / 2 + margin.top})`; const roseWrap = g .append('g') .attr('transform', centerTranslate) @@ -146,7 +172,7 @@ function roseVis(slice, payload) { // Compute proportion const P = maxRadius / maxSum; const Q = P * maxRadius; - const computeOuterRadius = (value, innerRadius) => fd.rose_area_proportion ? + const computeOuterRadius = (value, innerRadius) => useAreaProportions ? Math.sqrt(Q * value + innerRadius * innerRadius) : P * value + innerRadius; @@ -537,4 +563,29 @@ function roseVis(slice, payload) { }); } -module.exports = roseVis; +Rose.propTypes = propTypes; + +function adaptor(slice, payload) { + const { selector, formData } = slice; + const { + color_scheme: colorScheme, + date_time_format: dateTimeFormat, + number_format: numberFormat, + rich_tooltip: useRichTooltip, + rose_area_proportion: useAreaProportions, + } = formData; + const element = document.querySelector(selector); + + return Rose(element, { + data: payload.data, + width: slice.width(), + height: slice.height(), + colorScheme, + dateTimeFormat, + numberFormat, + useRichTooltip, + useAreaProportions, + }); +} + +export default adaptor; From 32423e61b2b9fa2837e7998357cf9e70f44e1898 Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Wed, 29 Aug 2018 15:21:04 -0700 Subject: [PATCH 5/9] [SIP-5] Refactor and repair partition (#5718) * Extract slice and formData * reorder functions * fix tooltip * remove commented code * remove commented code and rename variables * remove verboseMap * rename kx, ky to zoomX, zoomY (cherry picked from commit 00f2771f23b5d72c2f40c6906de0c59707c479bf) --- .../assets/src/visualizations/partition.css | 21 +- .../assets/src/visualizations/partition.js | 299 +++++++++++------- 2 files changed, 210 insertions(+), 110 deletions(-) diff --git a/superset/assets/src/visualizations/partition.css b/superset/assets/src/visualizations/partition.css index e23cca795203f..a0fdaedea0064 100644 --- a/superset/assets/src/visualizations/partition.css +++ b/superset/assets/src/visualizations/partition.css @@ -1,3 +1,7 @@ +.partition { + position: relative; +} + .partition .chart { display: block; margin: auto; @@ -18,10 +22,25 @@ .partition g text { font-weight: bold; - pointer-events: none; fill: rgba(0, 0, 0, 0.8); } .partition g:hover text { fill: rgba(0, 0, 0, 1); } + +.partition .partition-tooltip { + position: absolute; + top: 0; + left: 0; + opacity: 0; + padding: 5px; + pointer-events: none; + background-color: rgba(255,255,255, 0.75); + border-radius: 5px; +} + +.partition-tooltip td { + padding-left: 5px; + font-size: 11px; +} diff --git a/superset/assets/src/visualizations/partition.js b/superset/assets/src/visualizations/partition.js index 23e4474e62606..fa7bbad869aeb 100644 --- a/superset/assets/src/visualizations/partition.js +++ b/superset/assets/src/visualizations/partition.js @@ -1,19 +1,14 @@ /* eslint no-param-reassign: [2, {"props": false}] */ -/* eslint no-use-before-define: ["error", { "functions": false }] */ import d3 from 'd3'; -import { - d3TimeFormatPreset, -} from '../modules/utils'; +import PropTypes from 'prop-types'; +import { hierarchy } from 'd3-hierarchy'; +import { d3TimeFormatPreset } from '../modules/utils'; import { getColorFromScheme } from '../modules/colors'; - import './partition.css'; -d3.hierarchy = require('d3-hierarchy').hierarchy; -d3.partition = require('d3-hierarchy').partition; - +// Compute dx, dy, x, y for each node and +// return an array of nodes in breadth-first order function init(root) { - // Compute dx, dy, x, y for each node and - // return an array of nodes in breadth-first order const flat = []; const dy = 1.0 / (root.height + 1); let prev = null; @@ -33,36 +28,85 @@ function init(root) { return flat; } +// Declare PropTypes for recursive data structures +// https://github.com/facebook/react/issues/5676 +const lazyFunction = f => (() => f().apply(this, arguments)); + const leafType = PropTypes.shape({ + name: PropTypes.string, + val: PropTypes.number.isRequired, +}); + const parentShape = { + name: PropTypes.string, + val: PropTypes.number.isRequired, + children: PropTypes.arrayOf(PropTypes.oneOfType([ + PropTypes.shape(lazyFunction(() => parentShape)), + leafType, + ])), +}; + const nodeType = PropTypes.oneOfType([ + PropTypes.shape(parentShape), + leafType, +]); + +const propTypes = { + data: PropTypes.arrayOf(nodeType), // array of rootNode + width: PropTypes.number, + height: PropTypes.number, + colorScheme: PropTypes.string, + dateTimeFormat: PropTypes.string, + equalDateSize: PropTypes.bool, + groupBy: PropTypes.arrayOf(PropTypes.string), + useLogScale: PropTypes.bool, + metrics: PropTypes.arrayOf(PropTypes.oneOfType([ + PropTypes.string, + PropTypes.object, + ])), + numberFormat: PropTypes.string, + partitionLimit: PropTypes.number, + partitionThreshold: PropTypes.number, + useRichTooltip: PropTypes.bool, + timeSeriesOption: PropTypes.string, +}; + // This vis is based on // http://mbostock.github.io/d3/talk/20111018/partition.html -function partitionVis(slice, payload) { - const data = payload.data; - const fd = slice.formData; - const div = d3.select(slice.selector); - const metrics = fd.metrics || []; +function Icicle(element, props) { + PropTypes.checkPropTypes(propTypes, props, 'prop', 'Icicle'); + + const { + width, + height, + data, + colorScheme, + dateTimeFormat, + equalDateSize, + groupBy, + useLogScale = false, + metrics = [], + numberFormat, + partitionLimit, + partitionThreshold, + useRichTooltip, + timeSeriesOption = 'not_time', + } = props; + + const div = d3.select(element); // Chart options - const logScale = fd.log_scale || false; - const chartType = fd.time_series_option || 'not_time'; + const chartType = timeSeriesOption; const hasTime = ['adv_anal', 'time_series'].indexOf(chartType) >= 0; - const format = d3.format(fd.number_format); - const timeFormat = d3TimeFormatPreset(fd.date_time_format); + const format = d3.format(numberFormat); + const timeFormat = d3TimeFormatPreset(dateTimeFormat); div.selectAll('*').remove(); - d3.selectAll('.nvtooltip').remove(); - const tooltip = d3 - .select('body') + const tooltip = div .append('div') - .attr('class', 'nvtooltip') - .style('opacity', 0) - .style('top', 0) - .style('left', 0) - .style('position', 'fixed'); + .classed('partition-tooltip', true); function drawVis(i, dat) { const datum = dat[i]; - const w = slice.width(); - const h = slice.height() / data.length; + const w = width; + const h = height / data.length; const x = d3.scale.linear().range([0, w]); const y = d3.scale.linear().range([0, h]); @@ -83,7 +127,7 @@ function partitionVis(slice, payload) { viz.style('padding-top', '3px'); } - const root = d3.hierarchy(datum); + const root = hierarchy(datum); function hasDateNode(n) { return metrics.indexOf(n.data.name) >= 0 && hasTime; @@ -103,12 +147,12 @@ function partitionVis(slice, payload) { // the time column, perform a date-time format if (n.parent && hasDateNode(n.parent)) { // Format timestamp values - n.weight = fd.equal_date_size ? 1 : n.value; + n.weight = equalDateSize ? 1 : n.value; n.value = n.name; n.name = timeFormat(n.name); } - if (logScale) n.weight = Math.log(n.weight + 1); - n.disp = n.disp && !isNaN(n.disp) && isFinite(n.disp) ? format(n.disp) : ''; + if (useLogScale) n.weight = Math.log(n.weight + 1); + n.disp = n.disp && !Number.isNaN(n.disp) && isFinite(n.disp) ? format(n.disp) : ''; }); // Perform sort by weight root.sort((a, b) => { @@ -121,20 +165,20 @@ function partitionVis(slice, payload) { // Prune data based on partition limit and threshold // both are applied at the same time - if (fd.partition_threshold && fd.partition_threshold >= 0) { + if (partitionThreshold && partitionThreshold >= 0) { // Compute weight sums as we go root.each((n) => { n.sum = n.children ? n.children.reduce((a, v) => a + v.weight, 0) || 1 : 1; if (n.children) { // Dates are not ordered by weight if (hasDateNode(n)) { - if (fd.equal_date_size) { + if (equalDateSize) { return; } const removeIndices = []; // Keep at least one child for (let j = 1; j < n.children.length; j++) { - if (n.children[j].weight / n.sum < fd.partition_threshold) { + if (n.children[j].weight / n.sum < partitionThreshold) { removeIndices.push(j); } } @@ -145,7 +189,7 @@ function partitionVis(slice, payload) { // Find first child that falls below the threshold let j; for (j = 1; j < n.children.length; j++) { - if (n.children[j].weight / n.sum < fd.partition_threshold) { + if (n.children[j].weight / n.sum < partitionThreshold) { break; } } @@ -154,11 +198,11 @@ function partitionVis(slice, payload) { } }); } - if (fd.partition_limit && fd.partition_limit >= 0) { + if (partitionLimit && partitionLimit >= 0) { root.each((n) => { - if (n.children && n.children.length > fd.partition_limit) { + if (n.children && n.children.length > partitionLimit) { if (!hasDateNode(n)) { - n.children = n.children.slice(0, fd.partition_limit); + n.children = n.children.slice(0, partitionLimit); } } }); @@ -168,7 +212,6 @@ function partitionVis(slice, payload) { n.sum = n.children ? n.children.reduce((a, v) => a + v.weight, 0) || 1 : 1; }); - const verboseMap = slice.datasource.verbose_map; function getCategory(depth) { if (!depth) { return 'Metric'; @@ -176,8 +219,7 @@ function partitionVis(slice, payload) { if (hasTime && depth === 1) { return 'Date'; } - const col = fd.groupby[depth - (hasTime ? 2 : 1)]; - return verboseMap[col] || col; + return groupBy[depth - (hasTime ? 2 : 1)]; } function getAncestors(d) { @@ -192,55 +234,65 @@ function partitionVis(slice, payload) { function positionAndPopulate(tip, d) { let t = ''; - if (!fd.rich_tooltip) { - t += ( - '' - ); - t += ( - '' + - '' + - `` + - `` + - '' - ); - } else { + if (useRichTooltip) { const nodes = getAncestors(d); - nodes.forEach((n) => { + nodes.reverse().forEach((n) => { const atNode = n.depth === d.depth; t += ''; t += ( - `` + - `' + + '' + + `` + `` + `` + - `` + '' ); }); + } else { + t += ( + '' + ); + t += ( + '' + + '' + + `` + + `` + + '' + ); } t += '
' + - `${getCategory(d.depth)}` + - '
' + - `
' + - '
${d.name}${d.disp}
` + + '
' + '
' + '
${getCategory(n.depth)}${n.name}${n.disp}${getCategory(n.depth)}
' + + `${getCategory(d.depth)}` + + '
' + + `
' + + '
${d.name}${d.disp}
'; + const [tipX, tipY] = d3.mouse(element); tip.html(t) - .style('left', (d3.event.pageX + 13) + 'px') - .style('top', (d3.event.pageY - 10) + 'px'); + .style('left', (tipX + 15) + 'px') + .style('top', (tipY) + 'px'); + } + + const nodes = init(root); + + let zoomX = w / root.dx; + let zoomY = h / 1; + + // Keep text centered in its division + function transform(d) { + return `translate(8,${d.dx * zoomY / 2})`; } const g = viz .selectAll('g') - .data(init(root)) - .enter() + .data(nodes) + .enter() .append('svg:g') .attr('transform', d => `translate(${x(d.y)},${y(d.x)})`) - .on('click', click) .on('mouseover', (d) => { tooltip .interrupt() @@ -260,40 +312,6 @@ function partitionVis(slice, payload) { .style('opacity', 0); }); - let kx = w / root.dx; - let ky = h / 1; - - g.append('svg:rect') - .attr('width', root.dy * kx) - .attr('height', d => d.dx * ky); - - g.append('svg:text') - .attr('transform', transform) - .attr('dy', '0.35em') - .style('opacity', d => d.dx * ky > 12 ? 1 : 0) - .text((d) => { - if (!d.disp) { - return d.name; - } - return `${d.name}: ${d.disp}`; - }); - - // Apply color scheme - g.selectAll('rect') - .style('fill', (d) => { - d.color = getColorFromScheme(d.name, fd.color_scheme); - return d.color; - }); - - // Zoom out when clicking outside vis - // d3.select(window) - // .on('click', () => click(root)); - - // Keep text centered in its division - function transform(d) { - return `translate(8,${d.dx * ky / 2})`; - } - // When clicking a subdivision, the vis will zoom in to it function click(d) { if (!d.children) { @@ -303,8 +321,8 @@ function partitionVis(slice, payload) { } return false; } - kx = (d.y ? w - 40 : w) / (1 - d.y); - ky = h / d.dx; + zoomX = (d.y ? w - 40 : w) / (1 - d.y); + zoomY = h / d.dx; x.domain([d.y, 1]).range([d.y ? 40 : 0, w]); y.domain([d.x, d.x + d.dx]); @@ -314,20 +332,83 @@ function partitionVis(slice, payload) { .attr('transform', nd => `translate(${x(nd.y)},${y(nd.x)})`); t.select('rect') - .attr('width', d.dy * kx) - .attr('height', nd => nd.dx * ky); + .attr('width', d.dy * zoomX) + .attr('height', nd => nd.dx * zoomY); t.select('text') .attr('transform', transform) - .style('opacity', nd => nd.dx * ky > 12 ? 1 : 0); + .style('opacity', nd => nd.dx * zoomY > 12 ? 1 : 0); d3.event.stopPropagation(); return true; } + + g.on('click', click); + + g.append('svg:rect') + .attr('width', root.dy * zoomX) + .attr('height', d => d.dx * zoomY); + + g.append('svg:text') + .attr('transform', transform) + .attr('dy', '0.35em') + .style('opacity', d => d.dx * zoomY > 12 ? 1 : 0) + .text((d) => { + if (!d.disp) { + return d.name; + } + return `${d.name}: ${d.disp}`; + }); + + // Apply color scheme + g.selectAll('rect') + .style('fill', (d) => { + d.color = getColorFromScheme(d.name, colorScheme); + return d.color; + }); } + for (let i = 0; i < data.length; i++) { drawVis(i, data); } } -module.exports = partitionVis; +Icicle.propTypes = propTypes; + +function adaptor(slice, payload) { + const { selector, formData, datasource } = slice; + const { + color_scheme: colorScheme, + date_time_format: dateTimeFormat, + equal_date_size: equalDateSize, + groupby: groupBy, + log_scale: useLogScale, + metrics, + number_format: numberFormat, + partition_limit: partitionLimit, + partition_threshold: partitionThreshold, + rich_tooltip: useRichTooltip, + time_series_option: timeSeriesOption, + } = formData; + const { verbose_map: verboseMap } = datasource; + const element = document.querySelector(selector); + + return Icicle(element, { + data: payload.data, + width: slice.width(), + height: slice.height(), + colorScheme, + dateTimeFormat, + equalDateSize, + groupBy: groupBy.map(g => verboseMap[g] || g), + useLogScale, + metrics, + numberFormat, + partitionLimit: partitionLimit && parseInt(partitionLimit, 10), + partitionThreshold: partitionThreshold && parseInt(partitionThreshold, 10), + useRichTooltip, + timeSeriesOption, + }); +} + +export default adaptor; From f8117714052db2daae75de19dba7d9484ccb7df0 Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Thu, 30 Aug 2018 14:43:40 -0700 Subject: [PATCH 6/9] [SIP-5] Repair and refactor CountryMap (#5721) * Extract slice and formData * update css indent * remove no-effect call * improve text label * adjust text size * fix bound calculation * use string literal * make path constant (cherry picked from commit f72cdc38dfcc2d1bf5574a8e0204927090225720) --- .../assets/src/visualizations/country_map.css | 32 +-- .../assets/src/visualizations/country_map.js | 213 +++++++++++------- 2 files changed, 150 insertions(+), 95 deletions(-) diff --git a/superset/assets/src/visualizations/country_map.css b/superset/assets/src/visualizations/country_map.css index 8a16105aaaec8..7bde0c8c6976f 100644 --- a/superset/assets/src/visualizations/country_map.css +++ b/superset/assets/src/visualizations/country_map.css @@ -1,5 +1,5 @@ .country_map svg { - background-color: #feffff; + background-color: #feffff; } .country_map { @@ -7,30 +7,36 @@ } .country_map .background { - fill: rgba(255,255,255,0); - pointer-events: all; + fill: rgba(255,255,255,0); + pointer-events: all; } .country_map .map-layer { - fill: #fff; - stroke: #aaa; + fill: #fff; + stroke: #aaa; } .country_map .effect-layer { - pointer-events: none; + pointer-events: none; } -.country_map text { - font-weight: 300; - color: #333333; +.country_map .text-layer { + color: #333333; + text-anchor: middle; + pointer-events: none; +} + +.country_map text.result-text { + font-weight: 300; + font-size: 24px; } .country_map text.big-text { - font-size: 30px; - font-weight: 400; - color: #333333; + font-weight: 700; + font-size: 16px; } .country_map path.region { - cursor: pointer; + cursor: pointer; + stroke: #eee; } diff --git a/superset/assets/src/visualizations/country_map.js b/superset/assets/src/visualizations/country_map.js index 09d325de6781e..92c799bff3d9f 100644 --- a/superset/assets/src/visualizations/country_map.js +++ b/superset/assets/src/visualizations/country_map.js @@ -1,83 +1,112 @@ import d3 from 'd3'; -import './country_map.css'; +import PropTypes from 'prop-types'; import { colorScalerFactory } from '../modules/colors'; +import './country_map.css'; - -function countryMapChart(slice, payload) { - // CONSTANTS - const fd = payload.form_data; - let path; - let g; - let bigText; - let resultText; - const container = slice.container; - const data = payload.data; - const format = d3.format(fd.number_format); - - const colorScaler = colorScalerFactory(fd.linear_color_scheme, data, v => v.metric); +const propTypes = { + data: PropTypes.arrayOf(PropTypes.shape({ + country_id: PropTypes.string, + metric: PropTypes.number, + })), + width: PropTypes.number, + height: PropTypes.number, + country: PropTypes.string, + linearColorScheme: PropTypes.string, + mapBaseUrl: PropTypes.string, + numberFormat: PropTypes.string, +}; + +const maps = {}; + +function CountryMap(element, props) { + PropTypes.checkPropTypes(propTypes, props, 'prop', 'CountryMap'); + + const { + data, + width, + height, + country, + linearColorScheme, + mapBaseUrl = '/static/assets/src/visualizations/countries', + numberFormat, + } = props; + + const container = element; + const format = d3.format(numberFormat); + const colorScaler = colorScalerFactory(linearColorScheme, data, v => v.metric); const colorMap = {}; data.forEach((d) => { colorMap[d.country_id] = colorScaler(d.metric); }); const colorFn = d => colorMap[d.properties.ISO] || 'none'; - let centered; - path = d3.geo.path(); - d3.select(slice.selector).selectAll('*').remove(); - const div = d3.select(slice.selector) - .append('svg:svg') - .attr('width', slice.width()) - .attr('height', slice.height()) + const path = d3.geo.path(); + const div = d3.select(container); + div.selectAll('*').remove(); + container.style.height = `${height}px`; + container.style.width = `${width}px`; + const svg = div.append('svg:svg') + .attr('width', width) + .attr('height', height) .attr('preserveAspectRatio', 'xMidYMid meet'); + const backgroundRect = svg.append('rect') + .attr('class', 'background') + .attr('width', width) + .attr('height', height); + const g = svg.append('g'); + const mapLayer = g.append('g') + .classed('map-layer', true); + const textLayer = g.append('g') + .classed('text-layer', true) + .attr('transform', `translate(${width / 2}, 45)`); + const bigText = textLayer.append('text') + .classed('big-text', true); + const resultText = textLayer.append('text') + .classed('result-text', true) + .attr('dy', '1em'); - container.css('height', slice.height()); - container.css('width', slice.width()); + let centered; const clicked = function (d) { + const hasCenter = d && centered !== d; let x; let y; let k; - let bigTextX; - let bigTextY; - let bigTextSize; - let resultTextX; - let resultTextY; + const halfWidth = width / 2; + const halfHeight = height / 2; - if (d && centered !== d) { + if (hasCenter) { const centroid = path.centroid(d); x = centroid[0]; y = centroid[1]; - bigTextX = centroid[0]; - bigTextY = centroid[1] - 40; - resultTextX = centroid[0]; - resultTextY = centroid[1] - 40; - bigTextSize = '6px'; k = 4; centered = d; } else { - x = slice.width() / 2; - y = slice.height() / 2; - bigTextX = 0; - bigTextY = 0; - resultTextX = 0; - resultTextY = 0; - bigTextSize = '30px'; + x = halfWidth; + y = halfHeight; k = 1; centered = null; } g.transition() .duration(750) - .attr('transform', 'translate(' + slice.width() / 2 + ',' + slice.height() / 2 + ')scale(' + k + ')translate(' + -x + ',' + -y + ')'); + .attr('transform', `translate(${halfWidth},${halfHeight})scale(${k})translate(${-x},${-y})`); + textLayer + .style('opacity', 0) + .attr('transform', `translate(0,0)translate(${x},${hasCenter ? (y - 5) : 45})`) + .transition() + .duration(750) + .style('opacity', 1); bigText.transition() .duration(750) - .attr('transform', 'translate(0,0)translate(' + bigTextX + ',' + bigTextY + ')') - .style('font-size', bigTextSize); + .style('font-size', hasCenter ? 6 : 16); resultText.transition() .duration(750) - .attr('transform', 'translate(0,0)translate(' + resultTextX + ',' + resultTextY + ')'); + .style('font-size', hasCenter ? 16 : 24); }; + backgroundRect.on('click', clicked); + const selectAndDisplayNameOfRegion = function (feature) { let name = ''; if (feature && feature.properties) { @@ -114,44 +143,29 @@ function countryMapChart(slice, payload) { resultText.text(''); }; - div.append('rect') - .attr('class', 'background') - .attr('width', slice.width()) - .attr('height', slice.height()) - .on('click', clicked); - - g = div.append('g'); - const mapLayer = g.append('g') - .classed('map-layer', true); - bigText = g.append('text') - .classed('big-text', true) - .attr('x', 20) - .attr('y', 45); - resultText = g.append('text') - .classed('result-text', true) - .attr('x', 20) - .attr('y', 60); - - const url = `/static/assets/src/visualizations/countries/${fd.select_country.toLowerCase()}.geojson`; - d3.json(url, function (error, mapData) { + function drawMap(mapData) { const features = mapData.features; const center = d3.geo.centroid(mapData); - let scale = 150; - let offset = [slice.width() / 2, slice.height() / 2]; - let projection = d3.geo.mercator().scale(scale).center(center) - .translate(offset); - - path = path.projection(projection); - + const scale = 100; + const projection = d3.geo.mercator() + .scale(scale) + .center(center) + .translate([width / 2, height / 2]); + path.projection(projection); + + // Compute scale that fits container. const bounds = path.bounds(mapData); - const hscale = scale * slice.width() / (bounds[1][0] - bounds[0][0]); - const vscale = scale * slice.height() / (bounds[1][1] - bounds[0][1]); - scale = (hscale < vscale) ? hscale : vscale; - const offsetWidth = slice.width() - (bounds[0][0] + bounds[1][0]) / 2; - const offsetHeigth = slice.height() - (bounds[0][1] + bounds[1][1]) / 2; - offset = [offsetWidth, offsetHeigth]; - projection = d3.geo.mercator().center(center).scale(scale).translate(offset); - path = path.projection(projection); + const hscale = scale * width / (bounds[1][0] - bounds[0][0]); + const vscale = scale * height / (bounds[1][1] - bounds[0][1]); + const newScale = (hscale < vscale) ? hscale : vscale; + + // Compute bounds and offset using the updated scale. + projection.scale(newScale); + const newBounds = path.bounds(mapData); + projection.translate([ + width - (newBounds[0][0] + newBounds[1][0]) / 2, + height - (newBounds[0][1] + newBounds[1][1]) / 2, + ]); // Draw each province as a path mapLayer.selectAll('path') @@ -164,8 +178,43 @@ function countryMapChart(slice, payload) { .on('mouseenter', mouseenter) .on('mouseout', mouseout) .on('click', clicked); + } + + const countryKey = country.toLowerCase(); + const map = maps[countryKey]; + if (map) { + drawMap(map); + } else { + const url = `${mapBaseUrl}/${countryKey}.geojson`; + d3.json(url, function (error, mapData) { + if (!error) { + maps[countryKey] = mapData; + drawMap(mapData); + } + }); + } + +} + +CountryMap.propTypes = propTypes; + +function adaptor(slice, payload) { + const { selector, formData } = slice; + const { + linear_color_scheme: linearColorScheme, + number_format: numberFormat, + select_country: country, + } = formData; + const element = document.querySelector(selector); + + return CountryMap(element, { + data: payload.data, + width: slice.width(), + height: slice.height(), + country, + linearColorScheme, + numberFormat, }); - container.show(); } -module.exports = countryMapChart; +export default adaptor; From 7bf35e47c857b16a46a37082c2c084e8fcf79263 Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Wed, 5 Sep 2018 10:34:44 -0700 Subject: [PATCH 7/9] [SIP-5] Refactor calendar chart (#5760) * remove stroke * update style and annotate data type * update prop type * bring back utc code * add comments (cherry picked from commit 2811498ec97c55ca74eecb82681c2cda167cef82) --- superset/assets/src/explore/controls.jsx | 2 +- .../assets/src/visualizations/cal_heatmap.css | 4 - .../assets/src/visualizations/cal_heatmap.js | 120 +++++++++++++++--- .../assets/vendor/cal-heatmap/cal-heatmap.css | 5 - 4 files changed, 101 insertions(+), 30 deletions(-) diff --git a/superset/assets/src/explore/controls.jsx b/superset/assets/src/explore/controls.jsx index cbc5576e23a81..49e30424cb664 100644 --- a/superset/assets/src/explore/controls.jsx +++ b/superset/assets/src/explore/controls.jsx @@ -1027,7 +1027,7 @@ export const controls = { isInt: true, validators: [v.integer], renderTrigger: true, - default: 0, + default: 2, label: t('Cell Padding'), description: t('The distance between cells, in pixels'), }, diff --git a/superset/assets/src/visualizations/cal_heatmap.css b/superset/assets/src/visualizations/cal_heatmap.css index b6be2d09d8288..6fd4f5b059e18 100644 --- a/superset/assets/src/visualizations/cal_heatmap.css +++ b/superset/assets/src/visualizations/cal_heatmap.css @@ -8,7 +8,3 @@ margin-left: 20px; margin-top: 5px; } -.graph-legend rect { - stroke: #aaa; - stroke-location: inside; -} diff --git a/superset/assets/src/visualizations/cal_heatmap.js b/superset/assets/src/visualizations/cal_heatmap.js index 97c6e57132516..af91dcee6891c 100644 --- a/superset/assets/src/visualizations/cal_heatmap.js +++ b/superset/assets/src/visualizations/cal_heatmap.js @@ -1,31 +1,73 @@ import d3 from 'd3'; - +import PropTypes from 'prop-types'; import { colorScalerFactory } from '../modules/colors'; import CalHeatMap from '../../vendor/cal-heatmap/cal-heatmap'; -import '../../vendor/cal-heatmap/cal-heatmap.css'; import { d3TimeFormatPreset, d3FormatPreset } from '../modules/utils'; -import './cal_heatmap.css'; import { UTC } from '../modules/dates'; +import '../../vendor/cal-heatmap/cal-heatmap.css'; +import './cal_heatmap.css'; const UTCTS = uts => UTC(new Date(uts)).getTime(); -function calHeatmap(slice, payload) { - const fd = slice.formData; - const steps = fd.steps; - const valueFormatter = d3FormatPreset(fd.y_axis_format); - const timeFormatter = d3TimeFormatPreset(fd.x_axis_time_format); +const propTypes = { + data: PropTypes.shape({ + // Object hashed by metric name, + // then hashed by timestamp (in seconds, not milliseconds) as float + // the innermost value is count + // e.g. { count_distinct_something: { 1535034236.0: 3 } } + data: PropTypes.object, + domain: PropTypes.string, + range: PropTypes.number, + // timestamp in milliseconds + start: PropTypes.number, + subdomain: PropTypes.string, + }), + height: PropTypes.number, + cellPadding: PropTypes.number, + cellRadius: PropTypes.number, + cellSize: PropTypes.number, + linearColorScheme: PropTypes.string, + showLegend: PropTypes.bool, + showMetricName: PropTypes.bool, + showValues: PropTypes.bool, + steps: PropTypes.number, + timeFormat: PropTypes.string, + valueFormat: PropTypes.string, + verboseMap: PropTypes.object, +}; + +function Calendar(element, props) { + PropTypes.checkPropTypes(propTypes, props, 'prop', 'Calendar'); + + const { + data, + height, + cellPadding = 3, + cellRadius = 0, + cellSize = 10, + linearColorScheme, + showLegend, + showMetricName, + showValues, + steps, + timeFormat, + valueFormat, + verboseMap, + } = props; - const container = d3.select(slice.selector).style('height', slice.height()); + const valueFormatter = d3FormatPreset(valueFormat); + const timeFormatter = d3TimeFormatPreset(timeFormat); + + const container = d3.select(element) + .style('height', height); container.selectAll('*').remove(); const div = container.append('div'); - const data = payload.data; - const subDomainTextFormat = fd.show_values ? (date, value) => valueFormatter(value) : null; - const cellPadding = fd.cell_padding !== '' ? fd.cell_padding : 2; - const cellRadius = fd.cell_radius || 0; - const cellSize = fd.cell_size || 10; + const subDomainTextFormat = showValues ? (date, value) => valueFormatter(value) : null; // Trick to convert all timestamps to UTC + // TODO: Verify if this conversion is really necessary + // since all timestamps should always be in UTC. const metricsData = {}; Object.keys(data.data).forEach((metric) => { metricsData[metric] = {}; @@ -36,15 +78,16 @@ function calHeatmap(slice, payload) { Object.keys(metricsData).forEach((metric) => { const calContainer = div.append('div'); - if (fd.show_metric_name) { - calContainer.append('h4').text(slice.verboseMetricName(metric)); + if (showMetricName) { + calContainer.text(`Metric: ${verboseMap[metric] || metric}`); } const timestamps = metricsData[metric]; const extents = d3.extent(Object.keys(timestamps), key => timestamps[key]); const step = (extents[1] - extents[0]) / (steps - 1); - const colorScale = colorScalerFactory(fd.linear_color_scheme, null, null, extents); + const colorScale = colorScalerFactory(linearColorScheme, null, null, extents); - const legend = d3.range(steps).map(i => extents[0] + (step * i)); + const legend = d3.range(steps) + .map(i => extents[0] + (step * i)); const legendColors = legend.map(colorScale); const cal = new CalHeatMap(); @@ -72,7 +115,7 @@ function calHeatmap(slice, payload) { max: legendColors[legendColors.length - 1], empty: 'white', }, - displayLegend: fd.show_legend, + displayLegend: showLegend, itemName: '', valueFormatter, timeFormatter, @@ -80,4 +123,41 @@ function calHeatmap(slice, payload) { }); }); } -module.exports = calHeatmap; + +Calendar.propTypes = propTypes; + +function adaptor(slice, payload) { + const { selector, formData, datasource } = slice; + const { + cell_padding: cellPadding, + cell_radius: cellRadius, + cell_size: cellSize, + linear_color_scheme: linearColorScheme, + show_legend: showLegend, + show_metric_name: showMetricName, + show_values: showValues, + steps, + x_axis_time_format: timeFormat, + y_axis_format: valueFormat, + } = formData; + const { verbose_map: verboseMap } = datasource; + const element = document.querySelector(selector); + + return Calendar(element, { + data: payload.data, + height: slice.height(), + cellPadding, + cellRadius, + cellSize, + linearColorScheme, + showLegend, + showMetricName, + showValues, + steps, + timeFormat, + valueFormat, + verboseMap, + }); +} + +export default adaptor; diff --git a/superset/assets/vendor/cal-heatmap/cal-heatmap.css b/superset/assets/vendor/cal-heatmap/cal-heatmap.css index 068997c19a184..d6e18bc7dc49d 100644 --- a/superset/assets/vendor/cal-heatmap/cal-heatmap.css +++ b/superset/assets/vendor/cal-heatmap/cal-heatmap.css @@ -4,11 +4,6 @@ display: block; } -.cal-heatmap-container .graph -{ - font-family: "Lucida Grande", Lucida, Verdana, sans-serif; -} - .cal-heatmap-container .graph-label { fill: #999; From 00e13108d19ec6961eb4d0e1000f210d923e8a02 Mon Sep 17 00:00:00 2001 From: Christine Chambers Date: Tue, 4 Sep 2018 18:08:00 -0700 Subject: [PATCH 8/9] bug: don't show query overlay when panning mapbox maps (#5814) * bug: don't show query overlay when panning mapbox maps Since we don't want to prompt user to rerun query every time they pan a map, prevent the query overlay from showing by setting dontRefreshOnChange for viewport_latitude and viewport_longitude controls. * Prevent query overlay on viewport zoom also. (cherry picked from commit a411516ff57c73c5dfca41838ed84928dc713041) --- superset/assets/src/explore/controls.jsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/superset/assets/src/explore/controls.jsx b/superset/assets/src/explore/controls.jsx index 49e30424cb664..5e7acf90f7a08 100644 --- a/superset/assets/src/explore/controls.jsx +++ b/superset/assets/src/explore/controls.jsx @@ -1736,6 +1736,8 @@ export const controls = { default: 11, description: t('Zoom level of the map'), places: 8, + // Viewport zoom shouldn't prompt user to re-run query + dontRefreshOnChange: true, }, viewport_latitude: { @@ -1745,6 +1747,8 @@ export const controls = { isFloat: true, description: t('Latitude of default viewport'), places: 8, + // Viewport latitude changes shouldn't prompt user to re-run query + dontRefreshOnChange: true, }, viewport_longitude: { @@ -1754,6 +1758,8 @@ export const controls = { isFloat: true, description: t('Longitude of default viewport'), places: 8, + // Viewport longitude changes shouldn't prompt user to re-run query + dontRefreshOnChange: true, }, render_while_dragging: { From fc11a3c49518d5b03f93aa89da1ca86a9b19e833 Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Wed, 5 Sep 2018 12:12:30 -0700 Subject: [PATCH 9/9] [SIP-5] Refactor MapBox (#5783) * Break MapBox into smaller pieces * Replace React.createElement with regular jsx * detach setControlValue * enable render trigger * Pass explicit props rather than pass all that exists in payload.data. Also use formData when possible. * Rename sliceWidth, sliceHeight to width, height. Use deconstructor. Extract function. * use arrow function * fix linting and remove css (cherry picked from commit bebbdb85d2cc301d2cb86dc6a6da46a285cdd7da) --- superset/assets/src/explore/controls.jsx | 3 + .../src/visualizations/MapBox/MapBox.css | 3 + .../src/visualizations/MapBox/MapBox.jsx | 225 ++++++++++++++++ .../ScatterPlotGlowOverlay.jsx} | 246 ++++-------------- superset/assets/src/visualizations/index.js | 2 +- superset/assets/src/visualizations/mapbox.css | 16 -- 6 files changed, 281 insertions(+), 214 deletions(-) create mode 100644 superset/assets/src/visualizations/MapBox/MapBox.css create mode 100644 superset/assets/src/visualizations/MapBox/MapBox.jsx rename superset/assets/src/visualizations/{mapbox.jsx => MapBox/ScatterPlotGlowOverlay.jsx} (59%) delete mode 100644 superset/assets/src/visualizations/mapbox.css diff --git a/superset/assets/src/explore/controls.jsx b/superset/assets/src/explore/controls.jsx index 5e7acf90f7a08..133491f9f0868 100644 --- a/superset/assets/src/explore/controls.jsx +++ b/superset/assets/src/explore/controls.jsx @@ -1732,6 +1732,7 @@ export const controls = { viewport_zoom: { type: 'TextControl', label: t('Zoom'), + renderTrigger: true, isFloat: true, default: 11, description: t('Zoom level of the map'), @@ -1743,6 +1744,7 @@ export const controls = { viewport_latitude: { type: 'TextControl', label: t('Default latitude'), + renderTrigger: true, default: 37.772123, isFloat: true, description: t('Latitude of default viewport'), @@ -1754,6 +1756,7 @@ export const controls = { viewport_longitude: { type: 'TextControl', label: t('Default longitude'), + renderTrigger: true, default: -122.405293, isFloat: true, description: t('Longitude of default viewport'), diff --git a/superset/assets/src/visualizations/MapBox/MapBox.css b/superset/assets/src/visualizations/MapBox/MapBox.css new file mode 100644 index 0000000000000..3ec640dac6e99 --- /dev/null +++ b/superset/assets/src/visualizations/MapBox/MapBox.css @@ -0,0 +1,3 @@ +.mapbox .slice_container div { + padding-top: 0px; +} diff --git a/superset/assets/src/visualizations/MapBox/MapBox.jsx b/superset/assets/src/visualizations/MapBox/MapBox.jsx new file mode 100644 index 0000000000000..81f41f074b1d2 --- /dev/null +++ b/superset/assets/src/visualizations/MapBox/MapBox.jsx @@ -0,0 +1,225 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ReactDOM from 'react-dom'; +import MapGL from 'react-map-gl'; +import Immutable from 'immutable'; +import supercluster from 'supercluster'; +import ViewportMercator from 'viewport-mercator-project'; +import ScatterPlotGlowOverlay from './ScatterPlotGlowOverlay'; + +import { + DEFAULT_LONGITUDE, + DEFAULT_LATITUDE, + DEFAULT_ZOOM, +} from '../../utils/common'; +import './MapBox.css'; + +const NOOP = () => {}; +const DEFAULT_POINT_RADIUS = 60; +const DEFAULT_MAX_ZOOM = 16; + +const propTypes = { + width: PropTypes.number, + height: PropTypes.number, + aggregatorName: PropTypes.string, + clusterer: PropTypes.object, + globalOpacity: PropTypes.number, + mapStyle: PropTypes.string, + mapboxApiKey: PropTypes.string, + onViewportChange: PropTypes.func, + pointRadius: PropTypes.number, + pointRadiusUnit: PropTypes.string, + renderWhileDragging: PropTypes.bool, + rgb: PropTypes.array, + viewportLatitude: PropTypes.number, + viewportLongitude: PropTypes.number, + viewportZoom: PropTypes.number, +}; + +const defaultProps = { + globalOpacity: 1, + onViewportChange: NOOP, + pointRadius: DEFAULT_POINT_RADIUS, + pointRadiusUnit: 'Pixels', + viewportLatitude: DEFAULT_LATITUDE, + viewportLongitude: DEFAULT_LONGITUDE, + viewportZoom: DEFAULT_ZOOM, +}; + +class MapBox extends React.Component { + constructor(props) { + super(props); + + const { + viewportLatitude: latitude, + viewportLongitude: longitude, + viewportZoom: zoom, + } = this.props; + + this.state = { + viewport: { + longitude, + latitude, + zoom, + startDragLngLat: [longitude, latitude], + }, + }; + this.onViewportChange = this.onViewportChange.bind(this); + } + + onViewportChange(viewport) { + this.setState({ viewport }); + this.props.onViewportChange(viewport); + } + + render() { + const { + width, + height, + aggregatorName, + globalOpacity, + mapStyle, + mapboxApiKey, + pointRadius, + pointRadiusUnit, + renderWhileDragging, + rgb, + } = this.props; + const { viewport } = this.state; + const { latitude, longitude, zoom } = viewport; + const mercator = new ViewportMercator({ + width, + height, + longitude, + latitude, + zoom, + }); + const topLeft = mercator.unproject([0, 0]); + const bottomRight = mercator.unproject([width, height]); + const bbox = [topLeft[0], bottomRight[1], bottomRight[0], topLeft[1]]; + const clusters = this.props.clusterer.getClusters(bbox, Math.round(zoom)); + const isDragging = viewport.isDragging === undefined ? false : + viewport.isDragging; + return ( + + { + const coordinates = location.get('geometry').get('coordinates'); + return [coordinates.get(0), coordinates.get(1)]; + }} + /> + + ); + } +} + +MapBox.propTypes = propTypes; +MapBox.defaultProps = defaultProps; + +function createReducer(aggregatorName, customMetric) { + if (aggregatorName === 'sum' || !customMetric) { + return (a, b) => a + b; + } else if (aggName === 'min') { + return Math.min; + } else if (aggName === 'max') { + return Math.max; + } + return function (a, b) { + if (a instanceof Array) { + if (b instanceof Array) { + return a.concat(b); + } + a.push(b); + return a; + } + if (b instanceof Array) { + b.push(a); + return b; + } + return [a, b]; + }; +} + +function mapbox(slice, payload, setControlValue) { + const { formData, selector } = slice; + const { + customMetric, + geoJSON, + mapboxApiKey, + } = payload.data; + const { + clustering_radius: clusteringRadius, + global_opacity: globalOpacity, + mapbox_color: color, + mapbox_style: mapStyle, + pandas_aggfunc: aggregatorName, + point_radius: pointRadius, + point_radius_unit: pointRadiusUnit, + render_while_dragging: renderWhileDragging, + viewport_latitude: viewportLatitude, + viewport_longitude: viewportLongitude, + viewport_zoom: viewportZoom, + } = formData; + + // Validate mapbox color + const rgb = /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/ + .exec(color); + if (rgb === null) { + slice.error('Color field must be of form \'rgb(%d, %d, %d)\''); + return; + } + + const clusterer = supercluster({ + radius: clusteringRadius, + maxZoom: DEFAULT_MAX_ZOOM, + metricKey: 'metric', + metricReducer: createReducer(aggregatorName, customMetric), + }); + clusterer.load(geoJSON.features); + + ReactDOM.render( + { + setControlValue('viewport_longitude', longitude); + setControlValue('viewport_latitude', latitude); + setControlValue('viewport_zoom', zoom); + }} + pointRadius={pointRadius === 'Auto' ? DEFAULT_POINT_RADIUS : pointRadius} + pointRadiusUnit={pointRadiusUnit} + renderWhileDragging={renderWhileDragging} + rgb={rgb} + viewportLatitude={viewportLatitude} + viewportLongitude={viewportLongitude} + viewportZoom={viewportZoom} + />, + document.querySelector(selector), + ); +} + +export default mapbox; diff --git a/superset/assets/src/visualizations/mapbox.jsx b/superset/assets/src/visualizations/MapBox/ScatterPlotGlowOverlay.jsx similarity index 59% rename from superset/assets/src/visualizations/mapbox.jsx rename to superset/assets/src/visualizations/MapBox/ScatterPlotGlowOverlay.jsx index 1a156ae520e9d..ea4e115de3b00 100644 --- a/superset/assets/src/visualizations/mapbox.jsx +++ b/superset/assets/src/visualizations/MapBox/ScatterPlotGlowOverlay.jsx @@ -1,28 +1,46 @@ -/* eslint-disable no-param-reassign */ -/* eslint-disable react/no-multi-comp */ import d3 from 'd3'; +import Immutable from 'immutable'; import React from 'react'; import PropTypes from 'prop-types'; -import ReactDOM from 'react-dom'; -import MapGL from 'react-map-gl'; -import Immutable from 'immutable'; -import supercluster from 'supercluster'; import ViewportMercator from 'viewport-mercator-project'; - import { kmToPixels, rgbLuminance, isNumeric, MILES_PER_KM, - DEFAULT_LONGITUDE, - DEFAULT_LATITUDE, - DEFAULT_ZOOM, -} from '../utils/common'; -import './mapbox.css'; +} from '../../utils/common'; + +const propTypes = { + locations: PropTypes.instanceOf(Immutable.List).isRequired, + lngLatAccessor: PropTypes.func, + renderWhileDragging: PropTypes.bool, + globalOpacity: PropTypes.number, + dotRadius: PropTypes.number, + dotFill: PropTypes.string, + compositeOperation: PropTypes.string, +}; + +const defaultProps = { + lngLatAccessor: location => [location.get(0), location.get(1)], + renderWhileDragging: true, + dotRadius: 4, + dotFill: '#1FBAD6', + globalOpacity: 1, + // Same as browser default. + compositeOperation: 'source-over', +}; -const NOOP = () => {}; +const contextTypes = { + viewport: PropTypes.object, + isDragging: PropTypes.bool, +}; class ScatterPlotGlowOverlay extends React.Component { + constructor(props) { + super(props); + this.setCanvasRef = this.setCanvasRef.bind(this); + } + componentDidMount() { this.redraw(); } @@ -30,6 +48,11 @@ class ScatterPlotGlowOverlay extends React.Component { componentDidUpdate() { this.redraw(); } + + setCanvasRef(element) { + this.canvas = element; + } + drawText(ctx, pixel, options = {}) { const IS_DARK_THRESHOLD = 110; const { fontHeight = 0, label = '', radius = 0, rgb = [0, 0, 0], shadow = false } = options; @@ -62,8 +85,7 @@ class ScatterPlotGlowOverlay extends React.Component { redraw() { const props = this.props; const pixelRatio = window.devicePixelRatio || 1; - const canvas = this.refs.overlay; - const ctx = canvas.getContext('2d'); + const ctx = this.canvas.getContext('2d'); const radius = props.dotRadius; const mercator = new ViewportMercator(props); const rgb = props.rgb; @@ -185,9 +207,9 @@ class ScatterPlotGlowOverlay extends React.Component { } }, this); } - ctx.restore(); } + render() { let width = 0; let height = 0; @@ -198,11 +220,11 @@ class ScatterPlotGlowOverlay extends React.Component { const { globalOpacity } = this.props; const pixelRatio = window.devicePixelRatio || 1; return ( - React.createElement('canvas', { - ref: 'overlay', - width: width * pixelRatio, - height: height * pixelRatio, - style: { + [location.get(0), location.get(1)], - renderWhileDragging: true, - dotRadius: 4, - dotFill: '#1FBAD6', - globalOpacity: 1, - // Same as browser default. - compositeOperation: 'source-over', -}; -ScatterPlotGlowOverlay.contextTypes = { - viewport: PropTypes.object, - isDragging: PropTypes.bool, -}; - -class MapboxViz extends React.Component { - constructor(props) { - super(props); - const longitude = this.props.viewportLongitude || DEFAULT_LONGITUDE; - const latitude = this.props.viewportLatitude || DEFAULT_LATITUDE; - - this.state = { - viewport: { - longitude, - latitude, - zoom: this.props.viewportZoom || DEFAULT_ZOOM, - startDragLngLat: [longitude, latitude], - }, - }; - this.onViewportChange = this.onViewportChange.bind(this); - } - - onViewportChange(viewport) { - this.setState({ viewport }); - this.props.setControlValue('viewport_longitude', viewport.longitude); - this.props.setControlValue('viewport_latitude', viewport.latitude); - this.props.setControlValue('viewport_zoom', viewport.zoom); - } - - render() { - const mercator = new ViewportMercator({ - width: this.props.sliceWidth, - height: this.props.sliceHeight, - longitude: this.state.viewport.longitude, - latitude: this.state.viewport.latitude, - zoom: this.state.viewport.zoom, - }); - const topLeft = mercator.unproject([0, 0]); - const bottomRight = mercator.unproject([this.props.sliceWidth, this.props.sliceHeight]); - const bbox = [topLeft[0], bottomRight[1], bottomRight[0], topLeft[1]]; - const clusters = this.props.clusterer.getClusters(bbox, Math.round(this.state.viewport.zoom)); - const isDragging = this.state.viewport.isDragging === undefined ? false : - this.state.viewport.isDragging; - return ( - - - + }} + /> ); } } -MapboxViz.propTypes = { - aggregatorName: PropTypes.string, - clusterer: PropTypes.object, - setControlValue: PropTypes.func, - globalOpacity: PropTypes.number, - mapStyle: PropTypes.string, - mapboxApiKey: PropTypes.string, - pointRadius: PropTypes.number, - pointRadiusUnit: PropTypes.string, - renderWhileDragging: PropTypes.bool, - rgb: PropTypes.array, - sliceHeight: PropTypes.number, - sliceWidth: PropTypes.number, - viewportLatitude: PropTypes.number, - viewportLongitude: PropTypes.number, - viewportZoom: PropTypes.number, -}; - -function mapbox(slice, json, setControlValue) { - const div = d3.select(slice.selector); - const DEFAULT_POINT_RADIUS = 60; - const DEFAULT_MAX_ZOOM = 16; - // Validate mapbox color - const rgb = /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/.exec(json.data.color); - if (rgb === null) { - slice.error('Color field must be of form \'rgb(%d, %d, %d)\''); - return; - } - - const aggName = json.data.aggregatorName; - let reducer; - - if (aggName === 'sum' || !json.data.customMetric) { - reducer = function (a, b) { - return a + b; - }; - } else if (aggName === 'min') { - reducer = Math.min; - } else if (aggName === 'max') { - reducer = Math.max; - } else { - reducer = function (a, b) { - if (a instanceof Array) { - if (b instanceof Array) { - return a.concat(b); - } - a.push(b); - return a; - } - if (b instanceof Array) { - b.push(a); - return b; - } - return [a, b]; - }; - } - - const clusterer = supercluster({ - radius: json.data.clusteringRadius, - maxZoom: DEFAULT_MAX_ZOOM, - metricKey: 'metric', - metricReducer: reducer, - }); - clusterer.load(json.data.geoJSON.features); - - div.selectAll('*').remove(); - ReactDOM.render( - , - div.node(), - ); -} +ScatterPlotGlowOverlay.propTypes = propTypes; +ScatterPlotGlowOverlay.defaultProps = defaultProps; +ScatterPlotGlowOverlay.contextTypes = contextTypes; -module.exports = mapbox; +export default ScatterPlotGlowOverlay; diff --git a/superset/assets/src/visualizations/index.js b/superset/assets/src/visualizations/index.js index df24b67158128..bb878bd700e6c 100644 --- a/superset/assets/src/visualizations/index.js +++ b/superset/assets/src/visualizations/index.js @@ -89,7 +89,7 @@ const vizMap = { [VIZ_TYPES.line_multi]: () => loadVis(import(/* webpackChunkName: "line_multi" */ './line_multi.js')), [VIZ_TYPES.time_pivot]: loadNvd3, - [VIZ_TYPES.mapbox]: () => loadVis(import(/* webpackChunkName: "mapbox" */ './mapbox.jsx')), + [VIZ_TYPES.mapbox]: () => loadVis(import(/* webpackChunkName: "mapbox" */ './MapBox/MapBox.jsx')), [VIZ_TYPES.markup]: () => loadVis(import(/* webpackChunkName: "markup" */ './markup.js')), [VIZ_TYPES.para]: () => loadVis(import(/* webpackChunkName: "parallel_coordinates" */ './parallel_coordinates.js')), diff --git a/superset/assets/src/visualizations/mapbox.css b/superset/assets/src/visualizations/mapbox.css deleted file mode 100644 index babb33be0eace..0000000000000 --- a/superset/assets/src/visualizations/mapbox.css +++ /dev/null @@ -1,16 +0,0 @@ -.mapbox div.widget .slice_container { - cursor: grab; - cursor: -moz-grab; - cursor: -webkit-grab; - overflow: hidden; -} - -.mapbox div.widget .slice_container:active { - cursor: grabbing; - cursor: -moz-grabbing; - cursor: -webkit-grabbing; -} - -.mapbox .slice_container div { - padding-top: 0px; -}