a[TIME_COLUMN] - b[TIME_COLUMN]);
- bigNumber = sortedData[sortedData.length - 1][metricName];
- if (compareLag > 0) {
- const compareIndex = sortedData.length - (compareLag + 1);
- if (compareIndex >= 0) {
- const compareValue = sortedData[compareIndex][metricName];
- percentChange = compareValue === 0
- ? 0 : (bigNumber - compareValue) / Math.abs(compareValue);
- const formatPercentChange = d3.format('+.1%');
- formattedSubheader = `${formatPercentChange(percentChange)} ${compareSuffix}`;
- }
- }
- trendlineData = showTrendline
- ? sortedData.map(point => ({ x: point[TIME_COLUMN], y: point[metricName] }))
- : null;
- } else {
- bigNumber = data[0][metricName];
- trendlineData = null;
- }
-
- let className = '';
- if (percentChange > 0) {
- className = 'positive';
- } else if (percentChange < 0) {
- className = 'negative';
- }
-
- return {
- bigNumber,
- trendlineData,
- className,
- subheader: formattedSubheader,
- showTrendline,
- };
-}
-
-function adaptor(slice, payload) {
- const { formData, containerId } = slice;
-
- const transformedData = transform(payload.data, formData);
- const startYAxisAtZero = formData.start_y_axis_at_zero;
- const formatValue = d3FormatPreset(formData.y_axis_format);
- let userColor;
- if (formData.color_picker) {
- const { r, g, b } = formData.color_picker;
- userColor = color.rgb(r, g, b).hex();
- }
-
- ReactDOM.render(
- ,
- document.getElementById(containerId),
- );
-}
-
-export default adaptor;
+export default createAdaptor(Component, transformProps);
diff --git a/superset/assets/src/visualizations/BigNumber/index.js b/superset/assets/src/visualizations/BigNumber/index.js
deleted file mode 100644
index 3aaef63c3..000000000
--- a/superset/assets/src/visualizations/BigNumber/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import adaptor from './adaptor';
-import BigNumber from './BigNumber';
-
-export { BigNumber };
-export default adaptor;
diff --git a/superset/assets/src/visualizations/BigNumber/transformProps.js b/superset/assets/src/visualizations/BigNumber/transformProps.js
new file mode 100644
index 000000000..7b06874a0
--- /dev/null
+++ b/superset/assets/src/visualizations/BigNumber/transformProps.js
@@ -0,0 +1,78 @@
+import * as color from 'd3-color';
+import d3 from 'd3';
+import { d3FormatPreset } from '../../modules/utils';
+import { renderTooltipFactory } from './BigNumber';
+
+const TIME_COLUMN = '__timestamp';
+
+export default function transformProps(basicChartInput) {
+ const { formData, payload } = basicChartInput;
+ const {
+ colorPicker,
+ compareLag: compareLagInput,
+ compareSuffix = '',
+ metric,
+ showTrendLine,
+ startYAxisAtZero,
+ subheader = '',
+ vizType,
+ yAxisFormat,
+ } = formData;
+ const { data } = payload;
+
+ let mainColor;
+ if (colorPicker) {
+ const { r, g, b } = colorPicker;
+ mainColor = color.rgb(r, g, b).hex();
+ }
+
+ let bigNumber;
+ let trendLineData;
+ const metricName = metric.label || metric;
+ const compareLag = +compareLagInput || 0;
+ const supportTrendLine = vizType === 'big_number';
+ const supportAndShowTrendLine = supportTrendLine && showTrendLine;
+ let percentChange = 0;
+ let formattedSubheader = subheader;
+ if (supportTrendLine) {
+ const sortedData = [...data].sort((a, b) => a[TIME_COLUMN] - b[TIME_COLUMN]);
+ bigNumber = sortedData[sortedData.length - 1][metricName];
+ if (compareLag > 0) {
+ const compareIndex = sortedData.length - (compareLag + 1);
+ if (compareIndex >= 0) {
+ const compareValue = sortedData[compareIndex][metricName];
+ percentChange = compareValue === 0
+ ? 0 : (bigNumber - compareValue) / Math.abs(compareValue);
+ const formatPercentChange = d3.format('+.1%');
+ formattedSubheader = `${formatPercentChange(percentChange)} ${compareSuffix}`;
+ }
+ }
+ trendLineData = supportAndShowTrendLine
+ ? sortedData.map(point => ({ x: point[TIME_COLUMN], y: point[metricName] }))
+ : null;
+ } else {
+ bigNumber = data[0][metricName];
+ trendLineData = null;
+ }
+
+ let className = '';
+ if (percentChange > 0) {
+ className = 'positive';
+ } else if (percentChange < 0) {
+ className = 'negative';
+ }
+
+ const formatValue = d3FormatPreset(yAxisFormat);
+
+ return {
+ bigNumber,
+ className,
+ formatBigNumber: formatValue,
+ mainColor,
+ renderTooltip: renderTooltipFactory(formatValue),
+ showTrendLine: supportAndShowTrendLine,
+ startYAxisAtZero,
+ subheader: formattedSubheader,
+ trendLineData,
+ };
+}
diff --git a/superset/assets/src/visualizations/EventFlow.jsx b/superset/assets/src/visualizations/EventFlow.jsx
deleted file mode 100644
index e4b9685c6..000000000
--- a/superset/assets/src/visualizations/EventFlow.jsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
-
-import {
- App,
- withParentSize,
- cleanEvents,
- TS,
- EVENT_NAME,
- ENTITY_ID,
-} from '@data-ui/event-flow';
-import { t } from '../locales';
-
-/*
- * This function takes the slice object and json payload as input and renders a
- * responsive component using the json data.
- */
-function renderEventFlow(slice, json) {
- const container = document.querySelector(slice.selector);
- const hasData = json.data && json.data.length > 0;
-
- // the slice container overflows ~80px in explorer, so we have to correct for this
- const isExplorer = (/explore/).test(window.location.pathname);
-
- const ResponsiveVis = withParentSize(({
- parentWidth,
- parentHeight,
- ...rest
- }) => (
-
- ));
-
- // render the component if we have data, otherwise render a no-data message
- let Component;
- if (hasData) {
- const userKey = json.form_data.entity;
- const eventNameKey = json.form_data.all_columns_x;
-
- // map from the Superset form fields to 's expected data keys
- const accessorFunctions = {
- [TS]: datum => new Date(datum.__timestamp), // eslint-disable-line no-underscore-dangle
- [EVENT_NAME]: datum => datum[eventNameKey],
- [ENTITY_ID]: datum => String(datum[userKey]),
- };
-
- const dirtyData = json.data;
- const cleanData = cleanEvents(dirtyData, accessorFunctions);
- const minEventCount = slice.formData.min_leaf_node_event_count;
-
- Component = ;
- } else {
- Component = {t('Sorry, there appears to be no data')}
;
- }
-
- ReactDOM.render(Component, container);
-}
-
-module.exports = renderEventFlow;
diff --git a/superset/assets/src/visualizations/EventFlow/EventFlow.jsx b/superset/assets/src/visualizations/EventFlow/EventFlow.jsx
new file mode 100644
index 000000000..7359bda95
--- /dev/null
+++ b/superset/assets/src/visualizations/EventFlow/EventFlow.jsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { App, withParentSize } from '@data-ui/event-flow';
+import { t } from '../../locales';
+
+const propTypes = {
+ className: PropTypes.string,
+ data: PropTypes.array,
+ initialMinEventCount: PropTypes.number,
+};
+const defaultProps = {
+ className: '',
+ data: null,
+};
+
+function isExplorer() {
+ return (/explore/).test(window.location.pathname);
+}
+
+// The slice container overflows ~80px in explorer,
+// so we have to correct for this.
+const ResponsiveVis = withParentSize(({
+ parentWidth,
+ parentHeight,
+ ...rest
+}) => (
+
+));
+
+function CustomEventFlow(props) {
+ const { data, initialMinEventCount } = props;
+ if (data) {
+ return (
+
+ );
+ }
+ return (
+ {t('Sorry, there appears to be no data')}
+ );
+}
+
+CustomEventFlow.propTypes = propTypes;
+CustomEventFlow.defaultProps = defaultProps;
+
+export default CustomEventFlow;
diff --git a/superset/assets/src/visualizations/EventFlow/adaptor.jsx b/superset/assets/src/visualizations/EventFlow/adaptor.jsx
new file mode 100644
index 000000000..63f319b88
--- /dev/null
+++ b/superset/assets/src/visualizations/EventFlow/adaptor.jsx
@@ -0,0 +1,5 @@
+import createAdaptor from '../../utils/createAdaptor';
+import Component from './EventFlow';
+import transformProps from './transformProps';
+
+export default createAdaptor(Component, transformProps);
diff --git a/superset/assets/src/visualizations/EventFlow/transformProps.js b/superset/assets/src/visualizations/EventFlow/transformProps.js
new file mode 100644
index 000000000..b5176936a
--- /dev/null
+++ b/superset/assets/src/visualizations/EventFlow/transformProps.js
@@ -0,0 +1,36 @@
+import {
+ cleanEvents,
+ TS,
+ EVENT_NAME,
+ ENTITY_ID,
+} from '@data-ui/event-flow';
+
+export default function transformProps(basicChartInput) {
+ const { formData, payload } = basicChartInput;
+ const {
+ allColumnsX,
+ entity,
+ minLeafNodeEventCount,
+ } = formData;
+ const { data } = payload;
+
+ const hasData = data && data.length > 0;
+ if (hasData) {
+ const userKey = entity;
+ const eventNameKey = allColumnsX;
+
+ // map from the Superset form fields to 's expected data keys
+ const accessorFunctions = {
+ [TS]: datum => new Date(datum.__timestamp), // eslint-disable-line no-underscore-dangle
+ [EVENT_NAME]: datum => datum[eventNameKey],
+ [ENTITY_ID]: datum => String(datum[userKey]),
+ };
+
+ const cleanData = cleanEvents(data, accessorFunctions);
+ return {
+ data: cleanData,
+ initialMinEventCount: minLeafNodeEventCount,
+ };
+ }
+ return { data: null };
+}
diff --git a/superset/assets/src/visualizations/MapBox/MapBox.jsx b/superset/assets/src/visualizations/MapBox/MapBox.jsx
index f2381588e..6dd2eb253 100644
--- a/superset/assets/src/visualizations/MapBox/MapBox.jsx
+++ b/superset/assets/src/visualizations/MapBox/MapBox.jsx
@@ -1,16 +1,14 @@
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 './MapBox.css';
const NOOP = () => {};
-const DEFAULT_POINT_RADIUS = 60;
-const DEFAULT_MAX_ZOOM = 16;
+export const DEFAULT_MAX_ZOOM = 16;
+export const DEFAULT_POINT_RADIUS = 60;
const propTypes = {
width: PropTypes.number,
@@ -124,86 +122,4 @@ class MapBox extends React.Component {
MapBox.propTypes = propTypes;
MapBox.defaultProps = defaultProps;
-function mapbox(slice, payload, setControlValue) {
- const { formData, selector } = slice;
- const {
- hasCustomMetric,
- geoJSON,
- bounds,
- 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,
- } = 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 opts = {
- radius: clusteringRadius,
- maxZoom: DEFAULT_MAX_ZOOM,
- };
- if (hasCustomMetric) {
- opts.initial = () => ({
- sum: 0,
- squaredSum: 0,
- min: Infinity,
- max: -Infinity,
- });
- opts.map = prop => ({
- sum: prop.metric,
- squaredSum: Math.pow(prop.metric, 2),
- min: prop.metric,
- max: prop.metric,
- });
- opts.reduce = (accu, prop) => {
- // Temporarily disable param-reassignment linting to work with supercluster's api
- /* eslint-disable no-param-reassign */
- accu.sum += prop.sum;
- accu.squaredSum += prop.squaredSum;
- accu.min = Math.min(accu.min, prop.min);
- accu.max = Math.max(accu.max, prop.max);
- /* eslint-enable no-param-reassign */
- };
- }
- const clusterer = supercluster(opts);
- 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}
- bounds={bounds}
- />,
- document.querySelector(selector),
- );
-}
-
-export default mapbox;
+export default MapBox;
diff --git a/superset/assets/src/visualizations/MapBox/adaptor.jsx b/superset/assets/src/visualizations/MapBox/adaptor.jsx
new file mode 100644
index 000000000..cbe369fe1
--- /dev/null
+++ b/superset/assets/src/visualizations/MapBox/adaptor.jsx
@@ -0,0 +1,5 @@
+import createAdaptor from '../../utils/createAdaptor';
+import Component from './MapBox';
+import transformProps from './transformProps';
+
+export default createAdaptor(Component, transformProps);
diff --git a/superset/assets/src/visualizations/MapBox/transformProps.js b/superset/assets/src/visualizations/MapBox/transformProps.js
new file mode 100644
index 000000000..4180eef86
--- /dev/null
+++ b/superset/assets/src/visualizations/MapBox/transformProps.js
@@ -0,0 +1,79 @@
+import supercluster from 'supercluster';
+import { DEFAULT_POINT_RADIUS, DEFAULT_MAX_ZOOM } from './MapBox';
+
+export default function transformProps(basicChartInput) {
+ const { formData, onError, payload, setControlValue } = basicChartInput;
+ const {
+ bounds,
+ geoJSON,
+ hasCustomMetric,
+ mapboxApiKey,
+ } = payload.data;
+ const {
+ clusteringRadius,
+ globalOpacity,
+ mapboxColor,
+ mapboxStyle,
+ pandasAggfunc,
+ pointRadius,
+ pointRadiusUnit,
+ renderWhileDragging,
+ } = formData;
+
+ // Validate mapbox color
+ const rgb = /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/
+ .exec(mapboxColor);
+ if (rgb === null) {
+ onError('Color field must be of form \'rgb(%d, %d, %d)\'');
+ return {};
+ }
+
+ const opts = {
+ radius: clusteringRadius,
+ maxZoom: DEFAULT_MAX_ZOOM,
+ };
+ if (hasCustomMetric) {
+ opts.initial = () => ({
+ sum: 0,
+ squaredSum: 0,
+ min: Infinity,
+ max: -Infinity,
+ });
+ opts.map = prop => ({
+ sum: prop.metric,
+ squaredSum: Math.pow(prop.metric, 2),
+ min: prop.metric,
+ max: prop.metric,
+ });
+ opts.reduce = (accu, prop) => {
+ // Temporarily disable param-reassignment linting to work with supercluster's api
+ /* eslint-disable no-param-reassign */
+ accu.sum += prop.sum;
+ accu.squaredSum += prop.squaredSum;
+ accu.min = Math.min(accu.min, prop.min);
+ accu.max = Math.max(accu.max, prop.max);
+ /* eslint-enable no-param-reassign */
+ };
+ }
+ const clusterer = supercluster(opts);
+ clusterer.load(geoJSON.features);
+
+ return {
+ aggregatorName: pandasAggfunc,
+ bounds,
+ clusterer,
+ globalOpacity,
+ hasCustomMetric,
+ mapboxApiKey,
+ mapStyle: mapboxStyle,
+ onViewportChange({ latitude, longitude, zoom }) {
+ setControlValue('viewport_longitude', longitude);
+ setControlValue('viewport_latitude', latitude);
+ setControlValue('viewport_zoom', zoom);
+ },
+ pointRadius: pointRadius === 'Auto' ? DEFAULT_POINT_RADIUS : pointRadius,
+ pointRadiusUnit,
+ renderWhileDragging,
+ rgb,
+ };
+}
diff --git a/superset/assets/src/visualizations/TimeTable/TimeTable.jsx b/superset/assets/src/visualizations/TimeTable/TimeTable.jsx
index 8ce9a7d7a..c414a7cbb 100644
--- a/superset/assets/src/visualizations/TimeTable/TimeTable.jsx
+++ b/superset/assets/src/visualizations/TimeTable/TimeTable.jsx
@@ -1,4 +1,3 @@
-import ReactDOM from 'react-dom';
import React from 'react';
import PropTypes from 'prop-types';
import d3 from 'd3';
@@ -270,58 +269,4 @@ class TimeTable extends React.PureComponent {
TimeTable.propTypes = propTypes;
TimeTable.defaultProps = defaultProps;
-function adaptor(slice, payload) {
- const { containerId, formData, datasource } = slice;
- const {
- column_collection: columnConfigs,
- groupby,
- metrics,
- url,
- } = formData;
- const { records, columns } = payload.data;
- const isGroupBy = groupby.length > 0;
-
- // When there is a "group by",
- // each row in the table is a database column
- // Otherwise,
- // each row in the table is a metric
- let rows;
- if (isGroupBy) {
- rows = columns.map(column => (typeof column === 'object')
- ? column
- : { label: column });
- } else {
- const metricMap = datasource.metrics
- .reduce((acc, current) => {
- const map = acc;
- map[current.metric_name] = current;
- return map;
- }, {});
-
- rows = metrics.map(metric => (typeof metric === 'object')
- ? metric
- : metricMap[metric]);
- }
-
- // TODO: Better parse this from controls instead of mutative value here.
- columnConfigs.forEach((column) => {
- const c = column;
- if (c.timeLag !== undefined && c.timeLag !== null && c.timeLag !== '') {
- c.timeLag = parseInt(c.timeLag, 10);
- }
- });
-
- ReactDOM.render(
- ,
- document.getElementById(containerId),
- );
-}
-
-export default adaptor;
+export default TimeTable;
diff --git a/superset/assets/src/visualizations/TimeTable/adaptor.jsx b/superset/assets/src/visualizations/TimeTable/adaptor.jsx
new file mode 100644
index 000000000..34daa9023
--- /dev/null
+++ b/superset/assets/src/visualizations/TimeTable/adaptor.jsx
@@ -0,0 +1,5 @@
+import createAdaptor from '../../utils/createAdaptor';
+import Component from './TimeTable';
+import transformProps from './transformProps';
+
+export default createAdaptor(Component, transformProps);
diff --git a/superset/assets/src/visualizations/TimeTable/transformProps.js b/superset/assets/src/visualizations/TimeTable/transformProps.js
new file mode 100644
index 000000000..11a1a1abf
--- /dev/null
+++ b/superset/assets/src/visualizations/TimeTable/transformProps.js
@@ -0,0 +1,49 @@
+export default function transformProps(basicChartInput) {
+ const { datasource, formData, payload } = basicChartInput;
+ const {
+ columnCollection,
+ groupby,
+ metrics,
+ url,
+ } = formData;
+ const { records, columns } = payload.data;
+ const isGroupBy = groupby.length > 0;
+
+ // When there is a "group by",
+ // each row in the table is a database column
+ // Otherwise,
+ // each row in the table is a metric
+ let rows;
+ if (isGroupBy) {
+ rows = columns.map(column => (typeof column === 'object')
+ ? column
+ : { label: column });
+ } else {
+ const metricMap = datasource.metrics
+ .reduce((acc, current) => {
+ const map = acc;
+ map[current.metric_name] = current;
+ return map;
+ }, {});
+
+ rows = metrics.map(metric => (typeof metric === 'object')
+ ? metric
+ : metricMap[metric]);
+ }
+
+ // TODO: Better parse this from controls instead of mutative value here.
+ columnCollection.forEach((column) => {
+ const c = column;
+ if (c.timeLag !== undefined && c.timeLag !== null && c.timeLag !== '') {
+ c.timeLag = parseInt(c.timeLag, 10);
+ }
+ });
+
+ return {
+ data: records,
+ columnConfigs: columnCollection,
+ rows,
+ rowType: isGroupBy ? 'column' : 'metric',
+ url,
+ };
+}
diff --git a/superset/assets/src/visualizations/index.js b/superset/assets/src/visualizations/index.js
index 5d9b3b4d9..5ed2ce8ce 100644
--- a/superset/assets/src/visualizations/index.js
+++ b/superset/assets/src/visualizations/index.js
@@ -65,9 +65,9 @@ const vizMap = {
[VIZ_TYPES.area]: loadNvd3,
[VIZ_TYPES.bar]: loadNvd3,
[VIZ_TYPES.big_number]: () =>
- loadVis(import(/* webpackChunkName: 'big_number' */ './BigNumber/index.js')),
+ loadVis(import(/* webpackChunkName: 'big_number' */ './BigNumber/adaptor.jsx')),
[VIZ_TYPES.big_number_total]: () =>
- loadVis(import(/* webpackChunkName: "big_number" */ './BigNumber/index.js')),
+ loadVis(import(/* webpackChunkName: "big_number" */ './BigNumber/adaptor.jsx')),
[VIZ_TYPES.box_plot]: loadNvd3,
[VIZ_TYPES.bubble]: loadNvd3,
[VIZ_TYPES.bullet]: loadNvd3,
@@ -89,7 +89,7 @@ const vizMap = {
[VIZ_TYPES.line_multi]: () =>
loadVis(import(/* webpackChunkName: "line_multi" */ './nvd3/LineMulti.js')),
[VIZ_TYPES.time_pivot]: loadNvd3,
- [VIZ_TYPES.mapbox]: () => loadVis(import(/* webpackChunkName: "mapbox" */ './MapBox/MapBox.jsx')),
+ [VIZ_TYPES.mapbox]: () => loadVis(import(/* webpackChunkName: "mapbox" */ './MapBox/adaptor.jsx')),
[VIZ_TYPES.markup]: () => loadVis(import(/* webpackChunkName: "markup" */ './markup.js')),
[VIZ_TYPES.para]: () =>
loadVis(import(/* webpackChunkName: "parallel_coordinates" */ './ParallelCoordinates/adaptor.jsx')),
@@ -101,7 +101,7 @@ const vizMap = {
[VIZ_TYPES.sunburst]: () => loadVis(import(/* webpackChunkName: "sunburst" */ './Sunburst/adaptor.jsx')),
[VIZ_TYPES.table]: () => loadVis(import(/* webpackChunkName: "table" */ './Table/adaptor.jsx')),
[VIZ_TYPES.time_table]: () =>
- loadVis(import(/* webpackChunkName: "time_table" */ './TimeTable/TimeTable.jsx')),
+ loadVis(import(/* webpackChunkName: "time_table" */ './TimeTable/adaptor.jsx')),
[VIZ_TYPES.treemap]: () => loadVis(import(/* webpackChunkName: "treemap" */ './Treemap/adaptor.jsx')),
[VIZ_TYPES.country_map]: () =>
loadVis(import(/* webpackChunkName: "country_map" */ './CountryMap/adaptor.jsx')),
@@ -111,7 +111,7 @@ const vizMap = {
loadVis(import(/* webpackChunkName: "world_map" */ './WorldMap/adaptor.jsx')),
[VIZ_TYPES.dual_line]: loadNvd3,
[VIZ_TYPES.event_flow]: () =>
- loadVis(import(/* webpackChunkName: "EventFlow" */ './EventFlow.jsx')),
+ loadVis(import(/* webpackChunkName: "EventFlow" */ './EventFlow/adaptor.jsx')),
[VIZ_TYPES.paired_ttest]: () =>
loadVis(import(/* webpackChunkName: "paired_ttest" */ './PairedTTest/adaptor.jsx')),
[VIZ_TYPES.partition]: () =>