diff --git a/superset/assets/javascripts/explorev2/actions/exploreActions.js b/superset/assets/javascripts/explorev2/actions/exploreActions.js
index 776014b0a..29ea8e40d 100644
--- a/superset/assets/javascripts/explorev2/actions/exploreActions.js
+++ b/superset/assets/javascripts/explorev2/actions/exploreActions.js
@@ -1,6 +1,7 @@
/* eslint camelcase: 0 */
const $ = window.$ = require('jquery');
const FAVESTAR_BASE_URL = '/superset/favstar/slice';
+import { getExploreUrl } from '../exploreUtils';
export const SET_DATASOURCE_TYPE = 'SET_DATASOURCE_TYPE';
export function setDatasourceType(datasourceType) {
@@ -89,13 +90,18 @@ export function chartUpdateStarted() {
}
export const CHART_UPDATE_SUCCEEDED = 'CHART_UPDATE_SUCCEEDED';
-export function chartUpdateSucceeded(query) {
- return { type: CHART_UPDATE_SUCCEEDED, query };
+export function chartUpdateSucceeded(queryResponse) {
+ return { type: CHART_UPDATE_SUCCEEDED, queryResponse };
}
export const CHART_UPDATE_FAILED = 'CHART_UPDATE_FAILED';
-export function chartUpdateFailed(error, query) {
- return { type: CHART_UPDATE_FAILED, error, query };
+export function chartUpdateFailed(queryResponse) {
+ return { type: CHART_UPDATE_FAILED, queryResponse };
+}
+
+export const CHART_RENDERING_FAILED = 'CHART_RENDERING_FAILED';
+export function chartRenderingFailed(error) {
+ return { type: CHART_RENDERING_FAILED, error };
}
export const UPDATE_EXPLORE_ENDPOINTS = 'UPDATE_EXPLORE_ENDPOINTS';
@@ -167,3 +173,16 @@ export const UPDATE_CHART_STATUS = 'UPDATE_CHART_STATUS';
export function updateChartStatus(status) {
return { type: UPDATE_CHART_STATUS, status };
}
+
+export const RUN_QUERY = 'RUN_QUERY';
+export function runQuery(formData, datasourceType) {
+ return function (dispatch) {
+ dispatch(updateChartStatus('loading'));
+ const url = getExploreUrl(formData, datasourceType, 'json');
+ $.getJSON(url, function (queryResponse) {
+ dispatch(chartUpdateSucceeded(queryResponse));
+ }).fail(function (err) {
+ dispatch(chartUpdateFailed(err));
+ });
+ };
+}
diff --git a/superset/assets/javascripts/explorev2/components/ChartContainer.jsx b/superset/assets/javascripts/explorev2/components/ChartContainer.jsx
index 2412b77a0..a025d50c2 100644
--- a/superset/assets/javascripts/explorev2/components/ChartContainer.jsx
+++ b/superset/assets/javascripts/explorev2/components/ChartContainer.jsx
@@ -23,12 +23,8 @@ const propTypes = {
viz_type: PropTypes.string.isRequired,
height: PropTypes.string.isRequired,
containerId: PropTypes.string.isRequired,
- json_endpoint: PropTypes.string.isRequired,
- csv_endpoint: PropTypes.string.isRequired,
- standalone_endpoint: PropTypes.string.isRequired,
query: PropTypes.string,
column_formats: PropTypes.object,
- data: PropTypes.any,
chartStatus: PropTypes.string,
isStarred: PropTypes.bool.isRequired,
chartUpdateStartTime: PropTypes.number.isRequired,
@@ -37,88 +33,59 @@ const propTypes = {
table_name: PropTypes.string,
};
-class ChartContainer extends React.Component {
+class ChartContainer extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
selector: `#${props.containerId}`,
- mockSlice: {},
- viz: {},
};
}
- componentWillMount() {
- const mockSlice = this.getMockedSliceObject(this.props);
- this.setState({
- mockSlice,
- viz: visMap[this.props.viz_type](mockSlice),
- });
- }
-
- componentDidMount() {
- this.state.viz.render();
- }
-
- componentWillReceiveProps(nextProps) {
- if (nextProps.chartStatus === 'loading') {
- const mockSlice = this.getMockedSliceObject(nextProps);
- this.setState({
- mockSlice,
- viz: visMap[nextProps.viz_type](mockSlice),
- });
+ renderViz() {
+ const mockSlice = this.getMockedSliceObject();
+ try {
+ visMap[this.props.viz_type](mockSlice, this.props.queryResponse);
+ this.setState({ mockSlice });
+ } catch (e) {
+ this.props.actions.chartRenderingFailed(e);
}
}
componentDidUpdate(prevProps) {
- if (this.props.chartStatus === 'loading') {
- this.state.viz.render();
- }
- if (prevProps.height !== this.props.height) {
- this.state.viz.resize();
+ if (
+ prevProps.queryResponse !== this.props.queryResponse ||
+ prevProps.height !== this.props.height
+ ) {
+ this.renderViz();
}
}
- getMockedSliceObject(props) {
+ getMockedSliceObject() {
+ const props = this.props;
return {
viewSqlQuery: props.query,
-
- data: {
- csv_endpoint: props.csv_endpoint,
- json_endpoint: props.json_endpoint,
- standalone_endpoint: props.standalone_endpoint,
- },
-
containerId: props.containerId,
-
- jsonEndpoint: () => props.json_endpoint,
-
+ selector: this.state.selector,
container: {
html: (data) => {
// this should be a callback to clear the contents of the slice container
$(this.state.selector).html(data);
},
-
css: (dim, size) => {
// dimension can be 'height'
// pixel string can be '300px'
// should call callback to adjust height of chart
$(this.state.selector).css(dim, size);
},
- height: () => parseInt(this.props.height, 10) - 100,
-
- show: () => { this.render(); },
-
+ height: () => parseInt(props.height, 10) - 100,
+ show: () => { },
get: (n) => ($(this.state.selector).get(n)),
-
find: (classname) => ($(this.state.selector).find(classname)),
-
},
width: () => this.chartContainerRef.getBoundingClientRect().width,
- height: () => parseInt(this.props.height, 10) - 100,
-
- selector: this.state.selector,
+ height: () => parseInt(props.height, 10) - 100,
setFilter: () => {
// set filter according to data in store
@@ -130,32 +97,25 @@ class ChartContainer extends React.Component {
{}
),
- done: (payload) => {
- // finished rendering callback
- // Todo: end timer and chartLoading set to success
- props.actions.chartUpdateSucceeded(payload.query);
- },
-
+ done: () => {},
clearError: () => {
// no need to do anything here since Alert is closable
// query button will also remove Alert
},
-
- error(msg) {
- let payload = { error: msg };
- try {
- payload = JSON.parse(msg);
- } catch (e) {
- // pass
- }
- props.actions.chartUpdateFailed(payload.error, payload.query);
- },
+ error() {},
d3format: (col, number) => {
// mock d3format function in Slice object in superset.js
const format = props.column_formats[col];
return d3format(format, number);
},
+
+ data: {
+ csv_endpoint: props.queryResponse.csv_endpoint,
+ json_endpoint: props.queryResponse.json_endpoint,
+ standalone_endpoint: props.queryResponse.standalone_endpoint,
+ },
+
};
}
@@ -199,7 +159,7 @@ class ChartContainer extends React.Component {
}
{ this.chartContainerRef = ref; }}
+ ref={ref => { this.chartContainerRef = ref; }}
className={this.props.viz_type}
style={{
opacity: loading ? '0.25' : '1',
@@ -251,11 +211,13 @@ class ChartContainer extends React.Component {
state={CHART_STATUS_MAP[this.props.chartStatus]}
style={{ fontSize: '10px', marginRight: '5px' }}
/>
-
+ {this.state.mockSlice &&
+
+ }
}
@@ -276,18 +238,15 @@ function mapStateToProps(state) {
slice_name: state.viz.form_data.slice_name,
viz_type: state.viz.form_data.viz_type,
can_download: state.can_download,
- csv_endpoint: state.viz.csv_endpoint,
- json_endpoint: state.viz.json_endpoint,
- standalone_endpoint: state.viz.standalone_endpoint,
chartUpdateStartTime: state.chartUpdateStartTime,
chartUpdateEndTime: state.chartUpdateEndTime,
query: state.viz.query,
column_formats: state.viz.column_formats,
- data: state.viz.data,
chartStatus: state.chartStatus,
isStarred: state.isStarred,
alert: state.chartAlert,
table_name: state.viz.form_data.datasource_name,
+ queryResponse: state.queryResponse,
};
}
diff --git a/superset/assets/javascripts/explorev2/components/ExploreViewContainer.jsx b/superset/assets/javascripts/explorev2/components/ExploreViewContainer.jsx
index d43ffb55e..83bdf18ea 100644
--- a/superset/assets/javascripts/explorev2/components/ExploreViewContainer.jsx
+++ b/superset/assets/javascripts/explorev2/components/ExploreViewContainer.jsx
@@ -30,6 +30,7 @@ class ExploreViewContainer extends React.Component {
componentDidMount() {
window.addEventListener('resize', this.handleResize.bind(this));
+ this.runQuery();
}
componentWillReceiveProps(nextProps) {
@@ -38,7 +39,7 @@ class ExploreViewContainer extends React.Component {
&& autoQueryFields.indexOf(field) !== -1)
);
if (refreshChart) {
- this.onQuery(nextProps.form_data);
+ this.onQuery();
}
}
@@ -46,12 +47,12 @@ class ExploreViewContainer extends React.Component {
window.removeEventListener('resize', this.handleResize.bind(this));
}
- onQuery(form_data) {
- this.props.actions.chartUpdateStarted();
+ onQuery() {
+ this.runQuery();
history.pushState(
{},
document.title,
- getExploreUrl(form_data, this.props.datasource_type)
+ getExploreUrl(this.props.form_data, this.props.datasource_type)
);
// remove alerts when query
this.props.actions.removeControlPanelAlert();
@@ -63,6 +64,11 @@ class ExploreViewContainer extends React.Component {
return `${window.innerHeight - navHeight}px`;
}
+
+ runQuery() {
+ this.props.actions.runQuery(this.props.form_data, this.props.datasource_type);
+ }
+
handleResize() {
clearTimeout(this.resizeTimer);
this.resizeTimer = setTimeout(() => {
@@ -118,7 +124,7 @@ class ExploreViewContainer extends React.Component {
@@ -147,10 +153,10 @@ ExploreViewContainer.propTypes = propTypes;
function mapStateToProps(state) {
return {
- datasource_type: state.datasource_type,
- form_data: state.viz.form_data,
chartStatus: state.chartStatus,
+ datasource_type: state.datasource_type,
fields: state.fields,
+ form_data: state.viz.form_data,
};
}
diff --git a/superset/assets/javascripts/explorev2/index.jsx b/superset/assets/javascripts/explorev2/index.jsx
index dc48dd35b..9a3a461fc 100644
--- a/superset/assets/javascripts/explorev2/index.jsx
+++ b/superset/assets/javascripts/explorev2/index.jsx
@@ -32,6 +32,7 @@ const bootstrappedState = Object.assign(
chartUpdateStartTime: now(),
chartUpdateEndTime: null,
chartStatus: 'loading',
+ queryResponse: null,
}
);
bootstrappedState.viz.form_data.datasource = parseInt(bootstrapData.datasource_id, 10);
diff --git a/superset/assets/javascripts/explorev2/reducers/exploreReducer.js b/superset/assets/javascripts/explorev2/reducers/exploreReducer.js
index 56a6976c5..1ea3c1d67 100644
--- a/superset/assets/javascripts/explorev2/reducers/exploreReducer.js
+++ b/superset/assets/javascripts/explorev2/reducers/exploreReducer.js
@@ -2,7 +2,6 @@
import { defaultFormData } from '../stores/store';
import * as actions from '../actions/exploreActions';
import { now } from '../../modules/dates';
-import { getExploreUrl } from '../exploreUtils';
export const exploreReducer = function (state, action) {
const actionHandlers = {
@@ -70,34 +69,30 @@ export const exploreReducer = function (state, action) {
state,
{
chartStatus: 'success',
- viz: Object.assign({}, state.viz, { query: action.query }),
+ queryResponse: action.queryResponse,
}
);
},
[actions.CHART_UPDATE_STARTED]() {
- const chartUpdateStartTime = now();
- const form_data = Object.assign({}, state.viz.form_data);
- const datasource_type = state.datasource_type;
- const vizUpdates = {
- json_endpoint: getExploreUrl(form_data, datasource_type, 'json'),
- csv_endpoint: getExploreUrl(form_data, datasource_type, 'csv'),
- standalone_endpoint:
- getExploreUrl(form_data, datasource_type, 'standalone'),
- };
return Object.assign({}, state,
{
chartStatus: 'loading',
chartUpdateEndTime: null,
- chartUpdateStartTime,
- viz: Object.assign({}, state.viz, vizUpdates),
+ chartUpdateStartTime: now(),
});
},
+ [actions.CHART_RENDERING_FAILED]() {
+ return Object.assign({}, state, {
+ chartStatus: 'failed',
+ chartAlert: 'An error occurred while rendering the visualization: ' + action.error,
+ });
+ },
[actions.CHART_UPDATE_FAILED]() {
return Object.assign({}, state, {
chartStatus: 'failed',
- chartAlert: action.error,
+ chartAlert: action.queryResponse.error,
chartUpdateEndTime: now(),
- viz: Object.assign({}, state.viz, { query: action.query }),
+ queryResponse: action.queryResponse,
});
},
[actions.UPDATE_CHART_STATUS]() {
@@ -108,7 +103,10 @@ export const exploreReducer = function (state, action) {
return newState;
},
[actions.REMOVE_CHART_ALERT]() {
- return Object.assign({}, state, { chartAlert: null });
+ if (state.chartAlert !== null) {
+ return Object.assign({}, state, { chartAlert: null });
+ }
+ return state;
},
[actions.SAVE_SLICE_FAILED]() {
return Object.assign({}, state, { saveModalAlert: 'Failed to save slice' });
diff --git a/superset/assets/javascripts/modules/superset.js b/superset/assets/javascripts/modules/superset.js
index 60289147f..fa5a843cd 100644
--- a/superset/assets/javascripts/modules/superset.js
+++ b/superset/assets/javascripts/modules/superset.js
@@ -222,14 +222,19 @@ const px = function () {
timer = setInterval(stopwatch, 10);
$('#timer').removeClass('label-danger label-success');
$('#timer').addClass('label-warning');
- this.viz.render();
+ $.getJSON(this.jsonEndpoint(), queryResponse => {
+ try {
+ vizMap[data.form_data.viz_type](this, queryResponse);
+ this.done(queryResponse);
+ } catch (e) {
+ this.error('An error occurred while rendering the visualization: ' + e);
+ }
+ }).fail(err => {
+ this.error(err.responseText, err);
+ });
},
resize() {
- token.find('img.loading').show();
- container.fadeTo(0.5, 0.25);
- container.css('height', this.height());
- this.viz.render();
- this.viz.resize();
+ this.render();
},
addFilter(col, vals) {
controller.addFilter(sliceId, col, vals);
@@ -247,7 +252,6 @@ const px = function () {
controller.removeFilter(sliceId, col, vals);
},
};
- slice.viz = vizMap[data.form_data.viz_type](slice);
return slice;
};
// Export public functions
diff --git a/superset/assets/visualizations/big_number.js b/superset/assets/visualizations/big_number.js
index 68db689a6..3d801a4dc 100644
--- a/superset/assets/visualizations/big_number.js
+++ b/superset/assets/visualizations/big_number.js
@@ -3,197 +3,183 @@ import { formatDate } from '../javascripts/modules/dates';
require('./big_number.css');
-function bigNumberVis(slice) {
- function render() {
- const div = d3.select(slice.selector);
- d3.json(slice.jsonEndpoint(), function (error, payload) {
- // Define the percentage bounds that define color from red to green
- if (error !== null) {
- slice.error(error.responseText, error);
- return;
- }
- div.html(''); // reset
+function bigNumberVis(slice, payload) {
+ const div = d3.select(slice.selector);
+ // Define the percentage bounds that define color from red to green
+ div.html(''); // reset
- const fd = payload.form_data;
- const json = payload.data;
+ const fd = payload.form_data;
+ const json = payload.data;
- const f = d3.format(fd.y_axis_format);
- const fp = d3.format('+.1%');
- const width = slice.width();
- const height = slice.height();
- const svg = div.append('svg');
- svg.attr('width', width);
- svg.attr('height', height);
- const data = json.data;
- let vCompare;
- let v;
- if (fd.viz_type === 'big_number') {
- v = data[data.length - 1][1];
+ const f = d3.format(fd.y_axis_format);
+ const fp = d3.format('+.1%');
+ const width = slice.width();
+ const height = slice.height();
+ const svg = div.append('svg');
+ svg.attr('width', width);
+ svg.attr('height', height);
+ const data = json.data;
+ let vCompare;
+ let v;
+ if (fd.viz_type === 'big_number') {
+ v = data[data.length - 1][1];
+ } else {
+ v = data[0][0];
+ }
+ if (json.compare_lag > 0) {
+ const pos = data.length - (json.compare_lag + 1);
+ if (pos >= 0) {
+ const vAnchor = data[pos][1];
+ if (vAnchor !== 0) {
+ vCompare = (v - vAnchor) / Math.abs(vAnchor);
} else {
- v = data[0][0];
+ vCompare = 0;
}
- if (json.compare_lag > 0) {
- const pos = data.length - (json.compare_lag + 1);
- if (pos >= 0) {
- const vAnchor = data[pos][1];
- if (vAnchor !== 0) {
- vCompare = (v - vAnchor) / Math.abs(vAnchor);
- } else {
- vCompare = 0;
- }
- }
- }
- const dateExt = d3.extent(data, (d) => d[0]);
- const valueExt = d3.extent(data, (d) => d[1]);
+ }
+ }
+ const dateExt = d3.extent(data, (d) => d[0]);
+ const valueExt = d3.extent(data, (d) => d[1]);
- const margin = 20;
- const scaleX = d3.time.scale.utc().domain(dateExt).range([margin, width - margin]);
- const scaleY = d3.scale.linear().domain(valueExt).range([height - (margin), margin]);
- const colorRange = [d3.hsl(0, 1, 0.3), d3.hsl(120, 1, 0.3)];
- const scaleColor = d3.scale
- .linear().domain([-1, 1])
- .interpolate(d3.interpolateHsl)
- .range(colorRange)
- .clamp(true);
- const line = d3.svg.line()
- .x(function (d) {
- return scaleX(d[0]);
- })
- .y(function (d) {
- return scaleY(d[1]);
- })
- .interpolate('basis');
+ const margin = 20;
+ const scaleX = d3.time.scale.utc().domain(dateExt).range([margin, width - margin]);
+ const scaleY = d3.scale.linear().domain(valueExt).range([height - (margin), margin]);
+ const colorRange = [d3.hsl(0, 1, 0.3), d3.hsl(120, 1, 0.3)];
+ const scaleColor = d3.scale
+ .linear().domain([-1, 1])
+ .interpolate(d3.interpolateHsl)
+ .range(colorRange)
+ .clamp(true);
+ const line = d3.svg.line()
+ .x(function (d) {
+ return scaleX(d[0]);
+ })
+ .y(function (d) {
+ return scaleY(d[1]);
+ })
+ .interpolate('basis');
- let y = height / 2;
- let g = svg.append('g');
- // Printing big number
- g.append('g').attr('class', 'digits')
- .attr('opacity', 1)
- .append('text')
- .attr('x', width / 2)
- .attr('y', y)
- .attr('class', 'big')
- .attr('alignment-baseline', 'middle')
- .attr('id', 'bigNumber')
- .style('font-weight', 'bold')
- .style('cursor', 'pointer')
- .text(f(v))
- .style('font-size', d3.min([height, width]) / 3.5)
- .style('text-anchor', 'middle')
- .attr('fill', 'black');
+ let y = height / 2;
+ let g = svg.append('g');
+ // Printing big number
+ g.append('g').attr('class', 'digits')
+ .attr('opacity', 1)
+ .append('text')
+ .attr('x', width / 2)
+ .attr('y', y)
+ .attr('class', 'big')
+ .attr('alignment-baseline', 'middle')
+ .attr('id', 'bigNumber')
+ .style('font-weight', 'bold')
+ .style('cursor', 'pointer')
+ .text(f(v))
+ .style('font-size', d3.min([height, width]) / 3.5)
+ .style('text-anchor', 'middle')
+ .attr('fill', 'black');
- // Printing big number subheader text
- if (json.subheader !== null) {
- g.append('text')
- .attr('x', width / 2)
- .attr('y', (height / 16) * 12)
- .text(json.subheader)
- .attr('id', 'subheader_text')
- .style('font-size', d3.min([height, width]) / 8)
- .style('text-anchor', 'middle');
- }
-
- if (fd.viz_type === 'big_number') {
- // Drawing trend line
-
- g.append('path')
- .attr('d', function () {
- return line(data);
- })
- .attr('stroke-width', 5)
- .attr('opacity', 0.5)
- .attr('fill', 'none')
- .attr('stroke-linecap', 'round')
- .attr('stroke', 'grey');
-
- g = svg.append('g')
- .attr('class', 'digits')
- .attr('opacity', 1);
-
- if (vCompare !== null) {
- y = (height / 8) * 3;
- }
-
- const c = scaleColor(vCompare);
-
- // Printing compare %
- if (vCompare) {
- g.append('text')
- .attr('x', width / 2)
- .attr('y', (height / 16) * 12)
- .text(fp(vCompare) + json.compare_suffix)
- .style('font-size', d3.min([height, width]) / 8)
- .style('text-anchor', 'middle')
- .attr('fill', c)
- .attr('stroke', c);
- }
-
- const gAxis = svg.append('g').attr('class', 'axis').attr('opacity', 0);
- g = gAxis.append('g');
- const xAxis = d3.svg.axis()
- .scale(scaleX)
- .orient('bottom')
- .ticks(4)
- .tickFormat(formatDate);
- g.call(xAxis);
- g.attr('transform', 'translate(0,' + (height - margin) + ')');
-
- g = gAxis.append('g').attr('transform', 'translate(' + (width - margin) + ',0)');
- const yAxis = d3.svg.axis()
- .scale(scaleY)
- .orient('left')
- .tickFormat(d3.format(fd.y_axis_format))
- .tickValues(valueExt);
- g.call(yAxis);
- g.selectAll('text')
- .style('text-anchor', 'end')
- .attr('y', '-7')
- .attr('x', '-4');
-
- g.selectAll('text')
- .style('font-size', '10px');
-
- div.on('mouseover', function () {
- const el = d3.select(this);
- el.selectAll('path')
- .transition()
- .duration(500)
- .attr('opacity', 1)
- .style('stroke-width', '2px');
- el.selectAll('g.digits')
- .transition()
- .duration(500)
- .attr('opacity', 0.1);
- el.selectAll('g.axis')
- .transition()
- .duration(500)
- .attr('opacity', 1);
- })
- .on('mouseout', function () {
- const el = d3.select(this);
- el.select('path')
- .transition()
- .duration(500)
- .attr('opacity', 0.5)
- .style('stroke-width', '5px');
- el.selectAll('g.digits')
- .transition()
- .duration(500)
- .attr('opacity', 1);
- el.selectAll('g.axis')
- .transition()
- .duration(500)
- .attr('opacity', 0);
- });
- }
- slice.done(payload);
- });
+ // Printing big number subheader text
+ if (json.subheader !== null) {
+ g.append('text')
+ .attr('x', width / 2)
+ .attr('y', (height / 16) * 12)
+ .text(json.subheader)
+ .attr('id', 'subheader_text')
+ .style('font-size', d3.min([height, width]) / 8)
+ .style('text-anchor', 'middle');
}
- return {
- render,
- resize: render,
- };
+ if (fd.viz_type === 'big_number') {
+ // Drawing trend line
+
+ g.append('path')
+ .attr('d', function () {
+ return line(data);
+ })
+ .attr('stroke-width', 5)
+ .attr('opacity', 0.5)
+ .attr('fill', 'none')
+ .attr('stroke-linecap', 'round')
+ .attr('stroke', 'grey');
+
+ g = svg.append('g')
+ .attr('class', 'digits')
+ .attr('opacity', 1);
+
+ if (vCompare !== null) {
+ y = (height / 8) * 3;
+ }
+
+ const c = scaleColor(vCompare);
+
+ // Printing compare %
+ if (vCompare) {
+ g.append('text')
+ .attr('x', width / 2)
+ .attr('y', (height / 16) * 12)
+ .text(fp(vCompare) + json.compare_suffix)
+ .style('font-size', d3.min([height, width]) / 8)
+ .style('text-anchor', 'middle')
+ .attr('fill', c)
+ .attr('stroke', c);
+ }
+
+ const gAxis = svg.append('g').attr('class', 'axis').attr('opacity', 0);
+ g = gAxis.append('g');
+ const xAxis = d3.svg.axis()
+ .scale(scaleX)
+ .orient('bottom')
+ .ticks(4)
+ .tickFormat(formatDate);
+ g.call(xAxis);
+ g.attr('transform', 'translate(0,' + (height - margin) + ')');
+
+ g = gAxis.append('g').attr('transform', 'translate(' + (width - margin) + ',0)');
+ const yAxis = d3.svg.axis()
+ .scale(scaleY)
+ .orient('left')
+ .tickFormat(d3.format(fd.y_axis_format))
+ .tickValues(valueExt);
+ g.call(yAxis);
+ g.selectAll('text')
+ .style('text-anchor', 'end')
+ .attr('y', '-7')
+ .attr('x', '-4');
+
+ g.selectAll('text')
+ .style('font-size', '10px');
+
+ div.on('mouseover', function () {
+ const el = d3.select(this);
+ el.selectAll('path')
+ .transition()
+ .duration(500)
+ .attr('opacity', 1)
+ .style('stroke-width', '2px');
+ el.selectAll('g.digits')
+ .transition()
+ .duration(500)
+ .attr('opacity', 0.1);
+ el.selectAll('g.axis')
+ .transition()
+ .duration(500)
+ .attr('opacity', 1);
+ })
+ .on('mouseout', function () {
+ const el = d3.select(this);
+ el.select('path')
+ .transition()
+ .duration(500)
+ .attr('opacity', 0.5)
+ .style('stroke-width', '5px');
+ el.selectAll('g.digits')
+ .transition()
+ .duration(500)
+ .attr('opacity', 1);
+ el.selectAll('g.axis')
+ .transition()
+ .duration(500)
+ .attr('opacity', 0);
+ });
+ }
}
module.exports = bigNumberVis;
diff --git a/superset/assets/visualizations/cal_heatmap.js b/superset/assets/visualizations/cal_heatmap.js
index 349fdc96b..e80635088 100644
--- a/superset/assets/visualizations/cal_heatmap.js
+++ b/superset/assets/visualizations/cal_heatmap.js
@@ -7,47 +7,32 @@ require('../node_modules/cal-heatmap/cal-heatmap.css');
const CalHeatMap = require('cal-heatmap');
-function calHeatmap(slice) {
- const render = function () {
- const div = d3.select(slice.selector);
- d3.json(slice.jsonEndpoint(), function (error, json) {
- const data = json.data;
- if (error !== null) {
- slice.error(error.responseText, error);
- return;
- }
+function calHeatmap(slice, payload) {
+ const div = d3.select(slice.selector);
+ const data = payload.data;
- div.selectAll('*').remove();
- const cal = new CalHeatMap();
+ div.selectAll('*').remove();
+ const cal = new CalHeatMap();
- const timestamps = data.timestamps;
- const extents = d3.extent(Object.keys(timestamps), (key) => timestamps[key]);
- const step = (extents[1] - extents[0]) / 5;
+ const timestamps = data.timestamps;
+ const extents = d3.extent(Object.keys(timestamps), (key) => timestamps[key]);
+ const step = (extents[1] - extents[0]) / 5;
- try {
- cal.init({
- start: data.start,
- data: timestamps,
- itemSelector: slice.selector,
- tooltip: true,
- domain: data.domain,
- subDomain: data.subdomain,
- range: data.range,
- browsing: true,
- legend: [extents[0], extents[0] + step, extents[0] + step * 2, extents[0] + step * 3],
- });
- } catch (e) {
- slice.error(e);
- }
-
- slice.done(json);
+ try {
+ cal.init({
+ start: data.start,
+ data: timestamps,
+ itemSelector: slice.selector,
+ tooltip: true,
+ domain: data.domain,
+ subDomain: data.subdomain,
+ range: data.range,
+ browsing: true,
+ legend: [extents[0], extents[0] + step, extents[0] + step * 2, extents[0] + step * 3],
});
- };
-
- return {
- render,
- resize: render,
- };
+ } catch (e) {
+ slice.error(e);
+ }
}
module.exports = calHeatmap;
diff --git a/superset/assets/visualizations/directed_force.js b/superset/assets/visualizations/directed_force.js
index 0ec4214ed..e2003f122 100644
--- a/superset/assets/visualizations/directed_force.js
+++ b/superset/assets/visualizations/directed_force.js
@@ -4,179 +4,164 @@ import d3 from 'd3';
require('./directed_force.css');
/* Modified from http://bl.ocks.org/d3noob/5141278 */
-function directedForceVis(slice) {
- const render = function () {
- const div = d3.select(slice.selector);
- const width = slice.width();
- const height = slice.height() - 25;
- d3.json(slice.jsonEndpoint(), function (error, json) {
- if (error !== null) {
- slice.error(error.responseText, error);
- return;
- }
- const linkLength = json.form_data.link_length || 200;
- const charge = json.form_data.charge || -500;
+const directedForceVis = function (slice, json) {
+ const div = d3.select(slice.selector);
+ const width = slice.width();
+ const height = slice.height() - 25;
+ const linkLength = json.form_data.link_length || 200;
+ const charge = json.form_data.charge || -500;
- const links = json.data;
- const nodes = {};
- // Compute the distinct nodes from the links.
- links.forEach(function (link) {
- link.source = nodes[link.source] || (nodes[link.source] = {
- name: link.source,
- });
- link.target = nodes[link.target] || (nodes[link.target] = {
- name: link.target,
- });
- link.value = Number(link.value);
-
- const targetName = link.target.name;
- const sourceName = link.source.name;
-
- if (nodes[targetName].total === undefined) {
- nodes[targetName].total = link.value;
- }
- if (nodes[sourceName].total === undefined) {
- nodes[sourceName].total = 0;
- }
- if (nodes[targetName].max === undefined) {
- nodes[targetName].max = 0;
- }
- if (link.value > nodes[targetName].max) {
- nodes[targetName].max = link.value;
- }
- if (nodes[targetName].min === undefined) {
- nodes[targetName].min = 0;
- }
- if (link.value > nodes[targetName].min) {
- nodes[targetName].min = link.value;
- }
-
- nodes[targetName].total += link.value;
- });
-
- /* eslint-disable no-use-before-define */
- // add the curvy lines
- function tick() {
- path.attr('d', function (d) {
- const dx = d.target.x - d.source.x;
- const dy = d.target.y - d.source.y;
- const dr = Math.sqrt(dx * dx + dy * dy);
- return (
- 'M' +
- d.source.x + ',' +
- d.source.y + 'A' +
- dr + ',' + dr + ' 0 0,1 ' +
- d.target.x + ',' +
- d.target.y
- );
- });
-
- node.attr('transform', function (d) {
- return 'translate(' + d.x + ',' + d.y + ')';
- });
- }
- /* eslint-enable no-use-before-define */
-
- const force = d3.layout.force()
- .nodes(d3.values(nodes))
- .links(links)
- .size([width, height])
- .linkDistance(linkLength)
- .charge(charge)
- .on('tick', tick)
- .start();
-
- div.selectAll('*').remove();
- const svg = div.append('svg')
- .attr('width', width)
- .attr('height', height);
-
-
- // build the arrow.
- svg.append('svg:defs').selectAll('marker')
- .data(['end']) // Different link/path types can be defined here
- .enter()
- .append('svg:marker') // This section adds in the arrows
- .attr('id', String)
- .attr('viewBox', '0 -5 10 10')
- .attr('refX', 15)
- .attr('refY', -1.5)
- .attr('markerWidth', 6)
- .attr('markerHeight', 6)
- .attr('orient', 'auto')
- .append('svg:path')
- .attr('d', 'M0,-5L10,0L0,5');
-
- const edgeScale = d3.scale.linear()
- .range([0.1, 0.5]);
- // add the links and the arrows
- const path = svg.append('svg:g').selectAll('path')
- .data(force.links())
- .enter()
- .append('svg:path')
- .attr('class', 'link')
- .style('opacity', function (d) {
- return edgeScale(d.value / d.target.max);
- })
- .attr('marker-end', 'url(#end)');
-
- // define the nodes
- const node = svg.selectAll('.node')
- .data(force.nodes())
- .enter()
- .append('g')
- .attr('class', 'node')
- .on('mouseenter', function () {
- d3.select(this)
- .select('circle')
- .transition()
- .style('stroke-width', 5);
-
- d3.select(this)
- .select('text')
- .transition()
- .style('font-size', 25);
- })
- .on('mouseleave', function () {
- d3.select(this)
- .select('circle')
- .transition()
- .style('stroke-width', 1.5);
- d3.select(this)
- .select('text')
- .transition()
- .style('font-size', 12);
- })
- .call(force.drag);
-
- // add the nodes
- const ext = d3.extent(d3.values(nodes), function (d) {
- return Math.sqrt(d.total);
- });
- const circleScale = d3.scale.linear()
- .domain(ext)
- .range([3, 30]);
-
- node.append('circle')
- .attr('r', function (d) {
- return circleScale(Math.sqrt(d.total));
- });
-
- // add the text
- node.append('text')
- .attr('x', 6)
- .attr('dy', '.35em')
- .text(function (d) {
- return d.name;
- });
-
- slice.done(json);
+ const links = json.data;
+ const nodes = {};
+ // Compute the distinct nodes from the links.
+ links.forEach(function (link) {
+ link.source = nodes[link.source] || (nodes[link.source] = {
+ name: link.source,
});
- };
- return {
- render,
- resize: render,
- };
-}
+ link.target = nodes[link.target] || (nodes[link.target] = {
+ name: link.target,
+ });
+ link.value = Number(link.value);
+
+ const targetName = link.target.name;
+ const sourceName = link.source.name;
+
+ if (nodes[targetName].total === undefined) {
+ nodes[targetName].total = link.value;
+ }
+ if (nodes[sourceName].total === undefined) {
+ nodes[sourceName].total = 0;
+ }
+ if (nodes[targetName].max === undefined) {
+ nodes[targetName].max = 0;
+ }
+ if (link.value > nodes[targetName].max) {
+ nodes[targetName].max = link.value;
+ }
+ if (nodes[targetName].min === undefined) {
+ nodes[targetName].min = 0;
+ }
+ if (link.value > nodes[targetName].min) {
+ nodes[targetName].min = link.value;
+ }
+
+ nodes[targetName].total += link.value;
+ });
+
+ /* eslint-disable no-use-before-define */
+ // add the curvy lines
+ function tick() {
+ path.attr('d', function (d) {
+ const dx = d.target.x - d.source.x;
+ const dy = d.target.y - d.source.y;
+ const dr = Math.sqrt(dx * dx + dy * dy);
+ return (
+ 'M' +
+ d.source.x + ',' +
+ d.source.y + 'A' +
+ dr + ',' + dr + ' 0 0,1 ' +
+ d.target.x + ',' +
+ d.target.y
+ );
+ });
+
+ node.attr('transform', function (d) {
+ return 'translate(' + d.x + ',' + d.y + ')';
+ });
+ }
+ /* eslint-enable no-use-before-define */
+
+ const force = d3.layout.force()
+ .nodes(d3.values(nodes))
+ .links(links)
+ .size([width, height])
+ .linkDistance(linkLength)
+ .charge(charge)
+ .on('tick', tick)
+ .start();
+
+ div.selectAll('*').remove();
+ const svg = div.append('svg')
+ .attr('width', width)
+ .attr('height', height);
+
+ // build the arrow.
+ svg.append('svg:defs').selectAll('marker')
+ .data(['end']) // Different link/path types can be defined here
+ .enter()
+ .append('svg:marker') // This section adds in the arrows
+ .attr('id', String)
+ .attr('viewBox', '0 -5 10 10')
+ .attr('refX', 15)
+ .attr('refY', -1.5)
+ .attr('markerWidth', 6)
+ .attr('markerHeight', 6)
+ .attr('orient', 'auto')
+ .append('svg:path')
+ .attr('d', 'M0,-5L10,0L0,5');
+
+ const edgeScale = d3.scale.linear()
+ .range([0.1, 0.5]);
+ // add the links and the arrows
+ const path = svg.append('svg:g').selectAll('path')
+ .data(force.links())
+ .enter()
+ .append('svg:path')
+ .attr('class', 'link')
+ .style('opacity', function (d) {
+ return edgeScale(d.value / d.target.max);
+ })
+ .attr('marker-end', 'url(#end)');
+
+ // define the nodes
+ const node = svg.selectAll('.node')
+ .data(force.nodes())
+ .enter()
+ .append('g')
+ .attr('class', 'node')
+ .on('mouseenter', function () {
+ d3.select(this)
+ .select('circle')
+ .transition()
+ .style('stroke-width', 5);
+
+ d3.select(this)
+ .select('text')
+ .transition()
+ .style('font-size', 25);
+ })
+ .on('mouseleave', function () {
+ d3.select(this)
+ .select('circle')
+ .transition()
+ .style('stroke-width', 1.5);
+ d3.select(this)
+ .select('text')
+ .transition()
+ .style('font-size', 12);
+ })
+ .call(force.drag);
+
+ // add the nodes
+ const ext = d3.extent(d3.values(nodes), function (d) {
+ return Math.sqrt(d.total);
+ });
+ const circleScale = d3.scale.linear()
+ .domain(ext)
+ .range([3, 30]);
+
+ node.append('circle')
+ .attr('r', function (d) {
+ return circleScale(Math.sqrt(d.total));
+ });
+
+ // add the text
+ node.append('text')
+ .attr('x', 6)
+ .attr('dy', '.35em')
+ .text(function (d) {
+ return d.name;
+ });
+};
module.exports = directedForceVis;
diff --git a/superset/assets/visualizations/filter_box.jsx b/superset/assets/visualizations/filter_box.jsx
index 539c18888..80276fe39 100644
--- a/superset/assets/visualizations/filter_box.jsx
+++ b/superset/assets/visualizations/filter_box.jsx
@@ -1,5 +1,4 @@
// JS
-const $ = require('jquery');
import d3 from 'd3';
import React from 'react';
@@ -109,40 +108,29 @@ class FilterBox extends React.Component {
FilterBox.propTypes = propTypes;
FilterBox.defaultProps = defaultProps;
-function filterBox(slice) {
- const refresh = function () {
- const d3token = d3.select(slice.selector);
- d3token.selectAll('*').remove();
+function filterBox(slice, payload) {
+ const d3token = d3.select(slice.selector);
+ d3token.selectAll('*').remove();
- // filter box should ignore the dashboard's filters
- const url = slice.jsonEndpoint({ extraFilters: false });
- $.getJSON(url, (payload) => {
- const fd = payload.form_data;
- const filtersChoices = {};
- // Making sure the ordering of the fields matches the setting in the
- // dropdown as it may have been shuffled while serialized to json
- payload.form_data.groupby.forEach((f) => {
- filtersChoices[f] = payload.data[f];
- });
- ReactDOM.render(
-
,
- document.getElementById(slice.containerId)
- );
- slice.done(payload);
- })
- .fail(function (xhr) {
- slice.error(xhr.responseText, xhr);
- });
- };
- return {
- render: refresh,
- resize: () => {},
- };
+ // filter box should ignore the dashboard's filters
+ // TODO FUCK
+ // const url = slice.jsonEndpoint({ extraFilters: false });
+ const fd = payload.form_data;
+ const filtersChoices = {};
+ // Making sure the ordering of the fields matches the setting in the
+ // dropdown as it may have been shuffled while serialized to json
+ payload.form_data.groupby.forEach((f) => {
+ filtersChoices[f] = payload.data[f];
+ });
+ ReactDOM.render(
+
,
+ document.getElementById(slice.containerId)
+ );
}
module.exports = filterBox;
diff --git a/superset/assets/visualizations/heatmap.js b/superset/assets/visualizations/heatmap.js
index 285b23976..bc95806d3 100644
--- a/superset/assets/visualizations/heatmap.js
+++ b/superset/assets/visualizations/heatmap.js
@@ -8,225 +8,210 @@ require('./heatmap.css');
// Inspired from http://bl.ocks.org/mbostock/3074470
// https://jsfiddle.net/cyril123/h0reyumq/
-function heatmapVis(slice) {
- function refresh() {
- // Header for panel in explore v2
- const header = document.getElementById('slice-header');
- const headerHeight = header ? 30 + header.getBoundingClientRect().height : 0;
- const margin = {
- top: headerHeight,
- right: 10,
- bottom: 35,
- left: 35,
- };
-
- d3.json(slice.jsonEndpoint(), function (error, payload) {
- if (error) {
- slice.error(error.responseText, error);
- return;
- }
- const data = payload.data;
- // Dynamically adjusts based on max x / y category lengths
- function adjustMargins() {
- const pixelsPerCharX = 4.5; // approx, depends on font size
- const pixelsPerCharY = 6.8; // approx, depends on font size
- let longestX = 1;
- let longestY = 1;
- let datum;
-
- for (let i = 0; i < data.length; i++) {
- datum = data[i];
- longestX = Math.max(longestX, datum.x.length || 1);
- longestY = Math.max(longestY, datum.y.length || 1);
- }
-
- margin.left = Math.ceil(Math.max(margin.left, pixelsPerCharY * longestY));
- margin.bottom = Math.ceil(Math.max(margin.bottom, pixelsPerCharX * longestX));
- }
-
- function ordScale(k, rangeBands, reverse = false) {
- let domain = {};
- $.each(data, function (i, d) {
- domain[d[k]] = true;
- });
- domain = Object.keys(domain).sort(function (a, b) {
- return b - a;
- });
- if (reverse) {
- domain.reverse();
- }
- if (rangeBands === undefined) {
- return d3.scale.ordinal().domain(domain).range(d3.range(domain.length));
- }
- return d3.scale.ordinal().domain(domain).rangeBands(rangeBands);
- }
-
- slice.container.html('');
- const matrix = {};
- const fd = payload.form_data;
-
- 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('.3p');
-
- const xScale = ordScale('x');
- const yScale = ordScale('y', undefined, true);
- const xRbScale = ordScale('x', [0, hmWidth]);
- const yRbScale = ordScale('y', [hmHeight, 0]);
- const X = 0;
- const Y = 1;
- const heatmapDim = [xRbScale.domain().length, yRbScale.domain().length];
-
- const color = colorScalerFactory(fd.linear_color_scheme);
-
- const scale = [
- d3.scale.linear()
- .domain([0, heatmapDim[X]])
- .range([0, hmWidth]),
- d3.scale.linear()
- .domain([0, heatmapDim[Y]])
- .range([0, hmHeight]),
- ];
-
- const container = d3.select(slice.selector);
-
- 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('left', margin.left + 'px')
- .style('top', margin.top + headerHeight + 'px')
- .style('position', 'absolute');
-
- const svg = container.append('svg')
- .attr('width', width)
- .attr('height', height)
- .style('left', '0px')
- .style('top', headerHeight + 'px')
- .style('position', 'absolute');
-
- const rect = svg.append('g')
- .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
- .append('rect')
- .style('fill-opacity', 0)
- .attr('stroke', 'black')
- .attr('width', hmWidth)
- .attr('height', hmHeight);
-
- const tip = d3.tip()
- .attr('class', 'd3-tip')
- .offset(function () {
- const k = d3.mouse(this);
- const x = k[0] - (hmWidth / 2);
- return [k[1] - 20, x];
- })
- .html(function () {
- let s = '';
- const k = d3.mouse(this);
- const m = Math.floor(scale[0].invert(k[0]));
- const n = Math.floor(scale[1].invert(k[1]));
- 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 += '
' + fd.metric + ': ' + obj.v + '
';
- s += '
%: ' + fp(obj.perc) + '
';
- tip.style('display', null);
- } else {
- // this is a hack to hide the tooltip because we have map it to a single
- // d3-tip toggles opacity and calling hide here is undone by the lib after this call
- tip.style('display', 'none');
- }
- return s;
- });
-
- rect.call(tip);
-
- const xAxis = d3.svg.axis()
- .scale(xRbScale)
- .tickValues(xRbScale.domain().filter(
- function (d, i) {
- return !(i % (parseInt(fd.xscale_interval, 10)));
- }))
- .orient('bottom');
-
- const yAxis = d3.svg.axis()
- .scale(yRbScale)
- .tickValues(yRbScale.domain().filter(
- function (d, i) {
- return !(i % (parseInt(fd.yscale_interval, 10)));
- }))
- .orient('left');
-
- svg.append('g')
- .attr('class', 'x axis')
- .attr('transform', 'translate(' + margin.left + ',' + (margin.top + hmHeight) + ')')
- .call(xAxis)
- .selectAll('text')
- .style('text-anchor', 'end')
- .attr('transform', 'rotate(-45)');
-
- svg.append('g')
- .attr('class', 'y axis')
- .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
- .call(yAxis);
-
- rect.on('mousemove', tip.show);
- rect.on('mouseout', tip.hide);
-
- const context = canvas.node().getContext('2d');
- context.imageSmoothingEnabled = false;
-
- // Compute the pixel colors; scaled by CSS.
- function createImageObj() {
- const imageObj = new Image();
- const image = context.createImageData(heatmapDim[0], heatmapDim[1]);
- const pixs = {};
- $.each(data, function (i, d) {
- const c = d3.rgb(color(d.perc));
- const x = xScale(d.x);
- const y = yScale(d.y);
- pixs[x + (y * xScale.domain().length)] = c;
- if (matrix[x] === undefined) {
- matrix[x] = {};
- }
- if (matrix[x][y] === undefined) {
- matrix[x][y] = d;
- }
- });
-
- let p = -1;
- for (let i = 0; i < heatmapDim[0] * heatmapDim[1]; i++) {
- let c = pixs[i];
- let alpha = 255;
- if (c === undefined) {
- c = d3.rgb('#F00');
- alpha = 0;
- }
- image.data[++p] = c.r;
- image.data[++p] = c.g;
- image.data[++p] = c.b;
- image.data[++p] = alpha;
- }
- context.putImageData(image, 0, 0);
- imageObj.src = canvas.node().toDataURL();
- }
-
- createImageObj();
-
- slice.done(payload);
- });
- }
- return {
- render: refresh,
- resize: refresh,
+function heatmapVis(slice, payload) {
+ // Header for panel in explore v2
+ const header = document.getElementById('slice-header');
+ const headerHeight = header ? 30 + header.getBoundingClientRect().height : 0;
+ const margin = {
+ top: headerHeight,
+ right: 10,
+ bottom: 35,
+ left: 35,
};
+
+ const data = payload.data;
+ // Dynamically adjusts based on max x / y category lengths
+ function adjustMargins() {
+ const pixelsPerCharX = 4.5; // approx, depends on font size
+ const pixelsPerCharY = 6.8; // approx, depends on font size
+ let longestX = 1;
+ let longestY = 1;
+ let datum;
+
+ for (let i = 0; i < data.length; i++) {
+ datum = data[i];
+ longestX = Math.max(longestX, datum.x.length || 1);
+ longestY = Math.max(longestY, datum.y.length || 1);
+ }
+
+ margin.left = Math.ceil(Math.max(margin.left, pixelsPerCharY * longestY));
+ margin.bottom = Math.ceil(Math.max(margin.bottom, pixelsPerCharX * longestX));
+ }
+
+ function ordScale(k, rangeBands, reverse = false) {
+ let domain = {};
+ $.each(data, function (i, d) {
+ domain[d[k]] = true;
+ });
+ domain = Object.keys(domain).sort(function (a, b) {
+ return b - a;
+ });
+ if (reverse) {
+ domain.reverse();
+ }
+ if (rangeBands === undefined) {
+ return d3.scale.ordinal().domain(domain).range(d3.range(domain.length));
+ }
+ return d3.scale.ordinal().domain(domain).rangeBands(rangeBands);
+ }
+
+ slice.container.html('');
+ const matrix = {};
+ const fd = payload.form_data;
+
+ 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('.3p');
+
+ const xScale = ordScale('x');
+ const yScale = ordScale('y', undefined, true);
+ const xRbScale = ordScale('x', [0, hmWidth]);
+ const yRbScale = ordScale('y', [hmHeight, 0]);
+ const X = 0;
+ const Y = 1;
+ const heatmapDim = [xRbScale.domain().length, yRbScale.domain().length];
+
+ const color = colorScalerFactory(fd.linear_color_scheme);
+
+ const scale = [
+ d3.scale.linear()
+ .domain([0, heatmapDim[X]])
+ .range([0, hmWidth]),
+ d3.scale.linear()
+ .domain([0, heatmapDim[Y]])
+ .range([0, hmHeight]),
+ ];
+
+ const container = d3.select(slice.selector);
+
+ 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('left', margin.left + 'px')
+ .style('top', margin.top + headerHeight + 'px')
+ .style('position', 'absolute');
+
+ const svg = container.append('svg')
+ .attr('width', width)
+ .attr('height', height)
+ .style('left', '0px')
+ .style('top', headerHeight + 'px')
+ .style('position', 'absolute');
+
+ const rect = svg.append('g')
+ .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
+ .append('rect')
+ .style('fill-opacity', 0)
+ .attr('stroke', 'black')
+ .attr('width', hmWidth)
+ .attr('height', hmHeight);
+
+ const tip = d3.tip()
+ .attr('class', 'd3-tip')
+ .offset(function () {
+ const k = d3.mouse(this);
+ const x = k[0] - (hmWidth / 2);
+ return [k[1] - 20, x];
+ })
+ .html(function () {
+ let s = '';
+ const k = d3.mouse(this);
+ const m = Math.floor(scale[0].invert(k[0]));
+ const n = Math.floor(scale[1].invert(k[1]));
+ 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 += '
' + fd.metric + ': ' + obj.v + '
';
+ s += '
%: ' + fp(obj.perc) + '
';
+ tip.style('display', null);
+ } else {
+ // this is a hack to hide the tooltip because we have map it to a single
+ // d3-tip toggles opacity and calling hide here is undone by the lib after this call
+ tip.style('display', 'none');
+ }
+ return s;
+ });
+
+ rect.call(tip);
+
+ const xAxis = d3.svg.axis()
+ .scale(xRbScale)
+ .tickValues(xRbScale.domain().filter(
+ function (d, i) {
+ return !(i % (parseInt(fd.xscale_interval, 10)));
+ }))
+ .orient('bottom');
+
+ const yAxis = d3.svg.axis()
+ .scale(yRbScale)
+ .tickValues(yRbScale.domain().filter(
+ function (d, i) {
+ return !(i % (parseInt(fd.yscale_interval, 10)));
+ }))
+ .orient('left');
+
+ svg.append('g')
+ .attr('class', 'x axis')
+ .attr('transform', 'translate(' + margin.left + ',' + (margin.top + hmHeight) + ')')
+ .call(xAxis)
+ .selectAll('text')
+ .style('text-anchor', 'end')
+ .attr('transform', 'rotate(-45)');
+
+ svg.append('g')
+ .attr('class', 'y axis')
+ .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
+ .call(yAxis);
+
+ rect.on('mousemove', tip.show);
+ rect.on('mouseout', tip.hide);
+
+ const context = canvas.node().getContext('2d');
+ context.imageSmoothingEnabled = false;
+
+ // Compute the pixel colors; scaled by CSS.
+ function createImageObj() {
+ const imageObj = new Image();
+ const image = context.createImageData(heatmapDim[0], heatmapDim[1]);
+ const pixs = {};
+ $.each(data, function (i, d) {
+ const c = d3.rgb(color(d.perc));
+ const x = xScale(d.x);
+ const y = yScale(d.y);
+ pixs[x + (y * xScale.domain().length)] = c;
+ if (matrix[x] === undefined) {
+ matrix[x] = {};
+ }
+ if (matrix[x][y] === undefined) {
+ matrix[x][y] = d;
+ }
+ });
+
+ let p = -1;
+ for (let i = 0; i < heatmapDim[0] * heatmapDim[1]; i++) {
+ let c = pixs[i];
+ let alpha = 255;
+ if (c === undefined) {
+ c = d3.rgb('#F00');
+ alpha = 0;
+ }
+ image.data[++p] = c.r;
+ image.data[++p] = c.g;
+ image.data[++p] = c.b;
+ image.data[++p] = alpha;
+ }
+ context.putImageData(image, 0, 0);
+ imageObj.src = canvas.node().toDataURL();
+ }
+ createImageObj();
}
module.exports = heatmapVis;
diff --git a/superset/assets/visualizations/histogram.js b/superset/assets/visualizations/histogram.js
index 4bb3a9a97..e6949816b 100644
--- a/superset/assets/visualizations/histogram.js
+++ b/superset/assets/visualizations/histogram.js
@@ -3,9 +3,8 @@ import d3 from 'd3';
require('./histogram.css');
-function histogram(slice) {
- let div;
-
+function histogram(slice, payload) {
+ const div = d3.select(slice.selector);
const draw = function (data, numBins) {
// Set Margins
const margin = {
@@ -127,26 +126,9 @@ function histogram(slice) {
.classed('minor', true);
};
- const render = function () {
- div = d3.select(slice.selector);
- d3.json(slice.jsonEndpoint(), function (error, json) {
- if (error !== null) {
- slice.error(error.responseText, error);
- return;
- }
-
- const numBins = Number(json.form_data.link_length) || 10;
-
- div.selectAll('*').remove();
- draw(json.data, numBins);
- slice.done(json);
- });
- };
-
- return {
- render,
- resize: render,
- };
+ const numBins = Number(payload.form_data.link_length) || 10;
+ div.selectAll('*').remove();
+ draw(payload.data, numBins);
}
module.exports = histogram;
diff --git a/superset/assets/visualizations/horizon.js b/superset/assets/visualizations/horizon.js
index 8d713e05d..785ed0cd7 100644
--- a/superset/assets/visualizations/horizon.js
+++ b/superset/assets/visualizations/horizon.js
@@ -190,53 +190,38 @@ const horizonChart = function () {
return my;
};
-function horizonViz(slice) {
- function refresh() {
- d3.json(slice.jsonEndpoint(), function (error, payload) {
- const fd = payload.form_data;
- if (error) {
- slice.error(error.responseText, error);
- return;
- }
-
- const div = d3.select(slice.selector);
- div.selectAll('*').remove();
- let extent;
- if (fd.horizon_color_scale === 'overall') {
- let allValues = [];
- payload.data.forEach(function (d) {
- allValues = allValues.concat(d.values);
- });
- extent = d3.extent(allValues, (d) => d.y);
- } else if (fd.horizon_color_scale === 'change') {
- payload.data.forEach(function (series) {
- const t0y = series.values[0].y; // value at time 0
- series.values = series.values.map((d) =>
- Object.assign({}, d, { y: d.y - t0y })
- );
- });
- }
- div.selectAll('.horizon')
- .data(payload.data)
- .enter()
- .append('div')
- .attr('class', 'horizon')
- .each(function (d, i) {
- horizonChart()
- .height(fd.series_height)
- .width(slice.width())
- .extent(extent)
- .title(d.key)
- .call(this, d.values, i);
- });
-
- slice.done(payload);
+function horizonViz(slice, payload) {
+ const fd = payload.form_data;
+ const div = d3.select(slice.selector);
+ div.selectAll('*').remove();
+ let extent;
+ if (fd.horizon_color_scale === 'overall') {
+ let allValues = [];
+ payload.data.forEach(function (d) {
+ allValues = allValues.concat(d.values);
+ });
+ extent = d3.extent(allValues, (d) => d.y);
+ } else if (fd.horizon_color_scale === 'change') {
+ payload.data.forEach(function (series) {
+ const t0y = series.values[0].y; // value at time 0
+ series.values = series.values.map((d) =>
+ Object.assign({}, d, { y: d.y - t0y })
+ );
});
}
- return {
- render: refresh,
- resize: refresh,
- };
+ div.selectAll('.horizon')
+ .data(payload.data)
+ .enter()
+ .append('div')
+ .attr('class', 'horizon')
+ .each(function (d, i) {
+ horizonChart()
+ .height(fd.series_height)
+ .width(slice.width())
+ .extent(extent)
+ .title(d.key)
+ .call(this, d.values, i);
+ });
}
module.exports = horizonViz;
diff --git a/superset/assets/visualizations/iframe.js b/superset/assets/visualizations/iframe.js
index ae2855e0f..2792b0d0c 100644
--- a/superset/assets/visualizations/iframe.js
+++ b/superset/assets/visualizations/iframe.js
@@ -1,25 +1,12 @@
const $ = require('jquery');
-function iframeWidget(slice) {
- function refresh() {
- $('#code').attr('rows', '15');
- $.getJSON(slice.jsonEndpoint(), function (payload) {
- const url = slice.render_template(payload.form_data.url);
- slice.container.html('');
- const iframe = slice.container.find('iframe');
- iframe.css('height', slice.height());
- iframe.attr('src', url);
- slice.done(payload);
- })
- .fail(function (xhr) {
- slice.error(xhr.responseText, xhr);
- });
- }
-
- return {
- render: refresh,
- resize: refresh,
- };
+function iframeWidget(slice, payload) {
+ $('#code').attr('rows', '15');
+ const url = slice.render_template(payload.form_data.url);
+ slice.container.html('');
+ const iframe = slice.container.find('iframe');
+ iframe.css('height', slice.height());
+ iframe.attr('src', url);
}
module.exports = iframeWidget;
diff --git a/superset/assets/visualizations/mapbox.jsx b/superset/assets/visualizations/mapbox.jsx
index 249ffcca6..eceed728a 100644
--- a/superset/assets/visualizations/mapbox.jsx
+++ b/superset/assets/visualizations/mapbox.jsx
@@ -273,84 +273,68 @@ MapboxViz.propTypes = {
viewportZoom: React.PropTypes.number,
};
-function mapbox(slice) {
+function mapbox(slice, json) {
+ const div = d3.select(slice.selector);
const DEFAULT_POINT_RADIUS = 60;
const DEFAULT_MAX_ZOOM = 16;
let clusterer;
- const render = function () {
- const div = d3.select(slice.selector);
- d3.json(slice.jsonEndpoint(), function (error, json) {
- if (error !== null) {
- slice.error(error.responseText);
- return;
+ // 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;
}
-
- // 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;
+ if (b instanceof Array) {
+ b.push(a);
+ return b;
}
+ return [a, b];
+ };
+ }
- const aggName = json.data.aggregatorName;
- let reducer;
+ clusterer = supercluster({
+ radius: json.data.clusteringRadius,
+ maxZoom: DEFAULT_MAX_ZOOM,
+ metricKey: 'metric',
+ metricReducer: reducer,
+ });
+ clusterer.load(json.data.geoJSON.features);
- 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];
- };
- }
-
- 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()
- );
-
- slice.done(json);
- });
- };
-
- return {
- render,
- resize() {},
- };
+ div.selectAll('*').remove();
+ ReactDOM.render(
+ ,
+ div.node()
+ );
}
module.exports = mapbox;
diff --git a/superset/assets/visualizations/markup.js b/superset/assets/visualizations/markup.js
index 70abd4c38..6a3cee098 100644
--- a/superset/assets/visualizations/markup.js
+++ b/superset/assets/visualizations/markup.js
@@ -2,21 +2,9 @@ const $ = require('jquery');
require('./markup.css');
-function markupWidget(slice) {
- function refresh() {
- $('#code').attr('rows', '15');
- $.getJSON(slice.jsonEndpoint(), function (payload) {
- slice.container.html(payload.data.html);
- slice.done(payload);
- })
- .fail(function (xhr) {
- slice.error(xhr.responseText, xhr);
- });
- }
- return {
- render: refresh,
- resize: refresh,
- };
+function markupWidget(slice, payload) {
+ $('#code').attr('rows', '15');
+ slice.container.html(payload.data.html);
}
module.exports = markupWidget;
diff --git a/superset/assets/visualizations/nvd3_vis.js b/superset/assets/visualizations/nvd3_vis.js
index df8a199c4..b8bbf37d7 100644
--- a/superset/assets/visualizations/nvd3_vis.js
+++ b/superset/assets/visualizations/nvd3_vis.js
@@ -53,346 +53,320 @@ const addTotalBarValues = function (chart, data, stacked) {
});
};
-function nvd3Vis(slice) {
+function nvd3Vis(slice, payload) {
let chart;
let colorKey = 'key';
- const render = function () {
- d3.json(slice.jsonEndpoint(), function (error, payload) {
- slice.container.html('');
- // Check error first, otherwise payload can be null
- if (error) {
- slice.error(error.responseText, error);
- return;
+ slice.container.html('');
+ slice.clearError();
+
+ // Calculates the longest label size for stretching bottom margin
+ function calculateStretchMargins(payloadData) {
+ let stretchMargin = 0;
+ const pixelsPerCharX = 4.5; // approx, depends on font size
+ let maxLabelSize = 10; // accommodate for shorter labels
+ payloadData.data.forEach((d) => {
+ const axisLabels = d.values;
+ for (let i = 0; i < axisLabels.length; i++) {
+ maxLabelSize = Math.max(axisLabels[i].x.length, maxLabelSize);
}
-
- slice.clearError();
-
- // Calculates the longest label size for stretching bottom margin
- function calculateStretchMargins(payloadData) {
- let stretchMargin = 0;
- const pixelsPerCharX = 4.5; // approx, depends on font size
- let maxLabelSize = 10; // accommodate for shorter labels
- payloadData.data.forEach((d) => {
- const axisLabels = d.values;
- for (let i = 0; i < axisLabels.length; i++) {
- maxLabelSize = Math.max(axisLabels[i].x.length, maxLabelSize);
- }
- });
- stretchMargin = Math.ceil(pixelsPerCharX * maxLabelSize);
- return stretchMargin;
- }
-
- let width = slice.width();
- const fd = payload.form_data;
-
- const barchartWidth = function () {
- let bars;
- if (fd.bar_stacked) {
- bars = d3.max(payload.data, function (d) { return d.values.length; });
- } else {
- bars = d3.sum(payload.data, function (d) { return d.values.length; });
- }
- if (bars * minBarWidth > width) {
- return bars * minBarWidth;
- }
- return width;
- };
-
- const vizType = fd.viz_type;
- const f = d3.format('.3s');
- const reduceXTicks = fd.reduce_x_ticks || false;
- let stacked = false;
- let row;
-
- const drawGraph = function () {
- switch (vizType) {
- case 'line':
- if (fd.show_brush) {
- chart = nv.models.lineWithFocusChart();
- chart.focus.xScale(d3.time.scale.utc());
- chart.x2Axis
- .showMaxMin(fd.x_axis_showminmax)
- .staggerLabels(false);
- } else {
- chart = nv.models.lineChart();
- }
- // To alter the tooltip header
- // chart.interactiveLayer.tooltip.headerFormatter(function(){return '';});
- chart.xScale(d3.time.scale.utc());
- chart.interpolate(fd.line_interpolation);
- chart.xAxis
- .showMaxMin(fd.x_axis_showminmax)
- .staggerLabels(false);
- break;
-
- case 'dual_line':
- chart = nv.models.multiChart();
- chart.interpolate('linear');
- break;
-
- case 'bar':
- chart = nv.models.multiBarChart()
- .showControls(fd.show_controls)
- .groupSpacing(0.1);
-
- if (!reduceXTicks) {
- width = barchartWidth();
- }
- chart.width(width);
- chart.xAxis
- .showMaxMin(false)
- .staggerLabels(true);
-
- stacked = fd.bar_stacked;
- chart.stacked(stacked);
-
- if (fd.show_bar_value) {
- setTimeout(function () {
- addTotalBarValues(chart, payload.data, stacked);
- }, animationTime);
- }
- break;
-
- case 'dist_bar':
- chart = nv.models.multiBarChart()
- .showControls(fd.show_controls)
- .reduceXTicks(reduceXTicks)
- .rotateLabels(45)
- .groupSpacing(0.1); // Distance between each group of bars.
-
- chart.xAxis
- .showMaxMin(false);
-
- stacked = fd.bar_stacked;
- chart.stacked(stacked);
- if (fd.order_bars) {
- payload.data.forEach((d) => {
- d.values.sort(
- function compare(a, b) {
- if (a.x < b.x) return -1;
- if (a.x > b.x) return 1;
- return 0;
- }
- );
- });
- }
- if (fd.show_bar_value) {
- setTimeout(function () {
- addTotalBarValues(chart, payload.data, stacked);
- }, animationTime);
- }
- if (!reduceXTicks) {
- width = barchartWidth();
- }
- chart.width(width);
- break;
-
- case 'pie':
- chart = nv.models.pieChart();
- colorKey = 'x';
- chart.valueFormat(f);
- if (fd.donut) {
- chart.donut(true);
- }
- chart.labelsOutside(fd.labels_outside);
- chart.labelThreshold(0.05) // Configure the minimum slice size for labels to show up
- .labelType(fd.pie_label_type);
- chart.cornerRadius(true);
- break;
-
- case 'column':
- chart = nv.models.multiBarChart()
- .reduceXTicks(false)
- .rotateLabels(45);
- break;
-
- case 'compare':
- chart = nv.models.cumulativeLineChart();
- chart.xScale(d3.time.scale.utc());
- chart.xAxis
- .showMaxMin(false)
- .staggerLabels(true);
- break;
-
- case 'bubble':
- row = (col1, col2) => `| ${col1} | ${col2} |
`;
- chart = nv.models.scatterChart();
- chart.showDistX(true);
- chart.showDistY(true);
- chart.tooltip.contentGenerator(function (obj) {
- const p = obj.point;
- let s = '';
- s += (
- `| ` +
- `${p[fd.entity]} (${p.group})` +
- ' |
');
- s += row(fd.x, f(p.x));
- s += row(fd.y, f(p.y));
- s += row(fd.size, f(p.size));
- s += '
';
- return s;
- });
- chart.pointRange([5, fd.max_bubble_size * fd.max_bubble_size]);
- break;
-
- case 'area':
- chart = nv.models.stackedAreaChart();
- chart.showControls(fd.show_controls);
- chart.style(fd.stacked_style);
- chart.xScale(d3.time.scale.utc());
- chart.xAxis
- .showMaxMin(false)
- .staggerLabels(true);
- break;
-
- case 'box_plot':
- colorKey = 'label';
- chart = nv.models.boxPlotChart();
- chart.x(function (d) {
- return d.label;
- });
- chart.staggerLabels(true);
- chart.maxBoxWidth(75); // prevent boxes from being incredibly wide
- break;
-
- case 'bullet':
- chart = nv.models.bulletChart();
- break;
-
- default:
- throw new Error('Unrecognized visualization for nvd3' + vizType);
- }
-
- if ('showLegend' in chart && typeof fd.show_legend !== 'undefined') {
- chart.showLegend(fd.show_legend);
- }
-
- let height = slice.height() - 15;
- if (vizType === 'bullet') {
- height = Math.min(height, 50);
- }
-
- chart.height(height);
- slice.container.css('height', height + 'px');
-
- if ((vizType === 'line' || vizType === 'area') && fd.rich_tooltip) {
- chart.useInteractiveGuideline(true);
- }
- if (fd.y_axis_zero) {
- chart.forceY([0]);
- } else if (fd.y_log_scale) {
- chart.yScale(d3.scale.log());
- }
- if (fd.x_log_scale) {
- chart.xScale(d3.scale.log());
- }
- let xAxisFormatter;
- if (vizType === 'bubble') {
- xAxisFormatter = d3.format('.3s');
- } else if (fd.x_axis_format === 'smart_date') {
- xAxisFormatter = formatDate;
- chart.xAxis.tickFormat(xAxisFormatter);
- } else if (fd.x_axis_format !== undefined) {
- xAxisFormatter = timeFormatFactory(fd.x_axis_format);
- chart.xAxis.tickFormat(xAxisFormatter);
- }
-
- if (chart.hasOwnProperty('x2Axis')) {
- chart.x2Axis.tickFormat(xAxisFormatter);
- height += 30;
- }
-
- if (vizType === 'bubble') {
- chart.xAxis.tickFormat(d3.format('.3s'));
- } else if (fd.x_axis_format === 'smart_date') {
- chart.xAxis.tickFormat(formatDate);
- } else if (fd.x_axis_format !== undefined) {
- chart.xAxis.tickFormat(timeFormatFactory(fd.x_axis_format));
- }
- if (chart.yAxis !== undefined) {
- chart.yAxis.tickFormat(d3.format('.3s'));
- }
-
- if (fd.y_axis_format && chart.yAxis) {
- chart.yAxis.tickFormat(d3.format(fd.y_axis_format));
- if (chart.y2Axis !== undefined) {
- chart.y2Axis.tickFormat(d3.format(fd.y_axis_format));
- }
- }
- if (vizType !== 'bullet') {
- chart.color((d) => category21(d[colorKey]));
- }
-
- if (fd.x_axis_label && fd.x_axis_label !== '' && chart.xAxis) {
- let distance = 0;
- if (fd.bottom_margin) {
- distance = fd.bottom_margin - 50;
- }
- chart.xAxis.axisLabel(fd.x_axis_label).axisLabelDistance(distance);
- }
-
- if (fd.y_axis_label && fd.y_axis_label !== '' && chart.yAxis) {
- chart.yAxis.axisLabel(fd.y_axis_label);
- chart.margin({ left: 90 });
- }
-
- if (fd.bottom_margin === 'auto') {
- if (vizType === 'dist_bar') {
- const stretchMargin = calculateStretchMargins(payload);
- chart.margin({ bottom: stretchMargin });
- } else {
- chart.margin({ bottom: 50 });
- }
- } else {
- chart.margin({ bottom: fd.bottom_margin });
- }
-
- let svg = d3.select(slice.selector).select('svg');
- if (svg.empty()) {
- svg = d3.select(slice.selector).append('svg');
- }
- if (vizType === 'dual_line') {
- chart.yAxis1.tickFormat(d3.format(fd.y_axis_format));
- chart.yAxis2.tickFormat(d3.format(fd.y_axis_2_format));
- chart.showLegend(true);
- chart.margin({ right: 50 });
- }
- svg
- .datum(payload.data)
- .transition().duration(500)
- .attr('height', height)
- .attr('width', width)
- .call(chart);
-
- if (fd.show_markers) {
- svg.selectAll('.nv-point')
- .style('stroke-opacity', 1)
- .style('fill-opacity', 1);
- }
-
- return chart;
- };
-
- const graph = drawGraph();
- nv.addGraph(graph);
- slice.done(payload);
});
- };
+ stretchMargin = Math.ceil(pixelsPerCharX * maxLabelSize);
+ return stretchMargin;
+ }
- const update = function () {
- if (chart && chart.update) {
- chart.height(slice.height());
- chart.width(slice.width());
- chart.update();
+ let width = slice.width();
+ const fd = payload.form_data;
+
+ const barchartWidth = function () {
+ let bars;
+ if (fd.bar_stacked) {
+ bars = d3.max(payload.data, function (d) { return d.values.length; });
+ } else {
+ bars = d3.sum(payload.data, function (d) { return d.values.length; });
}
+ if (bars * minBarWidth > width) {
+ return bars * minBarWidth;
+ }
+ return width;
};
+ const vizType = fd.viz_type;
+ const f = d3.format('.3s');
+ const reduceXTicks = fd.reduce_x_ticks || false;
+ let stacked = false;
+ let row;
- return {
- render,
- resize: update,
+ const drawGraph = function () {
+ switch (vizType) {
+ case 'line':
+ if (fd.show_brush) {
+ chart = nv.models.lineWithFocusChart();
+ chart.focus.xScale(d3.time.scale.utc());
+ chart.x2Axis
+ .showMaxMin(fd.x_axis_showminmax)
+ .staggerLabels(false);
+ } else {
+ chart = nv.models.lineChart();
+ }
+ // To alter the tooltip header
+ // chart.interactiveLayer.tooltip.headerFormatter(function(){return '';});
+ chart.xScale(d3.time.scale.utc());
+ chart.interpolate(fd.line_interpolation);
+ chart.xAxis
+ .showMaxMin(fd.x_axis_showminmax)
+ .staggerLabels(false);
+ break;
+
+ case 'dual_line':
+ chart = nv.models.multiChart();
+ chart.interpolate('linear');
+ break;
+
+ case 'bar':
+ chart = nv.models.multiBarChart()
+ .showControls(fd.show_controls)
+ .groupSpacing(0.1);
+
+ if (!reduceXTicks) {
+ width = barchartWidth();
+ }
+ chart.width(width);
+ chart.xAxis
+ .showMaxMin(false)
+ .staggerLabels(true);
+
+ stacked = fd.bar_stacked;
+ chart.stacked(stacked);
+
+ if (fd.show_bar_value) {
+ setTimeout(function () {
+ addTotalBarValues(chart, payload.data, stacked);
+ }, animationTime);
+ }
+ break;
+
+ case 'dist_bar':
+ chart = nv.models.multiBarChart()
+ .showControls(fd.show_controls)
+ .reduceXTicks(reduceXTicks)
+ .rotateLabels(45)
+ .groupSpacing(0.1); // Distance between each group of bars.
+
+ chart.xAxis
+ .showMaxMin(false);
+
+ stacked = fd.bar_stacked;
+ chart.stacked(stacked);
+ if (fd.order_bars) {
+ payload.data.forEach((d) => {
+ d.values.sort(
+ function compare(a, b) {
+ if (a.x < b.x) return -1;
+ if (a.x > b.x) return 1;
+ return 0;
+ }
+ );
+ });
+ }
+ if (fd.show_bar_value) {
+ setTimeout(function () {
+ addTotalBarValues(chart, payload.data, stacked);
+ }, animationTime);
+ }
+ if (!reduceXTicks) {
+ width = barchartWidth();
+ }
+ chart.width(width);
+ break;
+
+ case 'pie':
+ chart = nv.models.pieChart();
+ colorKey = 'x';
+ chart.valueFormat(f);
+ if (fd.donut) {
+ chart.donut(true);
+ }
+ chart.labelsOutside(fd.labels_outside);
+ chart.labelThreshold(0.05) // Configure the minimum slice size for labels to show up
+ .labelType(fd.pie_label_type);
+ chart.cornerRadius(true);
+ break;
+
+ case 'column':
+ chart = nv.models.multiBarChart()
+ .reduceXTicks(false)
+ .rotateLabels(45);
+ break;
+
+ case 'compare':
+ chart = nv.models.cumulativeLineChart();
+ chart.xScale(d3.time.scale.utc());
+ chart.xAxis
+ .showMaxMin(false)
+ .staggerLabels(true);
+ break;
+
+ case 'bubble':
+ row = (col1, col2) => `| ${col1} | ${col2} |
`;
+ chart = nv.models.scatterChart();
+ chart.showDistX(true);
+ chart.showDistY(true);
+ chart.tooltip.contentGenerator(function (obj) {
+ const p = obj.point;
+ let s = '';
+ s += (
+ `| ` +
+ `${p[fd.entity]} (${p.group})` +
+ ' |
');
+ s += row(fd.x, f(p.x));
+ s += row(fd.y, f(p.y));
+ s += row(fd.size, f(p.size));
+ s += '
';
+ return s;
+ });
+ chart.pointRange([5, fd.max_bubble_size * fd.max_bubble_size]);
+ break;
+
+ case 'area':
+ chart = nv.models.stackedAreaChart();
+ chart.showControls(fd.show_controls);
+ chart.style(fd.stacked_style);
+ chart.xScale(d3.time.scale.utc());
+ chart.xAxis
+ .showMaxMin(false)
+ .staggerLabels(true);
+ break;
+
+ case 'box_plot':
+ colorKey = 'label';
+ chart = nv.models.boxPlotChart();
+ chart.x(function (d) {
+ return d.label;
+ });
+ chart.staggerLabels(true);
+ chart.maxBoxWidth(75); // prevent boxes from being incredibly wide
+ break;
+
+ case 'bullet':
+ chart = nv.models.bulletChart();
+ break;
+
+ default:
+ throw new Error('Unrecognized visualization for nvd3' + vizType);
+ }
+
+ if ('showLegend' in chart && typeof fd.show_legend !== 'undefined') {
+ chart.showLegend(fd.show_legend);
+ }
+
+ let height = slice.height() - 15;
+ if (vizType === 'bullet') {
+ height = Math.min(height, 50);
+ }
+
+ chart.height(height);
+ slice.container.css('height', height + 'px');
+
+ if ((vizType === 'line' || vizType === 'area') && fd.rich_tooltip) {
+ chart.useInteractiveGuideline(true);
+ }
+ if (fd.y_axis_zero) {
+ chart.forceY([0]);
+ } else if (fd.y_log_scale) {
+ chart.yScale(d3.scale.log());
+ }
+ if (fd.x_log_scale) {
+ chart.xScale(d3.scale.log());
+ }
+ let xAxisFormatter;
+ if (vizType === 'bubble') {
+ xAxisFormatter = d3.format('.3s');
+ } else if (fd.x_axis_format === 'smart_date') {
+ xAxisFormatter = formatDate;
+ chart.xAxis.tickFormat(xAxisFormatter);
+ } else if (fd.x_axis_format !== undefined) {
+ xAxisFormatter = timeFormatFactory(fd.x_axis_format);
+ chart.xAxis.tickFormat(xAxisFormatter);
+ }
+
+ if (chart.hasOwnProperty('x2Axis')) {
+ chart.x2Axis.tickFormat(xAxisFormatter);
+ height += 30;
+ }
+
+ if (vizType === 'bubble') {
+ chart.xAxis.tickFormat(d3.format('.3s'));
+ } else if (fd.x_axis_format === 'smart_date') {
+ chart.xAxis.tickFormat(formatDate);
+ } else if (fd.x_axis_format !== undefined) {
+ chart.xAxis.tickFormat(timeFormatFactory(fd.x_axis_format));
+ }
+ if (chart.yAxis !== undefined) {
+ chart.yAxis.tickFormat(d3.format('.3s'));
+ }
+
+ if (fd.y_axis_format && chart.yAxis) {
+ chart.yAxis.tickFormat(d3.format(fd.y_axis_format));
+ if (chart.y2Axis !== undefined) {
+ chart.y2Axis.tickFormat(d3.format(fd.y_axis_format));
+ }
+ }
+ if (vizType !== 'bullet') {
+ chart.color((d) => category21(d[colorKey]));
+ }
+
+ if (fd.x_axis_label && fd.x_axis_label !== '' && chart.xAxis) {
+ let distance = 0;
+ if (fd.bottom_margin) {
+ distance = fd.bottom_margin - 50;
+ }
+ chart.xAxis.axisLabel(fd.x_axis_label).axisLabelDistance(distance);
+ }
+
+ if (fd.y_axis_label && fd.y_axis_label !== '' && chart.yAxis) {
+ chart.yAxis.axisLabel(fd.y_axis_label);
+ chart.margin({ left: 90 });
+ }
+
+ if (fd.bottom_margin === 'auto') {
+ if (vizType === 'dist_bar') {
+ const stretchMargin = calculateStretchMargins(payload);
+ chart.margin({ bottom: stretchMargin });
+ } else {
+ chart.margin({ bottom: 50 });
+ }
+ } else {
+ chart.margin({ bottom: fd.bottom_margin });
+ }
+
+ let svg = d3.select(slice.selector).select('svg');
+ if (svg.empty()) {
+ svg = d3.select(slice.selector).append('svg');
+ }
+ if (vizType === 'dual_line') {
+ chart.yAxis1.tickFormat(d3.format(fd.y_axis_format));
+ chart.yAxis2.tickFormat(d3.format(fd.y_axis_2_format));
+ chart.showLegend(true);
+ chart.margin({ right: 50 });
+ }
+ svg
+ .datum(payload.data)
+ .transition().duration(500)
+ .attr('height', height)
+ .attr('width', width)
+ .call(chart);
+
+ if (fd.show_markers) {
+ svg.selectAll('.nv-point')
+ .style('stroke-opacity', 1)
+ .style('fill-opacity', 1);
+ }
+ return chart;
};
+
+ const graph = drawGraph();
+ nv.addGraph(graph);
}
module.exports = nvd3Vis;
diff --git a/superset/assets/visualizations/parallel_coordinates.js b/superset/assets/visualizations/parallel_coordinates.js
index fdc2f1e62..cbb01b496 100644
--- a/superset/assets/visualizations/parallel_coordinates.js
+++ b/superset/assets/visualizations/parallel_coordinates.js
@@ -6,100 +6,87 @@ d3.divgrid = require('../vendor/parallel_coordinates/divgrid.js');
require('../vendor/parallel_coordinates/d3.parcoords.css');
require('./parallel_coordinates.css');
-function parallelCoordVis(slice) {
- function refresh() {
- $('#code').attr('rows', '15');
- $.getJSON(slice.jsonEndpoint(), function (payload) {
- const fd = payload.form_data;
- const data = payload.data;
+function parallelCoordVis(slice, payload) {
+ $('#code').attr('rows', '15');
+ const fd = payload.form_data;
+ const data = payload.data;
- let cols = fd.metrics;
- if (fd.include_series) {
- cols = [fd.series].concat(fd.metrics);
- }
-
- const ttypes = {};
- ttypes[fd.series] = 'string';
- fd.metrics.forEach(function (v) {
- ttypes[v] = 'number';
- });
-
- let ext = d3.extent(data, function (d) {
- return d[fd.secondary_metric];
- });
- ext = [ext[0], (ext[1] - ext[0]) / 2, ext[1]];
- const cScale = d3.scale.linear()
- .domain(ext)
- .range(['red', 'grey', 'blue'])
- .interpolate(d3.interpolateLab);
-
- const color = function (d) {
- return cScale(d[fd.secondary_metric]);
- };
- const container = d3.select(slice.selector);
- container.selectAll('*').remove();
- const effHeight = fd.show_datatable ? (slice.height() / 2) : slice.height();
-
- container.append('div')
- .attr('id', 'parcoords_' + slice.container_id)
- .style('height', effHeight + 'px')
- .classed('parcoords', true);
-
- const parcoords = d3.parcoords()('#parcoords_' + slice.container_id)
- .width(slice.width())
- .color(color)
- .alpha(0.5)
- .composite('darken')
- .height(effHeight)
- .data(data)
- .dimensions(cols)
- .types(ttypes)
- .render()
- .createAxes()
- .shadows()
- .reorderable()
- .brushMode('1D-axes');
-
- if (fd.show_datatable) {
- // create data table, row hover highlighting
- const grid = d3.divgrid();
- container.append('div')
- .style('height', effHeight + 'px')
- .datum(data)
- .call(grid)
- .classed('parcoords grid', true)
- .selectAll('.row')
- .on({
- mouseover(d) {
- parcoords.highlight([d]);
- },
- mouseout: parcoords.unhighlight,
- });
- // update data table on brush event
- parcoords.on('brush', function (d) {
- d3.select('.grid')
- .datum(d)
- .call(grid)
- .selectAll('.row')
- .on({
- mouseover(dd) {
- parcoords.highlight([dd]);
- },
- mouseout: parcoords.unhighlight,
- });
- });
- }
- slice.done(payload);
- })
- .fail(function (xhr) {
- slice.error(xhr.responseText, xhr);
- });
+ let cols = fd.metrics;
+ if (fd.include_series) {
+ cols = [fd.series].concat(fd.metrics);
}
- return {
- render: refresh,
- resize: refresh,
+ const ttypes = {};
+ ttypes[fd.series] = 'string';
+ fd.metrics.forEach(function (v) {
+ ttypes[v] = 'number';
+ });
+
+ let ext = d3.extent(data, function (d) {
+ return d[fd.secondary_metric];
+ });
+ ext = [ext[0], (ext[1] - ext[0]) / 2, ext[1]];
+ const cScale = d3.scale.linear()
+ .domain(ext)
+ .range(['red', 'grey', 'blue'])
+ .interpolate(d3.interpolateLab);
+
+ const color = function (d) {
+ return cScale(d[fd.secondary_metric]);
};
+ const container = d3.select(slice.selector);
+ container.selectAll('*').remove();
+ const effHeight = fd.show_datatable ? (slice.height() / 2) : slice.height();
+
+ container.append('div')
+ .attr('id', 'parcoords_' + slice.container_id)
+ .style('height', effHeight + 'px')
+ .classed('parcoords', true);
+
+ const parcoords = d3.parcoords()('#parcoords_' + slice.container_id)
+ .width(slice.width())
+ .color(color)
+ .alpha(0.5)
+ .composite('darken')
+ .height(effHeight)
+ .data(data)
+ .dimensions(cols)
+ .types(ttypes)
+ .render()
+ .createAxes()
+ .shadows()
+ .reorderable()
+ .brushMode('1D-axes');
+
+ if (fd.show_datatable) {
+ // create data table, row hover highlighting
+ const grid = d3.divgrid();
+ container.append('div')
+ .style('height', effHeight + 'px')
+ .datum(data)
+ .call(grid)
+ .classed('parcoords grid', true)
+ .selectAll('.row')
+ .on({
+ mouseover(d) {
+ parcoords.highlight([d]);
+ },
+ mouseout: parcoords.unhighlight,
+ });
+ // update data table on brush event
+ parcoords.on('brush', function (d) {
+ d3.select('.grid')
+ .datum(d)
+ .call(grid)
+ .selectAll('.row')
+ .on({
+ mouseover(dd) {
+ parcoords.highlight([dd]);
+ },
+ mouseout: parcoords.unhighlight,
+ });
+ });
+ }
}
module.exports = parallelCoordVis;
diff --git a/superset/assets/visualizations/pivot_table.js b/superset/assets/visualizations/pivot_table.js
index 19e4e9b19..05ed5b6e2 100644
--- a/superset/assets/visualizations/pivot_table.js
+++ b/superset/assets/visualizations/pivot_table.js
@@ -8,34 +8,22 @@ import 'datatables.net';
import dt from 'datatables.net-bs';
dt(window, $);
-module.exports = function (slice) {
+module.exports = function (slice, payload) {
const container = slice.container;
-
- function refresh() {
- $.getJSON(slice.jsonEndpoint(), function (json) {
- const fd = json.form_data;
- container.html(json.data);
- if (fd.groupby.length === 1) {
- const height = container.height();
- const table = container.find('table').DataTable({
- paging: false,
- searching: false,
- bInfo: false,
- scrollY: height + 'px',
- scrollCollapse: true,
- scrollX: true,
- });
- table.column('-1').order('desc').draw();
- fixDataTableBodyHeight(
- container.find('.dataTables_wrapper'), height);
- }
- slice.done(json);
- }).fail(function (xhr) {
- slice.error(xhr.responseText, xhr);
+ const fd = payload.form_data;
+ container.html(payload.data);
+ if (fd.groupby.length === 1) {
+ const height = container.height();
+ const table = container.find('table').DataTable({
+ paging: false,
+ searching: false,
+ bInfo: false,
+ scrollY: height + 'px',
+ scrollCollapse: true,
+ scrollX: true,
});
+ table.column('-1').order('desc').draw();
+ fixDataTableBodyHeight(
+ container.find('.dataTables_wrapper'), height);
}
- return {
- render: refresh,
- resize: refresh,
- };
};
diff --git a/superset/assets/visualizations/sankey.js b/superset/assets/visualizations/sankey.js
index 1507a0aae..e80fd6996 100644
--- a/superset/assets/visualizations/sankey.js
+++ b/superset/assets/visualizations/sankey.js
@@ -6,179 +6,164 @@ d3.sankey = require('d3-sankey').sankey;
require('./sankey.css');
-function sankeyVis(slice) {
- const render = function () {
- const div = d3.select(slice.selector);
- 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;
+function sankeyVis(slice, payload) {
+ const div = d3.select(slice.selector);
+ 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 formatNumber = d3.format(',.2f');
- div.selectAll('*').remove();
- const svg = div.append('svg')
- .attr('width', width + margin.left + margin.right)
- .attr('height', height + margin.top + margin.bottom)
- .append('g')
- .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+ div.selectAll('*').remove();
+ const svg = div.append('svg')
+ .attr('width', width + margin.left + margin.right)
+ .attr('height', height + margin.top + margin.bottom)
+ .append('g')
+ .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
- const tooltip = div.append('div')
- .attr('class', 'sankey-tooltip')
+ const tooltip = div.append('div')
+ .attr('class', 'sankey-tooltip')
+ .style('opacity', 0);
+
+ const sankey = d3.sankey()
+ .nodeWidth(15)
+ .nodePadding(10)
+ .size([width, height]);
+
+ const path = sankey.link();
+
+ let nodes = {};
+ // Compute the distinct nodes from the links.
+ const links = payload.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 });
+ link.value = Number(link.value);
+ return link;
+ });
+ nodes = d3.values(nodes);
+
+ sankey
+ .nodes(nodes)
+ .links(links)
+ .layout(32);
+
+ function getTooltipHtml(d) {
+ let html;
+
+ if (d.sourceLinks) { // is node
+ html = d.name + " Value: " + formatNumber(d.value) + '';
+ } else {
+ const val = formatNumber(d.value);
+ const sourcePercent = d3.round((d.value / d.source.value) * 100, 1);
+ const targetPercent = d3.round((d.value / d.target.value) * 100, 1);
+
+ html = [
+ "Path Value: ", val, '
',
+ "",
+ "",
+ (isFinite(sourcePercent) ? sourcePercent : '100'),
+ '% of ', d.source.name, '
',
+ "" +
+ (isFinite(targetPercent) ? targetPercent : '--') +
+ '% of ', d.target.name, 'target',
+ '
',
+ ].join('');
+ }
+ return html;
+ }
+
+ function onmouseover(d) {
+ tooltip
+ .html(function () { return getTooltipHtml(d); })
+ .transition()
+ .duration(200)
+ .style('left', (d3.event.offsetX + 10) + 'px')
+ .style('top', (d3.event.offsetY + 10) + 'px')
+ .style('opacity', 0.95);
+ }
+
+ function onmouseout() {
+ tooltip.transition()
+ .duration(100)
.style('opacity', 0);
+ }
- const sankey = d3.sankey()
- .nodeWidth(15)
- .nodePadding(10)
- .size([width, height]);
+ const link = svg.append('g').selectAll('.link')
+ .data(links)
+ .enter()
+ .append('path')
+ .attr('class', 'link')
+ .attr('d', path)
+ .style('stroke-width', (d) => Math.max(1, d.dy))
+ .sort((a, b) => b.dy - a.dy)
+ .on('mouseover', onmouseover)
+ .on('mouseout', onmouseout);
- const path = sankey.link();
+ function dragmove(d) {
+ d3.select(this)
+ .attr(
+ 'transform',
+ `translate(${d.x},${(d.y = Math.max(0, Math.min(height - d.dy, d3.event.y)))})`
+ );
+ sankey.relayout();
+ link.attr('d', path);
+ }
- d3.json(slice.jsonEndpoint(), function (error, json) {
- if (error !== null) {
- slice.error(error.responseText, error);
- return;
- }
- let nodes = {};
- // Compute the distinct nodes from the links.
- const links = json.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 });
- link.value = Number(link.value);
- return link;
- });
- nodes = d3.values(nodes);
+ const node = svg.append('g').selectAll('.node')
+ .data(nodes)
+ .enter()
+ .append('g')
+ .attr('class', 'node')
+ .attr('transform', function (d) {
+ return 'translate(' + d.x + ',' + d.y + ')';
+ })
+ .call(d3.behavior.drag()
+ .origin(function (d) {
+ return d;
+ })
+ .on('dragstart', function () {
+ this.parentNode.appendChild(this);
+ })
+ .on('drag', dragmove)
+ );
- sankey
- .nodes(nodes)
- .links(links)
- .layout(32);
+ node.append('rect')
+ .attr('height', function (d) {
+ return d.dy;
+ })
+ .attr('width', sankey.nodeWidth())
+ .style('fill', function (d) {
+ d.color = category21(d.name.replace(/ .*/, ''));
+ return d.color;
+ })
+ .style('stroke', function (d) {
+ return d3.rgb(d.color).darker(2);
+ })
+ .on('mouseover', onmouseover)
+ .on('mouseout', onmouseout);
- function getTooltipHtml(d) {
- let html;
-
- if (d.sourceLinks) { // is node
- html = d.name + " Value: " + formatNumber(d.value) + '';
- } else {
- const val = formatNumber(d.value);
- const sourcePercent = d3.round((d.value / d.source.value) * 100, 1);
- const targetPercent = d3.round((d.value / d.target.value) * 100, 1);
-
- html = [
- "Path Value: ", val, '
',
- "",
- "",
- (isFinite(sourcePercent) ? sourcePercent : '100'),
- '% of ', d.source.name, '
',
- "" +
- (isFinite(targetPercent) ? targetPercent : '--') +
- '% of ', d.target.name, 'target',
- '
',
- ].join('');
- }
- return html;
- }
-
- function onmouseover(d) {
- tooltip
- .html(function () { return getTooltipHtml(d); })
- .transition()
- .duration(200)
- .style('left', (d3.event.offsetX + 10) + 'px')
- .style('top', (d3.event.offsetY + 10) + 'px')
- .style('opacity', 0.95);
- }
-
- function onmouseout() {
- tooltip.transition()
- .duration(100)
- .style('opacity', 0);
- }
-
- const link = svg.append('g').selectAll('.link')
- .data(links)
- .enter()
- .append('path')
- .attr('class', 'link')
- .attr('d', path)
- .style('stroke-width', (d) => Math.max(1, d.dy))
- .sort((a, b) => b.dy - a.dy)
- .on('mouseover', onmouseover)
- .on('mouseout', onmouseout);
-
- function dragmove(d) {
- d3.select(this)
- .attr(
- 'transform',
- `translate(${d.x},${(d.y = Math.max(0, Math.min(height - d.dy, d3.event.y)))})`
- );
- sankey.relayout();
- link.attr('d', path);
- }
-
- const node = svg.append('g').selectAll('.node')
- .data(nodes)
- .enter()
- .append('g')
- .attr('class', 'node')
- .attr('transform', function (d) {
- return 'translate(' + d.x + ',' + d.y + ')';
- })
- .call(d3.behavior.drag()
- .origin(function (d) {
- return d;
- })
- .on('dragstart', function () {
- this.parentNode.appendChild(this);
- })
- .on('drag', dragmove)
- );
-
- node.append('rect')
- .attr('height', function (d) {
- return d.dy;
- })
- .attr('width', sankey.nodeWidth())
- .style('fill', function (d) {
- d.color = category21(d.name.replace(/ .*/, ''));
- return d.color;
- })
- .style('stroke', function (d) {
- return 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('dy', '.35em')
- .attr('text-anchor', 'end')
- .attr('transform', null)
- .text(function (d) {
- return d.name;
- })
- .filter(function (d) {
- return d.x < width / 2;
- })
- .attr('x', 6 + sankey.nodeWidth())
- .attr('text-anchor', 'start');
-
-
- slice.done(json);
- });
- };
- return {
- render,
- resize: render,
- };
+ node.append('text')
+ .attr('x', -6)
+ .attr('y', function (d) {
+ return 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;
+ })
+ .attr('x', 6 + sankey.nodeWidth())
+ .attr('text-anchor', 'start');
}
module.exports = sankeyVis;
diff --git a/superset/assets/visualizations/sunburst.js b/superset/assets/visualizations/sunburst.js
index 623888a0a..7d45a30c5 100644
--- a/superset/assets/visualizations/sunburst.js
+++ b/superset/assets/visualizations/sunburst.js
@@ -6,383 +6,368 @@ import { wrapSvgText } from '../javascripts/modules/utils';
require('./sunburst.css');
// Modified from http://bl.ocks.org/kerryrodden/7090426
-function sunburstVis(slice) {
- const render = function () {
- const container = d3.select(slice.selector);
- // vars with shared scope within this function
- const margin = { top: 10, right: 5, bottom: 10, left: 5 };
- const containerWidth = slice.width();
- const containerHeight = slice.height();
- const breadcrumbHeight = containerHeight * 0.085;
- const visWidth = containerWidth - margin.left - margin.right;
- const visHeight = containerHeight - margin.top - margin.bottom - breadcrumbHeight;
- const radius = Math.min(visWidth, visHeight) / 2;
+function sunburstVis(slice, payload) {
+ const container = d3.select(slice.selector);
- let colorByCategory = true; // color by category if primary/secondary metrics match
- let maxBreadcrumbs;
- let breadcrumbDims; // set based on data
- let totalSize; // total size of all segments; set after loading the data.
- let colorScale;
- let breadcrumbs;
- let vis;
- let arcs;
- let gMiddleText; // dom handles
+ // vars with shared scope within this function
+ const margin = { top: 10, right: 5, bottom: 10, left: 5 };
+ const containerWidth = slice.width();
+ const containerHeight = slice.height();
+ const breadcrumbHeight = containerHeight * 0.085;
+ const visWidth = containerWidth - margin.left - margin.right;
+ const visHeight = containerHeight - margin.top - margin.bottom - breadcrumbHeight;
+ const radius = Math.min(visWidth, visHeight) / 2;
- // Helper + path gen functions
- const partition = d3.layout.partition()
- .size([2 * Math.PI, radius * radius])
- .value(function (d) { return d.m1; });
+ let colorByCategory = true; // color by category if primary/secondary metrics match
+ let maxBreadcrumbs;
+ let breadcrumbDims; // set based on data
+ let totalSize; // total size of all segments; set after loading the data.
+ let colorScale;
+ let breadcrumbs;
+ let vis;
+ let arcs;
+ let gMiddleText; // dom handles
- const arc = d3.svg.arc()
- .startAngle((d) => d.x)
- .endAngle((d) => d.x + d.dx)
- .innerRadius(function (d) {
- return Math.sqrt(d.y);
- })
- .outerRadius(function (d) {
- return Math.sqrt(d.y + d.dy);
+ // Helper + path gen functions
+ const partition = d3.layout.partition()
+ .size([2 * Math.PI, radius * radius])
+ .value(function (d) { return d.m1; });
+
+ const arc = d3.svg.arc()
+ .startAngle((d) => d.x)
+ .endAngle((d) => d.x + d.dx)
+ .innerRadius(function (d) {
+ return Math.sqrt(d.y);
+ })
+ .outerRadius(function (d) {
+ return Math.sqrt(d.y + d.dy);
+ });
+
+ const formatNum = d3.format('.3s');
+ const formatPerc = d3.format('.3p');
+
+ container.select('svg').remove();
+
+ const svg = container.append('svg:svg')
+ .attr('width', containerWidth)
+ .attr('height', containerHeight);
+
+ function createBreadcrumbs(rawData) {
+ const firstRowData = rawData.data[0];
+ // -2 bc row contains 2x metrics, +extra for %label and buffer
+ maxBreadcrumbs = (firstRowData.length - 2) + 1;
+ breadcrumbDims = {
+ width: visWidth / maxBreadcrumbs,
+ height: breadcrumbHeight * 0.8, // more margin
+ spacing: 3,
+ tipTailWidth: 10,
+ };
+
+ breadcrumbs = svg.append('svg:g')
+ .attr('class', 'breadcrumbs')
+ .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ breadcrumbs.append('svg:text')
+ .attr('class', 'end-label');
+ }
+
+ // Given a node in a partition layout, return an array of all of its ancestor
+ // nodes, highest first, but excluding the root.
+ function getAncestors(node) {
+ const path = [];
+ let current = node;
+ while (current.parent) {
+ path.unshift(current);
+ current = current.parent;
+ }
+ return path;
+ }
+
+ // Generate a string that describes the points of a breadcrumb polygon.
+ function breadcrumbPoints(d, i) {
+ const points = [];
+ points.push('0,0');
+ points.push(breadcrumbDims.width + ',0');
+ points.push(
+ breadcrumbDims.width + breadcrumbDims.tipTailWidth + ',' + (breadcrumbDims.height / 2));
+ points.push(breadcrumbDims.width + ',' + breadcrumbDims.height);
+ points.push('0,' + breadcrumbDims.height);
+ if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
+ points.push(breadcrumbDims.tipTailWidth + ',' + (breadcrumbDims.height / 2));
+ }
+ return points.join(' ');
+ }
+
+ function updateBreadcrumbs(sequenceArray, percentageString) {
+ const g = breadcrumbs.selectAll('g')
+ .data(sequenceArray, function (d) {
+ return d.name + d.depth;
});
- const formatNum = d3.format('.3s');
- const formatPerc = d3.format('.3p');
+ // Add breadcrumb and label for entering nodes.
+ const entering = g.enter().append('svg:g');
- container.select('svg').remove();
-
- const svg = container.append('svg:svg')
- .attr('width', containerWidth)
- .attr('height', containerHeight);
-
- function createBreadcrumbs(rawData) {
- const firstRowData = rawData.data[0];
- // -2 bc row contains 2x metrics, +extra for %label and buffer
- maxBreadcrumbs = (firstRowData.length - 2) + 1;
- breadcrumbDims = {
- width: visWidth / maxBreadcrumbs,
- height: breadcrumbHeight * 0.8, // more margin
- spacing: 3,
- tipTailWidth: 10,
- };
-
- breadcrumbs = svg.append('svg:g')
- .attr('class', 'breadcrumbs')
- .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
-
- breadcrumbs.append('svg:text')
- .attr('class', 'end-label');
- }
-
- // Given a node in a partition layout, return an array of all of its ancestor
- // nodes, highest first, but excluding the root.
- function getAncestors(node) {
- const path = [];
- let current = node;
- while (current.parent) {
- path.unshift(current);
- current = current.parent;
- }
- return path;
- }
-
- // Generate a string that describes the points of a breadcrumb polygon.
- function breadcrumbPoints(d, i) {
- const points = [];
- points.push('0,0');
- points.push(breadcrumbDims.width + ',0');
- points.push(
- breadcrumbDims.width + breadcrumbDims.tipTailWidth + ',' + (breadcrumbDims.height / 2));
- points.push(breadcrumbDims.width + ',' + breadcrumbDims.height);
- points.push('0,' + breadcrumbDims.height);
- if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
- points.push(breadcrumbDims.tipTailWidth + ',' + (breadcrumbDims.height / 2));
- }
- return points.join(' ');
- }
-
- function updateBreadcrumbs(sequenceArray, percentageString) {
- const g = breadcrumbs.selectAll('g')
- .data(sequenceArray, function (d) {
- return d.name + d.depth;
+ entering.append('svg:polygon')
+ .attr('points', breadcrumbPoints)
+ .style('fill', function (d) {
+ return colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1);
});
- // Add breadcrumb and label for entering nodes.
- const entering = g.enter().append('svg:g');
-
- entering.append('svg:polygon')
- .attr('points', breadcrumbPoints)
- .style('fill', function (d) {
- return colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1);
- });
-
- entering.append('svg:text')
- .attr('x', (breadcrumbDims.width + breadcrumbDims.tipTailWidth) / 2)
- .attr('y', breadcrumbDims.height / 4)
- .attr('dy', '0.35em')
- .style('fill', function (d) {
- // Make text white or black based on the lightness of the background
- const col = d3.hsl(colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1));
- return col.l < 0.5 ? 'white' : 'black';
- })
- .attr('class', 'step-label')
- .text(function (d) { return d.name.replace(/_/g, ' '); })
- .call(wrapSvgText, breadcrumbDims.width, breadcrumbDims.height / 2);
-
- // Set position for entering and updating nodes.
- g.attr('transform', function (d, i) {
- return 'translate(' + i * (breadcrumbDims.width + breadcrumbDims.spacing) + ', 0)';
- });
-
- // Remove exiting nodes.
- g.exit().remove();
-
- // Now move and update the percentage at the end.
- breadcrumbs.select('.end-label')
- .attr('x', (sequenceArray.length + 0.5) * (breadcrumbDims.width + breadcrumbDims.spacing))
- .attr('y', breadcrumbDims.height / 2)
- .attr('dy', '0.35em')
- .text(percentageString);
-
- // Make the breadcrumb trail visible, if it's hidden.
- breadcrumbs.style('visibility', null);
- }
-
- // Fade all but the current sequence, and show it in the breadcrumb trail.
- function mouseenter(d) {
- const sequenceArray = getAncestors(d);
- const parentOfD = sequenceArray[sequenceArray.length - 2] || null;
-
- const absolutePercentage = (d.m1 / totalSize).toPrecision(3);
- const conditionalPercentage = parentOfD ? (d.m1 / parentOfD.m1).toPrecision(3) : null;
-
- const absolutePercString = formatPerc(absolutePercentage);
- const conditionalPercString = parentOfD ? formatPerc(conditionalPercentage) : '';
-
- // 3 levels of text if inner-most level, 4 otherwise
- const yOffsets = ['-25', '7', '35', '60'];
- let offsetIndex = 0;
-
- // If metrics match, assume we are coloring by category
- const metricsMatch = Math.abs(d.m1 - d.m2) < 0.00001;
-
- gMiddleText.selectAll('*').remove();
-
- gMiddleText.append('text')
- .attr('class', 'path-abs-percent')
- .attr('y', yOffsets[offsetIndex++])
- .text(absolutePercString + ' of total');
-
- if (conditionalPercString) {
- gMiddleText.append('text')
- .attr('class', 'path-cond-percent')
- .attr('y', yOffsets[offsetIndex++])
- .text(conditionalPercString + ' of parent');
- }
-
- gMiddleText.append('text')
- .attr('class', 'path-metrics')
- .attr('y', yOffsets[offsetIndex++])
- .text('m1: ' + formatNum(d.m1) + (metricsMatch ? '' : ', m2: ' + formatNum(d.m2)));
-
- gMiddleText.append('text')
- .attr('class', 'path-ratio')
- .attr('y', yOffsets[offsetIndex++])
- .text((metricsMatch ? '' : ('m2/m1: ' + formatPerc(d.m2 / d.m1))));
-
- // Reset and fade all the segments.
- arcs.selectAll('path')
- .style('stroke-width', null)
- .style('stroke', null)
- .style('opacity', 0.7);
-
- // Then highlight only those that are an ancestor of the current segment.
- arcs.selectAll('path')
- .filter(function (node) {
- return (sequenceArray.indexOf(node) >= 0);
+ entering.append('svg:text')
+ .attr('x', (breadcrumbDims.width + breadcrumbDims.tipTailWidth) / 2)
+ .attr('y', breadcrumbDims.height / 4)
+ .attr('dy', '0.35em')
+ .style('fill', function (d) {
+ // Make text white or black based on the lightness of the background
+ const col = d3.hsl(colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1));
+ return col.l < 0.5 ? 'white' : 'black';
})
- .style('opacity', 1)
- .style('stroke-width', '2px')
- .style('stroke', '#000');
+ .attr('class', 'step-label')
+ .text(function (d) { return d.name.replace(/_/g, ' '); })
+ .call(wrapSvgText, breadcrumbDims.width, breadcrumbDims.height / 2);
- updateBreadcrumbs(sequenceArray, absolutePercString);
+ // Set position for entering and updating nodes.
+ g.attr('transform', function (d, i) {
+ return 'translate(' + i * (breadcrumbDims.width + breadcrumbDims.spacing) + ', 0)';
+ });
+
+ // Remove exiting nodes.
+ g.exit().remove();
+
+ // Now move and update the percentage at the end.
+ breadcrumbs.select('.end-label')
+ .attr('x', (sequenceArray.length + 0.5) * (breadcrumbDims.width + breadcrumbDims.spacing))
+ .attr('y', breadcrumbDims.height / 2)
+ .attr('dy', '0.35em')
+ .text(percentageString);
+
+ // Make the breadcrumb trail visible, if it's hidden.
+ breadcrumbs.style('visibility', null);
+ }
+
+ // Fade all but the current sequence, and show it in the breadcrumb trail.
+ function mouseenter(d) {
+ const sequenceArray = getAncestors(d);
+ const parentOfD = sequenceArray[sequenceArray.length - 2] || null;
+
+ const absolutePercentage = (d.m1 / totalSize).toPrecision(3);
+ const conditionalPercentage = parentOfD ? (d.m1 / parentOfD.m1).toPrecision(3) : null;
+
+ const absolutePercString = formatPerc(absolutePercentage);
+ const conditionalPercString = parentOfD ? formatPerc(conditionalPercentage) : '';
+
+ // 3 levels of text if inner-most level, 4 otherwise
+ const yOffsets = ['-25', '7', '35', '60'];
+ let offsetIndex = 0;
+
+ // If metrics match, assume we are coloring by category
+ const metricsMatch = Math.abs(d.m1 - d.m2) < 0.00001;
+
+ gMiddleText.selectAll('*').remove();
+
+ gMiddleText.append('text')
+ .attr('class', 'path-abs-percent')
+ .attr('y', yOffsets[offsetIndex++])
+ .text(absolutePercString + ' of total');
+
+ if (conditionalPercString) {
+ gMiddleText.append('text')
+ .attr('class', 'path-cond-percent')
+ .attr('y', yOffsets[offsetIndex++])
+ .text(conditionalPercString + ' of parent');
}
- // Restore everything to full opacity when moving off the visualization.
- function mouseleave() {
- // Hide the breadcrumb trail
- breadcrumbs.style('visibility', 'hidden');
+ gMiddleText.append('text')
+ .attr('class', 'path-metrics')
+ .attr('y', yOffsets[offsetIndex++])
+ .text('m1: ' + formatNum(d.m1) + (metricsMatch ? '' : ', m2: ' + formatNum(d.m2)));
- gMiddleText.selectAll('*').remove();
+ gMiddleText.append('text')
+ .attr('class', 'path-ratio')
+ .attr('y', yOffsets[offsetIndex++])
+ .text((metricsMatch ? '' : ('m2/m1: ' + formatPerc(d.m2 / d.m1))));
- // Deactivate all segments during transition.
- arcs.selectAll('path').on('mouseenter', null);
+ // Reset and fade all the segments.
+ arcs.selectAll('path')
+ .style('stroke-width', null)
+ .style('stroke', null)
+ .style('opacity', 0.7);
- // Transition each segment to full opacity and then reactivate it.
- arcs.selectAll('path')
- .transition()
- .duration(200)
- .style('opacity', 1)
- .style('stroke', null)
- .style('stroke-width', null)
- .each('end', function () {
- d3.select(this).on('mouseenter', mouseenter);
- });
- }
+ // Then highlight only those that are an ancestor of the current segment.
+ arcs.selectAll('path')
+ .filter(function (node) {
+ return (sequenceArray.indexOf(node) >= 0);
+ })
+ .style('opacity', 1)
+ .style('stroke-width', '2px')
+ .style('stroke', '#000');
+
+ updateBreadcrumbs(sequenceArray, absolutePercString);
+ }
+
+ // Restore everything to full opacity when moving off the visualization.
+ function mouseleave() {
+ // Hide the breadcrumb trail
+ breadcrumbs.style('visibility', 'hidden');
+
+ gMiddleText.selectAll('*').remove();
+
+ // Deactivate all segments during transition.
+ arcs.selectAll('path').on('mouseenter', null);
+
+ // Transition each segment to full opacity and then reactivate it.
+ arcs.selectAll('path')
+ .transition()
+ .duration(200)
+ .style('opacity', 1)
+ .style('stroke', null)
+ .style('stroke-width', null)
+ .each('end', function () {
+ d3.select(this).on('mouseenter', mouseenter);
+ });
+ }
- function buildHierarchy(rows) {
- const root = {
- name: 'root',
- children: [],
- };
+ function buildHierarchy(rows) {
+ const root = {
+ name: 'root',
+ children: [],
+ };
- // each record [groupby1val, groupby2val, ( or 0)n, m1, m2]
- for (let i = 0; i < rows.length; i++) {
- const row = rows[i];
- const m1 = Number(row[row.length - 2]);
- const m2 = Number(row[row.length - 1]);
- const levels = row.slice(0, row.length - 2);
- if (isNaN(m1)) { // e.g. if this is a header row
- continue;
- }
- let currentNode = root;
- for (let level = 0; level < levels.length; level++) {
- const children = currentNode.children || [];
- const nodeName = levels[level];
- // If the next node has the name '0', it will
- const isLeafNode = (level >= levels.length - 1) || levels[level + 1] === 0;
- let childNode;
- let currChild;
+ // each record [groupby1val, groupby2val, ( or 0)n, m1, m2]
+ for (let i = 0; i < rows.length; i++) {
+ const row = rows[i];
+ const m1 = Number(row[row.length - 2]);
+ const m2 = Number(row[row.length - 1]);
+ const levels = row.slice(0, row.length - 2);
+ if (isNaN(m1)) { // e.g. if this is a header row
+ continue;
+ }
+ let currentNode = root;
+ for (let level = 0; level < levels.length; level++) {
+ const children = currentNode.children || [];
+ const nodeName = levels[level];
+ // If the next node has the name '0', it will
+ const isLeafNode = (level >= levels.length - 1) || levels[level + 1] === 0;
+ let childNode;
+ let currChild;
- if (!isLeafNode) {
- // Not yet at the end of the sequence; move down the tree.
- let foundChild = false;
- for (let k = 0; k < children.length; k++) {
- currChild = children[k];
- if (currChild.name === nodeName &&
- currChild.level === level) {
- // must match name AND level
+ if (!isLeafNode) {
+ // Not yet at the end of the sequence; move down the tree.
+ let foundChild = false;
+ for (let k = 0; k < children.length; k++) {
+ currChild = children[k];
+ if (currChild.name === nodeName &&
+ currChild.level === level) {
+// must match name AND level
- childNode = currChild;
- foundChild = true;
- break;
- }
+ childNode = currChild;
+ foundChild = true;
+ break;
}
- // If we don't already have a child node for this branch, create it.
- if (!foundChild) {
- childNode = {
- name: nodeName,
- children: [],
- level,
- };
- children.push(childNode);
- }
- currentNode = childNode;
- } else if (nodeName !== 0) {
- // Reached the end of the sequence; create a leaf node.
+ }
+ // If we don't already have a child node for this branch, create it.
+ if (!foundChild) {
childNode = {
name: nodeName,
- m1,
- m2,
+ children: [],
+ level,
};
children.push(childNode);
}
+ currentNode = childNode;
+ } else if (nodeName !== 0) {
+ // Reached the end of the sequence; create a leaf node.
+ childNode = {
+ name: nodeName,
+ m1,
+ m2,
+ };
+ children.push(childNode);
}
}
+ }
- function recurse(node) {
- if (node.children) {
- let sums;
- let m1 = 0;
- let m2 = 0;
- for (let i = 0; i < node.children.length; i++) {
- sums = recurse(node.children[i]);
- m1 += sums[0];
- m2 += sums[1];
- }
- node.m1 = m1;
- node.m2 = m2;
+ function recurse(node) {
+ if (node.children) {
+ let sums;
+ let m1 = 0;
+ let m2 = 0;
+ for (let i = 0; i < node.children.length; i++) {
+ sums = recurse(node.children[i]);
+ m1 += sums[0];
+ m2 += sums[1];
}
- return [node.m1, node.m2];
+ node.m1 = m1;
+ node.m2 = m2;
}
-
- recurse(root);
- return root;
+ return [node.m1, node.m2];
}
- // Main function to draw and set up the visualization, once we have the data.
- function createVisualization(rawData) {
- const tree = buildHierarchy(rawData.data);
+ recurse(root);
+ return root;
+ }
- vis = svg.append('svg:g')
- .attr('class', 'sunburst-vis')
- .attr('transform', (
- 'translate(' +
- `${(margin.left + (visWidth / 2))},` +
- `${(margin.top + breadcrumbHeight + (visHeight / 2))}` +
- ')'
- ))
- .on('mouseleave', mouseleave);
+ // Main function to draw and set up the visualization, once we have the data.
+ function createVisualization(rawData) {
+ const tree = buildHierarchy(rawData.data);
- arcs = vis.append('svg:g')
- .attr('id', 'arcs');
+ vis = svg.append('svg:g')
+ .attr('class', 'sunburst-vis')
+ .attr('transform', (
+ 'translate(' +
+ `${(margin.left + (visWidth / 2))},` +
+ `${(margin.top + breadcrumbHeight + (visHeight / 2))}` +
+ ')'
+ ))
+ .on('mouseleave', mouseleave);
- gMiddleText = vis.append('svg:g')
- .attr('class', 'center-label');
+ arcs = vis.append('svg:g')
+ .attr('id', 'arcs');
- // Bounding circle underneath the sunburst, to make it easier to detect
- // when the mouse leaves the parent g.
- arcs.append('svg:circle')
- .attr('r', radius)
- .style('opacity', 0);
+ gMiddleText = vis.append('svg:g')
+ .attr('class', 'center-label');
- // For efficiency, filter nodes to keep only those large enough to see.
- const nodes = partition.nodes(tree)
- .filter(function (d) {
- return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
- });
+ // Bounding circle underneath the sunburst, to make it easier to detect
+ // when the mouse leaves the parent g.
+ arcs.append('svg:circle')
+ .attr('r', radius)
+ .style('opacity', 0);
- let ext;
+ // For efficiency, filter nodes to keep only those large enough to see.
+ const nodes = partition.nodes(tree)
+ .filter(function (d) {
+ return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
+ });
- if (rawData.form_data.metric !== rawData.form_data.secondary_metric) {
- colorByCategory = false;
- ext = d3.extent(nodes, (d) => d.m2 / d.m1);
- colorScale = d3.scale.linear()
- .domain([ext[0], ext[0] + ((ext[1] - ext[0]) / 2), ext[1]])
- .range(['#00D1C1', 'white', '#FFB400']);
- }
+ let ext;
- const path = arcs.data([tree]).selectAll('path')
- .data(nodes)
- .enter()
- .append('svg:path')
- .attr('display', function (d) {
- return d.depth ? null : 'none';
- })
- .attr('d', arc)
- .attr('fill-rule', 'evenodd')
- .style('fill', (d) => colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1))
- .style('opacity', 1)
- .on('mouseenter', mouseenter);
-
- // Get total size of the tree = value of root node from partition.
- totalSize = path.node().__data__.value;
+ if (rawData.form_data.metric !== rawData.form_data.secondary_metric) {
+ colorByCategory = false;
+ ext = d3.extent(nodes, (d) => d.m2 / d.m1);
+ colorScale = d3.scale.linear()
+ .domain([ext[0], ext[0] + ((ext[1] - ext[0]) / 2), ext[1]])
+ .range(['#00D1C1', 'white', '#FFB400']);
}
+ const path = arcs.data([tree]).selectAll('path')
+ .data(nodes)
+ .enter()
+ .append('svg:path')
+ .attr('display', function (d) {
+ return d.depth ? null : 'none';
+ })
+ .attr('d', arc)
+ .attr('fill-rule', 'evenodd')
+ .style('fill', (d) => colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1))
+ .style('opacity', 1)
+ .on('mouseenter', mouseenter);
- d3.json(slice.jsonEndpoint(), function (error, rawData) {
- if (error !== null) {
- slice.error(error.responseText, error);
- return;
- }
- createBreadcrumbs(rawData);
- createVisualization(rawData);
- slice.done(rawData);
- });
- };
-
- return {
- render,
- resize: render,
- };
+ // Get total size of the tree = value of root node from partition.
+ totalSize = path.node().__data__.value;
+ }
+ createBreadcrumbs(payload);
+ createVisualization(payload);
}
module.exports = sunburstVis;
diff --git a/superset/assets/visualizations/table.js b/superset/assets/visualizations/table.js
index 4f4453d9e..ed6b95cda 100644
--- a/superset/assets/visualizations/table.js
+++ b/superset/assets/visualizations/table.js
@@ -10,156 +10,141 @@ import 'datatables.net';
import dt from 'datatables.net-bs';
dt(window, $);
-function tableVis(slice) {
+function tableVis(slice, payload) {
+ const container = $(slice.selector);
const fC = d3.format('0,000');
let timestampFormatter;
- function refresh() {
- const container = $(slice.selector);
- function onError(xhr) {
- slice.error(xhr.responseText, xhr);
- return;
+ const data = payload.data;
+ const fd = payload.form_data;
+ // Removing metrics (aggregates) that are strings
+ const realMetrics = [];
+ for (const k in data.records[0]) {
+ if (fd.metrics.indexOf(k) > -1 && !isNaN(data.records[0][k])) {
+ realMetrics.push(k);
}
- function onSuccess(json) {
- const data = json.data;
- const fd = json.form_data;
- // Removing metrics (aggregates) that are strings
- const realMetrics = [];
- for (const k in data.records[0]) {
- if (fd.metrics.indexOf(k) > -1 && !isNaN(data.records[0][k])) {
- realMetrics.push(k);
- }
- }
- const metrics = realMetrics;
+ }
+ const metrics = realMetrics;
- function col(c) {
- const arr = [];
- for (let i = 0; i < data.records.length; i++) {
- arr.push(data.records[i][c]);
- }
- return arr;
- }
- const maxes = {};
- for (let i = 0; i < metrics.length; i++) {
- maxes[metrics[i]] = d3.max(col(metrics[i]));
- }
-
- if (fd.table_timestamp_format === 'smart_date') {
- timestampFormatter = formatDate;
- } else if (fd.table_timestamp_format !== undefined) {
- timestampFormatter = timeFormatFactory(fd.table_timestamp_format);
- }
-
- const div = d3.select(slice.selector);
- div.html('');
- const table = div.append('table')
- .classed(
- 'dataframe dataframe table table-striped table-bordered ' +
- 'table-condensed table-hover dataTable no-footer', true)
- .attr('width', '100%');
-
- table.append('thead').append('tr')
- .selectAll('th')
- .data(data.columns)
- .enter()
- .append('th')
- .text(function (d) {
- return d;
- });
-
- table.append('tbody')
- .selectAll('tr')
- .data(data.records)
- .enter()
- .append('tr')
- .selectAll('td')
- .data((row) => data.columns.map((c) => {
- let val = row[c];
- if (c === 'timestamp') {
- val = timestampFormatter(val);
- }
- return {
- col: c,
- val,
- isMetric: metrics.indexOf(c) >= 0,
- };
- }))
- .enter()
- .append('td')
- .style('background-image', function (d) {
- if (d.isMetric) {
- const perc = Math.round((d.val / maxes[d.col]) * 100);
- return (
- `linear-gradient(to right, lightgrey, lightgrey ${perc}%, ` +
- `rgba(0,0,0,0) ${perc}%`
- );
- }
- return null;
- })
- .attr('title', (d) => {
- if (!isNaN(d.val)) {
- return fC(d.val);
- }
- return null;
- })
- .attr('data-sort', function (d) {
- return (d.isMetric) ? d.val : null;
- })
- .on('click', function (d) {
- if (!d.isMetric && fd.table_filter) {
- const td = d3.select(this);
- if (td.classed('filtered')) {
- slice.removeFilter(d.col, [d.val]);
- d3.select(this).classed('filtered', false);
- } else {
- d3.select(this).classed('filtered', true);
- slice.addFilter(d.col, [d.val]);
- }
- }
- })
- .style('cursor', function (d) {
- return (!d.isMetric) ? 'pointer' : '';
- })
- .html((d) => {
- if (d.isMetric) {
- return slice.d3format(d.col, d.val);
- }
- return d.val;
- });
- const height = slice.height();
- let paging = false;
- let pageLength;
- if (fd.page_length && fd.page_length > 0) {
- paging = true;
- pageLength = parseInt(fd.page_length, 10);
- }
- const datatable = container.find('.dataTable').DataTable({
- paging,
- pageLength,
- aaSorting: [],
- searching: fd.include_search,
- bInfo: false,
- scrollY: height + 'px',
- scrollCollapse: true,
- scrollX: true,
- });
- fixDataTableBodyHeight(
- container.find('.dataTables_wrapper'), height);
- // Sorting table by main column
- if (fd.metrics.length > 0) {
- const mainMetric = fd.metrics[0];
- datatable.column(data.columns.indexOf(mainMetric)).order('desc').draw();
- }
- slice.done(json);
- container.parents('.widget').find('.tooltip').remove();
+ function col(c) {
+ const arr = [];
+ for (let i = 0; i < data.records.length; i++) {
+ arr.push(data.records[i][c]);
}
- $.getJSON(slice.jsonEndpoint(), onSuccess).fail(onError);
+ return arr;
+ }
+ const maxes = {};
+ for (let i = 0; i < metrics.length; i++) {
+ maxes[metrics[i]] = d3.max(col(metrics[i]));
}
- return {
- render: refresh,
- resize() {},
- };
+ if (fd.table_timestamp_format === 'smart_date') {
+ timestampFormatter = formatDate;
+ } else if (fd.table_timestamp_format !== undefined) {
+ timestampFormatter = timeFormatFactory(fd.table_timestamp_format);
+ }
+
+ const div = d3.select(slice.selector);
+ div.html('');
+ const table = div.append('table')
+ .classed(
+ 'dataframe dataframe table table-striped table-bordered ' +
+ 'table-condensed table-hover dataTable no-footer', true)
+ .attr('width', '100%');
+
+ table.append('thead').append('tr')
+ .selectAll('th')
+ .data(data.columns)
+ .enter()
+ .append('th')
+ .text(function (d) {
+ return d;
+ });
+
+ table.append('tbody')
+ .selectAll('tr')
+ .data(data.records)
+ .enter()
+ .append('tr')
+ .selectAll('td')
+ .data((row) => data.columns.map((c) => {
+ let val = row[c];
+ if (c === 'timestamp') {
+ val = timestampFormatter(val);
+ }
+ return {
+ col: c,
+ val,
+ isMetric: metrics.indexOf(c) >= 0,
+ };
+ }))
+ .enter()
+ .append('td')
+ .style('background-image', function (d) {
+ if (d.isMetric) {
+ const perc = Math.round((d.val / maxes[d.col]) * 100);
+ return (
+ `linear-gradient(to right, lightgrey, lightgrey ${perc}%, ` +
+ `rgba(0,0,0,0) ${perc}%`
+ );
+ }
+ return null;
+ })
+ .attr('title', (d) => {
+ if (!isNaN(d.val)) {
+ return fC(d.val);
+ }
+ return null;
+ })
+ .attr('data-sort', function (d) {
+ return (d.isMetric) ? d.val : null;
+ })
+ .on('click', function (d) {
+ if (!d.isMetric && fd.table_filter) {
+ const td = d3.select(this);
+ if (td.classed('filtered')) {
+ slice.removeFilter(d.col, [d.val]);
+ d3.select(this).classed('filtered', false);
+ } else {
+ d3.select(this).classed('filtered', true);
+ slice.addFilter(d.col, [d.val]);
+ }
+ }
+ })
+ .style('cursor', function (d) {
+ return (!d.isMetric) ? 'pointer' : '';
+ })
+ .html((d) => {
+ if (d.isMetric) {
+ return slice.d3format(d.col, d.val);
+ }
+ return d.val;
+ });
+ const height = slice.height();
+ let paging = false;
+ let pageLength;
+ if (fd.page_length && fd.page_length > 0) {
+ paging = true;
+ pageLength = parseInt(fd.page_length, 10);
+ }
+ const datatable = container.find('.dataTable').DataTable({
+ paging,
+ pageLength,
+ aaSorting: [],
+ searching: fd.include_search,
+ bInfo: false,
+ scrollY: height + 'px',
+ scrollCollapse: true,
+ scrollX: true,
+ });
+ fixDataTableBodyHeight(
+ container.find('.dataTables_wrapper'), height);
+ // Sorting table by main column
+ if (fd.metrics.length > 0) {
+ const mainMetric = fd.metrics[0];
+ datatable.column(data.columns.indexOf(mainMetric)).order('desc').draw();
+ }
+ container.parents('.widget').find('.tooltip').remove();
}
module.exports = tableVis;
diff --git a/superset/assets/visualizations/treemap.js b/superset/assets/visualizations/treemap.js
index be9d0ac31..88d7e276f 100644
--- a/superset/assets/visualizations/treemap.js
+++ b/superset/assets/visualizations/treemap.js
@@ -5,9 +5,8 @@ import { category21 } from '../javascripts/modules/colors';
require('./treemap.css');
/* Modified from http://bl.ocks.org/ganeshv/6a8e9ada3ab7f2d88022 */
-function treemap(slice) {
- let div;
-
+function treemap(slice, payload) {
+ const div = d3.select(slice.selector);
const _draw = function (data, eltWidth, eltHeight, formData) {
const margin = { top: 0, right: 0, bottom: 0, left: 0 };
const navBarHeight = 36;
@@ -226,31 +225,13 @@ function treemap(slice) {
display(data);
};
- const render = function () {
- div = d3.select(slice.selector);
- d3.json(slice.jsonEndpoint(), function (error, json) {
- if (error !== null) {
- slice.error(error.responseText, error);
- return;
- }
- div.selectAll('*').remove();
- const width = slice.width();
- // facet muliple metrics (no sense in combining)
- const height = slice.height() / json.data.length;
- for (let i = 0, l = json.data.length; i < l; i ++) {
- _draw(json.data[i], width, height, json.form_data);
- }
-
- slice.done(json);
- });
- };
-
- return {
- render,
- resize: render,
- };
+ div.selectAll('*').remove();
+ const width = slice.width();
+ const height = slice.height() / payload.data.length;
+ for (let i = 0, l = payload.data.length; i < l; i ++) {
+ _draw(payload.data[i], width, height, payload.form_data);
+ }
}
module.exports = treemap;
-
diff --git a/superset/assets/visualizations/word_cloud.js b/superset/assets/visualizations/word_cloud.js
index eb1298d54..775a6d26e 100644
--- a/superset/assets/visualizations/word_cloud.js
+++ b/superset/assets/visualizations/word_cloud.js
@@ -3,74 +3,60 @@ import d3 from 'd3';
import cloudLayout from 'd3-cloud';
import { category21 } from '../javascripts/modules/colors';
-function wordCloudChart(slice) {
- function refresh() {
- const chart = d3.select(slice.selector);
- d3.json(slice.jsonEndpoint(), function (error, json) {
- if (error !== null) {
- slice.error(error.responseText, error);
- return;
- }
- const data = json.data;
- const range = [
- json.form_data.size_from,
- json.form_data.size_to,
- ];
- const rotation = json.form_data.rotation;
- let fRotation;
- if (rotation === 'square') {
- fRotation = () => ~~(Math.random() * 2) * 90;
- } else if (rotation === 'flat') {
- fRotation = () => 0;
- } else {
- fRotation = () => (~~(Math.random() * 6) - 3) * 30;
- }
- const size = [slice.width(), slice.height()];
+function wordCloudChart(slice, payload) {
+ const chart = d3.select(slice.selector);
+ const data = payload.data;
+ const range = [
+ payload.form_data.size_from,
+ payload.form_data.size_to,
+ ];
+ const rotation = payload.form_data.rotation;
+ let fRotation;
+ if (rotation === 'square') {
+ fRotation = () => ~~(Math.random() * 2) * 90;
+ } else if (rotation === 'flat') {
+ fRotation = () => 0;
+ } else {
+ fRotation = () => (~~(Math.random() * 6) - 3) * 30;
+ }
+ const size = [slice.width(), slice.height()];
- const scale = d3.scale.linear()
- .range(range)
- .domain(d3.extent(data, function (d) {
- return d.size;
- }));
+ const scale = d3.scale.linear()
+ .range(range)
+ .domain(d3.extent(data, function (d) {
+ return d.size;
+ }));
- function draw(words) {
- chart.selectAll('*').remove();
+ function draw(words) {
+ chart.selectAll('*').remove();
- chart.append('svg')
- .attr('width', layout.size()[0])
- .attr('height', layout.size()[1])
- .append('g')
- .attr('transform', `translate(${layout.size()[0] / 2},${layout.size()[1] / 2})`)
- .selectAll('text')
- .data(words)
- .enter()
- .append('text')
- .style('font-size', (d) => d.size + 'px')
- .style('font-family', 'Impact')
- .style('fill', (d) => category21(d.text))
- .attr('text-anchor', 'middle')
- .attr('transform', (d) => `translate(${d.x}, ${d.y}) rotate(${d.rotate})`)
- .text((d) => d.text);
- }
-
- const layout = cloudLayout()
- .size(size)
- .words(data)
- .padding(5)
- .rotate(fRotation)
- .font('serif')
- .fontSize((d) => scale(d.size))
- .on('end', draw);
-
- layout.start();
- slice.done(json);
- });
+ chart.append('svg')
+ .attr('width', layout.size()[0])
+ .attr('height', layout.size()[1])
+ .append('g')
+ .attr('transform', `translate(${layout.size()[0] / 2},${layout.size()[1] / 2})`)
+ .selectAll('text')
+ .data(words)
+ .enter()
+ .append('text')
+ .style('font-size', (d) => d.size + 'px')
+ .style('font-family', 'Impact')
+ .style('fill', (d) => category21(d.text))
+ .attr('text-anchor', 'middle')
+ .attr('transform', (d) => `translate(${d.x}, ${d.y}) rotate(${d.rotate})`)
+ .text((d) => d.text);
}
- return {
- render: refresh,
- resize: refresh,
- };
+ const layout = cloudLayout()
+ .size(size)
+ .words(data)
+ .padding(5)
+ .rotate(fRotation)
+ .font('serif')
+ .fontSize((d) => scale(d.size))
+ .on('end', draw);
+
+ layout.start();
}
module.exports = wordCloudChart;
diff --git a/superset/assets/visualizations/world_map.js b/superset/assets/visualizations/world_map.js
index 9884f1291..fada6666a 100644
--- a/superset/assets/visualizations/world_map.js
+++ b/superset/assets/visualizations/world_map.js
@@ -5,102 +5,90 @@ const Datamap = require('datamaps');
// CSS
require('./world_map.css');
-function worldMapChart(slice) {
- const render = function () {
- const container = slice.container;
- const div = d3.select(slice.selector);
+function worldMapChart(slice, payload) {
+ const container = slice.container;
+ const div = d3.select(slice.selector);
- container.css('height', slice.height());
+ container.css('height', slice.height());
+ div.selectAll('*').remove();
+ const fd = payload.form_data;
+ // Ignore XXX's to get better normalization
+ let data = payload.data.filter((d) => (d.country && d.country !== 'XXX'));
- d3.json(slice.jsonEndpoint(), function (error, json) {
- div.selectAll('*').remove();
- if (error !== null) {
- slice.error(error.responseText, error);
- return;
- }
- const fd = json.form_data;
- // Ignore XXX's to get better normalization
- let data = json.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 radiusScale = d3.scale.linear()
+ .domain([extRadius[0], extRadius[1]])
+ .range([1, fd.max_bubble_size]);
- const ext = d3.extent(data, function (d) {
- return d.m1;
- });
- const extRadius = d3.extent(data, function (d) {
- return d.m2;
- });
- const radiusScale = d3.scale.linear()
- .domain([extRadius[0], extRadius[1]])
- .range([1, fd.max_bubble_size]);
+ const colorScale = d3.scale.linear()
+ .domain([ext[0], ext[1]])
+ .range(['#FFF', 'black']);
- const colorScale = d3.scale.linear()
- .domain([ext[0], ext[1]])
- .range(['#FFF', 'black']);
+ data = data.map((d) => Object.assign({}, d, {
+ radius: radiusScale(d.m2),
+ fillColor: colorScale(d.m1),
+ }));
- data = data.map((d) => Object.assign({}, d, {
- radius: radiusScale(d.m2),
- fillColor: colorScale(d.m1),
- }));
+ const mapData = {};
+ data.forEach((d) => {
+ mapData[d.country] = d;
+ });
- const mapData = {};
- data.forEach((d) => {
- mapData[d.country] = d;
- });
+ const f = d3.format('.3s');
- const f = d3.format('.3s');
+ container.show();
- container.show();
+ const map = new Datamap({
+ element: slice.container.get(0),
+ data,
+ fills: {
+ defaultFill: '#ddd',
+ },
+ geographyConfig: {
+ popupOnHover: true,
+ highlightOnHover: true,
+ borderWidth: 1,
+ borderColor: '#fff',
+ highlightBorderColor: '#fff',
+ highlightFillColor: '#005a63',
+ highlightBorderWidth: 1,
+ popupTemplate: (geo, d) => (
+ `${d.name}
${f(d.m1)}
`
+ ),
+ },
+ bubblesConfig: {
+ borderWidth: 1,
+ borderOpacity: 1,
+ borderColor: '#005a63',
+ popupOnHover: true,
+ radius: null,
+ popupTemplate: (geo, d) => (
+ `${d.name}
${f(d.m2)}
`
+ ),
+ fillOpacity: 0.5,
+ animate: true,
+ highlightOnHover: true,
+ highlightFillColor: '#005a63',
+ highlightBorderColor: 'black',
+ highlightBorderWidth: 2,
+ highlightBorderOpacity: 1,
+ highlightFillOpacity: 0.85,
+ exitDelay: 100,
+ key: JSON.stringify,
+ },
+ });
- const map = new Datamap({
- element: slice.container.get(0),
- data,
- fills: {
- defaultFill: '#ddd',
- },
- geographyConfig: {
- popupOnHover: true,
- highlightOnHover: true,
- borderWidth: 1,
- borderColor: '#fff',
- highlightBorderColor: '#fff',
- highlightFillColor: '#005a63',
- highlightBorderWidth: 1,
- popupTemplate: (geo, d) => (
- `${d.name}
${f(d.m1)}
`
- ),
- },
- bubblesConfig: {
- borderWidth: 1,
- borderOpacity: 1,
- borderColor: '#005a63',
- popupOnHover: true,
- radius: null,
- popupTemplate: (geo, d) => (
- `${d.name}
${f(d.m2)}
`
- ),
- fillOpacity: 0.5,
- animate: true,
- highlightOnHover: true,
- highlightFillColor: '#005a63',
- highlightBorderColor: 'black',
- highlightBorderWidth: 2,
- highlightBorderOpacity: 1,
- highlightFillOpacity: 0.85,
- exitDelay: 100,
- key: JSON.stringify,
- },
- });
+ map.updateChoropleth(mapData);
- map.updateChoropleth(mapData);
-
- if (fd.show_bubbles) {
- map.bubbles(data);
- div.selectAll('circle.datamaps-bubble').style('fill', '#005a63');
- }
- slice.done(json);
- });
- };
-
- return { render, resize: render };
+ if (fd.show_bubbles) {
+ map.bubbles(data);
+ div.selectAll('circle.datamaps-bubble').style('fill', '#005a63');
+ }
}
module.exports = worldMapChart;
diff --git a/superset/viz.py b/superset/viz.py
index da16be32f..e0c8ac8cd 100755
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -357,8 +357,14 @@ class BaseViz(object):
if not payload:
is_cached = False
cache_timeout = self.cache_timeout
- data = self.get_data()
-
+ try:
+ data = self.get_data()
+ except Exception as e:
+ logging.exception(e)
+ if not self.error_message:
+ self.error_message = str(e)
+ self.status = utils.QueryStatus.FAILED
+ data = None
payload = {
'cache_key': cache_key,
'cache_timeout': cache_timeout,