diff --git a/package.json b/package.json index 2c84b6a93a..93a4c74b17 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,8 @@ "validator": "4.5.0", "whatwg-fetch": "0.10.1", "react-color": "2.2.6", - "shallowequal": "0.2.2" + "shallowequal": "0.2.2", + "rc-slider": "5.1.3" }, "devDependencies": { "postcss-loader": "^0.11.1", diff --git a/src/firefly/js/charts/ChartsCntlr.js b/src/firefly/js/charts/ChartsCntlr.js index 841ed1b93b..6bf60c846e 100644 --- a/src/firefly/js/charts/ChartsCntlr.js +++ b/src/firefly/js/charts/ChartsCntlr.js @@ -59,7 +59,7 @@ const FIRST_CDEL_ID = '0'; // first data element id (if missing) * @function dispatchChartAdd * @memberof firefly.action */ -export function dispatchChartAdd({chartId, chartType, chartDataElements, groupId='main', deletable, help_id, mounted=0, dispatcher= flux.process}) { +export function dispatchChartAdd({chartId, chartType, chartDataElements, groupId='main', deletable, help_id, mounted=undefined, dispatcher= flux.process}) { dispatcher({type: CHART_ADD, payload: {chartId, chartType, chartDataElements, groupId, deletable, help_id, mounted}}); } @@ -370,11 +370,13 @@ function reduceData(state={}, action={}) { switch (action.type) { case (CHART_ADD) : { - const {chartId, chartType, chartDataElements, ...rest} = action.payload; - + const {chartId, chartType, chartDataElements, mounted, ...rest} = action.payload; + // if a chart is replaced (added with the same id) mounted should not change + const nMounted = isUndefined(mounted) ? get(state, [chartId, 'mounted']) : mounted; state = updateSet(state, chartId, omitBy({ chartType, + mounted: nMounted, chartDataElements: chartDataElementsToObj(chartDataElements), ...rest }, isUndefined)); diff --git a/src/firefly/js/core/ReduxFlux.js b/src/firefly/js/core/ReduxFlux.js index d12c71fef7..b760e12336 100644 --- a/src/firefly/js/core/ReduxFlux.js +++ b/src/firefly/js/core/ReduxFlux.js @@ -19,7 +19,7 @@ import ImagePlotCntlr from '../visualize/ImagePlotCntlr.js'; import ExternalAccessCntlr from './ExternalAccessCntlr.js'; import * as TableStatsCntlr from '../charts/TableStatsCntlr.js'; import * as ChartsCntlr from '../charts/ChartsCntlr.js'; -import * as TablesCntlr from '../tables/TablesCntlr'; +import TablesCntlr from '../tables/TablesCntlr'; import {chartTypeFactory} from '../charts/ChartType.js'; import {SCATTER_TBLVIEW} from '../charts/chartTypes/ScatterTblView.jsx'; @@ -137,6 +137,7 @@ registerCntlr(BackgroundCntlr); registerCntlr(ImagePlotCntlr); registerCntlr(FieldGroupCntlr); registerCntlr(MouseReadoutCntlr); +registerCntlr(TablesCntlr); registerCntlr(DrawLayerCntlr.getDrawLayerCntlrDef(drawLayerFactory)); @@ -148,12 +149,6 @@ actionCreators.set(AppDataCntlr.GRAB_WINDOW_FOCUS, AppDataCntlr.grabWindowFocus) actionCreators.set(AppDataCntlr.HELP_LOAD, AppDataCntlr.onlineHelpLoad); actionCreators.set(ExternalAccessCntlr.EXTENSION_ACTIVATE, ExternalAccessCntlr.extensionActivateActionCreator); -actionCreators.set(TablesCntlr.TABLE_SEARCH, TablesCntlr.tableSearch); -actionCreators.set(TablesCntlr.TABLE_FETCH, TablesCntlr.tableFetch); -actionCreators.set(TablesCntlr.TABLE_SORT, TablesCntlr.tableFetch); -actionCreators.set(TablesCntlr.TABLE_FILTER, TablesCntlr.tableFetch); -actionCreators.set(TablesCntlr.TABLE_HIGHLIGHT, TablesCntlr.highlightRow); - actionCreators.set(TableStatsCntlr.LOAD_TBL_STATS, TableStatsCntlr.loadTblStats); actionCreators.set(ChartsCntlr.CHART_DATA_FETCH, ChartsCntlr.makeChartDataFetch(cdtFactory.getChartDataType)); actionCreators.set(ChartsCntlr.CHART_OPTIONS_REPLACE, ChartsCntlr.makeChartOptionsReplace(cdtFactory.getChartDataType)); diff --git a/src/firefly/js/rpc/SearchServicesJson.js b/src/firefly/js/rpc/SearchServicesJson.js index 705c218b85..b0d640f0bc 100644 --- a/src/firefly/js/rpc/SearchServicesJson.js +++ b/src/firefly/js/rpc/SearchServicesJson.js @@ -69,12 +69,15 @@ export function fetchTable(tableRequest, hlRowIdx) { } /** - * - * @param {Object} params + * returns the values of the data from the given parameters + * @param {Object} p parameters object + * @param {string} p.columnName name of the column + * @param {string} p.filePath location of the file on the server + * @param {string} p.selectedRows a comma-separated string of indices of the rows to get the data from * @return {Promise} */ -export const selectedValues = function(params) { - return doService(doJsonP(), ServerParams.SELECTED_VALUES, params) +export const selectedValues = function({columnName, filePath, selectedRows}) { + return doService(doJsonP(), ServerParams.SELECTED_VALUES, {columnName, filePath, selectedRows}) .then((data) => { // JsonUtil may not interpret array values correctly due to error-checking. // returning array as a prop 'values' inside an object instead. diff --git a/src/firefly/js/tables/FilterInfo.js b/src/firefly/js/tables/FilterInfo.js index 8dd4d85487..1c5d528811 100644 --- a/src/firefly/js/tables/FilterInfo.js +++ b/src/firefly/js/tables/FilterInfo.js @@ -181,18 +181,20 @@ export class FilterInfo { compareTo = Number(compareTo); } switch (op) { + case 'like' : + return compareTo.includes(val); + case '>' : + return compareTo > val; + case '<' : + return compareTo < val; + case '=' : + return compareTo === val; case '!=' : return compareTo !== val; case '>=' : return compareTo >= val; case '<=' : return compareTo <= val; - case '>' : - return compareTo > val; - case '=' : - return compareTo === val; - case 'like' : - return compareTo.includes(val); case 'in' : return val.includes(compareTo); default : diff --git a/src/firefly/js/tables/TableConnector.js b/src/firefly/js/tables/TableConnector.js index 32a457a82a..b173732b74 100644 --- a/src/firefly/js/tables/TableConnector.js +++ b/src/firefly/js/tables/TableConnector.js @@ -2,46 +2,47 @@ * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt */ -import {isEmpty, omitBy, isUndefined, cloneDeep, get} from 'lodash'; -import {flux} from '../Firefly.js'; +import {isEmpty, omitBy, isUndefined, cloneDeep, get, set} from 'lodash'; import * as TblCntlr from './TablesCntlr.js'; import * as TblUtil from './TableUtil.js'; import {SelectInfo} from './SelectInfo.js'; -import {FilterInfo} from './FilterInfo.js'; import {selectedValues} from '../rpc/SearchServicesJson.js'; export class TableConnector { constructor(tbl_id, tbl_ui_id, tableModel, showUnits=true, showFilters=false, pageSize) { - this.tbl_id = tbl_id; + this.tbl_id = tbl_id || tableModel.tbl_id; this.tbl_ui_id = tbl_ui_id; - this.localTableModel = tableModel; + this.tableModel = tableModel; this.origPageSize = pageSize; this.origShowUnits = showUnits; this.origShowFilters = showFilters; } - onSort(sortInfoString) { - var {tableModel, request} = TblUtil.getTblInfoById(this.tbl_id); - if (this.localTableModel) { - tableModel = TblUtil.sortTable(this.localTableModel, sortInfoString); - TblCntlr.dispatchTableReplace(tableModel); - } else { - request = Object.assign({}, request, {sortInfo: sortInfoString}); - TblCntlr.dispatchTableSort(request); + onMount() { + const {tbl_ui_id, tbl_id, tableModel} = this; + if (!TblUtil.getTableUiByTblId(tbl_id)) { + TblCntlr.dispatchTableUiUpdate({tbl_ui_id, tbl_id}); + if (tableModel && !tableModel.origTableModel) { + set(tableModel, 'request.tbl_id', tbl_id); + const workingTableModel = cloneDeep(tableModel); + workingTableModel.origTableModel = tableModel; + TblCntlr.dispatchTableInsert(workingTableModel, undefined, false); + } } } + onSort(sortInfoString) { + var {request} = TblUtil.getTblInfoById(this.tbl_id); + request = Object.assign({}, request, {sortInfo: sortInfoString}); + TblCntlr.dispatchTableSort(request); + } + onFilter(filterIntoString) { - var {tableModel, request} = TblUtil.getTblInfoById(this.tbl_id); - if (this.localTableModel) { - tableModel = filterIntoString ? TblUtil.filterTable(this.localTableModel, filterIntoString) : this.localTableModel; - TblCntlr.dispatchTableReplace(tableModel); - } else { - request = Object.assign({}, request, {filters: filterIntoString}); - TblCntlr.dispatchTableFilter(request); - } + var {request} = TblUtil.getTblInfoById(this.tbl_id); + request = Object.assign({}, request, {filters: filterIntoString}); + TblCntlr.dispatchTableFilter(request); } /** @@ -50,24 +51,8 @@ export class TableConnector { */ onFilterSelected(selected) { if (isEmpty(selected)) return; - - var {tableModel, request} = TblUtil.getTblInfoById(this.tbl_id); - if (this.localTableModel) { - // not implemented yet - } else { - const filterInfoCls = FilterInfo.parse(request.filters); - const filePath = get(tableModel, 'tableMeta.tblFilePath'); - if (filePath) { - getRowIdFor(filePath, selected).then( (selectedRowIdAry) => { - const value = selectedRowIdAry.reduce((rv, val, idx) => { - return rv + (idx ? ',':'') + val; - }, 'IN (') + ')'; - filterInfoCls.addFilter('ROWID', value); - request = Object.assign({}, request, {filters: filterInfoCls.serialize()}); - TblCntlr.dispatchTableFilter(request); - }); - } - } + const {request} = TblUtil.getTblInfoById(this.tbl_id); + TblCntlr.dispatchTableFilterSelrow(request, selected); } onPageSizeChange(nPageSize) { @@ -92,12 +77,7 @@ export class TableConnector { const {hlRowIdx, startIdx} = TblUtil.getTblInfoById(this.tbl_id); if (rowIdx !== hlRowIdx) { const highlightedRow = startIdx + rowIdx; - if (this.localTableModel) { - const tableModel = {tbl_id: this.tbl_id, highlightedRow}; - flux.process({type: TblCntlr.TABLE_UPDATE, payload: tableModel}); - } else { - TblCntlr.dispatchTableHighlight(this.tbl_id, highlightedRow); - } + TblCntlr.dispatchTableHighlight(this.tbl_id, highlightedRow); } } @@ -142,7 +122,7 @@ export class TableConnector { } onOptionReset() { - const ctable = this.localTableModel || TblUtil.getTblById(this.tbl_id); + const ctable = TblUtil.getTblById(this.tbl_id); var filterInfo = get(ctable, 'request.filters', '').trim(); filterInfo = filterInfo !== '' ? '' : undefined; const pageSize = get(ctable, 'request.pageSize') !== this.origPageSize ? this.origPageSize : undefined; @@ -157,10 +137,3 @@ export class TableConnector { } } - -function getRowIdFor(filePath, selected) { - if (isEmpty(selected)) return []; - - const params = {id: 'Table__SelectedValues', columnName: 'ROWID', filePath, selectedRows: String(selected)}; - return selectedValues(params); -} \ No newline at end of file diff --git a/src/firefly/js/tables/TableUtil.js b/src/firefly/js/tables/TableUtil.js index b88487b0c0..158dee3602 100644 --- a/src/firefly/js/tables/TableUtil.js +++ b/src/firefly/js/tables/TableUtil.js @@ -2,7 +2,7 @@ * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt */ -import {get, set, unset, has, isEmpty, uniqueId, cloneDeep, omit, omitBy, isNil, isPlainObject, isArray} from 'lodash'; +import {get, unset, has, isEmpty, uniqueId, cloneDeep, omit, omitBy, isNil, isPlainObject, isArray, padEnd} from 'lodash'; import * as TblCntlr from './TablesCntlr.js'; import {SortInfo, SORT_ASC, UNSORTED} from './SortInfo.js'; import {FilterInfo} from './FilterInfo.js'; @@ -12,6 +12,7 @@ import {encodeServerUrl} from '../util/WebUtil.js'; import {fetchTable} from '../rpc/SearchServicesJson.js'; import {DEF_BASE_URL} from '../core/JsonUtils.js'; import {ServerParams} from '../data/ServerParams.js'; +import {doUpload} from '../ui/FileUpload.jsx'; export const MAX_ROW = Math.pow(2,31) - 1; /* TABLE_REQUEST should match QueryUtil on the server-side */ @@ -212,7 +213,13 @@ export function cloneRequest(request, params = {}) { * @deprecated use firefly.util.table.fetchTable instead */ export function doFetchTable(tableRequest, hlRowIdx) { - return fetchTable(tableRequest, hlRowIdx); + const {tbl_id} = tableRequest; + const tableModel = getTblById(tbl_id) || {}; + if (tableModel.origTableModel) { + return Promise.resolve(processRequest(tableModel.origTableModel, tableRequest, hlRowIdx)); + } else { + return fetchTable(tableRequest, hlRowIdx); + } } export function doValidate(type, action) { @@ -552,24 +559,7 @@ export function smartMerge(target, source) { } /** - * sort the given tableModel based on the given request - * @param {TableModel} origTableModel original table model. this is returned when direction is UNSORTED. - * @param {string} sortInfoStr - * @returns {TableModel} - * @public - * @func sortTable - * @memberof firefly.util.table - */ -export function sortTable(origTableModel, sortInfoStr) { - const tableModel = cloneDeep(origTableModel); - set(tableModel, 'request.sortInfo', sortInfoStr); - const {data, columns} = tableModel.tableData; - sortTableData(data, columns, sortInfoStr); - return tableModel; -} - -/** - * sort table data in place. + * sort table data in-place. * @param {TableData} tableData * @param {TableColumn[]} columns * @param {string} sortInfoStr @@ -606,22 +596,53 @@ export function sortTableData(tableData, columns, sortInfoStr) { } /** - * return a new table after applying the given filters. - * @param {TableModel} tableModel + * filter the given table. This function update the table data in-place. + * @param {TableModel} table * @param {string} filterInfoStr filters are separated by comma(','). - * @returns {TableModel} * @memberof firefly.util.table * @func filterTable */ -export function filterTable(tableModel, filterInfoStr) { - const filtered = cloneDeep(tableModel); - set(filtered, 'request.filters', filterInfoStr); - const comparators = filterInfoStr.split(';').map((s) => s.trim()).map((s) => FilterInfo.createComparator(s, tableModel)); - filtered.tableData.data = tableModel.tableData.data.filter((row) => { - return comparators.reduce( (rval, match) => rval && match(row), true); - } ); - filtered.totalRows = tableModel.tableData.data.length; - return filtered; +export function filterTable(table, filterInfoStr) { + if (filterInfoStr) { + const comparators = filterInfoStr.split(';').map((s) => s.trim()).map((s) => FilterInfo.createComparator(s, table)); + table.tableData.data = table.tableData.data.filter((row, idx) => { + return comparators.reduce( (rval, match) => rval && match(row, idx), true); + } ); + table.totalRows = table.tableData.data.length; + } + return table.tableData; +} + +export function processRequest(origTableModel, tableRequest, hlRowIdx) { + const {filters, sortInfo, inclCols, startIdx, pageSize} = tableRequest; + + var nTable = cloneDeep(origTableModel); + nTable.origTableModel = origTableModel; + nTable.request = tableRequest; + var {data, columns} = nTable.tableData; + + if (filters || sortInfo) { // need to track original rowId. + columns.push({name: 'ROWID', type: 'int', visibility: 'hidden'}); + data.forEach((r, idx) => r.push(String(idx))); + } + + if (filters) { + filterTable(nTable, filters); + } + if (sortInfo) { + let {data, columns} = nTable.tableData; + data = sortTableData(data, columns, sortInfo); + } + if (inclCols) { + let {data, columns} = nTable.tableData; + const colAry = inclCols.split(',').map((s) => s.trim()); + columns = columns.filters( (c) => colAry.includes(c)); + const inclIdices = columns.map( (c) => origTableModel.tableData.indexOf(c)); + data = data.map( (r) => r.filters( (c, idx) => inclIdices.includes(idx))); + } + data = data.slice(startIdx, startIdx + pageSize); + nTable.highlightedRow = hlRowIdx || startIdx; + return nTable; } /** @@ -674,17 +695,39 @@ export function getTblInfo(tableModel, aPageSize) { * @func getTableSourceUrl */ export function getTableSourceUrl(tbl_ui_id) { + const {columns, request} = getTableUiById(tbl_ui_id) || {}; + return makeTableSourceUrl(columns, request); +} + +/** + * Async version of getTableSourceUrl. If the given tbl_ui_id is backed by a local TableModel, + * then we need to push/upload the content of the server before it can be referenced via url. + * @param {string} tbl_ui_id UI id of the table + * @returns {Promise.} + */ +export function getAsyncTableSourceUrl(tbl_ui_id) { + const {tbl_id, columns} = getTableUiById(tbl_ui_id) || {}; + const {tableData, tableMeta} = getTblById(tbl_id); + const ipacTable = tableToText(columns, tableData.data, true, tableMeta); + var filename = `${tbl_id}.tbl`; + const file = new File([new Blob([ipacTable])], filename); + return doUpload(file).then( ({status, cacheKey}) => { + const request = makeFileRequest('save as text', cacheKey, {pageSize: MAX_ROW}); + return makeTableSourceUrl(columns, request); + }); +} + +function makeTableSourceUrl(columns, request) { const def = { startIdx: 0, pageSize : MAX_ROW }; - const {columns, request} = getTableUiById(tbl_ui_id) || {}; const tableRequest = Object.assign(def, cloneDeep(request)); const visiCols = columns.filter( (col) => { - return isNil(col) || col.visibility === 'show'; - }).map( (col) => { - return col.name; - } ); + return isNil(col) || col.visibility === 'show'; + }).map( (col) => { + return col.name; + } ); if (visiCols.length !== columns.length) { tableRequest['inclCols'] = visiCols.toString(); } @@ -697,6 +740,41 @@ export function getTableSourceUrl(tbl_ui_id) { return encodeServerUrl(DEF_BASE_URL, params); } +export function tableToText(columns, dataAry, showUnits=false, tableMeta) { + + const colWidths = calcColumnWidths(columns, dataAry); + + var textMeta = ''; + if (tableMeta) textMeta = Object.entries(tableMeta).map(([k,v]) => `\\${k} = ${v}`).join('\n'); + + // column's name + var textHead = columns.reduce( (pval, col, idx) => { + return pval + (get(columns, [idx,'visibility'], 'show') === 'show' ? `${padEnd(col.label || col.name, colWidths[idx])}|` : ''); + }, '|'); + + // column's type + textHead += '\n' + columns.reduce( (pval, col, idx) => { + return pval + (get(columns, [idx,'visibility'], 'show') === 'show' ? `${padEnd(col.type || '', colWidths[idx])}|` : ''); + }, '|'); + + if (showUnits) { + textHead += '\n' + columns.reduce( (pval, col, idx) => { + return pval + (get(columns, [idx,'visibility'], 'show') === 'show' ? `${padEnd(col.units || '', colWidths[idx])}|` : ''); + }, '|'); + } + + var textData = dataAry.map( (row) => { + return row.reduce( (pv, cv, idx) => { + const cname = get(columns, [idx, 'name']); + if (!cname) return pv; // not defined in columns.. can ignore + return pv + (get(columns, [idx,'visibility'], 'show') === 'show' ? `${padEnd(cv || '', colWidths[idx])} ` : ''); + }, ' '); + }).join('\n'); + + return textMeta + '\n' + textHead + '\n' + textData; +} + + /** * returns an object map of the column name and its width. * The width is the number of characters needed to display diff --git a/src/firefly/js/tables/TablesCntlr.js b/src/firefly/js/tables/TablesCntlr.js index ef5ae1ddfb..4f0d1c0e85 100644 --- a/src/firefly/js/tables/TablesCntlr.js +++ b/src/firefly/js/tables/TablesCntlr.js @@ -12,6 +12,8 @@ import {uiReducer} from './reducer/TableUiReducer.js'; import {resultsReducer} from './reducer/TableResultsReducer.js'; import {dispatchAddSaga} from '../core/MasterSaga.js'; import {updateMerge} from '../util/WebUtil.js'; +import {FilterInfo} from './FilterInfo.js'; +import {selectedValues} from '../rpc/SearchServicesJson.js'; export const TABLE_SPACE_PATH = 'table_space'; export const TABLE_RESULTS_PATH = 'table_space.results.tables'; @@ -26,6 +28,12 @@ export const UI_PREFIX = 'tableUi'; */ export const TABLE_FETCH = `${DATA_PREFIX}.fetch`; +/** + * Insert a full TableModel into the sytem. If tbl_id exists, data will be replaced. + * Sequence of actions: TABLE_REPLACE -> TABLE_LOADED, with invokedBy = TABLE_FETCH + */ +export const TABLE_INSERT = `${DATA_PREFIX}.insert`; + /** * Fired when table is completely loaded on the server. * Payload contains getTblInfo() + invokedBy. @@ -48,6 +56,11 @@ export const TABLE_SORT = `${DATA_PREFIX}.sort`; */ export const TABLE_FILTER = `${DATA_PREFIX}.filter`; +/** + * Filter table data on selected rows. Sequence of actions: TABLE_FILTER -> TABLE_UPDATE(+) -> TABLE_LOADED, with invokedBy = TABLE_FILTER + */ +export const TABLE_FILTER_SELROW = `${DATA_PREFIX}.filterSelrow`; + /** * Fired when table selection changes. */ @@ -103,87 +116,33 @@ export const TABLE_REPLACE = `${DATA_PREFIX}.replace`; -/*---------------------------- CREATORS ----------------------------*/ - -export function tableSearch(action) { - return (dispatch) => { - //dispatch(validate(FETCH_TABLE, action)); - if (!action.err) { - var {request={}, options={}, tbl_group} = action.payload; - const {tbl_id} = request; - const title = get(request, 'META_INFO.title'); - request.pageSize = options.pageSize = options.pageSize || request.pageSize || 100; +export default {actionCreators, reducers}; - dispatchTableFetch(request); - if (!TblUtil.getTableInGroup(tbl_id, tbl_group)) { - const {tbl_group, removable} = options || {}; - dispatchTblResultsAdded(tbl_id, title, options, removable, tbl_group); - dispatchAddSaga(doOnTblLoaded, {tbl_id, callback:() => dispatchActiveTableChanged(tbl_id, tbl_group)}); - } - } +function actionCreators() { + return { + [TABLE_SEARCH]: tableSearch, + [TABLE_INSERT]: tableInsert, + [TABLE_HIGHLIGHT]: highlightRow, + [TABLE_FETCH]: tableFetch, + [TABLE_SORT]: tableFetch, + [TABLE_FILTER]: tableFetch, + [TABLE_FILTER_SELROW]: tableFilterSelrow, + [TBL_RESULTS_ADDED]: tblResultsAdded }; } -export function highlightRow(action) { - return (dispatch) => { - const {tbl_id} = action.payload; - var tableModel = TblUtil.getTblById(tbl_id); - var tmpModel = TblUtil.smartMerge(tableModel, action.payload); - const {hlRowIdx, startIdx, endIdx, pageSize} = TblUtil.getTblInfo(tmpModel); - if (TblUtil.isTblDataAvail(startIdx, endIdx, tableModel)) { - dispatch(action); - } else { - const request = cloneDeep(tableModel.request); - set(request, 'META_INFO.padResults', true); - Object.assign(request, {startIdx, pageSize}); - TblUtil.doFetchTable(request, startIdx+hlRowIdx).then ( (tableModel) => { - dispatch( {type:TABLE_HIGHLIGHT, payload: tableModel} ); - }).catch( (error) => { - dispatch({type: TABLE_HIGHLIGHT, payload: createErrorTbl(tbl_id, `Fail to load table. \n ${error}`)}); - }); - } +function reducers() { + return { + [TABLE_SPACE_PATH]: reducer }; } -export function tableFetch(action) { - return (dispatch) => { - if (!action.err) { - var {request, hlRowIdx} = action.payload; - const {tbl_id} = request; - - dispatchAddSaga( doOnTblLoaded, {tbl_id, callback:() => dispatchTableLoaded( Object.assign(TblUtil.getTblInfoById(tbl_id), {invokedBy: action.type}) )}); - dispatch( updateMerge(action, 'payload', {tbl_id}) ); - request.startIdx = 0; - TblUtil.doFetchTable(request, hlRowIdx).then ( (tableModel) => { - dispatch( {type: TABLE_UPDATE, payload: tableModel} ); - }).catch( (error) => { - dispatch({type: TABLE_UPDATE, payload: createErrorTbl(tbl_id, `Fail to load table. \n ${error}`)}); - }); - } - }; -} - - -/*---------------------------- REDUCERS -----------------------------*/ -export function reducer(state={data:{}, results: {}, ui:{}}, action={}) { - - var nstate = {...state}; - nstate.results = resultsReducer(nstate, action); - nstate.data = dataReducer(nstate, action); - nstate.ui = uiReducer(nstate, action); - - if (shallowequal(state, nstate)) { - return state; - } else { - return nstate; - } -} /*---------------------------- DISPATCHERS -----------------------------*/ /** * Initiate a search that returns a table which will be added to result view. - * @param request + * @param {TableRequest} request * @param {TblOptions} options table options * @param {function} dispatcher only for special dispatching uses such as remote */ @@ -191,6 +150,20 @@ export function dispatchTableSearch(request, options, dispatcher= flux.process) dispatcher( {type: TABLE_SEARCH, payload: pickBy({request, options}) }); } +/** + * insert this tableModel into the system and then add it to the result view. + * If one exists, it will be replaced. + * This is similar to dispatchTableSearch except fetching is not needed. The given tableModel is a full + * data set, and does not need server fetching. + * @param {TableModel} tableModel the tableModel to insert + * @param {TblOptions} options table options + * @param {boolean} [addUI=true] add this table to the UI + * @param {function} dispatcher only for special dispatching uses such as remote + */ +export function dispatchTableInsert(tableModel, options, addUI=true, dispatcher= flux.process) { + dispatcher( {type: TABLE_INSERT, payload: {tableModel, options, addUI}}); +} + /** * Fetch table data from the server. * @param request a table request params object. @@ -221,6 +194,17 @@ export function dispatchTableFilter(request, hlRowIdx, dispatcher= flux.process) dispatcher( {type: TABLE_FILTER, payload: {request, hlRowIdx} }); } +/** + * Filter the table given the request. + * @param {TableRequest} request a table request params object. + * @param {number[]} selected row indices + * @param {number} hlRowIdx set the highlightedRow. default to startIdx. + * @param {function} dispatcher only for special dispatching uses such as remote + */ +export function dispatchTableFilterSelrow(request, selected, hlRowIdx, dispatcher= flux.process) { + dispatcher( {type: TABLE_FILTER_SELROW, payload: {request, selected, hlRowIdx} }); +} + /** * Notify that a new table has been added and it is fully loaded. * @param tbl_info table info. see TableUtil.getTblInfo for details. @@ -270,19 +254,10 @@ export function dispatchTableReplace(tableModel) { * @param {string} tbl_id table id * @param {string} title title to be displayed with the table. * @param {TblOptions} options table options. - * @param {boolean} removable true if this table can be removed from view. - * @param {string} tbl_group table group. defaults to 'main' * @param {string} tbl_ui_id table ui id */ -export function dispatchTblResultsAdded(tbl_id, title, options, removable, tbl_group, tbl_ui_id=TblUtil.uniqueTblUiId()) { - title = title || tbl_id; - tbl_group = tbl_group || 'main'; - removable = isNil(removable) ? true : removable; - const pageSize = get(options, 'pageSize'); - if ( pageSize && !Number.isInteger(pageSize)) { - options.pageSize = parseInt(pageSize); - } - flux.process( {type: TBL_RESULTS_ADDED, payload: {tbl_id, tbl_group, title, removable, tbl_ui_id, options}}); +export function dispatchTblResultsAdded(tbl_id, title, options, tbl_ui_id) { + flux.process( {type: TBL_RESULTS_ADDED, payload: {tbl_id, title, tbl_ui_id, options}}); } /** @@ -313,6 +288,162 @@ export function dispatchActiveTableChanged(tbl_id, tbl_group='main') { /*---------------------------- PRIVATE -----------------------------*/ +/*---------------------------- CREATORS ----------------------------*/ + +function tableSearch(action) { + return (dispatch) => { + //dispatch(validate(FETCH_TABLE, action)); + if (!action.err) { + var {request={}, options={}} = action.payload; + const {tbl_group} = options; + const {tbl_id} = request; + const title = get(request, 'META_INFO.title'); + request.pageSize = options.pageSize = options.pageSize || request.pageSize || 100; + + dispatchTableFetch(request); + dispatchTblResultsAdded(tbl_id, title, options, tbl_group); + } + }; +} + +function tableInsert(action) { + return (dispatch) => { + const {tableModel={}, options={}, addUI=true} = action.payload || {}; + const {tbl_group} = options; + const title = tableModel.title || get(tableModel, 'request.META_INFO.title') || 'untitled'; + const tbl_id = tableModel.tbl_id || get(tableModel, 'request.tbl_id'); + + Object.assign(tableModel, {isFetching: false}); + set(tableModel, 'tableMeta.Loading-Status', 'COMPLETED'); + if (!tableModel.origTableModel) tableModel.origTableModel = tableModel; + if (addUI) dispatchTblResultsAdded(tbl_id, title, options, tbl_group); + dispatch( {type: TABLE_REPLACE, payload: tableModel} ); + dispatchTableLoaded(Object.assign( TblUtil.getTblInfo(tableModel), {invokedBy: TABLE_FETCH})); + }; +} + +function tblResultsAdded(action) { + return (dispatch) => { + //dispatch(validate(FETCH_TABLE, action)); + if (!action.err) { + var {tbl_id, title, options={}, tbl_ui_id} = action.payload; + + title = title || tbl_id; + options = Object.assign({tbl_group: 'main', removable: true}, options); + const pageSize = get(options, 'pageSize'); + if ( pageSize && !Number.isInteger(pageSize)) { + options.pageSize = parseInt(pageSize); + } + if (!TblUtil.getTableInGroup(tbl_id, options.tbl_group)) { + tbl_ui_id = tbl_ui_id || TblUtil.uniqueTblUiId(); + dispatch({type: TBL_RESULTS_ADDED, payload: {tbl_id, title, tbl_ui_id, options}}); + dispatchAddSaga(doOnTblLoaded, {tbl_id, callback:() => dispatchActiveTableChanged(tbl_id, options.tbl_group)}); + } + } + }; +} + +function highlightRow(action) { + return (dispatch) => { + const {tbl_id} = action.payload; + var tableModel = TblUtil.getTblById(tbl_id); + var tmpModel = TblUtil.smartMerge(tableModel, action.payload); + const {hlRowIdx, startIdx, endIdx, pageSize} = TblUtil.getTblInfo(tmpModel); + if (TblUtil.isTblDataAvail(startIdx, endIdx, tableModel)) { + dispatch(action); + } else { + const request = cloneDeep(tableModel.request); + set(request, 'META_INFO.padResults', true); + Object.assign(request, {startIdx, pageSize}); + TblUtil.doFetchTable(request, startIdx+hlRowIdx).then ( (tableModel) => { + dispatch( {type:TABLE_HIGHLIGHT, payload: tableModel} ); + }).catch( (error) => { + dispatch({type: TABLE_HIGHLIGHT, payload: createErrorTbl(tbl_id, `Fail to load table. \n ${error}`)}); + }); + } + }; +} + +function tableFetch(action) { + return (dispatch) => { + if (!action.err) { + var {request, hlRowIdx} = action.payload; + const {tbl_id} = request; + + dispatchAddSaga( doOnTblLoaded, {tbl_id, callback:() => dispatchTableLoaded( Object.assign(TblUtil.getTblInfoById(tbl_id), {invokedBy: action.type}) )}); + dispatch( updateMerge(action, 'payload', {tbl_id}) ); + request.startIdx = 0; + TblUtil.doFetchTable(request, hlRowIdx).then ( (tableModel) => { + const type = tableModel.origTableModel ? TABLE_REPLACE : TABLE_UPDATE; + dispatch( {type, payload: tableModel} ); + }).catch( (error) => { + dispatch({type: TABLE_UPDATE, payload: createErrorTbl(tbl_id, `Fail to load table. \n ${error}`)}); + }); + } + }; +} + +/** + * This function convert the selected rows into its associated ROWID, add it as a filter, then + * dispatch it to tableFilter for processing. + * @param {Action} action + * @returns {function} + */ +function tableFilterSelrow(action) { + return (dispatch) => { + var {request={}, hlRowIdx, selected=[]} = action.payload || {}; + const {tbl_id, filters} = request; + const tableModel = TblUtil.getTblById(tbl_id); + const filterInfoCls = FilterInfo.parse(filters); + + if (tableModel.origTableModel) { + const selRowIds = selected.map((idx) => TblUtil.getCellValue(tableModel, idx, 'ROWID') || idx).toString(); + filterInfoCls.addFilter('ROWID', `IN (${selRowIds})`); + request = Object.assign({}, request, {filters: filterInfoCls.serialize()}); + dispatchTableFilter(request, hlRowIdx); + } else { + const filePath = get(tableModel, 'tableMeta.tblFilePath'); + if (filePath) { + getRowIdFor(filePath, selected).then( (selectedRowIdAry) => { + const value = selectedRowIdAry.reduce((rv, val, idx) => { + return rv + (idx ? ',':'') + val; + }, 'IN (') + ')'; + filterInfoCls.addFilter('ROWID', value); + request = Object.assign({}, request, {filters: filterInfoCls.serialize()}); + dispatchTableFilter(request, hlRowIdx); + }); + } + } + }; +} +/*-----------------------------------------------------------------------------------------*/ + +/*---------------------------- REDUCERS -----------------------------*/ + +function reducer(state={data:{}, results: {}, ui:{}}, action={}) { + + var nstate = {...state}; + nstate.results = resultsReducer(nstate, action); + nstate.data = dataReducer(nstate, action); + nstate.ui = uiReducer(nstate, action); + + if (shallowequal(state, nstate)) { + return state; + } else { + return nstate; + } +} + +/*-----------------------------------------------------------------------------------------*/ + + + + +function getRowIdFor(filePath, selected) { + const params = {columnName: 'ROWID', filePath, selectedRows: String(selected)}; + return selectedValues(params); +} + /** * validates the action object based on the given type. * In case when a validation error occurs, the action's err property will be @@ -327,23 +458,21 @@ function validate(type, action) { /** - * this saga does the following: - * - * @param tbl_id table id to watch - * @param callback callback to execute when table is loaded. + * this saga watches for table update and invoke the given callback when + * the table given by tbl_id is fully loaded. + * @param {Object} p parameters object + * @param {string} p.tbl_id table id to watch + * @param {function} p.callback callback to execute when table is loaded. */ -export function* doOnTblLoaded({tbl_id, callback}) { +function* doOnTblLoaded({tbl_id, callback}) { var isLoaded = false, hasData = false; while (!(isLoaded && hasData)) { - const action = yield take([TABLE_UPDATE]); + const action = yield take([TABLE_UPDATE, TABLE_REPLACE]); const a_id = get(action, 'payload.tbl_id'); if (tbl_id === a_id) { - isLoaded = isLoaded || TblUtil.isTableLoaded(action.payload); const tableModel = TblUtil.getTblById(tbl_id); + isLoaded = isLoaded || TblUtil.isTableLoaded(tableModel); hasData = hasData || get(tableModel, 'tableData.columns.length'); if (get(tableModel, 'error')) { // there was an error loading this table. diff --git a/src/firefly/js/tables/reducer/TableDataReducer.js b/src/firefly/js/tables/reducer/TableDataReducer.js index 74ed10090d..68c3869955 100644 --- a/src/firefly/js/tables/reducer/TableDataReducer.js +++ b/src/firefly/js/tables/reducer/TableDataReducer.js @@ -47,6 +47,8 @@ export function dataReducer(state={data:{}}, action={}) { { const {tbl_id} = action.payload || {}; const nTable = Object.assign({isFetching:true, selectInfo: SelectInfo.newInstance({rowCount:0}).data}, action.payload); + const origTableModel = get(root, [tbl_id, 'origTableModel']); + if (origTableModel) nTable.origTableModel = origTableModel; return updateSet(root, [tbl_id], nTable); } case (Cntlr.TABLE_REPLACE) : diff --git a/src/firefly/js/tables/reducer/TableResultsReducer.js b/src/firefly/js/tables/reducer/TableResultsReducer.js index ab6e1b831f..4506155889 100644 --- a/src/firefly/js/tables/reducer/TableResultsReducer.js +++ b/src/firefly/js/tables/reducer/TableResultsReducer.js @@ -17,7 +17,8 @@ export function resultsReducer(state={results:{}}, action={}) { case (Cntlr.TBL_RESULTS_ADDED) : case (Cntlr.TBL_RESULTS_UPDATE) : { - const {tbl_id, tbl_group} = action.payload; + const {tbl_id, options={}} = action.payload; + const {tbl_group} = options; if (tbl_id ) { const changes = set({}, [tbl_group, 'tables', tbl_id], action.payload); set(changes, [tbl_group, 'name'], tbl_group); diff --git a/src/firefly/js/tables/ui/BasicTableView.jsx b/src/firefly/js/tables/ui/BasicTableView.jsx index fc6a949933..82b41c6484 100644 --- a/src/firefly/js/tables/ui/BasicTableView.jsx +++ b/src/firefly/js/tables/ui/BasicTableView.jsx @@ -6,9 +6,9 @@ import React, {PropTypes} from 'react'; import sCompare from 'react-addons-shallow-compare'; import FixedDataTable from 'fixed-data-table'; import Resizable from 'react-component-resizable'; -import {debounce, defer, get, isEmpty, padEnd} from 'lodash'; +import {debounce, defer, get, isEmpty} from 'lodash'; -import {calcColumnWidths} from '../TableUtil.js'; +import {tableToText} from '../TableUtil.js'; import {SelectInfo} from '../SelectInfo.js'; import {FilterInfo} from '../FilterInfo.js'; import {SortInfo} from '../SortInfo.js'; @@ -292,34 +292,3 @@ function makeColumns ({columns, columnWidths, data, selectable, showUnits, showF } -function tableToText(columns, dataAry, showUnits=false) { - - const colWidths = calcColumnWidths(columns, dataAry); - - // column's name - var textHead = columns.reduce( (pval, col, idx) => { - return pval + (get(columns, [idx,'visibility'], 'show') === 'show' ? `${padEnd(col.label || col.name, colWidths[idx])}|` : ''); - }, '|'); - - // column's type - textHead += '\n' + columns.reduce( (pval, col, idx) => { - return pval + (get(columns, [idx,'visibility'], 'show') === 'show' ? `${padEnd(col.type || '', colWidths[idx])}|` : ''); - }, '|'); - - if (showUnits) { - textHead += '\n' + columns.reduce( (pval, col, idx) => { - return pval + (get(columns, [idx,'visibility'], 'show') === 'show' ? `${padEnd(col.units || '', colWidths[idx])}|` : ''); - }, '|'); - } - - var textData = dataAry.reduce( (pval, row) => { - return pval + - row.reduce( (pv, cv, idx) => { - const cname = get(columns, [idx, 'name']); - if (!cname) return pv; // not defined in columns.. can ignore - return pv + (get(columns, [idx,'visibility'], 'show') === 'show' ? `${padEnd(cv || '', colWidths[idx])} ` : ''); - }, ' ') + '\n'; - }, ''); - return textHead + '\n' + textData; -} - diff --git a/src/firefly/js/tables/ui/TablePanel.jsx b/src/firefly/js/tables/ui/TablePanel.jsx index 3e95a18de3..474fb34cff 100644 --- a/src/firefly/js/tables/ui/TablePanel.jsx +++ b/src/firefly/js/tables/ui/TablePanel.jsx @@ -4,11 +4,11 @@ import React, {Component, PropTypes} from 'react'; import sCompare from 'react-addons-shallow-compare'; -import {isEmpty, get, truncate} from 'lodash'; +import {isEmpty, truncate} from 'lodash'; import {flux} from '../../Firefly.js'; import {download} from '../../util/WebUtil.js'; import * as TblUtil from '../TableUtil.js'; -import {dispatchTableReplace, dispatchTableUiUpdate, dispatchTableRemove, dispatchTblExpanded} from '../TablesCntlr.js'; +import {dispatchTableRemove, dispatchTblExpanded} from '../TablesCntlr.js'; import {TablePanelOptions} from './TablePanelOptions.jsx'; import {BasicTableView} from './BasicTableView.jsx'; import {TableConnector} from '../TableConnector.js'; @@ -35,7 +35,10 @@ export class TablePanel extends Component { var {tbl_id, tbl_ui_id, tableModel, showUnits, showFilters, pageSize} = props; if (!tbl_id && tableModel) { - tbl_id = get(tableModel, 'tbl_id', TblUtil.uniqueTblId()); + if (!tableModel.tbl_id) { + tableModel.tbl_id = TblUtil.uniqueTblId(); + } + tbl_id = tableModel.tbl_id; } tbl_ui_id = tbl_ui_id || TblUtil.uniqueTblUiId(); this.tableConnector = TableConnector.newInstance(tbl_id, tbl_ui_id, tableModel, showUnits, showFilters, pageSize); @@ -54,14 +57,7 @@ export class TablePanel extends Component { componentDidMount() { this.removeListener= flux.addListener(() => this.storeUpdate()); - const {tableModel} = this.props; - const {tbl_id, tbl_ui_id} = this.tableConnector; - if (!get(this.state, 'tbl_id')) { - dispatchTableUiUpdate({tbl_ui_id, tbl_id}); - if (tableModel) { - dispatchTableReplace(tableModel); - } - } + this.tableConnector.onMount(); } componentWillUnmount() { @@ -92,7 +88,11 @@ export class TablePanel extends Component { } saveTable() { const {tbl_ui_id} = this.tableConnector; - download(TblUtil.getTableSourceUrl(tbl_ui_id)); + if (this.tableConnector.tableModel) { + TblUtil.getAsyncTableSourceUrl(tbl_ui_id).then((url) => download(url)); + } else { + download(TblUtil.getTableSourceUrl(tbl_ui_id)); + } } toggleOptions() { this.tableConnector.onToggleOptions(!this.state.showOptions); diff --git a/src/firefly/js/templates/fireflyviewer/FireflyViewer.js b/src/firefly/js/templates/fireflyviewer/FireflyViewer.js index 91e71cdd24..70e498725a 100644 --- a/src/firefly/js/templates/fireflyviewer/FireflyViewer.js +++ b/src/firefly/js/templates/fireflyviewer/FireflyViewer.js @@ -18,6 +18,7 @@ import {TriViewPanel} from './TriViewPanel.jsx'; import {VisHeader} from '../../visualize/ui/VisHeader.jsx'; import {getActionFromUrl} from '../../core/History.js'; import {launchImageMetaDataSega} from '../../visualize/ui/TriViewImageSection.jsx'; +import {syncChartViewer, addDefaultScatter} from '../../visualize/saga/ChartsSync.js'; import {dispatchAddSaga} from '../../core/MasterSaga.js'; // import {deepDiff} from '../util/WebUtil.js'; @@ -43,6 +44,8 @@ export class FireflyViewer extends Component { super(props); this.state = this.getNextState(); dispatchAddSaga(layoutManager,{views: props.views}); + dispatchAddSaga(syncChartViewer); + dispatchAddSaga(addDefaultScatter); } getNextState() { diff --git a/src/firefly/js/templates/fireflyviewer/FireflyViewerManager.js b/src/firefly/js/templates/fireflyviewer/FireflyViewerManager.js index eb0ca06e5c..1aa2b89c08 100644 --- a/src/firefly/js/templates/fireflyviewer/FireflyViewerManager.js +++ b/src/firefly/js/templates/fireflyviewer/FireflyViewerManager.js @@ -7,11 +7,10 @@ import {get, filter, omitBy, isNil, isEmpty} from 'lodash'; import {LO_VIEW, LO_MODE, SHOW_DROPDOWN, SET_LAYOUT_MODE, getLayouInfo, dispatchUpdateLayoutInfo} from '../../core/LayoutCntlr.js'; import {clone} from '../../util/WebUtil.js'; -import {findGroupByTblId, getTblIdsByGroup,getActiveTableId, getTableInGroup} from '../../tables/TableUtil.js'; +import {findGroupByTblId, getTblIdsByGroup,getActiveTableId} from '../../tables/TableUtil.js'; import {TBL_RESULTS_ADDED, TABLE_LOADED, TABLE_REMOVE, TBL_RESULTS_ACTIVE} from '../../tables/TablesCntlr.js'; -import {CHART_ADD, CHART_REMOVE, getNumCharts, dispatchChartAdd} from '../../charts/ChartsCntlr.js'; -import {getDefaultXYPlotOptions, DT_XYCOLS} from '../../charts/dataTypes/XYColsCDT.js'; -import {SCATTER} from '../../charts/ChartUtil.js'; +import {CHART_ADD, CHART_REMOVE} from '../../charts/ChartsCntlr.js'; + import ImagePlotCntlr from '../../visualize/ImagePlotCntlr.js'; import {isMetaDataTable, isCatalogTable} from '../../metaConvert/converterUtils.js'; import {META_VIEWER_ID} from '../../visualize/ui/TriViewImageSection.jsx'; @@ -145,24 +144,6 @@ function handleNewTable(action, images, showImages, showTables, coverageLockedOn const {tbl_id} = action.payload; - // check if a default chart needs to be added - if (getNumCharts(tbl_id) === 0) { - // how do I know the default chart should be added? - // add a default chart if the group is main - if (getTableInGroup(tbl_id, 'main')) { - // default chart is xy plot of coordinate columns or first two numeric columns - const defaultOptions = getDefaultXYPlotOptions(tbl_id); - if (defaultOptions) { - dispatchChartAdd({ - chartId: 'xyplot-' + tbl_id, - chartType: SCATTER, - groupId: tbl_id, - chartDataElements: [{tblId: tbl_id, type: DT_XYCOLS, options: defaultOptions}] - }); - } - } - } - // check for catalog or meta images if (!showTables) return [showImages, images, coverageLockedOn]; // ignores this if table is not visible diff --git a/src/firefly/js/templates/lightcurve/LcPhaseFoldingPanel.jsx b/src/firefly/js/templates/lightcurve/LcPhaseFoldingPanel.jsx index d1f268eb29..2796120ba6 100644 --- a/src/firefly/js/templates/lightcurve/LcPhaseFoldingPanel.jsx +++ b/src/firefly/js/templates/lightcurve/LcPhaseFoldingPanel.jsx @@ -2,7 +2,7 @@ * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt */ -import './LCPanels.css'; + import React, {Component, PropTypes} from 'react'; import {TargetPanel} from '../../ui/TargetPanel.jsx'; import {InputGroup} from '../../ui/InputGroup.jsx'; @@ -27,10 +27,10 @@ import {dispatchShowDialog} from '../../core/ComponentCntlr.js'; import {dispatchTableSearch} from '../../tables/TablesCntlr.js'; import {loadXYPlot} from '../../charts/dataTypes/XYColsCDT.js'; - - import {RAW_TABLE, PHASE_FOLDED} from '../../templates/lightcurve/LcManager.js'; +import './LCPanels.css'; + const grpkey = 'LC_FORM_Panel'; function getDialogBuilder() { @@ -130,8 +130,6 @@ const defValues= { var LcPhaseFoldingDialog = React.createClass({ - - render() { return (
@@ -251,15 +249,16 @@ export function LcPFOptionsPanel ({fields}) {
+ forceReinit={true} + initialState= {{ + fieldKey: 'period', + value: '1.0', + //validator: Validate.floatRange.bind(null, 0.5, 1.5, 3,'period'), + tooltip: 'Period', + label : 'Period:', + labelWidth : 100 + }} /> +

diff --git a/src/firefly/js/templates/lightcurve/LcPhaseFoldingPopup.jsx b/src/firefly/js/templates/lightcurve/LcPhaseFoldingPopup.jsx new file mode 100644 index 0000000000..0647bbff5a --- /dev/null +++ b/src/firefly/js/templates/lightcurve/LcPhaseFoldingPopup.jsx @@ -0,0 +1,623 @@ +/* + * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt + */ + +import React, {Component, PropTypes} from 'react'; +import {dispatchShowDialog} from '../../core/ComponentCntlr.js'; +import {dispatchHideDropDown} from '../../core/LayoutCntlr.js'; +import CompleteButton from '../../ui/CompleteButton.jsx'; +import DialogRootContainer from '../../ui/DialogRootContainer.jsx'; +import {PopupPanel} from '../../ui/PopupPanel.jsx'; +import {FieldGroup} from '../../ui/FieldGroup.jsx'; +import {SuggestBoxInputField} from '../../ui/SuggestBoxInputField.jsx'; +import {ValidationField} from '../../ui/ValidationField.jsx'; +import {InputGroup} from '../../ui/InputGroup.jsx'; +import {doUpload} from '../../ui/FileUpload.jsx'; +import {RangeSlider} from '../../ui/RangeSlider.jsx'; +import {showInfoPopup} from '../../ui/PopupUtil.jsx'; +import {loadXYPlot} from '../../charts/dataTypes/XYColsCDT.js'; +import FieldGroupUtils from '../../fieldGroup/FieldGroupUtils'; +import {dispatchRestoreDefaults} from '../../fieldGroup/FieldGroupCntlr.js'; +import {makeTblRequest,getTblById, tableToText, makeFileRequest} from '../../tables/TableUtil.js'; +import {dispatchTableSearch} from '../../tables/TablesCntlr.js'; +import Validate from '../../util/Validate.js'; +import {LcPFOptionsPanel} from './LcPhaseFoldingPanel.jsx'; +import {RAW_TABLE, PHASE_FOLDED} from './LcManager.js'; +import {cloneDeep, get, set, omit, slice, replace, isNil} from 'lodash'; + +const popupId = 'PhaseFoldingPopup'; +const fKeyDef = { time: {fkey: 'timeCol', label: 'Time Column'}, + flux: {fkey: 'flux', label: 'Flux Column'}, + fluxerr: {fkey: 'fluxerror', label: 'Flux Error'}, + tz: {fkey:'tzero', label: 'Zero Point Time'}, + period: {fkey: 'period', label: 'Period (day)'}, + periodslider: {fkey: 'periodslider', label: ''}}; +const pfkey = 'LC_PF_Panel'; + +const PanelResizableStyle = { + width: 400, + minWidth: 400, + height: 300, + minHeight: 300, + marginLeft: 15, + overflow: 'auto', + padding: '2px' +}; + +const Header = { + whiteSpace: 'nowrap', + height: 'calc(100% - 1px)', + display: 'flex', + flexDirection: 'column', + alignItems: 'left', + justifyContent: 'space-between', + padding: '2px', + fontSize: 'large' +}; + +const defValues= { + fieldInt: { + fieldKey: 'fieldInt', + value: '3', + validator: Validate.intRange.bind(null, 1, 10, 'test field'), + tooltip: 'this is a tip for field 1', + label: 'Int Value:' + }, + FluxColumn: { + fieldKey: 'fluxcolumn', + value: '', + validator: Validate.floatRange.bind(null, 0.2, 20.5, 2, 'Flux Column'), + tooltip: 'Flux Column', + label: 'Flux Column:', + nullAllowed : true, + labelWidth: 100 + }, + field1: { + fieldKey: 'field1', + value: '3', + validator: Validate.intRange.bind(null, 1, 10, 'test field'), + tooltip: 'this is a tip for field 1', + label: 'Int Value:' + }, + field2: { + fieldKey: 'field2', + value: '', + validator: Validate.floatRange.bind(null, 1.5, 50.5, 2, 'a float field'), + tooltip: 'field 2 tool tip', + label: 'Float Value:', + nullAllowed : true, + labelWidth: 100 + }, + low: { + fieldKey: 'low', + value: '1', + validator: Validate.intRange.bind(null, 1, 100, 'low field'), + tooltip: 'this is a tip for low field', + label: 'Low Field:' + }, + high: { + fieldKey: 'high', + value: '3', + validator: Validate.intRange.bind(null, 1, 100, 'high field'), + tooltip: 'this is a tip for high field', + label: 'High Field:' + }, + periodMin: { + fieldKey: 'periodMin', + value: '3', + validator: Validate.floatRange.bind(null, 0.0, 365.0, 'period minimum'), + tooltip: 'this is a tip for period minumum', + label: '' + }, + periodMax: { + fieldKey: 'periodMax', + value: '3', + validator: Validate.floatRange.bind(null, 0.0, 365.0, 'period maximum'), + tooltip: 'this is a tip for period maximum', + label: '' + }, + periodSteps: { + fieldKey: 'periodSteps', + value: '3', + validator: Validate.intRange.bind(null, 1, 2000, 'period total step'), + tooltip: 'this is a tip for total period steps', + label: '' + } +}; + +const RES = 10; +const DEC = 8; + +var periodRange; +var timeZero; +var timeMax; +var currentPhaseFoldedTable; + +/** + * @summary show phase folding popup window + * @param {string} popTitle + */ + +export function showPhaseFoldingPopup(popTitle) { + var fields = FieldGroupUtils.getGroupFields(pfkey); + var timeColName = get(fields, [fKeyDef.time.fkey, 'value'], 'mjd'); + + periodRange = computePeriodRange(RAW_TABLE, timeColName); + + var popup = ( + {PhaseFoldingSetting()} +
+ +
+
); + + DialogRootContainer.defineDialog(popupId, popup); + dispatchShowDialog(popupId); +} + +/** + * @summary Phase folding pannel component + * @returns {XML} + * @constructor + */ +function PhaseFoldingSetting() { + var border = '1px solid #a3aeb9'; + var borderRadius = '5px'; + var childStyle = {border, borderRadius, height: 350, margin: 5, padding: 2}; + + return ( +
+
+ +
+
XY plot
+
+ ); +} + +/** + * @summary Phase folding panel component for setting parameters + */ +class LcPFOptionsBox extends Component { + constructor(props) { + super(props); + this.state = {fields: FieldGroupUtils.getGroupFields(pfkey)}; + } + + componentWillUnmount() { + this.iAmMounted= false; + if (this.unbinder) this.unbinder(); + } + + + componentDidMount() { + this.iAmMounted= true; + this.unbinder= FieldGroupUtils.bindToStore(pfkey, (fields) => { + if (fields!==this.state.fields && this.iAmMounted) { + this.setState({fields}); + } + }); + } + + render() { + var {fields} = this.state; + + return ( +
+ +
+ ); + + } +} + +function LcPFOptions({fields}) { + const validSuggestions = ['mjd','col1','col2']; + const {periodMax, periodMin, periodSteps, tzero} = fields || {}; + var sRange, step, min, max, zeroT; + + min = periodMin ? periodMin.value : periodRange.min; + max = periodMax ? periodMax.value : periodRange.max; + zeroT = tzero ? tzero.value : timeZero; + + sRange = [0.0, max]; + step = periodSteps ? (max - min)/periodSteps.value : (max-min)/periodRange.steps; + + const periodErr = `Period error: must be a float and not less than ${periodRange.min} (day)`; + const timeErr = `time zero error: must be a float and within [${timeZero}, ${timeMax}]`; + + return ( + + + + + Phase Folding +

+ { + let retVal = {valid: true, message: ''}; + if (!validSuggestions.includes(val)) { + retVal = {valid: false, message: `${val} is not a valid time column`}; + } + return retVal; + }, + tooltip: 'time column, mjd, col1, or col2', + label : `${fKeyDef.time.label}:`, + labelWidth : 100 + }} + getSuggestions = {(val)=>{ + const suggestions = validSuggestions.filter((el)=>{return el.startsWith(val);}); + return suggestions.length > 0 ? suggestions : validSuggestions; + }} + + /> + +
+ + +
+ + +
+ + +
+ + doPFCalculate()} + min={sRange[0]} + max={sRange[1]} + minStop={min} + marks={{[min]: {style: { left: '0%', marginLeft: -20, width: 40}, label: `${min}`}, + [sRange[1]]: {style: { left: '100%', marginLeft: -20, width: 40}, label: `${sRange[1]}`}}} + step={step} + defaultValue={min} + wrapperStyle={{marginBottom: 20, width: PanelResizableStyle.width*4/5}} + sliderStyle={{marginLeft: 10, marginTop: 20}} + /> + +

+ + + +
+
+
+ ); +} + +LcPFOptions.propTypes = { + fields: PropTypes.object +}; + +/** + * @summary validator for periopd value + * @param {number} min + * @param {number} max + * @param {number} precision + * @param {string} description + * @param {string} valStr + * @returns {{valid: boolean, message: string}} + */ +var periodValidator = function(min, max, precision, description, valStr) { + var retRes = Validate.floatRange(min, max, precision, description, valStr); + + if (retRes.valid) { + if (parseFloat(valStr) < periodRange.min) { + return {valid: false, message: description}; + } + } else { + retRes.message = description; + } + return retRes; +}; + + +/** + * @summary validator for float value string, empty string is invalid + * @param {number} min + * @param {number} max + * @param {number} precision + * @param {string} description + * @param {string} valStr + * @returns {{valid: boolean, message: *}} + */ +var floatValidator = function(min, max, precision, description, valStr) { + var retRes = Validate.floatRange(min, max, precision, description, valStr); + + if (retRes.valid) { + if (!valStr) { // empty string + return {valid: false, message: `${description}: not specified`}; + } + } + return retRes; +}; + +/** + * @summary time zero validator + * @param {number} min + * @param {number} max + * @param {number} precision + * @param {string} description + * @param {string} valStr + * @returns {{valid: boolean, message: string}} + */ +var timezeroValidator = function(min, max, precision, description, valStr) { + var retRes = Validate.floatRange(min, max, precision, description, valStr); + + if (retRes.valid) { + if (!valStr || parseFloat(valStr) < timeZero || parseFloat(valStr) > timeMax) { // empty string or not in range + return {valid: false, + message: description}; + } + } else { + retRes.message = description; + } + return retRes; +}; + +/** + * @summary phase folding parameter reducer // TODO: not sure how this works + * @param {object} inFields + * @param {object} action + * @return {object} + */ +var LcPFReducer= function(inFields, action) { + if (!inFields) { + var defV = Object.assign({}, defValues); + defV.periodMin.value = periodRange.min; + defV.periodMax.value = periodRange.max; + defV.periodSteps.value = periodRange.steps; + + return defV; + } + else { + var {low,high}= inFields; + // inFields= revalidateFields(inFields); + if (!low.valid || !high.valid) { + return inFields; + } + if (parseFloat(low.value)> parseFloat(high.value)) { + low= Object.assign({},low, {valid:false, message:'must be lower than high'}); + high= Object.assign({},high, {valid:false, message:'must be higher than low'}); + return Object.assign({},inFields,{low,high}); + } + else { + low= Object.assign({},low, low.validator(low.value)); + high= Object.assign({},high, high.validator(high.value)); + return Object.assign({},inFields,{low,high}); + } + } +}; + +/** + * @summary reset the setting + */ +function resetDefaults() { + dispatchRestoreDefaults(pfkey); +} + +/** + * @summary duplicate the phase cycle to the phase folded table + * @param {string} timeCol + * @param {number} period + * @param (TableModel} phaseTable + */ +function repeatDataCycle(timeCol, period, phaseTable) { + var tIdx = phaseTable.tableData.columns.findIndex((c) => (c.name === timeCol)); + var fIdx = phaseTable.tableData.columns.findIndex((c) => (c.name === 'phase')); + var {totalRows, tbl_id, title} = phaseTable; + + + slice(phaseTable.tableData.data, 0, totalRows).forEach((d) => { + var newRow = slice(d); + + newRow[tIdx] = `${parseFloat(d[tIdx]) + period}`; + newRow[fIdx] = `${parseFloat(d[fIdx]) + 1}`; + phaseTable.tableData.data.push(newRow); + }); + + totalRows *= 2; + + set(phaseTable, 'totalRows', totalRows); + set(phaseTable, ['tableMeta', 'RowsRetrieved'], `${totalRows}`); + + set(phaseTable, ['tableMeta', 'tbl_id'], tbl_id); + set(phaseTable, ['tableMeta', 'title'], title); + + var col = phaseTable.tableData.columns.length; + var sqlMeta = get(phaseTable, ['tableMeta', 'SQL']); + if (sqlMeta) { + set(phaseTable, ['tableMeta', 'SQL'], replace(sqlMeta, `${col-1}`, `${col}`)); + } + +} + +/** + * @summary upload the table with phase column to the server and display the table and the xy chart + * @param {boolean} hideDropDown + * @returns {Function} + */ +function setPFTableSuccess(hideDropDown = false) { + return (request) => { + var reqData = get(request, pfkey); + var timeName = get(reqData, fKeyDef.time.fkey, 'mjd'); + var period = get(reqData, fKeyDef.period.fkey); + + if (!currentPhaseFoldedTable) { + doPFCalculate(); + } + + repeatDataCycle(timeName, parseFloat(period), currentPhaseFoldedTable); + + const {tableData, tbl_id, tableMeta, title} = currentPhaseFoldedTable; + const ipacTable = tableToText(tableData.columns, tableData.data, true, tableMeta); + const file = new File([new Blob([ipacTable])], `${tbl_id}.tbl`); + + doUpload(file).then(({status, cacheKey}) => { + const tReq = makeFileRequest(title, cacheKey, null, {tbl_id}); + dispatchTableSearch(tReq, {removable: false}); + + let xyPlotParams = { + x: {columnOrExpr: 'phase', options: 'grid'}, + y: {columnOrExpr: 'w1mpro_ep', options: 'grid'} + }; + loadXYPlot({chartId: PHASE_FOLDED, tblId: PHASE_FOLDED, xyPlotParams}); + }); + + if (hideDropDown) { + dispatchHideDropDown(popupId); + } + }; +} + +function setPFTableFail() { + return (request) => { + return showInfoPopup('Phase folding parameter setting error'); + }; +} + +/** + * @summary callback to add phase column to raw table on slider's change + */ +function doPFCalculate() { + let fields = FieldGroupUtils.getGroupFields(pfkey); + + let rawTable = getTblById(RAW_TABLE); + currentPhaseFoldedTable = rawTable && addPhaseToTable(rawTable, fields); +} + + +/** + * @summary create table request object for phase folded table + * @param {Object} fields + * @param {TableModel} tbl + * @returns {TableRequest} + */ +function getPhaseFoldingRequest(fields, tbl) { + return makeTblRequest('PhaseFoldedProcessor', PHASE_FOLDED, { + 'period_days': fields.period.value, + 'table_name': 'folded_table', + 'time_col_name':fields.timeCol.value, + 'original_table': tbl.tableMeta.tblFilePath + }, {tbl_id:PHASE_FOLDED}); + +} +/** + * @summary create a table model with phase column + * @param {TableModel} tbl + * @param {Object} fields + * @returns {TableModel} + */ +function addPhaseToTable(tbl, fields) { + var {timeCol, period, tzero} = fields; + var {columns, data} = tbl.tableData; + var tIdx = timeCol&&timeCol.value ? columns.findIndex((c) => (c.name === timeCol.value)) : -1; + + if (tIdx < 0 || !tzero || !period) return null; + var pd = parseFloat(period.value); + var tz = parseFloat(tzero.value); + + + var tPF = Object.assign(cloneDeep(tbl), {tbl_id: PHASE_FOLDED, title: 'Phase Folded'}, + {request: getPhaseFoldingRequest(fields, tbl)}, + {highlightedRow: 0}); + tPF = omit(tPF, ['hlRowIdx', 'isFetching']); + + var phaseC = {desc: 'number of period elapsed since starting time.', + name: 'phase', type: 'double', width: 20 }; + + tPF.tableData.columns.push(phaseC); // add phase column + + tPF.tableData.data.forEach((d, index) => { // add phase value (in string) to each data + var t = d[tIdx]; + var q = (t - tz)/pd; + var p = q >= 0 ? (q - Math.floor(q)) : (q + Math.floor(-q)); + + tPF.tableData.data[index].push(`${p.toFixed(DEC)}`); + }); + + return tPF; +} + +/** + * @summary compute the period minimum and maximum based on table data + * @param {string} tbl_id + * @param {string} timeColName + * @returns {Object} + */ +function computePeriodRange(tbl_id, timeColName) { + var tbl = getTblById(tbl_id); + var {columns, data} = tbl.tableData; + var minTime = Number.MAX_VALUE; + var maxTime = 0; + var tIdx; + var min, max; + + tIdx = columns.findIndex((col) => (col.name === timeColName)); + data.forEach((dRow) => { + var dTime = dRow[tIdx]; + + if (dTime > maxTime) { + maxTime = dTime; + } else if (dTime < minTime) { + minTime = dTime; + } + }); + + timeZero = minTime; + timeMax = maxTime; + max = parseFloat(((maxTime - minTime) * 2).toFixed(DEC)); + min = parseFloat((1.0/86400).toFixed(DEC)); // set one second as the minimum value for period + + return {min, max, steps: data.length*RES}; +} \ No newline at end of file diff --git a/src/firefly/js/templates/lightcurve/LcResult.jsx b/src/firefly/js/templates/lightcurve/LcResult.jsx index 09d7613f7c..a00db427ce 100644 --- a/src/firefly/js/templates/lightcurve/LcResult.jsx +++ b/src/firefly/js/templates/lightcurve/LcResult.jsx @@ -98,10 +98,12 @@ export class LcResult extends Component { } if (showForm) { const fields= this.state; +/* content.form = (
+
{LcPFOptionsPanel(fields)} @@ -112,15 +114,32 @@ export class LcResult extends Component {
- +
- + +
+
+
+
+
); + */ + content.form = ( +
+
+ + +
+ +
+
+ +
+
); - } diff --git a/src/firefly/js/templates/lightcurve/LcViewer.jsx b/src/firefly/js/templates/lightcurve/LcViewer.jsx index 5c1912a142..048a7cf796 100644 --- a/src/firefly/js/templates/lightcurve/LcViewer.jsx +++ b/src/firefly/js/templates/lightcurve/LcViewer.jsx @@ -26,7 +26,9 @@ import {FileUpload} from '../../ui/FileUpload.jsx'; import {dispatchHideDropDown} from '../../core/LayoutCntlr.js'; import {dispatchTableSearch} from '../../tables/TablesCntlr.js'; import {loadXYPlot} from '../../charts/dataTypes/XYColsCDT.js'; +import {syncChartViewer} from '../../visualize/saga/ChartsSync.js'; import * as TblUtil from '../../tables/TableUtil.js'; +import {showPhaseFoldingPopup} from './LcPhaseFoldingPopup.jsx'; // import {deepDiff} from '../util/WebUtil.js'; @@ -39,6 +41,7 @@ export class LcViewer extends Component { super(props); this.state = this.getNextState(); dispatchAddSaga(lcManager); + dispatchAddSaga(syncChartViewer); } getNextState() { @@ -118,8 +121,9 @@ LcViewer.propTypes = { altAppIcon: PropTypes.string, footer: PropTypes.element, dropdownPanels: PropTypes.arrayOf(PropTypes.element), - style: PropTypes.object, + style: PropTypes.object }; + LcViewer.defaultProps = { appTitle: 'Light Curve' }; @@ -151,7 +155,8 @@ function BannerSection(props) { /** * A temporary upload panel for use during development phase. This should be removed or replaced with something else. */ -export const UploadPanel = () => { +export const UploadPanel = ({phaseButton}) => { + return (
{ label: 'Periodogram:' }} /> + + {phaseButton ? : null}
); @@ -206,9 +213,10 @@ UploadPanel.propTypes = { }; UploadPanel.defaultProps = { - name: 'LCUpload', + name: 'LCUpload' }; + function onSearchSubmit(request) { var treq, xyPlotParams; if ( get(request, RAW_TABLE) ){ diff --git a/src/firefly/js/ui/FileUpload.jsx b/src/firefly/js/ui/FileUpload.jsx index 1e140e05d5..7fb2101a83 100644 --- a/src/firefly/js/ui/FileUpload.jsx +++ b/src/firefly/js/ui/FileUpload.jsx @@ -77,25 +77,32 @@ function handleChange(ev, fireValueChange, type) { } function makeDoUpload(file, type) { - const options = { - method: 'multipart', - params: {type, file} // file should be the last param due to AnyFileUpload limitation - }; return () => { - return { - isLoading: true, - value : fetchUrl(UL_URL, options).then( (response) => { - return response.text().then( (text) => { - // text is in format ${status}::${message}::${cacheKey} - const resp = text.split('::'); - const valid = get(resp, [0]) === '200'; - const message = get(resp, [1]); - const value = get(resp, [2]); - return {isLoading: false, valid, message, value}; - }); - }).catch(function(err) { - return { isLoading: false, valid: false, message: `Unable to upload file: ${get(file, 'name')}`}; - }) - }; + return doUpload(file, {type}).then( ({status, message, cacheKey}) => { + const valid = status === '200'; + return {isLoading: false, valid, message, value:cacheKey}; + }).catch((err) => { + return { isLoading: false, valid: false, message: `Unable to upload file: ${get(file, 'name')}`}; + }); }; } + +/** + * post the data in + * @param {File} file + * @param {Object} params additional parameters if any + * @returns {Promise.<{Object}>} The returned object is : {status:string, message:string, cacheKey:string} + */ +export function doUpload(file, params={}) { + if (!file) return Promise.reject('Required file parameter not given'); + params = Object.assign(params, {file}); // file should be the last param due to AnyFileUpload limitation + const options = {method: 'multipart', params}; + + return fetchUrl(UL_URL, options).then( (response) => { + return response.text().then( (text) => { + // text is in format ${status}::${message}::${cacheKey} + const [status, message, cacheKey] = text.split('::'); + return {status, message, cacheKey}; + }); + }); +} \ No newline at end of file diff --git a/src/firefly/js/ui/RangeSlider.jsx b/src/firefly/js/ui/RangeSlider.jsx new file mode 100644 index 0000000000..3e6874d58f --- /dev/null +++ b/src/firefly/js/ui/RangeSlider.jsx @@ -0,0 +1,86 @@ +import React, {Component, PropTypes} from 'react'; +import { has, isNaN} from 'lodash'; +import {fieldGroupConnector} from './FieldGroupConnector.jsx'; +import {dispatchValueChange} from '../fieldGroup/FieldGroupCntlr.js'; +import {RangeSliderView, adjustMax, checkMarksObject} from './RangeSliderView.jsx' + +function getProps(params, fireValueChange) { + + return Object.assign({}, params, + { + handleChange: (v) => handleOnChange(v, params, fireValueChange), + handleMaxChange: (v) => handleOnMaxChange(v, params, fireValueChange) + }); +} + +/** + * @summary callback to handle slider value change + * @param {string} value + * @param {Object} params + * @param {function} fireValueChange + */ +function handleOnChange(value, params, fireValueChange){ + fireValueChange({ + value + }); + + var {min, max} = params; //displayValue in string, min, max, step: number + var {minStop=min, maxStop=max} = params; + var val = parseFloat(value); + + if (!isNaN(val) && val >= minStop && val <= maxStop) { + if (has(params, 'onValueChange')) { + params.onValueChange(val); + } + } +} + +/** + * @summary callback to handle the slider value change entered in the input field + * @param {string} vText + * @param {Object} params + * @param {function} fireValueChange + */ +function handleOnMaxChange(vText, params, fireValueChange) { + var value = vText; + var {steps, max} = adjustMax(parseFloat(vText), params.min, params.step); + + fireValueChange({ + value + }); + + var {groupKey} = params; + var payload = Object.assign({}, {value: max}, {groupKey, fieldKey: 'periodMax'}); + dispatchValueChange(payload); + payload = Object.assign({}, {value: steps}, {groupKey, fieldKey: 'periodSteps'}); + dispatchValueChange(payload); + + if (has(params, 'onValueChange')) { + params.onValueChange(parseFloat(value)); + } +} + +const propTypes={ + label: PropTypes.string, // slider label + value: PropTypes.string.required, // slider value + onValueChange: PropTypes.func, // callback on slider change + min: PropTypes.number, // minimum end of slider + max: PropTypes.number, // maximum end of slider + className: PropTypes.string, // class name attached to slider component + marks: PropTypes.objectOf(checkMarksObject), // marks shown on slider + step: PropTypes.number, // slider step size + vertical: PropTypes.bool, // slider is in vertical + defaultValue: PropTypes.number, // default value of slider + handle: PropTypes.element, // custom made slider handle + wrapperStyle: PropTypes.object, // wrapper style for entire component + sliderStyle: PropTypes.object, // style for slider component + labelWidth: PropTypes.number, // label width + tooltip: PropTypes.string, // tooltip on label + minStop: PropTypes.number, // minimum value the slider can be changed to + maxStop: PropTypes.number, // maximum value the slider can be changed to + canEnterValue: PropTypes.bool, // if the slider value can be mannually entered + errMsg: PropTypes.string // message for invalid value +}; + +export const RangeSlider = fieldGroupConnector(RangeSliderView, getProps, propTypes, null); + diff --git a/src/firefly/js/ui/RangeSliderView.jsx b/src/firefly/js/ui/RangeSliderView.jsx new file mode 100644 index 0000000000..17487e7cd6 --- /dev/null +++ b/src/firefly/js/ui/RangeSliderView.jsx @@ -0,0 +1,209 @@ +import React, {Component, PropTypes} from 'react'; +import {get, has, isNumber, isString, isObject, isNil, isNaN} from 'lodash'; +import {InputFieldView} from './InputFieldView.jsx'; + +import Slider from 'rc-slider'; +import './rc-slider.css'; + +const DEC = 8; + +/** + * @summary adjust the maximum to be the multiple of the step resolution if the maximum on the slider is updated + * @param {number} max + * @param {number} min + * @param {number} step + * @returns {{steps: number, max: number}} + */ +export function adjustMax(max, min, step) { + var newTotalSteps = Math.ceil((max - min) / step); + var newMax = parseFloat((newTotalSteps*step + min).toFixed(DEC)); + var res = (newMax - min)/newTotalSteps; + + return {steps: newTotalSteps, max: newMax, res}; +} + + +/** + * @summary slider component + */ +export class RangeSliderView extends Component { + constructor(props) { + super(props); + + this.state = {displayValue: props.value, min: props.min, max: props.max, step: props.step}; + + this.onSliderChange = this.onSliderChange.bind(this); + this.onValueChange = this.onValueChange.bind(this); + } + + componentWillReceiveProps(nextProps) { + this.setState( {displayValue: nextProps.value, min: nextProps.min, max: nextProps.max, step: nextProps.step} ); + } + + + onSliderChange(v) { + var {handleChange, canEnterValue, minStop, maxStop} = this.props; + + if (!canEnterValue) { + if (minStop && v < minStop ) { + v = minStop; + } else if (maxStop && v > maxStop) { + v = maxStop; + } + } + if (handleChange) { + handleChange(`${v}`); //value could be any number of decimal + } + + this.setState({displayValue: `${v}`}); + + } + + onValueChange(e) { + var vText = get(e, 'target.value'); + var {max, handleMaxChange, handleChange, min, step} = this.props; + + if (!isNil(vText) && vText) { + var val = parseFloat(vText); + + if (isNaN(val)) { + + if (handleChange) { + handleChange(vText); + } else { + this.setState({displayValue: vText}); + } + } else if (val > max) { // value exceeds current max + if (handleMaxChange) { + handleMaxChange(vText); + } else { + var aMax = adjustMax(val, min, step ); + this.setState({displayValue: vText, max: aMax.max, step: aMax.res}); + } + } else { // value within the min and max range + if (handleChange) { + handleChange(vText); + } else { + this.setState({displayValue: vText}); + } + } + } else { + if (handleChange) { + handleChange(''); + } else { + this.setState({displayValue: ''}); + } + } + } + + render() { + var {wrapperStyle={}, sliderStyle={}, className, value, marks, vertical, + defaultValue, handle, label, labelWidth, tooltip, errMsg, canEnterValue} = this.props; + var {min, max, displayValue, step} = this.state; //displayValue in string, min, max, step: number + var {minStop=min, maxStop=max} = this.props; + var val = parseFloat(displayValue); + var valid = (!isNaN(val)) && (val >= minStop && val <= maxStop); + var v = valid ? parseFloat(value) : min; + + var labelValue = () => { + if (!label) return null; + var msg = valid ? '' : errMsg || `invalid value: must be within [${minStop}, ${maxStop}]`; + + return ( + + + ); + }; + + var sliderValue = () => { + if (!label) return null; + + return ( +
+
{label}
+
{displayValue}
+
+ ); + }; + + return ( +
+ {canEnterValue?labelValue():sliderValue()} +
+ +
+
+ ); + } +} + +RangeSliderView.propTypes = { + min: PropTypes.number, + max: PropTypes.number, + className: PropTypes.string, + marks: PropTypes.objectOf(checkMarksObject), + step: PropTypes.number, + vertical: PropTypes.bool, + defaultValue: PropTypes.number, + value: PropTypes.string.isRequired, + handle: PropTypes.element, + label: PropTypes.string, + wrapperStyle: PropTypes.object, + sliderStyle: PropTypes.object, + labelWidth: PropTypes.number, + tooltip: PropTypes.string, + minStop: PropTypes.number, + maxStop: PropTypes.number, + canEnterValue: PropTypes.bool, + errMsg: PropTypes.string, + handleChange: PropTypes.func, + handleMaxChange: PropTypes.func +}; + +RangeSliderView.defaultProps = { + min: 0, + max: 100, + step: 1, + vertical: false, + defaultValue: 0, + value: '0.0', + label: '', + canEnterValue: true +}; + +export function checkMarksObject(props, propName, componentName) { + if (isNumber(propName) || + (isString(propName) && parseFloat(propName))) { + if (isString(props[propName]) || (isObject(props[propName]) && + has(props[propName], 'style') && has(props[propName], 'label'))) { + return null; + } else { + return new Error('invalid value assigned to ' + propName + ' in ' + componentName + '. Validation failed.'); + } + + } else { + return new Error('invalid ' + propName + ' supplied to ' + componentName + '. Validation failed'); + } +} + diff --git a/src/firefly/js/ui/rc-slider.css b/src/firefly/js/ui/rc-slider.css new file mode 100644 index 0000000000..8c16529b00 --- /dev/null +++ b/src/firefly/js/ui/rc-slider.css @@ -0,0 +1,280 @@ +.rc-slider { + position: relative; + height: 4px; + width: 100%; + border-radius: 6px; + background-color: #e9e9e9; + box-sizing: border-box; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} +.rc-slider * { + box-sizing: border-box; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} +.rc-slider-track { + position: absolute; + left: 0; + height: 4px; + border-radius: 6px; + background-color: #abe2fb; +} +.rc-slider-handle { + position: absolute; + margin-left: -7px; + margin-top: -5px; + width: 14px; + height: 14px; + cursor: pointer; + border-radius: 50%; + border: solid 2px #96dbfa; + background-color: #fff; +} +.rc-slider-handle:hover { + border-color: #57c5f7; +} +.rc-slider-handle-active:active { + border-color: #57c5f7; + box-shadow: 0 0 5px #57c5f7; +} +.rc-slider-mark { + position: absolute; + top: 10px; + left: 0; + width: 100%; + font-size: 12px; +} +.rc-slider-mark-text { + position: absolute; + display: inline-block; + vertical-align: middle; + text-align: center; + cursor: pointer; + color: #999; +} +.rc-slider-mark-text-active { + color: #666; +} +.rc-slider-step { + position: absolute; + width: 100%; + height: 4px; + margin: 5px 0; + background: transparent; +} +.rc-slider-dot { + position: absolute; + bottom: -2px; + margin-left: -4px; + width: 8px; + height: 8px; + border: 2px solid #e9e9e9; + background-color: #fff; + cursor: pointer; + border-radius: 50%; + vertical-align: middle; +} +.rc-slider-dot:first-child { + margin-left: -4px; +} +.rc-slider-dot:last-child { + margin-left: -4px; +} +.rc-slider-dot-active { + border-color: #96dbfa; +} +.rc-slider-disabled { + background-color: #e9e9e9; +} +.rc-slider-disabled .rc-slider-track { + background-color: #ccc; +} +.rc-slider-disabled .rc-slider-handle, +.rc-slider-disabled .rc-slider-dot { + border-color: #ccc; + background-color: #fff; + cursor: not-allowed; +} +.rc-slider-disabled .rc-slider-mark-text, +.rc-slider-disabled .rc-slider-dot { + cursor: not-allowed !important; +} +.rc-slider-vertical { + width: 4px; + height: 100%; +} +.rc-slider-vertical .rc-slider-track { + bottom: 0; + width: 4px; +} +.rc-slider-vertical .rc-slider-handle { + position: absolute; + margin-left: -5px; + margin-bottom: -7px; +} +.rc-slider-vertical .rc-slider-mark { + top: 0; + left: 10px; + height: 100%; +} +.rc-slider-vertical .rc-slider-step { + height: 100%; + width: 4px; +} +.rc-slider-vertical .rc-slider-dot { + left: 2px; + margin-bottom: -4px; +} +.rc-slider-vertical .rc-slider-dot:first-child { + margin-bottom: -4px; +} +.rc-slider-vertical .rc-slider-dot:last-child { + margin-bottom: -4px; +} +.rc-slider-tooltip-zoom-down-enter, +.rc-slider-tooltip-zoom-down-appear { + -webkit-animation-duration: .3s; + animation-duration: .3s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + display: block !important; + -webkit-animation-play-state: paused; + animation-play-state: paused; +} +.rc-slider-tooltip-zoom-down-leave { + -webkit-animation-duration: .3s; + animation-duration: .3s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + display: block !important; + -webkit-animation-play-state: paused; + animation-play-state: paused; +} +.rc-slider-tooltip-zoom-down-enter.rc-slider-tooltip-zoom-down-enter-active, +.rc-slider-tooltip-zoom-down-appear.rc-slider-tooltip-zoom-down-appear-active { + -webkit-animation-name: rcSliderTooltipZoomDownIn; + animation-name: rcSliderTooltipZoomDownIn; + -webkit-animation-play-state: running; + animation-play-state: running; +} +.rc-slider-tooltip-zoom-down-leave.rc-slider-tooltip-zoom-down-leave-active { + -webkit-animation-name: rcSliderTooltipZoomDownOut; + animation-name: rcSliderTooltipZoomDownOut; + -webkit-animation-play-state: running; + animation-play-state: running; +} +.rc-slider-tooltip-zoom-down-enter, +.rc-slider-tooltip-zoom-down-appear { + -webkit-transform: scale(0, 0); + transform: scale(0, 0); + -webkit-animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); + animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); +} +.rc-slider-tooltip-zoom-down-leave { + -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); +} +@-webkit-keyframes rcSliderTooltipZoomDownIn { + 0% { + opacity: 0; + -webkit-transform-origin: 50% 100%; + transform-origin: 50% 100%; + -webkit-transform: scale(0, 0); + transform: scale(0, 0); + } + 100% { + -webkit-transform-origin: 50% 100%; + transform-origin: 50% 100%; + -webkit-transform: scale(1, 1); + transform: scale(1, 1); + } +} +@keyframes rcSliderTooltipZoomDownIn { + 0% { + opacity: 0; + -webkit-transform-origin: 50% 100%; + transform-origin: 50% 100%; + -webkit-transform: scale(0, 0); + transform: scale(0, 0); + } + 100% { + -webkit-transform-origin: 50% 100%; + transform-origin: 50% 100%; + -webkit-transform: scale(1, 1); + transform: scale(1, 1); + } +} +@-webkit-keyframes rcSliderTooltipZoomDownOut { + 0% { + -webkit-transform-origin: 50% 100%; + transform-origin: 50% 100%; + -webkit-transform: scale(1, 1); + transform: scale(1, 1); + } + 100% { + opacity: 0; + -webkit-transform-origin: 50% 100%; + transform-origin: 50% 100%; + -webkit-transform: scale(0, 0); + transform: scale(0, 0); + } +} +@keyframes rcSliderTooltipZoomDownOut { + 0% { + -webkit-transform-origin: 50% 100%; + transform-origin: 50% 100%; + -webkit-transform: scale(1, 1); + transform: scale(1, 1); + } + 100% { + opacity: 0; + -webkit-transform-origin: 50% 100%; + transform-origin: 50% 100%; + -webkit-transform: scale(0, 0); + transform: scale(0, 0); + } +} +.rc-slider-tooltip { + position: absolute; + left: -9999px; + top: -9999px; + visibility: visible; + box-sizing: border-box; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} +.rc-slider-tooltip * { + box-sizing: border-box; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} +.rc-slider-tooltip-hidden { + display: none; +} +.rc-slider-tooltip-placement-top { + padding: 4px 0 8px 0; +} +.rc-slider-tooltip-inner { + padding: 6px 2px; + min-width: 24px; + height: 24px; + font-size: 12px; + line-height: 1; + color: #fff; + text-align: center; + text-decoration: none; + background-color: #6c6c6c; + border-radius: 6px; + box-shadow: 0 0 4px #d9d9d9; +} +.rc-slider-tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.rc-slider-tooltip-placement-top .rc-slider-tooltip-arrow { + bottom: 4px; + left: 50%; + margin-left: -4px; + border-width: 4px 4px 0; + border-top-color: #6c6c6c; +} diff --git a/src/firefly/js/visualize/saga/ChartsSync.js b/src/firefly/js/visualize/saga/ChartsSync.js index bff3ef3a47..535408463c 100644 --- a/src/firefly/js/visualize/saga/ChartsSync.js +++ b/src/firefly/js/visualize/saga/ChartsSync.js @@ -9,6 +9,9 @@ import * as TableStatsCntlr from '../../charts/TableStatsCntlr.js'; import * as TableUtil from '../../tables/TableUtil.js'; import * as ChartsCntlr from '../../charts/ChartsCntlr.js'; +import {getDefaultXYPlotOptions, DT_XYCOLS} from '../../charts/dataTypes/XYColsCDT.js'; +import {SCATTER} from '../../charts/ChartUtil.js'; + import {PLOT2D, DEFAULT_PLOT2D_VIEWER_ID, dispatchAddViewerItems, dispatchRemoveViewerItems, getViewerItemIds, getMultiViewRoot} from '../../visualize/MultiViewCntlr.js'; /** @@ -30,9 +33,6 @@ export function* syncCharts() { ChartsCntlr.updateChartData(chartId, tblId); } }); - if (action.type === ChartsCntlr.CHART_ADD) { - dispatchAddViewerItems(DEFAULT_PLOT2D_VIEWER_ID, [chartId], PLOT2D); - } break; } case ChartsCntlr.CHART_REMOVE: @@ -41,9 +41,6 @@ export function* syncCharts() { dispatchRemoveViewerItems(DEFAULT_PLOT2D_VIEWER_ID, [chartId]); break; } - case TablesCntlr.TBL_RESULTS_ACTIVE: - updateDefaultViewer(); - break; case TablesCntlr.TABLE_LOADED: const {tbl_id} = action.payload; if (ChartsCntlr.getNumCharts(tbl_id, true)>0) { // has related mounted charts @@ -58,6 +55,49 @@ export function* syncCharts() { } } +/** + * This saga makes synchronizes the default chart viewer with the active table + */ +export function* syncChartViewer() { + while (true) { + const action = yield take([ChartsCntlr.CHART_ADD, TablesCntlr.TBL_RESULTS_ACTIVE]); + switch (action.type) { + case ChartsCntlr.CHART_ADD: + case TablesCntlr.TBL_RESULTS_ACTIVE: + updateDefaultViewer(); + break; + } + } +} + +/** + * This saga adds a default chart + */ +export function* addDefaultScatter() { + while (true) { + const action = yield take([TablesCntlr.TABLE_LOADED]); + const {tbl_id} = action.payload; + // check if a default chart needs to be added + if (ChartsCntlr.getNumCharts(tbl_id) === 0) { + // how do I know the default chart should be added? + // add a default chart if the group is main + if (TableUtil.getTableInGroup(tbl_id, 'main')) { + // default chart is xy plot of coordinate columns or first two numeric columns + const defaultOptions = getDefaultXYPlotOptions(tbl_id); + if (defaultOptions) { + ChartsCntlr.dispatchChartAdd({ + chartId: 'xyplot-' + tbl_id, + chartType: SCATTER, + groupId: tbl_id, + chartDataElements: [{tblId: tbl_id, type: DT_XYCOLS, options: defaultOptions}] + }); + } + } + } + } +} + + function updateDefaultViewer() { const tblId = TableUtil.getActiveTableId(); const chartIds = [];