Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using user-defined Javascript to customize geospatial visualization #4173

Merged
merged 4 commits into from
Jan 11, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 28 additions & 1 deletion superset/assets/javascripts/chart/Chart.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import Mustache from 'mustache';
import { Tooltip } from 'react-bootstrap';

import { d3format } from '../modules/utils';
import ChartBody from './ChartBody';
import Loading from '../components/Loading';
import StackTraceMessage from '../components/StackTraceMessage';
import visMap from '../../visualizations/main';
import sandboxedEval from '../modules/sandbox';
import './chart.css';

const propTypes = {
annotationData: PropTypes.object,
Expand Down Expand Up @@ -49,6 +51,7 @@ const defaultProps = {
class Chart extends React.PureComponent {
constructor(props) {
super(props);
this.state = {};
// these properties are used by visualizations
this.annotationData = props.annotationData;
this.containerId = props.containerId;
Expand Down Expand Up @@ -99,6 +102,10 @@ class Chart extends React.PureComponent {
return this.props.getFilters();
}

setTooltip(tooltip) {
this.setState({ tooltip });
}

addFilter(col, vals, merge = true, refresh = true) {
this.props.addFilter(col, vals, merge, refresh);
}
Expand Down Expand Up @@ -140,6 +147,26 @@ class Chart extends React.PureComponent {
return Mustache.render(s, context);
}

renderTooltip() {
if (this.state.tooltip) {
/* eslint-disable react/no-danger */
return (
<Tooltip
className="chart-tooltip"
id="chart-tooltip"
placement="right"
positionTop={this.state.tooltip.y - 10}
positionLeft={this.state.tooltip.x + 30}
arrowOffsetTop={10}
>
<div dangerouslySetInnerHTML={{ __html: this.state.tooltip.content }} />
</Tooltip>
);
/* eslint-enable react/no-danger */
}
return null;
}

renderViz() {
const viz = visMap[this.props.vizType];
const fd = this.props.formData;
Expand All @@ -160,10 +187,10 @@ class Chart extends React.PureComponent {
const isLoading = this.props.chartStatus === 'loading';
return (
<div className={`token col-md-12 ${isLoading ? 'is-loading' : ''}`}>
{this.renderTooltip()}
{isLoading &&
<Loading size={25} />
}

{this.props.chartAlert &&
<StackTraceMessage
message={this.props.chartAlert}
Expand Down
4 changes: 4 additions & 0 deletions superset/assets/javascripts/chart/chart.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.chart-tooltip {
opacity: 0.75;
font-size: 12px;
}
102 changes: 76 additions & 26 deletions superset/assets/javascripts/explore/stores/controls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,6 @@ const sortAxisChoices = [
['value_desc', 'sum(value) descending'],
];

const sandboxUrl = 'https://github.com/apache/incubator-superset/blob/master/superset/assets/javascripts/modules/sandbox.js';
const sandboxedEvalInfo = (
<span>
{t('While this runs in a ')}
<a href="https://nodejs.org/api/vm.html#vm_script_runinnewcontext_sandbox_options">sandboxed vm</a>
, {t('a set of')}<a href={sandboxUrl}> useful objects are in context </a>
{t('to be used where necessary.')}
</span>);

const groupByControl = {
type: 'SelectControl',
multi: true,
Expand All @@ -77,6 +68,35 @@ const groupByControl = {
},
};

const sandboxUrl = (
'https://github.com/apache/incubator-superset/' +
'blob/master/superset/assets/javascripts/modules/sandbox.js');
const jsFunctionInfo = (
<div>
{t('For more information about objects are in context in the scope of this function, refer to the')}
<a href={sandboxUrl}>
{t(" source code of Superset's sandboxed parser")}.
</a>.
</div>
);
function jsFunctionControl(label, description, extraDescr = null, height = 100, defaultText = '') {
return {
type: 'TextAreaControl',
language: 'javascript',
label,
description,
height,
default: defaultText,
aboveEditorSection: (
<div>
<p>{description}</p>
<p>{jsFunctionInfo}</p>
{extraDescr}
</div>
),
};
}

export const controls = {
datasource: {
type: 'DatasourceControl',
Expand Down Expand Up @@ -1181,14 +1201,14 @@ export const controls = {
type: 'CheckboxControl',
label: t('Range Filter'),
renderTrigger: true,
default: false,
default: true,
description: t('Whether to display the time range interactive selector'),
},

date_filter: {
type: 'CheckboxControl',
label: t('Date Filter'),
default: false,
default: true,
description: t('Whether to include a time filter'),
},

Expand Down Expand Up @@ -1399,7 +1419,7 @@ export const controls = {
['mapbox://styles/mapbox/satellite-v9', 'Satellite'],
['mapbox://styles/mapbox/outdoors-v9', 'Outdoors'],
],
default: 'mapbox://styles/mapbox/streets-v9',
default: 'mapbox://styles/mapbox/light-v9',
description: t('Base layer map style'),
},

Expand Down Expand Up @@ -1804,20 +1824,6 @@ export const controls = {
default: false,
},

js_data: {
type: 'TextAreaControl',
label: t('Javascript data mutator'),
description: t('Define a function that receives intercepts the data objects and can mutate it'),
language: 'javascript',
default: '',
height: 100,
aboveEditorSection: (
<p>
Define a function that intercepts the <code>data</code> object passed to the visualization
and returns a similarly shaped object. {sandboxedEvalInfo}
</p>),
},

deck_slices: {
type: 'SelectAsyncControl',
multi: true,
Expand All @@ -1835,5 +1841,49 @@ export const controls = {
return data.result.map(o => ({ value: o.id, label: o.slice_name }));
},
},

js_datapoint_mutator: jsFunctionControl(
t('Javascript data point mutator'),
t('Define a javascript function that receives each data point and can alter it ' +
'before getting sent to the deck.gl layer'),
),

js_data: jsFunctionControl(
t('Javascript data mutator'),
t('Define a function that receives intercepts the data objects and can mutate it'),
),

js_tooltip: jsFunctionControl(
t('Javascript tooltip generator'),
t('Define a function that receives the input and outputs the content for a tooltip'),
),

js_onclick_href: jsFunctionControl(
t('Javascript onClick href'),
t('Define a function that returns a URL to navigate to when user clicks'),
),

js_columns: {
...groupByControl,
label: t('Extra data for JS'),
default: [],
description: t('List of extra columns made available in Javascript functions'),
},

stroked: {
type: 'CheckboxControl',
label: t('Stroked'),
renderTrigger: true,
description: t('Whether to display the stroke'),
default: false,
},

filled: {
type: 'CheckboxControl',
label: t('Filled'),
renderTrigger: true,
description: t('Whether to fill the objects'),
default: false,
},
};
export default controls;
29 changes: 29 additions & 0 deletions superset/assets/javascripts/explore/stores/visTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,15 @@ export const visTypes = {
['reverse_long_lat', null],
],
},
{
label: t('Advanced'),
controlSetRows: [
['js_columns'],
['js_datapoint_mutator'],
['js_tooltip'],
['js_onclick_href'],
],
},
],
},

Expand Down Expand Up @@ -491,9 +500,20 @@ export const visTypes = {
label: t('GeoJson Settings'),
controlSetRows: [
['fill_color_picker', 'stroke_color_picker'],
['filled', 'stroked'],
['extruded', null],
['point_radius_scale', null],
],
},
{
label: t('Advanced'),
controlSetRows: [
['js_columns'],
['js_datapoint_mutator'],
['js_tooltip'],
['js_onclick_href'],
],
},
],
},

Expand Down Expand Up @@ -529,6 +549,15 @@ export const visTypes = {
['dimension', 'color_scheme'],
],
},
{
label: t('Advanced'),
controlSetRows: [
['js_columns'],
['js_datapoint_mutator'],
['js_tooltip'],
['js_onclick_href'],
],
},
],
controlOverrides: {
dimension: {
Expand Down
2 changes: 2 additions & 0 deletions superset/assets/javascripts/modules/sandbox.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// A safe alternative to JS's eval
import vm from 'vm';
import _ from 'underscore';
import * as colors from './colors';

// Objects exposed here should be treated like a public API
// if `underscore` had backwards incompatible changes in a future release, we'd
// have to be careful about bumping the library as those changes could break user charts
const GLOBAL_CONTEXT = {
console,
_,
colors,
};

// Copied/modified from https://github.com/hacksparrow/safe-eval/blob/master/index.js
Expand Down
5 changes: 3 additions & 2 deletions superset/assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,16 @@
"d3-tip": "^0.6.7",
"datamaps": "^0.5.8",
"datatables.net-bs": "^1.10.15",
"deck.gl": "^4.1.5",
"deck.gl": "^5.0.1",
"distributions": "^1.0.0",
"dompurify": "^1.0.3",
"fastdom": "^1.0.6",
"geolib": "^2.0.24",
"immutable": "^3.8.2",
"jed": "^1.1.1",
"jquery": "3.1.1",
"lodash.throttle": "^4.1.1",
"luma.gl": "^4.0.5",
"luma.gl": "^5.0.1",
"mathjs": "^3.16.3",
"moment": "2.18.1",
"mustache": "^2.2.1",
Expand Down
2 changes: 1 addition & 1 deletion superset/assets/visualizations/deckgl/factory.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import layerGenerators from './layers';

export default function deckglFactory(slice, payload, setControlValue) {
const fd = slice.formData;
const layer = layerGenerators[fd.viz_type](fd, payload);
const layer = layerGenerators[fd.viz_type](fd, payload, slice);
const viewport = {
...fd.viewport,
width: slice.width(),
Expand Down
33 changes: 33 additions & 0 deletions superset/assets/visualizations/deckgl/layers/common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import dompurify from 'dompurify';
import sandboxedEval from '../../../javascripts/modules/sandbox';

export function commonLayerProps(formData, slice) {
const fd = formData;
let onHover;
if (fd.js_tooltip) {
const jsTooltip = sandboxedEval(fd.js_tooltip);
onHover = (o) => {
if (o.picked) {
slice.setTooltip({
content: dompurify.sanitize(jsTooltip(o)),
x: o.x,
y: o.y,
});
} else {
slice.setTooltip(null);
}
};
}
let onClick;
if (fd.js_onclick_href) {
onClick = (o) => {
const href = sandboxedEval(fd.js_onclick_href)(o);
window.open(href);
};
}
return {
onClick,
onHover,
pickable: Boolean(onHover),
};
}
Loading