diff --git a/src/firefly/js/ui/SuggestBoxInputField.jsx b/src/firefly/js/ui/SuggestBoxInputField.jsx index 69d5ded799..9d426f21d3 100644 --- a/src/firefly/js/ui/SuggestBoxInputField.jsx +++ b/src/firefly/js/ui/SuggestBoxInputField.jsx @@ -109,8 +109,8 @@ class SuggestBoxInputFieldView extends Component { this.state = { isOpen: false, displayValue: props.value, - valid: true, - message: '', + valid: get(props, 'valid', true), + message: get(props.message, ''), inputWidth: undefined, suggestions: [], mouseTrigger: false @@ -123,6 +123,12 @@ class SuggestBoxInputFieldView extends Component { this.updateSuggestions = debounce(this.updateSuggestions.bind(this), 200); } + componentWillReceiveProps(nextProps) { + const {valid, message, value} = nextProps; + if (valid !== this.state.valid || message !== this.state.message || value !== this.state.displayValue) { + this.setState({valid, message, displayValue: value}); + } + } onValueChange(ev) { var displayValue = get(ev, 'target.value'); @@ -230,7 +236,7 @@ class SuggestBoxInputFieldView extends Component { /> - {isOpen &&
this.setState({highlightedIdx : undefined})}> + {isOpen &&
this.setState({highlightedIdx : undefined})}> { - const x = Number(arow[xIdx]); - const y = Number(arow[yIdx]); - if (x>=xMin && x<=xMax && y>=yMin && y<=yMax) { - selectInfoCls.setRowSelect(index, true); - } - }); - const selectInfo = selectInfoCls.data; - TablesCntlr.dispatchTableSelect(tblId, selectInfo); + 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 + } else { + const {tblId, tableModel} = this.props; + const selection = get(this.props, 'tblPlotData.xyPlotParams.selection'); + const rows = get(this.props, 'tblPlotData.xyPlotData.rows'); + if (tableModel && rows && selection) { + const {xMin, xMax, yMin, yMax} = selection; + const selectInfoCls = SelectInfo.newInstance({rowCount: tableModel.totalRows}); + // add all rows which fall into selection + const xIdx = 0, yIdx = 1, rowIdx = 2; + rows.forEach((arow) => { + const x = Number(arow[xIdx]); + const y = Number(arow[yIdx]); + if (x >= xMin && x <= xMax && y >= yMin && y <= yMax) { + selectInfoCls.setRowSelect(Number(arow[rowIdx]), true); + } + }); + const selectInfo = selectInfoCls.data; + TablesCntlr.dispatchTableSelect(tblId, selectInfo); + } } } } @@ -389,15 +396,19 @@ class ChartsPanel extends React.Component { selectionNotEmpty(selection) { const rows = get(this.props, 'tblPlotData.xyPlotData.rows'); - if (rows && selection) { - const {xMin, xMax, yMin, yMax} = selection; - const xIdx = 0, yIdx = 1; - const aPt = rows.find((arow) => { - const x = Number(arow[xIdx]); - const y = Number(arow[yIdx]); - return (x >= xMin && x <= xMax && y >= yMin && y <= yMax); - }); - return Boolean(aPt); + if (rows) { + if (selection) { + const {xMin, xMax, yMin, yMax} = selection; + const xIdx = 0, yIdx = 1; + const aPt = rows.find((arow) => { + const x = Number(arow[xIdx]); + const y = Number(arow[yIdx]); + return (x >= xMin && x <= xMax && y >= yMin && y <= yMax); + }); + return Boolean(aPt); + } else { + return true; // empty selection replacing non-empty + } } else { return false; } @@ -413,7 +424,7 @@ class ChartsPanel extends React.Component { src={ZOOM_IN} onClick={() => this.addZoom()} /> - {!get(this.props, 'tblPlotData.xyPlotData.decimateKey') && this.addSelection()} diff --git a/src/firefly/js/visualize/XYPlot.jsx b/src/firefly/js/visualize/XYPlot.jsx index 4c11f1b639..587aeccd9f 100644 --- a/src/firefly/js/visualize/XYPlot.jsx +++ b/src/firefly/js/visualize/XYPlot.jsx @@ -1,7 +1,7 @@ /* * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt */ -import {isUndefined, debounce} from 'lodash'; +import {isUndefined, debounce, get} from 'lodash'; import shallowequal from 'shallowequal'; import React, {PropTypes} from 'react'; import ReactHighcharts from 'react-highcharts/bundle/highcharts'; @@ -15,10 +15,7 @@ export const axisParamsShape = PropTypes.shape({ columnOrExpr : PropTypes.string, label : PropTypes.string, unit : PropTypes.string, - options : PropTypes.string, // ex. 'grid,log,flip' - nbins : PropTypes.number, - min : PropTypes.number, - max : PropTypes.number + options : PropTypes.string // ex. 'grid,log,flip' }); export const selectionShape = PropTypes.shape({ @@ -33,12 +30,14 @@ export const plotParamsShape = PropTypes.shape({ stretch : PropTypes.oneOf(['fit','fill']), selection : selectionShape, zoom : selectionShape, + nbins : PropTypes.shape({x : PropTypes.number, y : PropTypes.number}), + shading : PropTypes.oneOf(['lin', 'log']), x : axisParamsShape, y : axisParamsShape }); const plotDataShape = PropTypes.shape({ - rows: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)), + rows: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)), // [x,y,rowIdx,weight] decimateKey: PropTypes.string, xMin: PropTypes.number, xMax: PropTypes.number, @@ -49,11 +48,11 @@ const plotDataShape = PropTypes.shape({ idStr: PropTypes.string }); -const UNSELECTED = 'unselected'; +const DATAPOINTS = 'data'; const SELECTED = 'selected'; const HIGHLIGHTED = 'highlighted'; -const unselectedColor = 'rgba(63, 127, 191, 0.5)'; +const datapointsColor = 'rgba(63, 127, 191, 0.5)'; const selectedColor = 'rgba(21, 138, 15, 0.5)'; const highlightedColor = 'rgba(250, 243, 40, 1)'; const selectionRectColor = 'rgba(165, 165, 165, 0.5)'; @@ -102,7 +101,10 @@ const getWeightBasedGroup = function(weight, minWeight, maxWeight, logShading=tr // should not reach }; - +const getWeightedDataDescr = function(defaultDescr, numericData, minWeight, maxWeight, logShading) { + if (numericData.length < 1) { return defaultDescr; } + return getWeightBasedGroup(numericData[0].weight, minWeight, maxWeight, logShading, false); +}; const getXAxisOptions = function(params) { const xTitle = params.x.label + (params.x.unit ? ', ' + params.x.unit : ''); @@ -144,7 +146,7 @@ const selFinite = (v1, v2) => {return Number.isFinite(v1) ? v1 : v2;}; @param options {object} has hD - half difference between width and height of the rectangle */ ReactHighcharts.Highcharts.SVGRenderer.prototype.symbols.rectangle = function (x, y, w, h, options) { - const hD = !isUndefined(options.hD) ? options.hD : Math.round(h/4); + const hD = get(options, 'hD', Math.round(h/4)); // SVG path for the rectangle return [ 'M', x, y+hD, @@ -198,8 +200,8 @@ export class XYPlot extends React.Component { shouldComponentUpdate(nextProps) { const {data, width, height, params, highlighted, selectInfo, desc} = this.props; - if (nextProps.data !== data || - (nextProps.selectInfo !== selectInfo && !data.decimateKey)) { + // only rerender when plot data change + if (nextProps.data !== data) { return true; } else { const chart = this.refs.chart && this.refs.chart.getChart(); @@ -208,6 +210,23 @@ export class XYPlot extends React.Component { 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)) { @@ -216,6 +235,8 @@ export class XYPlot extends React.Component { chart.get(HIGHLIGHTED).setData(highlightedData); } + + // plot parameters change if (params !== newParams) { const xoptions = {}; const yoptions = {}; @@ -247,6 +268,7 @@ export class XYPlot extends React.Component { } + // size change if (newWidth !== width || newHeight !== height) { const {chartWidth, chartHeight} = this.calculateChartSize(newWidth, newHeight); chart.setSize(chartWidth, chartHeight, false); @@ -280,6 +302,9 @@ export class XYPlot extends React.Component { 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); @@ -300,7 +325,7 @@ export class XYPlot extends React.Component { // update marker's size const {xUnitPx, yUnitPx} = getDeciSymbolSize(chart, data.decimateKey); chart.series.forEach((series) => { - series.name.includes(UNSELECTED) && series.update({ + series.name.includes(DATAPOINTS) && series.update({ marker: {radius: xUnitPx/2.0, hD: (xUnitPx-yUnitPx)/2.0}}, false); }); chart.redraw(); @@ -361,7 +386,7 @@ export class XYPlot extends React.Component { makeSeries(chart) { //const chart = this.refs.chart && this.refs.chart.getChart(); if (chart) { - const {data, selectInfo, highlighted, onHighlightChange} = this.props; + const {data, params, selectInfo, highlighted, onHighlightChange} = this.props; const {rows, decimateKey, weightMin, weightMax} = data; let allSeries, marker; @@ -388,32 +413,35 @@ export class XYPlot extends React.Component { let pushFunc; if (selectInfo) { const selectInfoCls = SelectInfo.newInstance(selectInfo, 0); - // split data into selected and unselected - pushFunc = (numdata, nrow, idx) => { - selectInfoCls.isSelected(idx) ? - numdata.selected.push({x: nrow[0], y: nrow[1], rowIdx: nrow[2]}) : - numdata.unselected.push({x: nrow[0], y: nrow[1], rowIdx: nrow[2]}); + // set all and selected data + pushFunc = (numdata, nrow) => { + numdata.all.push({x: nrow[0], y: nrow[1], rowIdx: nrow[2]}); + if (selectInfoCls.isSelected(nrow[2])) { + numdata.selected.push({x: nrow[0], y: nrow[1], rowIdx: nrow[2]}); + } }; } else { pushFunc = (numdata, nrow) => { - numdata.unselected.push({x: nrow[0], y: nrow[1], rowIdx: nrow[2]}); + numdata.all.push({x: nrow[0], y: nrow[1], rowIdx: nrow[2]}); }; } - const numericData = rows.reduce((numdata, arow, idx) => { + const numericData = rows.reduce((numdata, arow) => { const nrow = arow.map(toNumber); - pushFunc(numdata, nrow, idx); + pushFunc(numdata, nrow); return numdata; - }, {selected: [], unselected: []}); + }, {selected: [], all: []}); + marker = {symbol: 'circle'}; allSeries = [ { - id: UNSELECTED, - name: UNSELECTED, - color: unselectedColor, - data: numericData.unselected, + id: DATAPOINTS, + name: DATAPOINTS, + color: datapointsColor, + data: numericData.all, marker, turboThreshold: 0, + showInLegend: false, point }, { @@ -423,6 +451,7 @@ export class XYPlot extends React.Component { data: numericData.selected, marker, turboThreshold: 0, + showInLegend: false, point } ]; @@ -445,7 +474,7 @@ export class XYPlot extends React.Component { for (var i= 0, l = rows.length; i < l; i++) { const nrow = rows[i].map(toNumber); const weight = nrow[3]; - const group = getWeightBasedGroup(weight, weightMin, weightMax); + const group = getWeightBasedGroup(weight, weightMin, weightMax, params.shading==='log'); const {x,y} = getCenter(nrow[0], nrow[1]); numericDataArr[group-1].push({x, y, rowIdx: nrow[2], weight}); } @@ -455,12 +484,13 @@ export class XYPlot extends React.Component { allSeries = numericDataArr.map((numericData, idx) => { return { - id: UNSELECTED+idx, - name: UNSELECTED+idx, + id: DATAPOINTS+idx, + name: getWeightedDataDescr(DATAPOINTS+idx, numericData, weightMin, weightMax, params.shading==='log'), color: weightBasedColors[idx], data: numericData, marker, turboThreshold: 0, + showInLegend: numericData.length>0 && (xUnitPx>2 && xUnitPx<20 && yUnitPx>2 && yUnitPx<20), // legend symbol size can not be adjusted as of now point }; }); @@ -474,7 +504,8 @@ export class XYPlot extends React.Component { name: HIGHLIGHTED, color: highlightedColor, marker: {symbol: 'circle', lineColor: '#404040', lineWidth: 1, radius: 5}, - data: highlightedData + data: highlightedData, + showInLegend: false }, true, false); } } @@ -493,6 +524,8 @@ export class XYPlot extends React.Component { const makeSeries = this.makeSeries; + const component = this; + var config = { chart: { animation: this.shouldAnimate(), @@ -505,6 +538,11 @@ export class XYPlot extends React.Component { borderWidth: 3, zoomType: 'xy', events: { + click() { + if (component.props.params.selection && onSelection) { + onSelection(); + } + }, selection(event) { if (onSelection) { onSelectionEvent(event); @@ -531,7 +569,13 @@ export class XYPlot extends React.Component { enabled: true }, legend: { - enabled: false + enabled: true, + align: 'right', + layout: 'vertical', + verticalAlign: 'top', + symbolHeight: 12, + symbolWidth: 12, + symbolRadius: 6 }, title: { text: desc @@ -592,7 +636,8 @@ export class XYPlot extends React.Component { name: 'minmax', color: '#f0f0f0', marker: {radius: 2}, - data: [[selFinite(xMin, xDataMin), selFinite(yMin,yDataMin)], [selFinite(xMax, xDataMax), selFinite(yMax,yDataMax)]] + data: [[selFinite(xMin, xDataMin), selFinite(yMin,yDataMin)], [selFinite(xMax, xDataMax), selFinite(yMax,yDataMax)]], + showInLegend: false }], credits: { enabled: false // removes a reference to Highcharts.com from the chart @@ -601,7 +646,7 @@ export class XYPlot extends React.Component { return ( -
+
); diff --git a/src/firefly/js/visualize/XYPlotCntlr.js b/src/firefly/js/visualize/XYPlotCntlr.js index e28cdbcfb4..4d9cad546e 100644 --- a/src/firefly/js/visualize/XYPlotCntlr.js +++ b/src/firefly/js/visualize/XYPlotCntlr.js @@ -41,6 +41,8 @@ const RESET_ZOOM = `${XYPLOT_DATA_KEY}/RESET_ZOOM`; xyPlotParams: { title: string xyRatio: number + nbins {x,y} + shading: string (lin|log) selection: {xMin, xMax, yMin, yMax} // currently selected rectangle zoom: {xMin, xMax, yMin, yMax} // currently zoomed rectangle stretch: string (fit|fill) @@ -49,18 +51,12 @@ const RESET_ZOOM = `${XYPLOT_DATA_KEY}/RESET_ZOOM`; label unit options: [grid,log,flip] - nbins - min - max } y: { columnOrExpr label unit options: [grid,log,flip] - nbins - min - max } } */ @@ -244,11 +240,19 @@ function fetchPlotData(dispatch, activeTableServerRequest, xyPlotParams, chartId 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, 10000, 1.0, ...limits) + 'decimate' : serializeDecimateInfo(xyPlotParams.x.columnOrExpr, xyPlotParams.y.columnOrExpr, maxBins, xyRatio, ...limits) }); req.tbl_id = `xy-${chartId}`; diff --git a/src/firefly/js/visualize/XYPlotOptions.jsx b/src/firefly/js/visualize/XYPlotOptions.jsx index dc16aaad26..bac4588c63 100644 --- a/src/firefly/js/visualize/XYPlotOptions.jsx +++ b/src/firefly/js/visualize/XYPlotOptions.jsx @@ -3,7 +3,7 @@ */ import React, {PropTypes} from 'react'; -import {get} from 'lodash'; +import {get, isUndefined, omitBy} from 'lodash'; import ColValuesStatistics from './ColValuesStatistics.js'; import CompleteButton from '../ui/CompleteButton.jsx'; @@ -17,7 +17,9 @@ import {SuggestBoxInputField} from '../ui/SuggestBoxInputField.jsx'; import {FieldGroupCollapsible} from '../ui/panel/CollapsiblePanel.jsx'; import {plotParamsShape} from './XYPlot.jsx'; -import {showInfoPopup} from '../ui/PopupUtil.jsx'; +const DECI_ENABLE_SIZE = 5000; + +const helpStyle = {fontStyle: 'italic', color: '#808080', paddingBottom: 10}; /* * Split content into prior content and the last alphanumeric token in the text @@ -65,12 +67,6 @@ var XYPlotOptions = React.createClass({ const xName = get(flds, ['x.columnOrExpr']); const yName = get(flds, ['y.columnOrExpr']); - // workaround for validator not being called for unchanged fields - if (!xName || !yName) { - showInfoPopup('X and Y must not be empty.', 'Action required.'); - return; - } - const xOptions = get(flds, ['x.options']); let xLabel = get(flds, ['x.label']), xUnit = get(flds, ['x.unit']); if (!xLabel) { xLabel = xName; } @@ -82,7 +78,8 @@ var XYPlotOptions = React.createClass({ if (!yLabel) { yLabel = yName; } if (!yUnit) {yUnit = this.getUnit(yName); } - + const nbinsX = get(flds, ['nbins.x']); + const nbinsY = get(flds, ['nbins.y']); /* const axisParamsShape = PropTypes.shape({ @@ -90,24 +87,25 @@ var XYPlotOptions = React.createClass({ label : PropTypes.string, unit : PropTypes.string, options : PropTypes.string, // ex. 'grid,log,flip' - nbins : PropTypes.number, - min : PropTypes.number, - max : PropTypes.number }); const xyPlotParamsShape = PropTypes.shape({ - xyRatio : PropTypes.number, + xyRatio : PropTypes.string, stretch : PropTypes.oneOf(['fit','fill']), + nbins : PropTypes.shape({x : PropTypes.number, y : PropTypes.number}), + shading : PropTypes.oneOf(['lin', 'log']), x : axisParamsShape, y : axisParamsShape }); */ - const xyPlotParams = { - xyRatio : flds.xyRatio ? flds.xyRatio : undefined, + const xyPlotParams = omitBy({ + xyRatio : flds.xyRatio || undefined, stretch : flds.stretch, + nbins : (nbinsX && nbinsY) ? {x: Number(nbinsX), y: Number(nbinsY)} : undefined, + shading: flds.shading || undefined, x : { columnOrExpr : xName, label : xLabel, unit : xUnit, options : xOptions}, y : { columnOrExpr : yName, label : yLabel, unit : yUnit, options : yOptions} - }; + }, isUndefined); this.props.onOptionsSelected(xyPlotParams); }, @@ -116,6 +114,54 @@ var XYPlotOptions = React.createClass({ // TODO: do I need to do anything here? }, + renderBinningOptions() { + const { colValStats, groupKey, xyPlotParams }= this.props; + const displayBinningOptions = Boolean(colValStats.find((el) => { return el.numpoints>DECI_ENABLE_SIZE; })); + if (displayBinningOptions) { + return ( + + +
+ +
); + } else { return null; } + }, render() { const { colValStats, groupKey, xyPlotParams }= this.props; @@ -157,11 +203,16 @@ var XYPlotOptions = React.createClass({

+
+ For X and Y, enter a column or an expression
+ ex. log(col); 100*col1/col2; col1-col2 +

+
+ Enter display aspect ratio below.
+ Leave it blank to use all available space.
+
+ {this.renderBinningOptions()}
{ + if (histogramState[cid].tblId === tbl_id) { + const histogramParams = histogramState[cid].histogramParams; + HistogramCntlr.dispatchLoadColData(cid, histogramParams, request); + } + }); } xyPlotState = flux.getState()[XYPlotCntlr.XYPLOT_DATA_KEY]; @@ -47,13 +59,6 @@ export function* syncCharts() { } }); - 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, request); - } - }); break; } }