diff --git a/src/firefly/js/api/ApiHighlevelBuild.js b/src/firefly/js/api/ApiHighlevelBuild.js index 28bc9ecf08..72f166880a 100644 --- a/src/firefly/js/api/ApiHighlevelBuild.js +++ b/src/firefly/js/api/ApiHighlevelBuild.js @@ -357,12 +357,12 @@ function makePlotId() { //================================================================ function showXYPlot(llApi, targetDiv, params={}) { - const {dispatchSetupTblTracking, dispatchTableFetch}= llApi.action; + const {dispatchTableFetch}= llApi.action; const {TBL_RESULTS_ACTIVE} = llApi.action.type; const {renderDOM} = llApi.util; const {makeFileRequest, getActiveTableId, uniqueTblId} = llApi.util.table; const {uniqueChartId, loadPlotDataForTbl} = llApi.util.chart; - const {ChartsContainer, ChartsTableViewPanel}= llApi.ui; + const {ChartsTableViewPanel}= llApi.ui; const {addActionListener} = llApi.util; if ((typeof targetDiv).match(/string|HTMLDivElement/) === null) { @@ -397,7 +397,6 @@ function showXYPlot(llApi, targetDiv, params={}) { ); tblId = searchRequest.tbl_id; dispatchTableFetch(searchRequest); - dispatchSetupTblTracking(tblId); } const chartId = uniqueChartId(tblId||tblGroup); diff --git a/src/firefly/js/tables/TablesCntlr.js b/src/firefly/js/tables/TablesCntlr.js index 7c0c48b68b..c8345897b6 100644 --- a/src/firefly/js/tables/TablesCntlr.js +++ b/src/firefly/js/tables/TablesCntlr.js @@ -10,7 +10,6 @@ import shallowequal from 'shallowequal'; import {dataReducer} from './reducer/TableDataReducer.js'; import {uiReducer} from './reducer/TableUiReducer.js'; import {resultsReducer} from './reducer/TableResultsReducer.js'; -import {dispatchSetupTblTracking} from '../visualize/TableStatsCntlr.js'; import {dispatchAddSaga} from '../core/MasterSaga.js'; export const TABLE_SPACE_PATH = 'table_space'; @@ -48,8 +47,7 @@ export function tableSearch(action) { var {request, options, tbl_group} = action.payload; const {tbl_id} = request; const title = get(request, 'META_INFO.title'); - - dispatchSetupTblTracking(tbl_id); + dispatchTableFetch(request); if (!TblUtil.getTableInGroup(tbl_id, tbl_group)) { const {tbl_group, removable} = options || {}; diff --git a/src/firefly/js/visualize/ChartUtil.js b/src/firefly/js/visualize/ChartUtil.js index 2acc013b1a..3c9f1f563b 100644 --- a/src/firefly/js/visualize/ChartUtil.js +++ b/src/firefly/js/visualize/ChartUtil.js @@ -18,6 +18,8 @@ import {logError} from '../util/WebUtil.js'; import {XYPLOT_DATA_KEY} from '../visualize/XYPlotCntlr.js'; import {HISTOGRAM_DATA_KEY} from '../visualize/HistogramCntlr'; +export const SCATTER = 'scatter'; +export const HISTOGRAM = 'histogram'; /** * This method returns an object with the keys x,y,highlightedRow @@ -63,20 +65,33 @@ function getExpressionValue(strExpr, tableModel, rowIdx) { } } + +export function getChartSpace(chartType) { + switch(chartType) { + case SCATTER: + return flux.getState()[XYPLOT_DATA_KEY]; + case HISTOGRAM: + return flux.getState()[HISTOGRAM_DATA_KEY]; + default: + logError(`Unknown chart type ${chartType}`); + return undefined; + } +} + export function hasRelatedCharts(tblId, space) { if (space) { return Boolean(Object.keys(space).find((chartId) => { return space[chartId].tblId === tblId; })); } else { - return hasRelatedCharts(tblId, flux.getState()[XYPLOT_DATA_KEY]) || - hasRelatedCharts(tblId, flux.getState()[HISTOGRAM_DATA_KEY]); + return hasRelatedCharts(tblId, getChartSpace(SCATTER)) || + hasRelatedCharts(tblId, getChartSpace(HISTOGRAM)); } } export function getTblIdForChartId(chartId) { - return get(flux.getState()[XYPLOT_DATA_KEY], [chartId, 'tblId']) || - get(flux.getState()[HISTOGRAM_DATA_KEY], [chartId, 'tblId']); + return get(getChartSpace(SCATTER), [chartId, 'tblId']) || + get(getChartSpace(HISTOGRAM), [chartId, 'tblId']); } export function numRelatedCharts(tblId) { @@ -86,7 +101,7 @@ export function numRelatedCharts(tblId) { keys.forEach( (key) => { const space = flux.getState()[key]; for (c in space) { - if (space[c].tblId === tblId) { + if (space.hasOwnProperty(c) && space[c].tblId === tblId) { numRelated++; } } diff --git a/src/firefly/js/visualize/ChartsCntlr.js b/src/firefly/js/visualize/ChartsCntlr.js index 90172c0ce4..9d0333bcaa 100644 --- a/src/firefly/js/visualize/ChartsCntlr.js +++ b/src/firefly/js/visualize/ChartsCntlr.js @@ -2,13 +2,20 @@ * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt */ +import {has, get, isUndefined, omit} from 'lodash'; + import {flux} from '../Firefly.js'; -import {updateSet} from '../util/WebUtil.js'; +import {updateSet, updateMerge} from '../util/WebUtil.js'; + +import * as TablesCntlr from '../tables/TablesCntlr.js'; export const CHART_SPACE_PATH = 'charts'; export const UI_PREFIX = `${CHART_SPACE_PATH}.ui`; export const CHART_UI_EXPANDED = `${UI_PREFIX}.expanded`; +export const CHART_MOUNTED = `${UI_PREFIX}/mounted`; +export const CHART_UNMOUNTED = `${UI_PREFIX}/unmounted`; +export const DELETE = `${UI_PREFIX}/delete`; /** * request to put a chart into an expanded mode. @@ -21,15 +28,132 @@ export function dispatchChartExpanded(chartId, tblId, chartType, optionsPopup) { flux.process( {type: CHART_UI_EXPANDED, payload: {chartId, tblId, chartType, optionsPopup}}); } +/* + * Dispatched when chart becomes visible (is rendered for the first time after being invisible) + * When chart is mounted, its data need to be in sync with the related table + * @param {string} tblId - table id (table data for this chart) + * @param {string} chartId - chart id + * @param {string} chartType - chart type, ex. scatter, histogram + */ +export function dispatchChartMounted(tblId, chartId, chartType, dispatcher= flux.process) { + dispatcher({type: CHART_MOUNTED, payload: {tblId, chartId, chartType}}); +} + +/* + * Dispatched when chart becomes invisible + * When chart is unmounted, its data synced with the related table only when it becomes mounted again + * @param {string} tblId - table id (table data for this chart) + * @param {string} chartId - chart id + * @param {string} chartType - chart type, ex. scatter, histogram + */ +export function dispatchChartUnmounted(tblId, chartId, chartType, dispatcher= flux.process) { + dispatcher({type: CHART_UNMOUNTED, payload: {tblId, chartId, chartType}}); +} + +/* + * Delete chart and related data + * @param {string} tblId - table id (table data for this chart) + * @param {string} chartId - chart id + * @param {string} chartType - chart type, ex. scatter, histogram + */ +export function dispatchDelete(tblId, chartId, chartType, dispatcher= flux.process) { + dispatcher({type: DELETE, payload: {tblId, chartId, chartType}}); +} + +const uniqueId = (chartId, chartType) => { return `${chartType}|${chartId}`; }; +const isChartType = (uChartId, chartType) => { return uChartId.startsWith(chartType); }; + +const chartActions = [CHART_UI_EXPANDED,CHART_MOUNTED,CHART_UNMOUNTED,DELETE]; + export function reducer(state={ui:{}}, action={}) { - //var root = state.ui; - switch (action.type) { - case (CHART_UI_EXPANDED) : - const {chartId, tblId, chartType, optionsPopup} = action.payload; - //return updateSet(root, 'expanded', {chartId, tblId}); - return updateSet(state, 'ui.expanded', {chartId, tblId, chartType, optionsPopup}); - default: - //return root; - return state; + if (action.type === TablesCntlr.TABLE_REMOVE) { + const {tbl_id} = action.payload; + if (has(state, tbl_id)) { + return Object.assign({}, omit(state, [tbl_id])); + } + return state; + } else if (chartActions.indexOf(action.type) > -1) { + const {chartId, tblId, chartType} = action.payload; + const uChartId = uniqueId(chartId, chartType); + switch (action.type) { + + case (CHART_UI_EXPANDED) : + const {optionsPopup} = action.payload; + //return updateSet(root, 'expanded', {chartId, tblId}); + return updateSet(state, 'ui.expanded', {chartId, tblId, chartType, optionsPopup}); + case (CHART_MOUNTED) : + + if (!has(state, ['tbl', tblId])) { + return updateSet(state, ['tbl',tblId], {[uChartId]: {mounted: true}}); + } else { + if (get(state, ['tbl', tblId, uChartId, 'mounted'])) { + //other version of the same chart is mounted + let n = get(state, ['tbl', tblId, uChartId, 'n']); + n = n ? Number(n) : 1; + return updateMerge(state, ['tbl', tblId, uChartId], {upToDate: true, n: n + 1}); + } else { + return updateSet(state, ['tbl', tblId, uChartId], {mounted: true}); + } + } + case (CHART_UNMOUNTED) : + if (has(state, ['tbl', tblId])) { + if (get(state, ['tbl', tblId, uChartId, 'mounted'])) { + let n = get(state, ['tbl', tblId, uChartId, 'n']); + n = n ? Number(n) : 1; + if (n > 1) { + //multiple versions of the same chart are mounted + return updateMerge(state, ['tbl', tblId, uChartId], {n: n - 1}); + } + } else { + return updateSet(state, ['tbl', tblId, uChartId], {mounted: false}); + } + } + return state; + case (DELETE) : + if (has(state, ['tbl', tblId, uChartId])) { + if (Object.keys(state['tbl'][tblId]).length > 1) { + return updateSet(state, ['tbl', tblId], omit(state['tbl'][tblId], [uChartId])); + } else { + return updateSet(state, 'tbl', omit(state['tbl'], [tblId])); + } + } + return state; + default: + return state; + } + } else { + return state; } +} + + +export function getExpandedChartProps() { + return get(flux.getState(), [CHART_SPACE_PATH, 'ui', 'expanded']); +} + + +export function getNumRelatedCharts(tblId, mounted, chartType) { + let numRelated = 0; + const byTblSpace = get(flux.getState(), [CHART_SPACE_PATH, 'tbl']); + if (byTblSpace) { + if (isUndefined(tblId)) { + Object.keys(byTblSpace).forEach((tblId)=>{ + numRelated += getNumRelatedCharts(tblId, mounted); + }); + } else if (byTblSpace[tblId]) { + Object.keys(byTblSpace[tblId]).forEach((uChartId) => { + if (isUndefined(mounted)||byTblSpace[tblId][uChartId].mounted===mounted) { + if (isUndefined(chartType) || isChartType(uChartId, chartType)) { + numRelated++; + } + } + }); + } + } + return numRelated; +} + +export function isChartMounted(tblId, chartId, chartType) { + const uChartId = uniqueId(chartId, chartType); + return Boolean(get(flux.getState(), [CHART_SPACE_PATH, 'tbl', tblId, uChartId, 'mounted'])); } \ No newline at end of file diff --git a/src/firefly/js/visualize/ChartsContainer.jsx b/src/firefly/js/visualize/ChartsContainer.jsx index 2ea0864b27..5500e7d756 100644 --- a/src/firefly/js/visualize/ChartsContainer.jsx +++ b/src/firefly/js/visualize/ChartsContainer.jsx @@ -8,17 +8,13 @@ import shallowequal from 'shallowequal'; import {flux} from '../Firefly.js'; -import {get} from 'lodash'; import {LO_VIEW, LO_MODE, dispatchSetLayoutMode, getExpandedMode} from '../core/LayoutCntlr.js'; import {CloseButton} from '../ui/CloseButton.jsx'; import {ChartsTableViewPanel} from '../visualize/ChartsTableViewPanel.jsx'; -import {CHART_SPACE_PATH} from '../visualize/ChartsCntlr.js'; +import {getExpandedChartProps} from '../visualize/ChartsCntlr.js'; -export function getExpandedChartProps() { - return get(flux.getState(),[CHART_SPACE_PATH, 'ui', 'expanded']); -} function nextState(props) { const {closeable, chartId, tblId, chartType, optionsPopup} = props; @@ -34,16 +30,18 @@ export class ChartsContainer extends Component { } componentWillReceiveProps(np) { - if (!this.isUnmounted && !shallowequal(this.props, np)) { + if (this.iAmMounted && !shallowequal(this.props, np)) { this.setState(nextState(np)); } } componentDidMount() { this.removeListener= flux.addListener(() => this.storeUpdate()); + this.iAmMounted = true; } componentWillUnmount() { + this.iAmMounted = false; this.removeListener && this.removeListener(); } @@ -53,7 +51,7 @@ export class ChartsContainer extends Component { storeUpdate() { - if (!this.isUnmounted) { + if (this.iAmMounted) { const expandedMode = this.props.expandedMode && getExpandedMode() === LO_VIEW.xyPlots; if (expandedMode !== this.state.expandedMode) { this.setState(nextState(this.props)); diff --git a/src/firefly/js/visualize/ChartsTableViewPanel.jsx b/src/firefly/js/visualize/ChartsTableViewPanel.jsx index 0a2bdc80f9..d652dec1bb 100644 --- a/src/firefly/js/visualize/ChartsTableViewPanel.jsx +++ b/src/firefly/js/visualize/ChartsTableViewPanel.jsx @@ -19,11 +19,11 @@ import {FilterInfo} from '../tables/FilterInfo.js'; import * as TableStatsCntlr from '../visualize/TableStatsCntlr.js'; import * as HistogramCntlr from '../visualize/HistogramCntlr.js'; import * as XYPlotCntlr from '../visualize/XYPlotCntlr.js'; -import {dispatchChartExpanded} from '../visualize/ChartsCntlr.js'; +import {dispatchChartExpanded, dispatchDelete, dispatchChartMounted, dispatchChartUnmounted} from '../visualize/ChartsCntlr.js'; import {LO_MODE, LO_VIEW, dispatchSetLayoutMode} from '../core/LayoutCntlr.js'; -import {getHighlighted, getTblIdForChartId, numRelatedCharts} from './ChartUtil.js'; +import {SCATTER, HISTOGRAM, getHighlighted, getTblIdForChartId, numRelatedCharts} from './ChartUtil.js'; import XYPlotOptions from '../visualize/XYPlotOptions.jsx'; import {XYPlot} from '../visualize/XYPlot.jsx'; @@ -48,30 +48,16 @@ import CLEAR_FILTERS from 'html/images/icons-2014/24x24_FilterOff_Circle.png'; import LOADING from 'html/images/gxt/loading.gif'; -const SCATTER = 'scatter'; -const HISTOGRAM = 'histogram'; const OPTIONS_WIDTH = 330; -const PREF_CHART_TYPE = 'pref.chartType'; - const selectionBtnStyle = {verticalAlign: 'top', paddingLeft: 20, cursor: 'pointer'}; -function getChartType(props) { - if (props.chartId !== props.tblId) { - if (props.tblPlotData) { return SCATTER; } - if (props.tblHistogramData) { return HISTOGRAM; } - } else { - return props.chartType; - } - //return (localStorage.getItem(PREF_CHART_TYPE) || SCATTER); -} class ChartsPanel extends React.Component { constructor(props) { super(props); this.state = { - chartType: getChartType(props), optionsShown: !props.chartId, immediateResize: false }; @@ -81,7 +67,7 @@ class ChartsPanel extends React.Component { var widthPx = size.width; var heightPx = size.height; //console.log('width: '+widthPx+', height: '+heightPx); - if (widthPx !== this.state.widthPx || heightPx != this.state.heightPx) { + if (widthPx !== this.state.widthPx || heightPx !== this.state.heightPx) { this.setState({widthPx, heightPx, immediateResize: false}); } } @@ -110,7 +96,6 @@ class ChartsPanel extends React.Component { this.selectionNotEmpty = this.selectionNotEmpty.bind(this); this.renderSelectionButtons = this.renderSelectionButtons.bind(this); this.renderToolbar = this.renderToolbar.bind(this); - this.onChartTypeChange = this.onChartTypeChange.bind(this); this.renderOptions = this.renderOptions.bind(this); } @@ -121,7 +106,7 @@ class ChartsPanel extends React.Component { nextProps.expandedMode !== this.props.expandedMode || nextProps.deletable !== this.props.deletable; if (!doUpdate) { - const chartType = getChartType(nextProps); + const {chartType} = nextProps; if (chartType === SCATTER) { // scatter plot doUpdate = @@ -129,7 +114,7 @@ class ChartsPanel extends React.Component { (nextProps.tableModel && (nextProps.tableModel.highlightedRow !== get(this.props, 'tableModel.highlightedRow') || nextProps.tableModel.selectInfo !== get(this.props, 'tableModel.selectInfo') )); - } else { + } else if (chartType === HISTOGRAM){ // histogram doUpdate = nextProps.tblHistogramData !== this.props.tblHistogramData; } @@ -139,6 +124,8 @@ class ChartsPanel extends React.Component { componentDidMount() { this.handlePopups(); + const {tblId, chartId, chartType} = this.props; + dispatchChartMounted(tblId,chartId,chartType); this.iAmMounted = true; } @@ -147,11 +134,18 @@ class ChartsPanel extends React.Component { } componentWillReceiveProps(nextProps) { - this.setState({chartType: getChartType(nextProps)}); + const {tblId, chartId, chartType} = nextProps; + if (!tblId || !chartId) { return; } + if (chartId !== this.props.chartId || chartType !== this.props.chartType || !this.props.tblId) { + dispatchChartUnmounted(this.props.tblId, this.props.chartId, this.props.chartType); + dispatchChartMounted(tblId,chartId,chartType); + } } componentWillUnmount() { this.iAmMounted = false; + const {tblId, chartId, chartType} = this.props; + dispatchChartUnmounted(tblId, chartId, chartType); if (isDialogVisible(popupId)) { hideChartOptionsPopup(); } @@ -159,9 +153,9 @@ class ChartsPanel extends React.Component { handlePopups() { if (this.props.optionsPopup) { - const {optionsShown, chartType} = this.state; + const {optionsShown} = this.state; if (optionsShown) { - const {tableModel, tblStatsData, tblPlotData, tblHistogramData, chartId} = this.props; + const {tableModel, tblStatsData, tblPlotData, tblHistogramData, chartId, chartType} = this.props; // show options popup let popupTitle = 'Chart Options'; @@ -289,7 +283,7 @@ class ChartsPanel extends React.Component { } displaySelectionOptions() { - if (this.state.chartType === SCATTER) { + if (this.props.chartType === SCATTER) { const selection = get(this.props, 'tblPlotData.xyPlotParams.selection'); return Boolean(selection); } @@ -298,7 +292,7 @@ class ChartsPanel extends React.Component { } displayZoomOriginal() { - if (this.state.chartType === SCATTER) { + if (this.props.chartType === SCATTER) { const zoom = get(this.props, 'tblPlotData.xyPlotParams.zoom'); return Boolean(zoom); } @@ -307,26 +301,26 @@ class ChartsPanel extends React.Component { } addZoom() { - if (this.state.chartType === SCATTER) { + if (this.props.chartType === SCATTER) { XYPlotCntlr.dispatchZoom(this.props.chartId, this.props.tblId, get(this.props, 'tblPlotData.xyPlotParams.selection')); } } resetZoom() { - if (this.state.chartType === SCATTER) { + if (this.props.chartType === SCATTER) { XYPlotCntlr.dispatchZoom(this.props.chartId, this.props.tblId); } } displayUnselectAll () { - if (this.state.chartType === SCATTER) { + if (this.props.chartType === SCATTER) { const selectInfo = get(this.props, 'tableModel.selectInfo'); return selectInfo && (selectInfo.selectAll || selectInfo.exceptions.size>0); } } addSelection() { - if (this.state.chartType === SCATTER) { + if (this.props.chartType === SCATTER) { if (get(this.props, 'tblPlotData.xyPlotData.decimateKey')) { showInfoPopup('Your data set is too large to select. You must filter it down first.', `Can't Select`); // eslint-disable-line quotes @@ -354,7 +348,7 @@ class ChartsPanel extends React.Component { } resetSelection() { - if (this.state.chartType === SCATTER) { + if (this.props.chartType === SCATTER) { const {tblId, tableModel} = this.props; if (tableModel) { const selectInfoCls = SelectInfo.newInstance({rowCount: tableModel.totalRows}); @@ -370,7 +364,7 @@ class ChartsPanel extends React.Component { } addFilter() { - if (this.state.chartType === SCATTER) { + if (this.props.chartType === SCATTER) { const {tblPlotData, tableModel} = this.props; const selection = get(tblPlotData, 'xyPlotParams.selection'); const xCol = get(tblPlotData, 'xyPlotParams.x.columnOrExpr'); @@ -463,8 +457,7 @@ class ChartsPanel extends React.Component { } renderToolbar() { - const {expandable, expandedMode, tblId, chartId, optionsPopup, deletable} = this.props; - const chartType = this.state.chartType; + const {expandable, expandedMode, tblId, chartId, chartType, optionsPopup, deletable} = this.props; return (
} { expandable && !expandedMode && - isBoolean(deletable) ? deletable : numRelatedCharts(tblId) > 1 && // when deletable is undefined, use related charts criterion + (isBoolean(deletable) ? deletable : numRelatedCharts(tblId) > 1) && // when deletable is undefined, use related charts criterion { - if (chartType === SCATTER) { - XYPlotCntlr.dispatchDelete(chartId); - } else if (chartType === HISTOGRAM) { - HistogramCntlr.dispatchDelete(chartId); - } - }} + onClick={() => {dispatchDelete(tblId, chartId, chartType);}} />}
@@ -502,22 +489,9 @@ class ChartsPanel extends React.Component { } - onChartTypeChange(ev) { - // the value of the group is the value of the selected option - var val = ev.target.value; - var checked = ev.target.checked; - - if (checked) { - if (val !== this.state.chartType) { - localStorage.setItem(PREF_CHART_TYPE, val); - this.setState({chartType : val}); - } - } - } - renderOptions() { - const {optionsShown, chartType, heightPx} = this.state; - const { tableModel, tblStatsData, tblPlotData, tblHistogramData, optionsPopup, chartId} = this.props; + const {optionsShown, heightPx} = this.state; + const { tableModel, tblStatsData, tblPlotData, tblHistogramData, optionsPopup, chartId, chartType} = this.props; if (optionsShown && !optionsPopup) { return (
@@ -531,7 +505,7 @@ class ChartsPanel extends React.Component { render() { - var {tblStatsData} = this.props; + var {tblStatsData, chartType} = this.props; if (!(tblStatsData && tblStatsData.isColStatsReady) ) { return ( ); } else { - var {widthPx, heightPx, chartType} = this.state; + var {widthPx, heightPx} = this.state; const knownSize = widthPx && heightPx; if (chartType === SCATTER && !this.props.tblPlotData || @@ -575,6 +549,7 @@ ChartsPanel.propTypes = { tableModel : PropTypes.object, tblStatsData : PropTypes.object, chartId: PropTypes.string, + chartType: PropTypes.oneOf(['scatter', 'histogram']), tblPlotData : PropTypes.object, tblHistogramData : PropTypes.object, width : PropTypes.string, @@ -653,12 +628,12 @@ export class OptionsWrapper extends React.Component { } shouldComponentUpdate(nProps) { - return nProps.chartId != this.props.chartId || + return nProps.chartId !== this.props.chartId || get(nProps, 'tblPlotData.xyPlotParams') !== get(this.props, 'tblPlotData.xyPlotParams') || get(nProps, 'tblHistogramData.histogramParams') !== get(this.props, 'tblHistogramData.histogramParams') || get(nProps, 'tableModel.tbl_id') !== get(this.props, 'tableModel.tbl_id') || get(nProps, 'tblStatsData.isColStatsReady') !== get(this.props, 'tblStatsData.isColStatsReady') || - nProps.chartType != this.props.chartType; + nProps.chartType !== this.props.chartType; } // componentDidUpdate(prevProps, prevState) { diff --git a/src/firefly/js/visualize/HistogramCntlr.js b/src/firefly/js/visualize/HistogramCntlr.js index c093d26733..7796c6a618 100644 --- a/src/firefly/js/visualize/HistogramCntlr.js +++ b/src/firefly/js/visualize/HistogramCntlr.js @@ -3,11 +3,12 @@ */ import {flux} from '../Firefly.js'; -import {has, omit} from 'lodash'; +import {get, has, omit} from 'lodash'; import {updateSet, updateMerge} from '../util/WebUtil.js'; import {doFetchTable, getTblById, isFullyLoaded, makeTblRequest} from '../tables/TableUtil.js'; import * as TablesCntlr from '../tables/TablesCntlr.js'; +import {DELETE} from './ChartsCntlr.js'; /* Possible structure of store: @@ -16,6 +17,7 @@ import * as TablesCntlr from '../tables/TablesCntlr.js'; { // tblHistogramData tblId: string // table id + tblSource: string // source of the table isColDataReady: boolean histogramData: [[numInBin: int, min: double, max: double]*] histogramParams: { @@ -35,7 +37,6 @@ import * as TablesCntlr from '../tables/TablesCntlr.js'; export const HISTOGRAM_DATA_KEY = 'histogram'; export const LOAD_COL_DATA = `${HISTOGRAM_DATA_KEY}/LOAD_COL_DATA`; export const UPDATE_COL_DATA = `${HISTOGRAM_DATA_KEY}/UPDATE_COL_DATA`; -export const DELETE = `${HISTOGRAM_DATA_KEY}/DELETE`; /* @@ -48,14 +49,6 @@ export const dispatchLoadColData = function(chartId, histogramParams, tblId, dis dispatcher({type: LOAD_COL_DATA, payload: {chartId, histogramParams, tblId}}); }; -/* - * Delete chart and related data - * @param {String} chartId - chart id - */ -export function dispatchDelete(chartId) { - flux.process({type: DELETE, payload: {chartId}}); -} - /* * Get column histogram data * @param {string} chartId - chart id @@ -73,23 +66,51 @@ const dispatchUpdateColData = function(chartId, isColDataReady, histogramData, h */ export const loadColData = function(rawAction) { return (dispatch) => { - dispatch({ type : LOAD_COL_DATA, payload : rawAction.payload }); + const {chartId, histogramParams, tblId} = rawAction.payload; - if (isFullyLoaded(tblId) && histogramParams) { - const searchRequest = getTblById(tblId)['request']; - fetchColData(dispatch, searchRequest, histogramParams, chartId); - } + const tblSource = get(getTblById(tblId), 'tableMeta.source'); + + const chartModel = get(flux.getState(), [HISTOGRAM_DATA_KEY, chartId]); + let serverCallNeeded = !chartModel || !chartModel.tblSource || chartModel.tblSource !== tblSource; + + if (serverCallNeeded || chartModel.histogramParams !== histogramParams) { + // when server call parameters do not change but chart parameters change, + // we do need to update parameters, but we can reuse the old chart data + serverCallNeeded = serverCallNeeded || serverParamsChanged(chartModel.histogramParams, histogramParams); + dispatch({type: LOAD_COL_DATA, payload: {...rawAction.payload, tblSource, serverCallNeeded}}); + if (serverCallNeeded) { + fetchColData(dispatch, tblId, histogramParams, chartId); + } + } }; }; -function getInitState() { - return {}; -} +function serverParamsChanged(oldParams, newParams) { + if (oldParams === newParams) { return false; } + if (!oldParams || !newParams) { return true; } + const newServerParams = getServerCallParameters(newParams); + const oldServerParams = getServerCallParameters(oldParams); + return newServerParams.some((p, i) => { + return p !== oldServerParams[i]; + }); +} +function getServerCallParameters(histogramParams) { + if (!histogramParams) { return []; } + + const serverParams = []; + serverParams.push(histogramParams.columnOrExpr); + serverParams.push(histogramParams.x && histogramParams.x.includes('log')); + serverParams.push(histogramParams.numBins); + serverParams.push(histogramParams.falsePositiveRate); + //serverParams.push(histogramParams.minCutoff); + //serverParams.push(histogramParams.maxCutoff); + return serverParams; +} -export function reducer(state=getInitState(), action={}) { +export function reducer(state={}, action={}) { switch (action.type) { case (TablesCntlr.TABLE_REMOVE) : { @@ -105,20 +126,29 @@ export function reducer(state=getInitState(), action={}) { } case (DELETE) : { - const chartId = action.payload.chartId; - return has(state, chartId) ? Object.assign({}, omit(state, [chartId])) : state; + const {chartId, chartType} = action.payload; + if (chartType === 'histogram' && has(state, chartId)) { + return Object.assign({}, omit(state, [chartId])); + } + return state; } case (LOAD_COL_DATA) : { - const {chartId, histogramParams, tblId} = action.payload; - return updateSet(state, chartId, {tblId, isColDataReady: false, histogramParams}); + const {chartId, tblId, histogramParams, tblSource, serverCallNeeded} = action.payload; + if (serverCallNeeded) { + return updateSet(state, chartId, {tblId, isColDataReady: false, tblSource, histogramParams}); + } else { + // only histogram parameters changed + return updateSet(state, [chartId, 'histogramParams'], histogramParams); + } } case (UPDATE_COL_DATA) : { - const {chartId, isColDataReady, histogramData, histogramParams} = action.payload; + const {chartId, isColDataReady, tblSource, histogramData, histogramParams} = action.payload; if (state[chartId].histogramParams === histogramParams) { return updateMerge(state, chartId, { isColDataReady, + tblSource, histogramData }); } else { @@ -143,13 +173,20 @@ function updateColData(data) { * fetches active table statistics data * set isColStatsReady to true once done. * @param {function} dispatch - * @param {Object} activeTableServerRequest table search request to obtain source table + * @param {Object} tblId table id of the source table * @param {Object} histogramParams object, which contains histogram parameters * @param {string} chartId - chart id */ -function fetchColData(dispatch, activeTableServerRequest, histogramParams, chartId) { +function fetchColData(dispatch, tblId, histogramParams, chartId) { + + if (!isFullyLoaded(tblId) || !histogramParams) { + return; + } + + const activeTableModel = getTblById(tblId); + const activeTableServerRequest = activeTableModel['request']; + const tblSource = get(activeTableModel, 'tableMeta.source'); - //const {tbl_id} = activeTableServerRequest; const sreq = Object.assign({}, omit(activeTableServerRequest, ['tbl_id', 'META_INFO']), {'startIdx' : 0, 'pageSize' : 1000000}); @@ -200,6 +237,7 @@ function fetchColData(dispatch, activeTableServerRequest, histogramParams, chart { chartId, isColDataReady : true, + tblSource, histogramParams, histogramData })); diff --git a/src/firefly/js/visualize/TableStatsCntlr.js b/src/firefly/js/visualize/TableStatsCntlr.js index 059f6d88da..47795d195b 100644 --- a/src/firefly/js/visualize/TableStatsCntlr.js +++ b/src/firefly/js/visualize/TableStatsCntlr.js @@ -21,18 +21,10 @@ import * as TablesCntlr from '../tables/TablesCntlr.js'; export const TBLSTATS_DATA_KEY = 'tblstats'; -export const SETUP_TBL_TRACKING = `${TBLSTATS_DATA_KEY}/SETUP_TBL_TRACKING`; + export const LOAD_TBL_STATS = `${TBLSTATS_DATA_KEY}/LOAD_TBL_STATS`; export const UPDATE_TBL_STATS = `${TBLSTATS_DATA_KEY}/UPDATE_TBL_STATS`; -/* - * Set up store, which will reflect the data relevant to the given table - * @param {string} tblId - table id - * @param {function} dispatcher only for special dispatching uses such as remote - */ -export function dispatchSetupTblTracking(tblId, dispatcher= flux.process) { - dispatcher({type: SETUP_TBL_TRACKING, payload: {tblId}}); -} /* * Get the number of points, min and max values, units and description for each table column @@ -74,14 +66,6 @@ function getInitState() { export function reducer(state=getInitState(), action={}) { switch (action.type) { - case (SETUP_TBL_TRACKING) : - { - const {tblId} = action.payload; - if (!state[tblId]) { - return updateSet(state, tblId, {isColStatsReady: false}); - } - return state; - } case (TablesCntlr.TABLE_REMOVE) : { const {tbl_id} = action.payload; diff --git a/src/firefly/js/visualize/XYPlot.jsx b/src/firefly/js/visualize/XYPlot.jsx index 587aeccd9f..84fa0cccc3 100644 --- a/src/firefly/js/visualize/XYPlot.jsx +++ b/src/firefly/js/visualize/XYPlot.jsx @@ -181,9 +181,26 @@ const getDeciSymbolSize = function(chart, decimateKeyStr) { return {xUnitPx, yUnitPx}; }; -/* - * Get data series - */ +const calculateChartSize = function(widthPx, heightPx, props) { + const {params} = props; + let chartWidth = undefined, chartHeight = undefined; + if (params.xyRatio) { + if (params.stretch === 'fit') { + chartHeight = Number(heightPx) - 2; + chartWidth = Number(params.xyRatio) * Number(chartHeight) + 20; + if (chartWidth > Number(widthPx)) { + chartHeight -= 15; // to accommodate scroll bar + } + } else { + chartWidth = Number(widthPx) - 15; + chartHeight = Number(widthPx) / Number(params.xyRatio); + } + } else { + chartWidth = Number(widthPx); + chartHeight = Number(heightPx); + } + return {chartWidth, chartHeight}; +}; export class XYPlot extends React.Component { @@ -191,7 +208,6 @@ export class XYPlot extends React.Component { super(props); this.updateSelectionRect = this.updateSelectionRect.bind(this); this.adjustPlotDisplay = this.adjustPlotDisplay.bind(this); - this.calculateChartSize=this.calculateChartSize.bind(this); this.debouncedResize = this.debouncedResize.bind(this); this.onSelectionEvent = this.onSelectionEvent.bind(this); this.shouldAnimate = this.shouldAnimate.bind(this); @@ -243,10 +259,21 @@ export class XYPlot extends React.Component { const newXOptions = getXAxisOptions(newParams); const newYOptions = getYAxisOptions(newParams); if (!shallowequal(getXAxisOptions(params), newXOptions)) { - Object.assign(xoptions, newXOptions); + Object.assign(xoptions, { + title: {text: newXOptions.xTitle}, + gridLineWidth: newXOptions.xGrid ? 1 : 0, + reversed: newXOptions.xReversed, + opposite: newYOptions.yReversed, + type: newXOptions.xLog ? 'logarithmic' : 'linear' + }); } if (!shallowequal(getYAxisOptions(params), newYOptions)) { - Object.assign(yoptions, newYOptions); + Object.assign(yoptions, { + title: {text: newYOptions.yTitle}, + gridLineWidth: newYOptions.yGrid ? 1 : 0, + reversed: newYOptions.yReversed, + type: newYOptions.yLog ? 'logarithmic' : 'linear' + }); } if (!shallowequal(params.zoom, newParams.zoom)) { const {xMin, xMax, yMin, yMax} = getZoomSelection(newParams); @@ -269,8 +296,9 @@ export class XYPlot extends React.Component { } // size change - if (newWidth !== width || newHeight !== height) { - const {chartWidth, chartHeight} = this.calculateChartSize(newWidth, newHeight); + if (newWidth !== width || newHeight !== height || + newParams.xyRatio !== params.xyRatio ||newParams.stretch != params.stretch) { + const {chartWidth, chartHeight} = calculateChartSize(newWidth, newHeight, nextProps); chart.setSize(chartWidth, chartHeight, false); if (this.pendingResize) { @@ -295,27 +323,6 @@ export class XYPlot extends React.Component { this.adjustPlotDisplay(); } - calculateChartSize(widthPx, heightPx) { - const {params} = this.props; - let chartWidth = undefined, chartHeight = undefined; - if (params.xyRatio) { - if (params.stretch === 'fit') { - chartHeight = Number(heightPx) - 2; - chartWidth = Number(params.xyRatio) * Number(chartHeight) + 20; - if (chartWidth > Number(widthPx)) { - chartHeight -= 15; // to accommodate scroll bar - } - } else { - chartWidth = Number(widthPx) - 15; - chartHeight = Number(widthPx) / Number(params.xyRatio); - } - } else { - chartWidth = Number(widthPx); - chartHeight = Number(heightPx); - } - return {chartWidth, chartHeight}; - } - debouncedResize() { return debounce(() => { const chart = this.refs.chart && this.refs.chart.getChart(); @@ -515,7 +522,7 @@ export class XYPlot extends React.Component { const {data, params, width, height, onSelection, desc} = this.props; const onSelectionEvent = this.onSelectionEvent; - const {chartWidth, chartHeight} = this.calculateChartSize(width, height); + const {chartWidth, chartHeight} = calculateChartSize(width, height, this.props); const {xTitle, xGrid, xReversed, xLog} = getXAxisOptions(params); const {yTitle, yGrid, yReversed, yLog} = getYAxisOptions(params); diff --git a/src/firefly/js/visualize/XYPlotCntlr.js b/src/firefly/js/visualize/XYPlotCntlr.js index 6c4a751d93..fbd620921e 100644 --- a/src/firefly/js/visualize/XYPlotCntlr.js +++ b/src/firefly/js/visualize/XYPlotCntlr.js @@ -8,6 +8,7 @@ import {get, has, omit, omitBy, isUndefined, isString} from 'lodash'; import {doFetchTable, getTblById, isFullyLoaded} from '../tables/TableUtil.js'; import * as TablesCntlr from '../tables/TablesCntlr.js'; +import {DELETE} from './ChartsCntlr.js'; import {serializeDecimateInfo} from '../tables/Decimate.js'; import {logError} from '../util/WebUtil.js'; import {getDefaultXYPlotParams} from '../visualize/ChartUtil.js'; @@ -15,7 +16,6 @@ import {getDefaultXYPlotParams} from '../visualize/ChartUtil.js'; export const XYPLOT_DATA_KEY = 'xyplot'; export const LOAD_PLOT_DATA = `${XYPLOT_DATA_KEY}/LOAD_COL_DATA`; export const UPDATE_PLOT_DATA = `${XYPLOT_DATA_KEY}/UPDATE_COL_DATA`; -export const DELETE = `${XYPLOT_DATA_KEY}/DELETE`; export const SET_SELECTION = `${XYPLOT_DATA_KEY}/SET_SELECTION`; const SET_ZOOM = `${XYPLOT_DATA_KEY}/SET_ZOOM`; const RESET_ZOOM = `${XYPLOT_DATA_KEY}/RESET_ZOOM`; @@ -27,6 +27,7 @@ const RESET_ZOOM = `${XYPLOT_DATA_KEY}/RESET_ZOOM`; { // tblXYPlotData tblId: string // table id + tblSource: string // source of the table isPlotDataReady: boolean decimatedUnzoomed: boolean // tells that unzoomed data are decimated xyPlotData: { @@ -74,14 +75,6 @@ export function dispatchLoadPlotData(chartId, xyPlotParams, tblId, dispatcher= f dispatcher({type: LOAD_PLOT_DATA, payload: {chartId: (chartId||tblId), xyPlotParams, tblId}}); } -/* - * Delete chart and related data - * @param {String} chartId - chart id - */ -export function dispatchDelete(chartId) { - flux.process({type: DELETE, payload: {chartId}}); -} - /* * Set selection to give user choice of actions on selection (zoom, filter, or select points) * @param {String} chartId - chart id @@ -143,11 +136,36 @@ function dispatchResetZoom(chartId) { export function loadPlotData (rawAction) { return (dispatch) => { const {chartId, xyPlotParams, tblId} = rawAction.payload; - dispatch({ type : LOAD_PLOT_DATA, payload : rawAction.payload }); - fetchPlotData(dispatch, tblId, xyPlotParams, chartId); + const tblSource = get(getTblById(tblId), 'tableMeta.source'); + + const chartModel = get(flux.getState(), [XYPLOT_DATA_KEY, chartId]); + let serverCallNeeded = !chartModel || !chartModel.tblSource || chartModel.tblSource !== tblSource; + + if (serverCallNeeded || chartModel.xyPlotParams !== xyPlotParams) { + // when server call parameters do not change but chart parameters change, + // we do need to update parameters, but we can reuse the old chart data + serverCallNeeded = serverCallNeeded || serverParamsChanged(chartModel.xyPlotParams, xyPlotParams); + + dispatch({ type : LOAD_PLOT_DATA, payload : {...rawAction.payload, tblSource, serverCallNeeded}}); + + if (serverCallNeeded) { + fetchPlotData(dispatch, tblId, xyPlotParams, chartId); + } + } }; } +function serverParamsChanged(oldParams, newParams) { + if (oldParams === newParams) { return false; } + if (!oldParams || !newParams) { return true; } + + const newServerParams = getServerCallParameters(newParams); + const oldServerParams = getServerCallParameters(oldParams); + return newServerParams.some((p, i) => { + return p !== oldServerParams[i]; + }); +} + /** * The data is an object with * chartId - string, chart id, @@ -178,22 +196,36 @@ export function reducer(state={}, action={}) { } case (DELETE) : { - const chartId = action.payload.chartId; - return has(state, chartId) ? Object.assign({}, omit(state, [chartId])) : state; + const {chartId, chartType} = action.payload; + if (chartType === 'scatter' && has(state, chartId)) { + return Object.assign({}, omit(state, [chartId])); + } + return state; } case (LOAD_PLOT_DATA) : { - const {chartId, xyPlotParams, tblId} = action.payload; - return updateSet(state, chartId, - { tblId, isPlotDataReady: false, xyPlotParams, decimatedUnzoomed: get(state, [chartId,'decimatedUnzoomed'])}); + const {chartId, xyPlotParams, tblId, tblSource, serverCallNeeded} = action.payload; + if (serverCallNeeded) { + return updateSet(state, chartId, + { + tblId, + isPlotDataReady: false, + tblSource, + xyPlotParams, + decimatedUnzoomed: get(state, [chartId, 'decimatedUnzoomed']) + }); + } else { + // only plot parameters changed + return updateSet(state, [chartId, 'xyPlotParams'], xyPlotParams); + } } case (UPDATE_PLOT_DATA) : { - const {isPlotDataReady, decimatedUnzoomed, xyPlotParams, xyPlotData, chartId, newParams} = action.payload; + const {chartId, isPlotDataReady, tblSource, decimatedUnzoomed, xyPlotParams, xyPlotData, newParams} = action.payload; if (!state[chartId].xyPlotParams || state[chartId].xyPlotParams === xyPlotParams) { const decimatedUnzoomedNext = isUndefined(decimatedUnzoomed) ? state[chartId].decimatedUnzoomed : decimatedUnzoomed; return updateMerge(state, chartId, - {isPlotDataReady, decimatedUnzoomed: decimatedUnzoomedNext, xyPlotData, xyPlotParams: newParams}); + {isPlotDataReady, tblSource, decimatedUnzoomed: decimatedUnzoomedNext, xyPlotData, xyPlotParams: newParams}); } return state; } @@ -231,13 +263,31 @@ export function reducer(state={}, action={}) { } } +function getServerCallParameters(xyPlotParams) { + if (!xyPlotParams) { return []; } + + if (xyPlotParams.zoom) { + var {xMin, xMax, yMin, yMax} = xyPlotParams.zoom; + } + + let maxBins = 10000; + let xyRatio = xyPlotParams.xyRatio || 1.0; + if (xyPlotParams.nbins) { + const {x, y} = xyPlotParams.nbins; + maxBins = x*y; + xyRatio = x/y; + } + // order should match the order of the parameters in serializeDecimateInfo + return [xyPlotParams.x.columnOrExpr, xyPlotParams.y.columnOrExpr, maxBins, xyRatio, xMin, xMax, yMin, yMax]; +} + /** * fetches xy plot data * set isColStatsReady to true once done. * @param dispatch - * @param activeTableServerRequest table search request to obtain source table + * @param tblId table search request to obtain source table * @param xyPlotParams object, which contains xy plot parameters * @param {string} chartId - chart id */ @@ -245,29 +295,18 @@ function fetchPlotData(dispatch, tblId, xyPlotParams, chartId) { if (!tblId || !isFullyLoaded(tblId)) {return; } - const activeTableServerRequest = getTblById(tblId)['request']; + const activeTableModel = getTblById(tblId); + const activeTableServerRequest = activeTableModel['request']; + const tblSource = get(activeTableModel, 'tableMeta.source'); if (!xyPlotParams) { xyPlotParams = getDefaultXYPlotParams(tblId); } - let limits = []; - if (xyPlotParams.zoom) { - const {xMin, xMax, yMin, yMax} = xyPlotParams.zoom; - limits = [xMin, xMax, yMin, yMax]; - } - - let maxBins = 10000; - let xyRatio = xyPlotParams.xyRatio || 1.0; - if (xyPlotParams.nbins) { - const {x, y} = xyPlotParams.nbins; - maxBins = x*y; - xyRatio = x/y; - } const req = Object.assign({}, omit(activeTableServerRequest, ['tbl_id', 'META_INFO']), { 'startIdx' : 0, 'pageSize' : 1000000, //'inclCols' : `${xyPlotParams.x.columnOrExpr},${xyPlotParams.y.columnOrExpr}`, // ignored if 'decimate' is present - 'decimate' : serializeDecimateInfo(xyPlotParams.x.columnOrExpr, xyPlotParams.y.columnOrExpr, maxBins, xyRatio, ...limits) + 'decimate' : serializeDecimateInfo(...getServerCallParameters(xyPlotParams)) }); req.tbl_id = `xy-${chartId}`; @@ -297,6 +336,7 @@ function fetchPlotData(dispatch, tblId, xyPlotParams, chartId) { dispatch(updatePlotData( { isPlotDataReady : true, + tblSource, // when zoomed, we don't know if the unzoomed data are decimated or not decimatedUnzoomed: Boolean(tableMeta['decimate_key']) || (xyPlotParams.zoom ? undefined : false), xyPlotParams, diff --git a/src/firefly/js/visualize/saga/ChartsSync.js b/src/firefly/js/visualize/saga/ChartsSync.js index ccc1b07092..0e9ddd41ee 100644 --- a/src/firefly/js/visualize/saga/ChartsSync.js +++ b/src/firefly/js/visualize/saga/ChartsSync.js @@ -3,17 +3,19 @@ */ import {take} from 'redux-saga/effects'; -import {has} from 'lodash'; +import {get} from 'lodash'; import {flux} from '../../Firefly.js'; +import {logError} from '../../util/WebUtil.js'; import * as TablesCntlr from '../../tables/TablesCntlr.js'; import * as TableStatsCntlr from '../TableStatsCntlr.js'; import * as TableUtil from '../../tables/TableUtil.js'; import * as XYPlotCntlr from '../XYPlotCntlr.js'; +import * as ChartsCntlr from '../ChartsCntlr.js'; import * as HistogramCntlr from '../HistogramCntlr.js'; -import {hasRelatedCharts, getDefaultXYPlotParams} from '../ChartUtil.js'; +import {SCATTER, HISTOGRAM, getChartSpace, hasRelatedCharts, getDefaultXYPlotParams} from '../ChartUtil.js'; /** * this saga handles chart related side effects @@ -22,49 +24,59 @@ export function* syncCharts() { var tableStatsState, xyPlotState, histogramState; while (true) { - const action= yield take([TableStatsCntlr.SETUP_TBL_TRACKING, TablesCntlr.TABLE_NEW_LOADED, TablesCntlr.TABLE_SORT]); + const action= yield take([ChartsCntlr.CHART_MOUNTED, TablesCntlr.TABLE_NEW_LOADED, TablesCntlr.TABLE_SORT]); + if (!ChartsCntlr.getNumRelatedCharts()) { continue; } const request= action.payload.request; switch (action.type) { - case TableStatsCntlr.SETUP_TBL_TRACKING: - const {tblId} = action.payload; + case ChartsCntlr.CHART_MOUNTED: + const {tblId, chartId, chartType} = action.payload; if (TableUtil.isFullyLoaded(tblId)) { - TableStatsCntlr.dispatchLoadTblStats(TableUtil.getTblById(tblId)['request']); - if (!hasRelatedCharts(tblId)) { - // default chart is xy plot of coordinate columns or first two numeric columns - const defaultParams = getDefaultXYPlotParams(tblId); - if (defaultParams) { - XYPlotCntlr.dispatchLoadPlotData(tblId, defaultParams, tblId); + if (ChartsCntlr.getNumRelatedCharts(tblId)===1) { + TableStatsCntlr.dispatchLoadTblStats(TableUtil.getTblById(tblId)['request']); + if (!hasRelatedCharts(tblId)) { + // default chart is xy plot of coordinate columns or first two numeric columns + const defaultParams = getDefaultXYPlotParams(tblId); + if (defaultParams) { + XYPlotCntlr.dispatchLoadPlotData(tblId, defaultParams, tblId); + } } } + updateChartDataIfNeeded(tblId, chartId, chartType); } + break; case TablesCntlr.TABLE_SORT: case TablesCntlr.TABLE_NEW_LOADED: const {tbl_id} = action.payload; - let hasRelated = false; - xyPlotState = flux.getState()[XYPlotCntlr.XYPLOT_DATA_KEY]; - Object.keys(xyPlotState).forEach((cid) => { - if (xyPlotState[cid].tblId === tbl_id) { - const xyPlotParams = xyPlotState[cid].xyPlotParams; - XYPlotCntlr.dispatchLoadPlotData(cid, xyPlotParams, tbl_id); - hasRelated = true; - } - }); - - // table statistics and histogram data do not change on table sort - if (action.type !== TablesCntlr.TABLE_SORT) { - histogramState = flux.getState()[HistogramCntlr.HISTOGRAM_DATA_KEY]; - Object.keys(histogramState).forEach((cid) => { - if (histogramState[cid].tblId === tbl_id) { - const histogramParams = histogramState[cid].histogramParams; - HistogramCntlr.dispatchLoadColData(cid, histogramParams, tbl_id); + // check if there are any mounted charts related to this table + if (ChartsCntlr.getNumRelatedCharts(tbl_id, true) > 0) { + let hasRelated = false; + xyPlotState = getChartSpace(SCATTER); + Object.keys(xyPlotState).forEach((cid) => { + if (xyPlotState[cid].tblId === tbl_id) { hasRelated = true; + if (ChartsCntlr.isChartMounted(tbl_id, cid, SCATTER)) { + const xyPlotParams = xyPlotState[cid].xyPlotParams; + XYPlotCntlr.dispatchLoadPlotData(cid, xyPlotParams, tbl_id); + } } }); - tableStatsState = flux.getState()[TableStatsCntlr.TBLSTATS_DATA_KEY]; - if (has(tableStatsState, tbl_id)) { + // table statistics and histogram data do not change on table sort + if (action.type !== TablesCntlr.TABLE_SORT) { + histogramState = getChartSpace(HISTOGRAM); + Object.keys(histogramState).forEach((cid) => { + if (histogramState[cid].tblId === tbl_id) { + hasRelated = true; + if (ChartsCntlr.isChartMounted(tbl_id, cid, 'histogram')) { + const histogramParams = histogramState[cid].histogramParams; + HistogramCntlr.dispatchLoadColData(cid, histogramParams, tbl_id); + } + } + }); + + tableStatsState = flux.getState()[TableStatsCntlr.TBLSTATS_DATA_KEY]; TableStatsCntlr.dispatchLoadTblStats(request); if (!hasRelated) { // default chart is xy plot of coordinate columns or first two numeric columns @@ -75,7 +87,6 @@ export function* syncCharts() { } } } - break; } } @@ -83,3 +94,26 @@ export function* syncCharts() { +function updateChartDataIfNeeded(tblId, chartId, chartType) { + const tblSource = get(TableUtil.getTblById(tblId), 'tableMeta.source'); + const chartSpace = getChartSpace(chartType); + const chartTblSource = get(chartSpace,[chartId,'tblSource']); + if (tblSource && (!chartTblSource || chartTblSource !== tblSource)) { + switch(chartType) { + case SCATTER: + const xyPlotParams = get(chartSpace, [chartId, 'xyPlotParams']); + if (xyPlotParams) { + XYPlotCntlr.dispatchLoadPlotData(chartId, xyPlotParams, tblId); + } + break; + case HISTOGRAM: + const histogramParams = get(chartSpace, [chartId, 'histogramParams']); + if (histogramParams) { + HistogramCntlr.dispatchLoadColData(chartId, histogramParams, tblId); + } + break; + default: + logError(`Unknown chart type ${chartType}`); + } + } +}