diff --git a/superset/assets/package.json b/superset/assets/package.json
index f326ec6d6b08e..6f2437c9de62a 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -79,6 +79,7 @@
"jed": "^1.1.1",
"jquery": "3.1.1",
"json-bigint": "^0.3.0",
+ "lodash": "^4.17.11",
"lodash.throttle": "^4.1.1",
"mapbox-gl": "^0.45.0",
"mathjs": "^3.20.2",
diff --git a/superset/assets/spec/javascripts/utils/convertKeysToCamelCase_spec.js b/superset/assets/spec/javascripts/utils/convertKeysToCamelCase_spec.js
new file mode 100644
index 0000000000000..6cae3c1874649
--- /dev/null
+++ b/superset/assets/spec/javascripts/utils/convertKeysToCamelCase_spec.js
@@ -0,0 +1,29 @@
+import { it, describe } from 'mocha';
+import { expect } from 'chai';
+import convertKeysToCamelCase from '../../../src/utils/convertKeysToCamelCase';
+
+describe.only('convertKeysToCamelCase(object)', () => {
+ it('returns undefined for undefined input', () => {
+ expect(convertKeysToCamelCase(undefined)).to.equal(undefined);
+ });
+ it('returns null for null input', () => {
+ expect(convertKeysToCamelCase(null)).to.equal(null);
+ });
+ it('returns a new object that has all keys in camelCase', () => {
+ const input = {
+ is_happy: true,
+ 'is-angry': false,
+ isHungry: false,
+ };
+ expect(convertKeysToCamelCase(input)).to.deep.equal({
+ isHappy: true,
+ isAngry: false,
+ isHungry: false,
+ });
+ });
+ it('throws error if input is not a plain object', () => {
+ expect(() => { convertKeysToCamelCase({}); }).to.not.throw();
+ expect(() => { convertKeysToCamelCase(''); }).to.throw();
+ expect(() => { convertKeysToCamelCase(new Map()); }).to.throw();
+ });
+});
diff --git a/superset/assets/src/utils/convertKeysToCamelCase.js b/superset/assets/src/utils/convertKeysToCamelCase.js
new file mode 100644
index 0000000000000..c1071e649438c
--- /dev/null
+++ b/superset/assets/src/utils/convertKeysToCamelCase.js
@@ -0,0 +1,11 @@
+import { mapKeys, camelCase, isPlainObject } from 'lodash/fp';
+
+export default function convertKeysToCamelCase(object) {
+ if (object === null || object === undefined) {
+ return object;
+ }
+ if (isPlainObject(object)) {
+ return mapKeys(k => camelCase(k), object);
+ }
+ throw new Error(`Cannot convert input that is not a plain object: ${object}`);
+}
diff --git a/superset/assets/src/utils/createAdaptor.jsx b/superset/assets/src/utils/createAdaptor.jsx
new file mode 100644
index 0000000000000..0de1d25dc8f24
--- /dev/null
+++ b/superset/assets/src/utils/createAdaptor.jsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import BasicChartInput from '../visualizations/models/BasicChartInput';
+
+const IDENTITY = x => x;
+
+export default function createAdaptor(Component, transformProps = IDENTITY) {
+ return function adaptor(slice, payload, setControlValue) {
+ const basicChartInput = new BasicChartInput(slice, payload, setControlValue);
+ ReactDOM.render(
+ ,
+ document.querySelector(slice.selector),
+ );
+ };
+}
diff --git a/superset/assets/src/utils/reactify.jsx b/superset/assets/src/utils/reactify.jsx
new file mode 100644
index 0000000000000..d52c81694fca3
--- /dev/null
+++ b/superset/assets/src/utils/reactify.jsx
@@ -0,0 +1,54 @@
+import React from 'react';
+
+export default function reactify(renderFn) {
+ class ReactifiedComponent extends React.Component {
+ constructor(props) {
+ super(props);
+ this.setContainerRef = this.setContainerRef.bind(this);
+ }
+
+ componentDidMount() {
+ this.execute();
+ }
+
+ componentDidUpdate() {
+ this.execute();
+ }
+
+ componentWillUnmount() {
+ this.container = null;
+ }
+
+ setContainerRef(c) {
+ this.container = c;
+ }
+
+ execute() {
+ if (this.container) {
+ renderFn(this.container, this.props);
+ }
+ }
+
+ render() {
+ const { id, className } = this.props;
+ return (
+
+ );
+ }
+ }
+
+ if (renderFn.displayName) {
+ ReactifiedComponent.displayName = renderFn.displayName;
+ }
+ if (renderFn.propTypes) {
+ ReactifiedComponent.propTypes = renderFn.propTypes;
+ }
+ if (renderFn.defaultProps) {
+ ReactifiedComponent.defaultProps = renderFn.defaultProps;
+ }
+ return ReactifiedComponent;
+}
diff --git a/superset/assets/src/visualizations/WorldMap/ReactWorldMap.js b/superset/assets/src/visualizations/WorldMap/ReactWorldMap.js
new file mode 100644
index 0000000000000..bc17f82b0be7f
--- /dev/null
+++ b/superset/assets/src/visualizations/WorldMap/ReactWorldMap.js
@@ -0,0 +1,4 @@
+import reactify from '../../utils/reactify';
+import WorldMap from './WorldMap';
+
+export default reactify(WorldMap);
diff --git a/superset/assets/src/visualizations/world_map.css b/superset/assets/src/visualizations/WorldMap/WorldMap.css
similarity index 100%
rename from superset/assets/src/visualizations/world_map.css
rename to superset/assets/src/visualizations/WorldMap/WorldMap.css
diff --git a/superset/assets/src/visualizations/world_map.js b/superset/assets/src/visualizations/WorldMap/WorldMap.js
similarity index 86%
rename from superset/assets/src/visualizations/world_map.js
rename to superset/assets/src/visualizations/WorldMap/WorldMap.js
index 6c4948a7dd6c1..d83d79411b0d7 100644
--- a/superset/assets/src/visualizations/world_map.js
+++ b/superset/assets/src/visualizations/WorldMap/WorldMap.js
@@ -1,7 +1,7 @@
import d3 from 'd3';
import PropTypes from 'prop-types';
import Datamap from 'datamaps';
-import './world_map.css';
+import './WorldMap.css';
const propTypes = {
data: PropTypes.arrayOf(PropTypes.shape({
@@ -109,20 +109,4 @@ function WorldMap(element, props) {
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;
+export default WorldMap;
diff --git a/superset/assets/src/visualizations/WorldMap/adaptor.jsx b/superset/assets/src/visualizations/WorldMap/adaptor.jsx
new file mode 100644
index 0000000000000..30d0400f35a11
--- /dev/null
+++ b/superset/assets/src/visualizations/WorldMap/adaptor.jsx
@@ -0,0 +1,5 @@
+import createAdaptor from '../../utils/createAdaptor';
+import WorldMap from './ReactWorldMap';
+import transformProps from './transformProps';
+
+export default createAdaptor(WorldMap, transformProps);
diff --git a/superset/assets/src/visualizations/WorldMap/transformProps.js b/superset/assets/src/visualizations/WorldMap/transformProps.js
new file mode 100644
index 0000000000000..4e56b03c06be7
--- /dev/null
+++ b/superset/assets/src/visualizations/WorldMap/transformProps.js
@@ -0,0 +1,10 @@
+export default function transformProps(basicChartInput) {
+ const { formData, payload } = basicChartInput;
+ const { maxBubbleSize, showBubbles } = formData;
+
+ return {
+ data: payload.data,
+ maxBubbleSize: parseInt(maxBubbleSize, 10),
+ showBubbles,
+ };
+}
diff --git a/superset/assets/src/visualizations/index.js b/superset/assets/src/visualizations/index.js
index 31feffc2abba4..e924dd4d12e06 100644
--- a/superset/assets/src/visualizations/index.js
+++ b/superset/assets/src/visualizations/index.js
@@ -108,7 +108,7 @@ const vizMap = {
[VIZ_TYPES.word_cloud]: () =>
loadVis(import(/* webpackChunkName: "word_cloud" */ './wordcloud/WordCloud.js')),
[VIZ_TYPES.world_map]: () =>
- loadVis(import(/* webpackChunkName: "world_map" */ './world_map.js')),
+ loadVis(import(/* webpackChunkName: "world_map" */ './WorldMap/adaptor.jsx')),
[VIZ_TYPES.dual_line]: loadNvd3,
[VIZ_TYPES.event_flow]: () =>
loadVis(import(/* webpackChunkName: "EventFlow" */ './EventFlow.jsx')),
diff --git a/superset/assets/src/visualizations/models/BasicChartInput.js b/superset/assets/src/visualizations/models/BasicChartInput.js
new file mode 100644
index 0000000000000..de4add533639b
--- /dev/null
+++ b/superset/assets/src/visualizations/models/BasicChartInput.js
@@ -0,0 +1,10 @@
+import convertKeysToCamelCase from '../../utils/convertKeysToCamelCase';
+
+export default class BasicChartInput {
+ constructor(slice, payload, setControlValue) {
+ this.annotationData = slice.annotationData;
+ this.formData = convertKeysToCamelCase(slice.formData);
+ this.payload = payload;
+ this.setControlValue = setControlValue;
+ }
+}
diff --git a/superset/assets/yarn.lock b/superset/assets/yarn.lock
index 317de4f77e3ca..0c537bd8ba079 100644
--- a/superset/assets/yarn.lock
+++ b/superset/assets/yarn.lock
@@ -7487,6 +7487,10 @@ lodash@4.17.10, lodash@^4.0.1, lodash@^4.0.8, lodash@^4.13.1, lodash@^4.14.0, lo
version "4.17.10"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
+lodash@^4.17.11:
+ version "4.17.11"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
+
log-symbols@2.2.0, log-symbols@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"