From 0f9e9b64936ffabe3484e61d128eb84e57b78d2a Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Fri, 13 Jan 2017 17:19:55 -0800 Subject: [PATCH] Simplifying the viz interface --- .../explorev2/actions/exploreActions.js | 27 +- .../explorev2/components/ChartContainer.jsx | 115 +-- .../components/ExploreViewContainer.jsx | 22 +- .../assets/javascripts/explorev2/index.jsx | 1 + .../explorev2/reducers/exploreReducer.js | 30 +- .../assets/javascripts/modules/superset.js | 18 +- superset/assets/visualizations/big_number.js | 356 +++++----- superset/assets/visualizations/cal_heatmap.js | 65 +- .../assets/visualizations/directed_force.js | 331 +++++---- superset/assets/visualizations/filter_box.jsx | 56 +- superset/assets/visualizations/heatmap.js | 411 ++++++----- superset/assets/visualizations/histogram.js | 28 +- superset/assets/visualizations/horizon.js | 75 +- superset/assets/visualizations/iframe.js | 27 +- superset/assets/visualizations/mapbox.jsx | 124 ++-- superset/assets/visualizations/markup.js | 18 +- superset/assets/visualizations/nvd3_vis.js | 600 ++++++++-------- .../visualizations/parallel_coordinates.js | 157 ++--- superset/assets/visualizations/pivot_table.js | 42 +- superset/assets/visualizations/sankey.js | 329 +++++---- superset/assets/visualizations/sunburst.js | 659 +++++++++--------- superset/assets/visualizations/table.js | 261 ++++--- superset/assets/visualizations/treemap.js | 35 +- superset/assets/visualizations/word_cloud.js | 118 ++-- superset/assets/visualizations/world_map.js | 160 ++--- superset/viz.py | 10 +- 26 files changed, 1891 insertions(+), 2184 deletions(-) diff --git a/superset/assets/javascripts/explorev2/actions/exploreActions.js b/superset/assets/javascripts/explorev2/actions/exploreActions.js index 84e2e6d179cc5..c44ba5a8fa370 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) { @@ -124,13 +125,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'; @@ -202,3 +208,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 2412b77a0584a..a025d50c2d9b9 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 d43ffb55eb81c..83bdf18eae982 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 8daa189340b1a..bc8983097fde2 100644 --- a/superset/assets/javascripts/explorev2/index.jsx +++ b/superset/assets/javascripts/explorev2/index.jsx @@ -33,6 +33,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 8be04285821ee..928a99691a86c 100644 --- a/superset/assets/javascripts/explorev2/reducers/exploreReducer.js +++ b/superset/assets/javascripts/explorev2/reducers/exploreReducer.js @@ -3,7 +3,6 @@ import { defaultFormData } from '../stores/store'; import * as actions from '../actions/exploreActions'; import { addToArr, removeFromArr, alterInArr } from '../../../utils/reducerUtils'; import { now } from '../../modules/dates'; -import { getExploreUrl } from '../exploreUtils'; export const exploreReducer = function (state, action) { const actionHandlers = { @@ -102,34 +101,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]() { @@ -140,7 +135,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 60289147f6430..fa5a843cda90b 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 68db689a6c749..3d801a4dc8f32 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 - - 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]; +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 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]; - } - 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; - } - } + vCompare = 0; } - 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'); - - let y = height / 2; - let g = svg.append('g'); - // Printing big number - g.append('g').attr('class', 'digits') - .attr('opacity', 1) - .append('text') + } + } + 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'); + + 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', 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) + .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', '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); + .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); }); } - - return { - render, - resize: render, - }; } module.exports = bigNumberVis; diff --git a/superset/assets/visualizations/cal_heatmap.js b/superset/assets/visualizations/cal_heatmap.js index 349fdc96be1c3..e80635088bc3d 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; - } - - 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; - - 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); +function calHeatmap(slice, payload) { + const div = d3.select(slice.selector); + const data = payload.data; + + 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; + + 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 0ec4214edc6e0..e2003f122eb15 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 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 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, }); - }; - 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 539c18888236a..80276fe394247 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 285b2397674ee..bc95806d36865 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 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, + }; - 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); - } + 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)); + } - 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(); - } + 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); + } - createImageObj(); + 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; + }); - slice.done(payload); + 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(); } - return { - render: refresh, - resize: refresh, - }; + createImageObj(); } module.exports = heatmapVis; diff --git a/superset/assets/visualizations/histogram.js b/superset/assets/visualizations/histogram.js index 4bb3a9a977cd8..e6949816b29f9 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 8d713e05d1849..785ed0cd73872 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 ae2855e0fff8d..2792b0d0cf320 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 46aacdf08ce22..99b1cee344572 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; + } - // 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; - 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; - } - if (b instanceof Array) { - b.push(a); - return b; - } - return [a, b]; - }; + 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() {}, - }; + 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() + ); } module.exports = mapbox; diff --git a/superset/assets/visualizations/markup.js b/superset/assets/visualizations/markup.js index 70abd4c38c995..6a3cee09813d3 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 df8a199c42ab3..b8bbf37d77d98 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); } + }); + 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; + }; - 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; }); + 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 { - bars = d3.sum(payload.data, function (d) { return d.values.length; }); - } - if (bars * minBarWidth > width) { - return bars * minBarWidth; + chart = nv.models.lineChart(); } - 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 += ( - `'); - s += row(fd.x, f(p.x)); - s += row(fd.y, f(p.y)); - s += row(fd.size, f(p.size)); - s += '
` + - `${p[fd.entity]} (${p.group})` + - '
'; - 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); + // 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(); } - - if ('showLegend' in chart && typeof fd.show_legend !== 'undefined') { - chart.showLegend(fd.show_legend); + 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); } - - 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); + 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.y_axis_zero) { - chart.forceY([0]); - } else if (fd.y_log_scale) { - chart.yScale(d3.scale.log()); + if (fd.show_bar_value) { + setTimeout(function () { + addTotalBarValues(chart, payload.data, stacked); + }, animationTime); } - if (fd.x_log_scale) { - chart.xScale(d3.scale.log()); + if (!reduceXTicks) { + width = barchartWidth(); } - 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); + 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 += ( + `'); + s += row(fd.x, f(p.x)); + s += row(fd.y, f(p.y)); + s += row(fd.size, f(p.size)); + s += '
` + + `${p[fd.entity]} (${p.group})` + + '
'; + 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; - if (chart.hasOwnProperty('x2Axis')) { - chart.x2Axis.tickFormat(xAxisFormatter); - height += 30; - } + case 'bullet': + chart = nv.models.bulletChart(); + break; - 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')); - } + default: + throw new Error('Unrecognized visualization for nvd3' + vizType); + } - 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 ('showLegend' in chart && typeof fd.show_legend !== 'undefined') { + chart.showLegend(fd.show_legend); + } - 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); - } + let height = slice.height() - 15; + if (vizType === 'bullet') { + height = Math.min(height, 50); + } - if (fd.y_axis_label && fd.y_axis_label !== '' && chart.yAxis) { - chart.yAxis.axisLabel(fd.y_axis_label); - chart.margin({ left: 90 }); - } + chart.height(height); + slice.container.css('height', height + 'px'); - 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 }); - } + 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); + } - 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); - } + if (chart.hasOwnProperty('x2Axis')) { + chart.x2Axis.tickFormat(xAxisFormatter); + height += 30; + } - return chart; - }; + 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')); + } - const graph = drawGraph(); - nv.addGraph(graph); - slice.done(payload); - }); - }; + 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])); + } - const update = function () { - if (chart && chart.update) { - chart.height(slice.height()); - chart.width(slice.width()); - chart.update(); + 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 }); + } - return { - render, - resize: update, + 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 fdc2f1e62170e..cbb01b4964804 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); - } + 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'; - }); + 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); + 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(); + 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); + 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'); + 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, - }); + 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); - }); + }); } - - return { - render: refresh, - resize: refresh, - }; } module.exports = parallelCoordVis; diff --git a/superset/assets/visualizations/pivot_table.js b/superset/assets/visualizations/pivot_table.js index 19e4e9b19e63f..05ed5b6e2eea2 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 1507a0aaeb1c3..e80fd6996cb12 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; - - 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 + ')'); - - 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(); - - 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); - - 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 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, +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'); + + 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') + .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 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'); } module.exports = sankeyVis; diff --git a/superset/assets/visualizations/sunburst.js b/superset/assets/visualizations/sunburst.js index 623888a0a9dd2..7d45a30c568c5 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; - - 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 - - // 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 + ')'); +function sunburstVis(slice, payload) { + 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; + + 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 + + // 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); + }); - breadcrumbs.append('svg:text') - .attr('class', 'end-label'); + 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; } - - // 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; + 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(' '); + } - // 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; + }); - function updateBreadcrumbs(sequenceArray, percentageString) { - const g = breadcrumbs.selectAll('g') - .data(sequenceArray, function (d) { - return d.name + d.depth; - }); + // Add breadcrumb and label for entering nodes. + const entering = g.enter().append('svg:g'); - // 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)'; - }); + entering.append('svg:polygon') + .attr('points', breadcrumbPoints) + .style('fill', function (d) { + return colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1); + }); - // Remove exiting nodes. - g.exit().remove(); + 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); - // 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); + // Set position for entering and updating nodes. + g.attr('transform', function (d, i) { + return 'translate(' + i * (breadcrumbDims.width + breadcrumbDims.spacing) + ', 0)'; + }); - // Make the breadcrumb trail visible, if it's hidden. - breadcrumbs.style('visibility', null); - } + // Remove exiting nodes. + g.exit().remove(); - // 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; + // 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); - const absolutePercentage = (d.m1 / totalSize).toPrecision(3); - const conditionalPercentage = parentOfD ? (d.m1 / parentOfD.m1).toPrecision(3) : null; + // Make the breadcrumb trail visible, if it's hidden. + breadcrumbs.style('visibility', null); + } - const absolutePercString = formatPerc(absolutePercentage); - const conditionalPercString = parentOfD ? formatPerc(conditionalPercentage) : ''; + // 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; - // 3 levels of text if inner-most level, 4 otherwise - const yOffsets = ['-25', '7', '35', '60']; - let offsetIndex = 0; + const absolutePercentage = (d.m1 / totalSize).toPrecision(3); + const conditionalPercentage = parentOfD ? (d.m1 / parentOfD.m1).toPrecision(3) : null; - // If metrics match, assume we are coloring by category - const metricsMatch = Math.abs(d.m1 - d.m2) < 0.00001; + const absolutePercString = formatPerc(absolutePercentage); + const conditionalPercString = parentOfD ? formatPerc(conditionalPercentage) : ''; - gMiddleText.selectAll('*').remove(); + // 3 levels of text if inner-most level, 4 otherwise + const yOffsets = ['-25', '7', '35', '60']; + let offsetIndex = 0; - gMiddleText.append('text') - .attr('class', 'path-abs-percent') - .attr('y', yOffsets[offsetIndex++]) - .text(absolutePercString + ' of total'); + // If metrics match, assume we are coloring by category + const metricsMatch = Math.abs(d.m1 - d.m2) < 0.00001; - if (conditionalPercString) { - gMiddleText.append('text') - .attr('class', 'path-cond-percent') - .attr('y', yOffsets[offsetIndex++]) - .text(conditionalPercString + ' of parent'); - } + gMiddleText.selectAll('*').remove(); - 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-abs-percent') + .attr('y', yOffsets[offsetIndex++]) + .text(absolutePercString + ' of total'); + if (conditionalPercString) { gMiddleText.append('text') - .attr('class', 'path-ratio') + .attr('class', 'path-cond-percent') .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); - }) - .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); - }); + .text(conditionalPercString + ' of parent'); } - - 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; - - 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; - } - } - // If we don't already have a child node for this branch, create it. - if (!foundChild) { - childNode = { - name: nodeName, - children: [], - level, - }; - children.push(childNode); + 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); + }) + .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: [], + }; + + // 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 + + childNode = currChild; + foundChild = true; + break; } - 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); - - vis = svg.append('svg:g') - .attr('class', 'sunburst-vis') - .attr('transform', ( - 'translate(' + - `${(margin.left + (visWidth / 2))},` + - `${(margin.top + breadcrumbHeight + (visHeight / 2))}` + - ')' - )) - .on('mouseleave', mouseleave); - - arcs = vis.append('svg:g') - .attr('id', 'arcs'); - - gMiddleText = vis.append('svg:g') - .attr('class', 'center-label'); - - // 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); - - // 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 - }); - - let ext; + recurse(root); + return root; + } + + // Main function to draw and set up the visualization, once we have the data. + function createVisualization(rawData) { + const tree = buildHierarchy(rawData.data); + + vis = svg.append('svg:g') + .attr('class', 'sunburst-vis') + .attr('transform', ( + 'translate(' + + `${(margin.left + (visWidth / 2))},` + + `${(margin.top + breadcrumbHeight + (visHeight / 2))}` + + ')' + )) + .on('mouseleave', mouseleave); + + arcs = vis.append('svg:g') + .attr('id', 'arcs'); + + gMiddleText = vis.append('svg:g') + .attr('class', 'center-label'); + + // 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); + + // 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']); } - - 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, - }; + 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; + } + createBreadcrumbs(payload); + createVisualization(payload); } module.exports = sunburstVis; diff --git a/superset/assets/visualizations/table.js b/superset/assets/visualizations/table.js index 4f4453d9eecde..ed6b95cda82a0 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])); - } + 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); - } + 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%'); + 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('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); + 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); } - 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(); + 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}%` + ); } - slice.done(json); - container.parents('.widget').find('.tooltip').remove(); - } - $.getJSON(slice.jsonEndpoint(), onSuccess).fail(onError); + 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); } - - return { - render: refresh, - resize() {}, - }; + 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 be9d0ac3194ad..88d7e276f1c4a 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 eb1298d540049..775a6d26e1e54 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()]; - - const scale = d3.scale.linear() - .range(range) - .domain(d3.extent(data, function (d) { - return d.size; - })); - - 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); - }); +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; + })); + + 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); } - 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 9884f12917e99..fada6666ad2a3 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 20d0d091e0658..ef6659fe67799 100755 --- a/superset/viz.py +++ b/superset/viz.py @@ -357,8 +357,14 @@ def get_payload(self, force=False): 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,