diff --git a/src/firefly/js/charts/ChartUtil.js b/src/firefly/js/charts/ChartUtil.js index 2aa429fba3..002683c6d7 100644 --- a/src/firefly/js/charts/ChartUtil.js +++ b/src/firefly/js/charts/ChartUtil.js @@ -120,7 +120,7 @@ function colWithName(cols, name) { function getNumericCols(cols) { const ncols = []; cols.forEach((c) => { - if (c.type.match(/^[dfil]/) != null) { // int, float, double, long .. or their short form. + if (c.type.match(/^[dfil]/) !== null) { // int, float, double, long .. or their short form. ncols.push(c); } }); @@ -153,7 +153,7 @@ export function getDefaultXYPlotParams(tbl_id) { // otherwise use the first one-two numeric columns if (!isCatalog) { const numericCols = getNumericCols(tableData.columns); - if (numericCols.length > 2) { + if (numericCols.length >= 2) { xCol = numericCols[0]; yCol = numericCols[1]; } else if (numericCols.length > 1) { diff --git a/src/firefly/js/charts/XYPlotCntlr.js b/src/firefly/js/charts/XYPlotCntlr.js index 74d1e0c34c..e85ea7fea5 100644 --- a/src/firefly/js/charts/XYPlotCntlr.js +++ b/src/firefly/js/charts/XYPlotCntlr.js @@ -313,27 +313,48 @@ function getDataBoundaries(xyPlotData) { } } +function getPaddedRange(min, max, isLog, factor) { + const range = max - min; + let paddedMin = min; + let paddedMax = max; + + if (range > 0) { + if (isLog) { + const minLog = Math.log10(min); + const maxLog = Math.log10(max); + const padLog = (maxLog - minLog) / factor; + paddedMin = Math.pow(10, (minLog-padLog)); + paddedMax = Math.pow(10, (maxLog+padLog)); + } else { + const pad = range / factor; + paddedMin = min - pad; + paddedMax = max + pad; + } + } + return {paddedMin, paddedMax}; +} /** * Pad and round data boundaries + * @param {Object} xyPlotParams - object with XY plot params * @param {Object} boundaries - object with xMin, xMax, yMin, yMax props * @param {Number} factor - part of the range to add on both sides */ -function getPaddedBoundaries(boundaries, factor=100) { +function getPaddedBoundaries(xyPlotParams, boundaries, factor=100) { if (!isEmpty(boundaries)) { let {xMin, xMax, yMin, yMax} = boundaries; - const xRange = xMax - xMin; + const xRange = xMax - xMin; if (xRange > 0) { - const xPad = xRange/factor; - xMin = xMin - xPad; - xMax = xMax + xPad; + const xOptions = get(xyPlotParams, 'x.options'); + const xLog = xOptions && xOptions.includes('log') && xMin>0; + ({paddedMin:xMin, paddedMax:xMax} = getPaddedRange(xMin, xMax, xLog, factor)); } const yRange = yMax - yMin; if (yRange > 0) { - const yPad = yRange/factor; - yMin = yMin - yPad; - yMax = yMax + yPad; + const yOptions = get(xyPlotParams, 'y.options'); + const yLog = yOptions && yOptions.includes('log') && yMin>0; + ({paddedMin:yMin, paddedMax:yMax} = getPaddedRange(yMin, yMax, yLog, factor)); } if (xRange > 0 || yRange > 0) { return {xMin, xMax, yMin, yMax}; @@ -449,7 +470,7 @@ function getUpdatedParams(xyPlotParams, tableModel, dataBoundaries) { const userSetBoundaries = get(xyPlotParams, 'userSetBoundaries', {}); const boundaries = Object.assign({}, userSetBoundaries); if (Object.keys(boundaries).length < 4 && !isEmpty(dataBoundaries)) { - const paddedDataBoundaries = getPaddedBoundaries(dataBoundaries); + const paddedDataBoundaries = getPaddedBoundaries(xyPlotParams, dataBoundaries); const [xMin, xMax, yMin, yMax] = ['xMin', 'xMax', 'yMin', 'yMax'].map( (v) => { return (Number.isFinite(boundaries[v]) ? boundaries[v] : paddedDataBoundaries[v]); }); diff --git a/src/firefly/js/charts/ui/XYPlot.jsx b/src/firefly/js/charts/ui/XYPlot.jsx index 580de8017e..e8e0012fc4 100644 --- a/src/firefly/js/charts/ui/XYPlot.jsx +++ b/src/firefly/js/charts/ui/XYPlot.jsx @@ -1,7 +1,7 @@ /* * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt */ -import {isUndefined, debounce, get} from 'lodash'; +import {isUndefined, debounce, get, omit} from 'lodash'; import shallowequal from 'shallowequal'; import React, {PropTypes} from 'react'; import ReactHighcharts from 'react-highcharts/bundle/highcharts'; @@ -57,6 +57,7 @@ const plotDataShape = PropTypes.shape({ const DATAPOINTS = 'data'; const SELECTED = 'selected'; const HIGHLIGHTED = 'highlighted'; +const MINMAX = 'minmax'; const datapointsColor = 'rgba(63, 127, 191, 0.5)'; const selectedColor = 'rgba(21, 138, 15, 0.5)'; @@ -247,115 +248,125 @@ export class XYPlot extends React.Component { } shouldComponentUpdate(nextProps) { + + const propsToOmit = ['onHighlightChange', 'onSelection', 'highlighted']; + + // no update is needed if properties did ot change + if (shallowequal(omit(this.props, propsToOmit), omit(nextProps, propsToOmit)) && + get(this.props,'highlighted.rowIdx') == get(nextProps,'highlighted.rowIdx')) { + return false; + } + const {data, width, height, params, highlighted, selectInfo, desc} = this.props; - // only rerender when plot data change + + // only re-render when the plot data change or an error occurs + // shading change for density plot changes series if (nextProps.data !== data || get(params, 'shading', defaultShading) !== get(nextProps.params, 'shading', defaultShading)) { return true; } else { const chart = this.refs.chart && this.refs.chart.getChart(); - if (chart) { - const {params:newParams, highlighted:newHighlighted, width:newWidth, height:newHeight, desc:newDesc } = nextProps; - if (newDesc !== desc) { - chart.setTitle(newDesc, undefined, false); - } - - // selection change (selection is not supported for decimated data) - if (data && data.rows && !data.decimateKey && nextProps.selectInfo !== selectInfo) { - const selectedData = []; - if (nextProps.selectInfo) { - const selectInfoCls = SelectInfo.newInstance(nextProps.selectInfo, 0); - data.rows.forEach((arow) => { - if (selectInfoCls.isSelected(Number(arow[2]))) { - const nrow = arow.map(toNumber); - selectedData.push({x: nrow[0], y: nrow[1], rowIdx: nrow[2]}); - } - }); - } - chart.get(SELECTED).setData(selectedData); - } - - // highlight change - if (!shallowequal(highlighted, newHighlighted)) { - const highlightedData = []; - if (!isUndefined(newHighlighted)) { - highlightedData.push(newHighlighted); + if (chart && chart.container && !this.error) { + const {params:newParams, width:newWidth, height:newHeight, highlighted:newHighlighted, selectInfo:newSelectInfo, desc:newDesc } = nextProps; + try { + if (newDesc !== desc) { + chart.setTitle(newDesc, undefined, false); } - chart.get(HIGHLIGHTED).setData(highlightedData); - } - - // plot parameters change - if (params !== newParams) { - const xoptions = {}; - const yoptions = {}; - const newXOptions = getXAxisOptions(newParams); - const newYOptions = getYAxisOptions(newParams); - if (!shallowequal(getXAxisOptions(params), newXOptions)) { - Object.assign(xoptions, { - title: {text: newXOptions.xTitle}, - gridLineWidth: newXOptions.xGrid ? 1 : 0, - reversed: newXOptions.xReversed, - opposite: newYOptions.yReversed, - type: newXOptions.xLog && canUseXLog(nextProps.data) ? 'logarithmic' : 'linear' - }); - } - if (!shallowequal(getYAxisOptions(params), newYOptions)) { - Object.assign(yoptions, { - title: {text: newYOptions.yTitle}, - gridLineWidth: newYOptions.yGrid ? 1 : 0, - reversed: newYOptions.yReversed, - type: newYOptions.yLog && canUseYLog(nextProps.data) ? 'logarithmic' : 'linear' - }); - } - if (!shallowequal(params.boundaries, newParams.boundaries)) { - const {xMin, xMax, yMin, yMax} = getZoomSelection(newParams); - const {xMin:xDataMin, xMax:xDataMax, yMin:yDataMin, yMax:yDataMax} = nextProps.data; - Object.assign(xoptions, {min: selFinite(xMin,xDataMin), max: selFinite(xMax, xDataMax)}); - Object.assign(yoptions, {min: selFinite(yMin, yDataMin), max: selFinite(yMax, yDataMax)}); - } - if (!shallowequal(params.zoom, newParams.zoom) || - !shallowequal(params.boundaries, newParams.boundaries)) { - const {xMin, xMax, yMin, yMax} = getZoomSelection(newParams); - const {xMin:xDataMin, xMax:xDataMax, yMin:yDataMin, yMax:yDataMax} = get(newParams, 'boundaries', {}); - Object.assign(xoptions, {min: selFinite(xMin,xDataMin), max: selFinite(xMax, xDataMax)}); - Object.assign(yoptions, {min: selFinite(yMin, yDataMin), max: selFinite(yMax, yDataMax)}); - } - const xUpdate = Reflect.ownKeys(xoptions).length > 0; - const yUpdate = Reflect.ownKeys(yoptions).length > 0; - if (xUpdate || yUpdate) { - const animate = this.shouldAnimate(); - xUpdate && chart.xAxis[0].update(xoptions, !yUpdate, animate); - yUpdate && chart.yAxis[0].update(yoptions, true, animate); + // selection change (selection is not supported for decimated data) + if (data && data.rows && !data.decimateKey && newSelectInfo !== selectInfo) { + const selectedData = []; + if (newSelectInfo) { + const selectInfoCls = SelectInfo.newInstance(newSelectInfo, 0); + data.rows.forEach((arow) => { + if (selectInfoCls.isSelected(Number(arow[2]))) { + const nrow = arow.map(toNumber); + selectedData.push({x: nrow[0], y: nrow[1], rowIdx: nrow[2]}); + } + }); + } + chart.get(SELECTED).setData(selectedData); } - if (!shallowequal(params.selection, newParams.selection)) { - this.updateSelectionRect(newParams.selection); + // highlight change + if (!shallowequal(highlighted, newHighlighted)) { + const highlightedData = []; + if (!isUndefined(newHighlighted)) { + highlightedData.push(newHighlighted); + } + chart.get(HIGHLIGHTED).setData(highlightedData); } - } - - // size change - if (newWidth !== width || newHeight !== height || - newParams.xyRatio !== params.xyRatio ||newParams.stretch != params.stretch) { - const {chartWidth, chartHeight} = calculateChartSize(newWidth, newHeight, nextProps); - if (Math.abs(chart.chartWidth - chartWidth) > 20 || Math.abs(chart.chartHeight - chartHeight) > 20) { - if (get(nextProps, ['data', 'decimateKey'])) { - // hide all series for resize - chart.series.forEach((series) => { - series.setVisible(false, false); + // plot parameters change + if (params !== newParams) { + const xoptions = {}; + const yoptions = {}; + const newXOptions = getXAxisOptions(newParams); + const newYOptions = getYAxisOptions(newParams); + if (!shallowequal(getXAxisOptions(params), newXOptions)) { + Object.assign(xoptions, { + title: {text: newXOptions.xTitle}, + gridLineWidth: newXOptions.xGrid ? 1 : 0, + reversed: newXOptions.xReversed, + opposite: newYOptions.yReversed, + type: newXOptions.xLog && canUseXLog(nextProps.data) ? 'logarithmic' : 'linear' }); - chart.showLoading('Resizing'); } - chart.setSize(chartWidth, chartHeight, false); + if (!shallowequal(getYAxisOptions(params), newYOptions)) { + Object.assign(yoptions, { + title: {text: newYOptions.yTitle}, + gridLineWidth: newYOptions.yGrid ? 1 : 0, + reversed: newYOptions.yReversed, + type: newYOptions.yLog && canUseYLog(nextProps.data) ? 'logarithmic' : 'linear' + }); + } + if (!shallowequal(params.zoom, newParams.zoom) || !shallowequal(params.boundaries, newParams.boundaries)) { + const {xMin, xMax, yMin, yMax} = getZoomSelection(newParams); + const {xMin:xDataMin, xMax:xDataMax, yMin:yDataMin, yMax:yDataMax} = get(newParams, 'boundaries', {}); + Object.assign(xoptions, {min: selFinite(xMin, xDataMin), max: selFinite(xMax, xDataMax)}); + Object.assign(yoptions, {min: selFinite(yMin, yDataMin), max: selFinite(yMax, yDataMax)}); + chart.get(MINMAX).setData([[xoptions.min, yoptions.min], [xoptions.max, yoptions.max]]); + } + const xUpdate = Reflect.ownKeys(xoptions).length > 0; + const yUpdate = Reflect.ownKeys(yoptions).length > 0; + if (xUpdate || yUpdate) { + const animate = this.shouldAnimate(); + xUpdate && chart.xAxis[0].update(xoptions, !yUpdate, animate); + yUpdate && chart.yAxis[0].update(yoptions, true, animate); + } + + if (!shallowequal(params.selection, newParams.selection)) { + this.updateSelectionRect(newParams.selection); + } + + } - if (this.pendingResize) { - // if resize is slow, we want to do it only once - this.pendingResize.cancel(); + // size change + if (newWidth !== width || newHeight !== height || + newParams.xyRatio !== params.xyRatio || newParams.stretch != params.stretch) { + const {chartWidth, chartHeight} = calculateChartSize(newWidth, newHeight, nextProps); + if (Math.abs(chart.chartWidth - chartWidth) > 20 || Math.abs(chart.chartHeight - chartHeight) > 20) { + + if (get(nextProps, ['data', 'decimateKey'])) { + // hide all series for resize + chart.series.forEach((series) => { + series.setVisible(false, false); + }); + chart.showLoading('Resizing'); + } + chart.setSize(chartWidth, chartHeight, false); + + if (this.pendingResize) { + // if resize is slow, we want to do it only once + this.pendingResize.cancel(); + } + this.pendingResize = this.debouncedResize(); + this.pendingResize(); } - this.pendingResize = this.debouncedResize(); - this.pendingResize(); } + } catch (error) { + this.error = error; + chart.showLoading(error); } return false; @@ -375,7 +386,7 @@ export class XYPlot extends React.Component { debouncedResize() { return debounce(() => { const chart = this.refs.chart && this.refs.chart.getChart(); - if (chart) { + if (chart && chart.container) { const {data} = this.props; if (data.decimateKey) { // update marker's size @@ -438,12 +449,14 @@ export class XYPlot extends React.Component { const xAxis = event.xAxis[0]; const yAxis = event.yAxis[0]; - this.props.onSelection({xMin: xAxis.min, xMax: xAxis.max, yMin: yAxis.min, yMax: yAxis.max}); + if (xAxis && yAxis) { + this.props.onSelection({xMin: xAxis.min, xMax: xAxis.max, yMin: yAxis.min, yMax: yAxis.max}); + } } makeSeries(chart) { //const chart = this.refs.chart && this.refs.chart.getChart(); - if (chart) { + if (chart && chart.container) { const {data, params, selectInfo, highlighted, onHighlightChange} = this.props; const {rows, decimateKey, weightMin, weightMax} = data; @@ -555,21 +568,31 @@ export class XYPlot extends React.Component { } - allSeries.forEach((series) => {chart.addSeries(series,false, false);}); - - chart.addSeries({ - id: HIGHLIGHTED, - name: HIGHLIGHTED, - color: highlightedColor, - marker: {symbol: 'circle', lineColor: '#404040', lineWidth: 1, radius: 4}, - data: highlightedData, - showInLegend: false - }, true, false); + + try { + allSeries.forEach((series) => { + chart.addSeries(series, false, false); + }); + + chart.addSeries({ + id: HIGHLIGHTED, + name: HIGHLIGHTED, + color: highlightedColor, + marker: {symbol: 'circle', lineColor: '#404040', lineWidth: 1, radius: 4}, + data: highlightedData, + showInLegend: false + }, true, false); + } catch (error) { + this.error = error; + chart.showLoading(error); + } } } render() { + this.error = undefined; + const {data, params, width, height, onSelection, desc} = this.props; const onSelectionEvent = this.onSelectionEvent; @@ -691,12 +714,12 @@ export class XYPlot extends React.Component { type: yLog && canUseYLog(data) ? 'logarithmic' : 'linear' }, series: [{ - // This series is to make sure the axis are created. + // This series is to make sure the axes are created. // Without actual series, xAxis creation is deferred // and there is no way to get value to pixel conversion // for sizing the symbol - id: 'minmax', - name: 'minmax', + id: MINMAX, + name: MINMAX, color: 'rgba(240, 240, 240, 0.1)', marker: {radius: 1}, data: [[selFinite(xMin, xDataMin), selFinite(yMin,yDataMin)], [selFinite(xMax, xDataMax), selFinite(yMax,yDataMax)]], @@ -713,7 +736,6 @@ export class XYPlot extends React.Component { } }; - return (