Skip to content

Commit

Permalink
Using JS to customize spatial viz and tooltips
Browse files Browse the repository at this point in the history
  • Loading branch information
mistercrunch committed Jan 9, 2018
1 parent b159e51 commit df22f29
Show file tree
Hide file tree
Showing 11 changed files with 194 additions and 45 deletions.
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;
}
82 changes: 58 additions & 24 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 @@ -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,33 @@ 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'),
},
};
export default controls;
18 changes: 18 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 @@ -529,6 +538,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),
};
}
16 changes: 13 additions & 3 deletions superset/assets/visualizations/deckgl/layers/path.jsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
import { PathLayer } from 'deck.gl';

export default function getLayer(formData, payload) {
import * as common from './common';
import sandboxedEval from '../../../javascripts/modules/sandbox';

export default function getLayer(formData, payload, slice) {
const fd = formData;
const c = fd.color_picker;
const fixedColor = [c.r, c.g, c.b, 255 * c.a];
const data = payload.data.paths.map(path => ({
path,
let data = payload.data.features.map(feature => ({
...feature,
path: feature.path,
width: fd.line_width,
color: fixedColor,
}));

if (fd.js_datapoint_mutator) {
const jsFnMutator = sandboxedEval(fd.js_datapoint_mutator);
data = data.map(jsFnMutator);
}

return new PathLayer({
id: `path-layer-${fd.slice_id}`,
data,
rounded: true,
widthScale: 1,
...common.commonLayerProps(fd, slice),
});
}
14 changes: 12 additions & 2 deletions superset/assets/visualizations/deckgl/layers/scatter.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { ScatterplotLayer } from 'deck.gl';

import * as common from './common';
import { getColorFromScheme, hexToRGB } from '../../../javascripts/modules/colors';
import { unitToRadius } from '../../../javascripts/modules/geo';
import sandboxedEval from '../../../javascripts/modules/sandbox';

export default function getLayer(formData, payload) {
export default function getLayer(formData, payload, slice) {
const fd = formData;
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
const fixedColor = [c.r, c.g, c.b, 255 * c.a];

const data = payload.data.features.map((d) => {
let data = payload.data.features.map((d) => {
let radius = unitToRadius(fd.point_unit, d.radius) || 10;
if (fd.multiplier) {
radius *= fd.multiplier;
Expand All @@ -25,11 +27,19 @@ export default function getLayer(formData, payload) {
color,
};
});

if (fd.js_datapoint_mutator) {
// Applying user defined data mutator if defined
const jsFnMutator = sandboxedEval(fd.js_datapoint_mutator);
data = data.map(jsFnMutator);
}

return new ScatterplotLayer({
id: `scatter-layer-${fd.slice_id}`,
data,
pickable: true,
fp64: true,
outline: false,
...common.commonLayerProps(fd, slice),
});
}
Loading

0 comments on commit df22f29

Please sign in to comment.