/* * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt */ /** * Utilities related to charts * Created by tatianag on 3/17/16. */ import { assign, flatten, get, uniqueId, isArray, isEmpty, range, set, isObject, isString, pick, cloneDeep, merge, has, isUndefined } from 'lodash'; import shallowequal from 'shallowequal'; import {getAppOptions} from '../core/AppDataCntlr.js'; import {getTblById, isFullyLoaded, isNumericType, watchTableChanges} from '../tables/TableUtil.js'; import {TABLE_HIGHLIGHT, TABLE_LOADED, TABLE_SELECT} from '../tables/TablesCntlr.js'; import {dispatchLoadTblStats} from './TableStatsCntlr.js'; import {dispatchChartUpdate, dispatchChartHighlighted, dispatchChartSelect, getChartData} from './ChartsCntlr.js'; import {Expression} from '../util/expr/Expression.js'; import {quoteNonAlphanumeric} from '../util/expr/Variable.js'; import {flattenObject} from '../util/WebUtil.js'; import {SelectInfo} from '../tables/SelectInfo.js'; import {getTraceTSEntries as histogramTSGetter} from './dataTypes/FireflyHistogram.js'; import {getTraceTSEntries as heatmapTSGetter} from './dataTypes/FireflyHeatmap.js'; import {getTraceTSEntries as genericTSGetter} from './dataTypes/FireflyGenericData.js'; import Color from '../util/Color.js'; import {MetaConst} from '../data/MetaConst'; import {ALL_COLORSCALE_NAMES, colorscaleNameToVal} from './Colorscale.js'; import {findTableCenterColumns} from '../util/VOAnalyzer.js'; export const DEFAULT_ALPHA = 0.5; export const SCATTER = 'scatter'; export const HEATMAP = 'heatmap'; export const SELECTED_COLOR = 'rgba(255, 200, 0, 1)'; export const SELECTED_PROPS = { name: '__SELECTED', marker: {color: SELECTED_COLOR}, error_x: {color: SELECTED_COLOR}, error_y: {color: SELECTED_COLOR} }; export const HIGHLIGHTED_COLOR = 'rgba(255, 165, 0, 1)'; export const HIGHLIGHTED_PROPS = { name: '__HIGHLIGHTED', marker: {color: HIGHLIGHTED_COLOR, line: {width: 2, color: HIGHLIGHTED_COLOR}}, // increase highlight marker with line border error_x: {color: HIGHLIGHTED_COLOR}, error_y: {color: HIGHLIGHTED_COLOR} }; const FSIZE = 12; export const TBL_SRC_PATTERN = /^tables::(.+)/; /** * Does the application only support single trace * @returns {*} true, if all charts are single trace */ export function singleTraceUI() { return get(getAppOptions(), 'charts.singleTraceUI'); } /** * Maximum table rows for scatter chart support, heatmap is created for larger tables * @returns {*} */ export function getMaxScatterRows() { return get(getAppOptions(), 'charts.maxRowsForScatter', 5000); } /** * For scatter charts, the minimum number of points to use 'scattergl' (Web GL); * If the number of points less than this, 'scatter' (SVG) is used. * Web GL and SVG traces can be displayed in the same chart. * @returns {*} */ export function getMinScatterGLRows() { return get(getAppOptions(), 'charts.minScatterGLRows', 1000); } /** * @summary Get unique chart id * @param {string} [prefix] - prefix * @returns {string} unique chart id * @public * @function uniqueChartId * @memberof firefly.util.chart */ export function uniqueChartId(prefix) { return uniqueId(prefix?prefix+'-c':'c'); } export function colWithName(cols, name) { return cols.find((c) => { return c.name===name; }); } export function getNumericCols(cols) { const ncols = []; const excludeNames = ['ROW_IDX', 'ROW_NUM']; cols.forEach((c) => { if (isNumericType(c)) { if (!excludeNames.includes(c.name)) { ncols.push(c); } } }); return ncols; } /** * @global * @public * @typedef {Object} XYPlotOptions - shallow object with XYPlot parameters * @prop {string} [source] location of the ipac table, url or file path; ignored when XY plot view is added to table * @prop {string} [tbl_id] table id of the table this plot is connected to * @prop {string} [chartTitle] title of the chart * @prop {string} xCol column or expression to use for x values, can contain multiple column names ex. log(col) or (col1-col2)/col3 * @prop {string} yCol column or expression to use for y values, can contain multiple column names ex. sin(col) or (col1-col2)/col3 * @prop {string} [plotStyle] points, linepoints, line * @prop {string} [xLabel] label to use with x axis * @prop {string} [yLabel] label to use with y axis * @prop {string} [xOptions] comma separated list of x axis options: grid,flip,log * @prop {string} [yOptions] comma separated list of y axis options: grid,flip,log * @prop {string} [xError] column or expression for X error * @prop {string} [yError] column or expression for Y error */ /** * @summary Convert shallow object with XYPlot parameters to scatter plot parameters object. * @param {XYPlotOptions} params - shallow object with XYPlot parameters * @returns {Object} - object, used to create Plotly chart * @public * @function makeXYPlotParams * @deprecated * @memberof firefly.util.chart */ export function makeXYPlotParams(params) { const {tbl_id, xCol, yCol, plotStyle, xError, yError, xLabel, yLabel, xOptions, yOptions, chartTitle} = params; let data, layout={xaxis:{}, yaxis:{showgrid: false}}; if (xCol && yCol) { const trace = {tbl_id}; trace.x = `tables::${xCol}`; trace.y = `tables::${yCol}`; if (xError) { trace.error_x = {array: `tables::${xError}`}; } if (yError) { trace.error_y = {array: `tables::${yError}`}; } trace.mode = 'markers'; if (plotStyle) { if (plotStyle === 'line') { trace.mode = 'lines'; } else if (plotStyle === 'linepoints') { trace.mode = 'lines+markers'; } } data = [trace]; } else { const defProps = getDefaultChartProps(tbl_id) || {}; if (!isObject(defProps)) { return; } data = defProps.data; layout = Object.assign(layout, defProps.layout); } if (xLabel) { layout.xaxis.title = xLabel; } if (yLabel) { layout.yaxis.title = yLabel; } if (xOptions) { if (xOptions.includes('grid')) { layout.xaxis.showgrid = true; } if (xOptions.includes('flip')) { layout.xaxis.autorange = 'reversed'; } if (xOptions.includes('log')) { layout.xaxis.type = 'log'; } } if (yOptions) { if (yOptions.includes('grid')) { layout.yaxis.showgrid = true; } if (yOptions.includes('flip')) { layout.yaxis.autorange = 'reversed'; } if (yOptions.includes('log')) { layout.yaxis.type = 'log'; } } if (chartTitle) { layout.title = chartTitle; } return {data, layout}; } /** * @global * @public * @typedef {Object} HistogramOptions * @summary shallow object with histogram parameters * @prop {string} [source] location of the ipac table, url or file path; ignored when histogram view is added to table * @prop {string} [tbl_id] table id of the table this plot is connected to * @prop {string} [chartTitle] title of the chart * @prop {string} col column or expression to use for histogram, can contain multiple column names ex. log(col) or (col1-col2)/col3 * @prop {number} [numBins=50] number of bins for fixed bins algorithm (default) * @prop {number} [falsePositiveRate] false positive rate for bayesian blocks algorithm * @prop {string} [xOptions] comma separated list of x axis options: flip,log * @prop {string} [yOptions] comma separated list of y axis options: flip,log */ /** * @summary Convert shallow object with Histogram parameters to histogram plot parameters object. * @param {HistogramOptions} params - shallow object with Histogram parameters * @returns {HistogramParams} - object, used to create Histogram chart * @public * @function makeHistogramParams * @deprecated * @memberof firefly.util.chart */ export function makeHistogramParams(params) { const {tbl_id, col, xOptions, yOptions, falsePositiveRate, binWidth} = params; let numBins = params.numBins; let fixedBinSizeSelection = params.fixedBinSizeSelection; if (!falsePositiveRate) { if (!fixedBinSizeSelection) { fixedBinSizeSelection = 'numBins'; } if (!numBins) { numBins = 50; } } const algorithm = numBins ? 'fixedSizeBins' : 'bayesianBlocks'; if (col) { const options = { columnOrExpr: col, algorithm, fixedBinSizeSelection, numBins, binWidth, falsePositiveRate }; const layout = {xaxis: {}, yaxis: {}}; if (xOptions) { if (xOptions.includes('flip')) { layout.xaxis.autorange = 'reversed'; } if (xOptions.includes('log')) { layout.xaxis.type = 'log'; } } if (yOptions) { if (yOptions.includes('flip')) { layout.yaxis.autorange = 'reversed'; } if (yOptions.includes('log')) { layout.yaxis.type = 'log'; } } return { data : [{ type: 'fireflyHistogram', firefly: { tbl_id, options } }], layout }; } } /** * For a given plotly point index, get rowIdx connecting a given plotly point to the table row. * @param traceData * @param pointIdx * @returns {number} */ export function getRowIdx(traceData, pointIdx) { // firefly.rowIdx array in the trace data connects plotly points to table row indexes return Number(get(traceData, `firefly.rowIdx.${pointIdx}`, pointIdx)); } /** * For a given table row index, get plotly point index * @param traceData * @param rowIdx * @returns {number} */ export function getPointIdx(traceData, rowIdx) { const rowIdxArray = get(traceData, 'firefly.rowIdx'); // use double equal in case we compare string to Number return rowIdxArray ? rowIdxArray.findIndex((e) => e == rowIdx) : rowIdx; } export function isScatter2d(type) { return type.includes('scatter') && !type.endsWith('3d'); } export function clearChartConn({chartId}) { const oldTablesources = get(getChartData(chartId), 'tablesources',[]); if (Array.isArray(oldTablesources)) { oldTablesources.forEach( (traceTS) => { if (traceTS._cancel) { traceTS._cancel(); traceTS._cancel = undefined; } // cancel the previous watcher if exists }); } } export function newTraceFrom(data, selIndexes, newTraceProps, traceAnnotations) { const sdata = cloneDeep(pick(data, ['x', 'y', 'z', 'legendgroup', 'error_x', 'error_y', 'text', 'hovertext', 'marker', 'hoverinfo', 'firefly' ])); Object.assign(sdata, {showlegend: false, type: get(data, 'type', 'scatter'), mode: 'markers'}); // the rowIdx doesn't exist for generic plotly chart case if (isScatter2d(get(data, 'type', '')) && !get(sdata, 'firefly.rowIdx') && get(sdata, 'x.length', 0) !== 0) { const rowIdx = range(get(sdata, 'x.length')).map(String); set(sdata, 'firefly.rowIdx', rowIdx); } if (isArray(traceAnnotations) && traceAnnotations.length > 0) { const annotations = cloneDeep(traceAnnotations); const color = get(newTraceProps, 'marker.color'); flattenAnnotations(annotations).forEach((a) => {a && (a.arrowcolor = color);}); set(sdata, 'firefly.annotations', annotations); } // walk through object and replace values where there's an array with only the selected indexes. function deepReplace(obj) { Object.entries(obj).forEach( ([k,v]) => { if (Array.isArray(v) && v.length > selIndexes.length) { obj[k] = selIndexes.reduce((p, sIdx) => { p.push(v[sIdx]); return p; }, []); } else if (isObject(v)) { deepReplace(v); } }); } deepReplace(sdata); const flatprops = flattenObject(newTraceProps); Object.entries(flatprops).forEach(([k,v]) => set(sdata, k, v)); return sdata; } /** * Get trace annotation as a one level deep array * @param {array} annotations - trace annotations (there could be none, a single, or an array of annotations per point */ export function flattenAnnotations(annotations) { if (isArray(annotations)) { const filtered = annotations.filter((e) => !isUndefined(e)); if (filtered.length > 0) { // trace annotations can have a single annotation or an array of annotations per point return flatten(filtered); } } return []; } export function updateSelected(chartId, selectInfo) { const selectInfoCls = SelectInfo.newInstance(selectInfo); const {data, activeTrace=0} = getChartData(chartId); const traceData = data[activeTrace]; const selIndexes = Array.from(selectInfoCls.getSelected()).map((e)=>getPointIdx(traceData, e)); if (selIndexes) { dispatchChartSelect({chartId, selIndexes}); } } export function updateHighlighted(chartId, traceNum, highlightedRow) { const traceData = get(getChartData(chartId), `data.${traceNum}`); dispatchChartHighlighted({chartId, highlighted: getPointIdx(traceData,highlightedRow)}); } export function getDataChangesForMappings({tableModel, mappings, traceNum}) { let getDataVal; const changes = {}; changes[`fireflyData.${traceNum}.isLoading`] = !Boolean(tableModel); if (tableModel) { const cols = tableModel.tableData.columns.map((c) => c.name); const transposed = tableModel.tableData.columns.map(() => []); tableModel.tableData.data.forEach((r) => { r.map((e, idx) => transposed[idx].push(e)); }); // tableModel columns are named as the paths to the trace arrays getDataVal = (k,v) => { // using plotly attribute path (key in the mappings object) as a column name // this makes it possible to use the same column as x and y, for example let idx = cols.indexOf(v); if (idx < 0) idx = cols.indexOf(k); if (idx >= 0) { return transposed[idx]; } else { // if value is a numeric constant, // we might be able to use it instead of array // example: marker.size can be a number or an array const numericConstant = parseFloat(v); if (!Number.isNaN(numericConstant)) { return numericConstant; } } }; } else { // no tableModel case is for pre-fetch changes changes[`fireflyData.${traceNum}.error`] = undefined; getDataVal = (k,v) => v; } if (mappings) { Object.entries(mappings).forEach(([k,v]) => { const key = k.startsWith('firefly') ? k : `data.${traceNum}.${k}`; changes[key] = getDataVal(key,v); }); } return changes; } /** * * @param {object} p * @param {string} p.chartId * @param {object[]} p.data */ export function handleTableSourceConnections({chartId, data, fireflyData}) { const tablesources = makeTableSources(chartId, data, fireflyData); const {tablesources:oldTablesources=[], activeTrace, curveNumberMap=[]} = getChartData(chartId); const hasTablesources = Array.isArray(tablesources) && tablesources.find((ts) => !isEmpty(ts)); if (!hasTablesources) return; const numTraces = Math.max(tablesources.length, oldTablesources.length); range(numTraces).forEach( (idx) => { // range instead of for-loop is to avoid the idx+1 JS's closure problem let traceTS = tablesources[idx]; const oldTraceTS = oldTablesources[idx] || {}; let doUpdate = false; if (isEmpty(traceTS)) { // no updates to this trace, but shared layout updates might affect this trace const {fireflyData:oldFireflyData} = getChartData(chartId); const chartDataType = get(oldFireflyData[idx], 'dataType'); traceTS = Object.assign({}, oldTraceTS, getTraceTSEntries({chartDataType, traceTS: oldTraceTS, chartId, traceNum: idx})); } else { // if mappings are resolved, we need to get info from old tablesource if (!traceTS.fetchData) { traceTS = Object.assign({}, oldTraceTS, traceTS); } } if (!tablesourcesEqual(traceTS, oldTraceTS)) { if (oldTraceTS && oldTraceTS._cancel) { oldTraceTS._cancel(); // cancel the previous watcher if exists oldTraceTS._cancel = undefined; traceTS._cancel = undefined; } doUpdate = true; } else { traceTS = oldTraceTS; } // make sure table watcher is set for all non-empty table sources if (!isEmpty(traceTS) && !traceTS._cancel) { //creates a new one.. and save the cancel handle if (doUpdate) { // fetch data syncs highlighted and selected with the table updateChartData(chartId, idx, traceTS); } else { if (idx === activeTrace && isFullyLoaded(traceTS.tbl_id)) { // update highlighted and selected const tableModel = getTblById(traceTS.tbl_id); const {highlightedRow, selectInfo={}} = tableModel; updateHighlighted(chartId, idx, highlightedRow); updateSelected(chartId, selectInfo); } } traceTS._cancel = setupTableWatcher(chartId, traceTS, idx); } tablesources[idx] = traceTS; }); const changes = {tablesources}; // update curveNumberMap if it does not contain all the traces if (curveNumberMap.length < tablesources.length) { const curveMap = range(tablesources.length).filter((idx) => (idx !== activeTrace)); curveMap.push(activeTrace); changes['curveNumberMap'] = curveMap; } dispatchChartUpdate({chartId, changes}); } export function setupTableWatcher(chartId, ts, idx) { return watchTableChanges(ts.tbl_id, [TABLE_LOADED, TABLE_HIGHLIGHT, TABLE_SELECT], (action) => updateChartData(chartId, idx, ts, action), uniqueId(`ucd-${ts.tbl_id}-trace`)); // watcher id for debugging } function tablesourcesEqual(newTS, oldTS) { // checking if the table or mappings options have changed // or table watcher has been cancelled return get(newTS, 'tbl_id') === get(oldTS, 'tbl_id') && get(newTS, 'resultSetID') === get(oldTS, 'resultSetID') && shallowequal(get(newTS, 'mappings'), get(oldTS, 'mappings')) && shallowequal(get(newTS, 'options'), get(oldTS, 'options')); } function updateChartData(chartId, traceNum, tablesource, action={}) { // make sure the chart is not yet removed if (isEmpty(getChartData(chartId))) { return; } const {tbl_id, resultSetID, mappings} = tablesource; if (action.type === TABLE_HIGHLIGHT) { // ignore if traceNum is not active const {activeTrace=0} = getChartData(chartId); if (traceNum !== activeTrace) return; const {highlightedRow} = action.payload; updateHighlighted(chartId, traceNum, highlightedRow); } else if (action.type === TABLE_SELECT) { const {activeTrace=0} = getChartData(chartId); if (traceNum !== activeTrace) return; const {selectInfo={}} = action.payload; updateSelected(chartId, selectInfo); } else { if (!isFullyLoaded(tbl_id)) return; const tableModel = getTblById(tbl_id); dispatchLoadTblStats(tableModel.request); const changes = getDataChangesForMappings({mappings, traceNum}); // save original table file path const resultSetIDNow = get(tableModel, 'tableMeta.resultSetID'); if (resultSetIDNow !== resultSetID) { changes[`tablesources.${traceNum}.resultSetID`] = resultSetIDNow; } if (!isEmpty(changes)) { dispatchChartUpdate({chartId, changes}); } // fetch data for both Firefly recognized or unrecognized plotly chart types if (tablesource.fetchData) { tablesource.fetchData(chartId, traceNum, tablesource); } } } function makeTableSources(chartId, data=[], fireflyData=[]) { const convertToDS = (flattenData) => Object.entries(flattenData) .filter(([,v]) => typeof v === 'string' && v.startsWith('tables::')) .reduce( (p, [k,v]) => { const [,colExp] = v.match(TBL_SRC_PATTERN) || []; if (colExp) set(p, ['mappings',k], colExp); return p; }, {}); // for some firefly specific chart types the data are const currentData = (data.length < fireflyData.length) ? fireflyData : data; return currentData.map((d, traceNum) => { const fireflyDataFlatten = flattenObject(fireflyData[traceNum] || {}, `fireflyData.${traceNum}`); // fireflyData mappings have full path const flattenData = assign(flattenObject(data[traceNum] || {}), fireflyDataFlatten); const ds = data[traceNum] ? convertToDS(flattenData) : {}; //avoid flattening arrays // table id can be a part of fireflyData too const tbl_id = get(data, `${traceNum}.tbl_id`) || get(fireflyData, `${traceNum}.tbl_id`); if (tbl_id) ds.tbl_id = tbl_id; // we use resultSetID to see if the table has changed (sorted, filtered, etc.) if (ds.tbl_id) { const tableModel = getTblById(ds.tbl_id); ds.resultSetID = get(tableModel, 'tableMeta.resultSetID'); } // set up table server request parameters (options) for firefly specific charts const chartDataType = get(fireflyData[traceNum], 'dataType'); if (!isEmpty(ds)) { Object.assign(ds, getTraceTSEntries({chartDataType, traceTS: ds, chartId, traceNum})); } return ds; }); } function getTraceTSEntries({chartDataType, traceTS, chartId, traceNum}) { if (chartDataType === 'fireflyHistogram') { return histogramTSGetter({traceTS, chartId, traceNum}); } else if (chartDataType === 'fireflyHeatmap') { return heatmapTSGetter({traceTS, chartId, traceNum}); } else { return genericTSGetter({traceTS, chartId, traceNum}); } } // does the default depend on the chart type? /** * set default value for layout and data * @param chartData * @param resetColor reset color generator for default color assignment of the chart */ export function applyDefaults(chartData={}, resetColor = true) { const nonPieChart = isEmpty(chartData) || !has(chartData, 'data') || chartData.data.find((d) => get(d, 'type') !== 'pie'); const noXYAxis = Boolean(!nonPieChart); const chartType = get(chartData, ['data', '0', 'type']); const hMode = chartType ==='fireflyHistogram'? 'x':'closest'; const defaultLayout = { hovermode: hMode, legend: { font: {size: FSIZE}, orientation: 'v', yanchor: 'top' }, xaxis: { autorange:true, showgrid: false, lineColor: '#e9e9e9', tickwidth: 1, ticklen: 5, titlefont: { size: FSIZE }, ticks: noXYAxis ? '' : 'outside', showline: !noXYAxis, showticklabels: !noXYAxis, zeroline: false, exponentformat:'e' }, yaxis: { autorange:true, showgrid: !noXYAxis, lineColor: '#e9e9e9', tickwidth: 1, ticklen: 5, titlefont: { size: FSIZE }, ticks: noXYAxis ? '' : 'outside', showline: !noXYAxis, showticklabels: !noXYAxis, zeroline: false, exponentformat:'e' } }; chartData.layout = merge(defaultLayout, chartData.layout); chartData.data && chartData.data.forEach((d, idx) => { d.name = defaultTraceName(d, idx, ''); const type = get(chartData, ['fireflyData', `${idx}.dataType`]) || get(d, 'type', 'scatter'); if (idx === 0 && resetColor) { // reset the color iterator getNextTraceColor(true); getNextTraceColorscale(true); } type && Object.entries(getDefaultColorAttributes(d, type, idx)).forEach(([k, v]) => set(chartData, k, v)); // default dragmode is select if box selection is supported type && !chartData.layout.dragmode && (chartData.layout.dragmode = isBoxSelectionSupported(type) ? 'select' : 'zoom'); }); } export function isBoxSelectionSupported(type) { if (!type || !isString(type)) return false; return ['heatmap', 'histogram2dcontour', 'histogram2d', 'scatter'].find((e) => type.toLowerCase().includes(e)); } /** * Convert column expression to internal (database) syntax * @param {object} p * @param {string} p.colOrExpr - column expression * @param {boolean} p.quoted - if true, quote variable names * @param {string[]} p.colNames - valid column names * @returns {*} */ export function formatColExpr({colOrExpr, quoted, colNames}) { if (!colOrExpr) return; // do not change the expression, if it's matching a column name if (colNames && colNames.find((c) => c === colOrExpr)) { return quoted ? `"${colOrExpr}"` : colOrExpr; } const expr = new Expression(colOrExpr, colNames); if (expr.isValid()) { // remove white space colOrExpr = expr.getCanonicalInput(); if (quoted) { // quote columns, assuming column names are alpha-numeric expr.getParsedVariables().forEach((v) => { if (!v.startsWith('"')) { const re = new RegExp('([^A-Za-z\d_"]|^)(' + v + ')([^A-Za-z\d_"]|$)', 'g'); while (colOrExpr.match(re)) { // while is needed to handle cases like v*v colOrExpr = colOrExpr.replace(re, '$1"$2"$3'); // add quotes } } }); colOrExpr = colOrExpr.replace(/"NULL"/g, 'NULL'); // unquote NULL } } return colOrExpr; } // plotly default color (items 0-7) + color-blind friendly colors export const TRACE_COLORS = [ '#1f77b4', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#17becf', '#333333', '#ff3333', '#00ccff', '#336600', '#9900cc', '#ff9933', '#009999', '#66ff33', '#cc9999', '#b22424', '#008fb2', '#244700', '#6b008f', '#b26b24', '#006b6b', '#47b224', '#8F6B6B']; export const TRACE_COLORSCALE = ALL_COLORSCALE_NAMES; export function toRGBA(c, alpha) { if (!alpha) { alpha = DEFAULT_ALPHA; } const [r, g, b, a] = Color.toRGBA(c, alpha); return `rgba(${r},${g},${b},${a})`; } export function *traceColorGenerator(colorList, isColor) { let nextIdx = -1; const f = (c) => isColor ? toRGBA(c, DEFAULT_ALPHA) : c; while (true) { const result = yield (nextIdx === -1) ? '' : f(colorList[nextIdx%colorList.length]); result ? nextIdx = -1 : nextIdx++; } } const nextTraceColor = traceColorGenerator(TRACE_COLORS, true); const nextTraceColorscale = traceColorGenerator(TRACE_COLORSCALE); const getNextTraceColor = (b) => nextTraceColor.next(b).value; const getNextTraceColorscale = (b) => nextTraceColorscale.next(b).value; /** * This function returns default attributes for a new trace (via UI). * @param chartId * @param type * @param traceNum - an object with the default color attributes for a trace * @returns {*} */ export function getNewTraceDefaults(chartId, type='', traceNum=0) { let retV; if (type.includes(SCATTER)) { // we only need to set marker color: other color attributes will be set based on marker color // this is handled by BasicOptions, which keeps all color attributes in sync with marker color const traceColor = defaultTraceColor({}, traceNum, chartId); retV = { [`data.${traceNum}.type`]: type, //make sure trace type is set [`data.${traceNum}.marker.color`]: traceColor, [`data.${traceNum}.marker.line`]: 'none', [`data.${traceNum}.showlegend`]: true, ['layout.xaxis.range']: undefined, //clear out fixed range ['layout.yaxis.range']: undefined //clear out fixed range }; colorsOnTypes['scatter'][0].forEach((p) => { retV[`data.${traceNum}.${p}`] = traceColor; } ); } else if (type.toLowerCase().includes(HEATMAP)) { retV = { [`data.${traceNum}.showlegend`]: true, ['layout.xaxis.range']: undefined, //clear out fixed range ['layout.yaxis.range']: undefined //clear out fixed range }; /* There are two approaches regarding unset color scale: pick a new one for every trace or keep it unset to let Plotly choose the best one for the data. */ // const traceColorscale = TRACE_COLORSCALE[traceNum % TRACE_COLORSCALE.length]; // const colorscaleVal = colorscaleNameToVal(traceColorscale); // if (colorscaleVal) { // retV[`data.${traceNum}.colorscale`] = colorscaleVal; // } // if (colorscaleVal !== traceColorscale) { // retV[`fireflyData.${traceNum}.colorscale`] = traceColorscale; // } } else { retV = { [`data.${traceNum}.marker.color`]: defaultTraceColor({}, traceNum, chartId), [`data.${traceNum}.showlegend`]: true }; } retV[`data.${traceNum}.name`] = defaultTraceName({}, traceNum, chartId); return retV; } function defaultTraceName(oneChartData, idx, chartId) { let name = get(oneChartData, 'name'); if (name) { return name; } else { // make sure that the name is unique const {data=[]} = getChartData(chartId); let i = idx; let unique = false; while (!unique) { name = `trace ${i}`; // make sure the trace name is unique if (data.findIndex((d) => (get(d, 'name') === name)) < 0) { unique = true; } i++; } return name; } } function defaultTraceColor(oneChartData, idx, chartId) { let color = get(oneChartData, 'marker.color'); if (color) { return color; } else { // make sure the color is unique const {data=[]} = getChartData(chartId); let i = idx; let unique = false; while (!unique) { color = toRGBA(TRACE_COLORS[i % TRACE_COLORS.length]); // make sure the trace name is unique if (data.findIndex((d) => (get(d, 'marker.color') === color)) < 0) { unique = true; } i++; } return color; } } export const colorsOnTypes = { bar: [['marker.color', 'error_x.color', 'error_y.color']], fireflyHistogram: [['marker.color']], box: [['marker.color', 'line.color']], heatmap: [['colorscale']], fireflyHeatmap: [['colorscale']], histogram: [['marker.color', 'error_x.color', 'error_y.color']], histogram2d: [['colorscale']], area: [['marker.color']], contour: [['colorscale' ]], histogram2dcontour: [['colorscale']], surface: [['colorscale']], mesh3d: [['colorscale']], chororpleth: [['colorscale']], scatter: [['marker.color', 'line.color', 'textfont.color', 'error_x.color', 'error_y.color']], scatter3d: [['marker.color', 'line.color', 'textfont.color', 'error_x.color', 'error_y.color']], scattergl: [['marker.color', 'line.color', 'textfont.color', 'error_x.color', 'error_y.color']], scattergeo: [['marker.color', 'line.color', 'textfont.color']], others: [['marker.color']] }; /** * This function is used to apply color attributes to a new chart * @param traceData * @param type * @param idx * @returns {{}} an object with color attributes */ function getDefaultColorAttributes(traceData, type, idx) { if (!type) return {}; const colorSettingObj = {}; const colorAttributes = Object.keys(colorsOnTypes).includes(type) ? colorsOnTypes[type] : colorsOnTypes.others; const color = getNextTraceColor(); colorAttributes[0].filter((att) => att.endsWith('color')).forEach((att) => { if (!get(traceData, att)) { colorSettingObj[`data.${idx}.${att}`] = color; } }); /* There are two approaches regarding unset color scale: pick a new one for every trace or keep it unset to let Plotly choose the best one for the data. */ // const colorscaleName = getNextTraceColorscale(); // const colorscaleVal = colorscaleNameToVal(colorscaleName); // colorAttributes[0].filter((att) => att.endsWith('colorscale')).forEach((att) => { // if (!get(traceData, att)) { // if (colorscaleVal) { // colorSettingObj[`data.${idx}.${att}`] = colorscaleVal; // } // if (colorscaleName !== colorscaleVal) { // colorSettingObj[`fireflyData.${idx}.${att}`] = colorscaleName; // } // } // }); return colorSettingObj; } export function getDefaultChartProps(tbl_id) { if (!isFullyLoaded(tbl_id)) { return; } const tblModel = getTblById(tbl_id); const {tableMeta, tableData, totalRows}= getTblById(tbl_id); if (!totalRows) { return; } // default chart props can be set in a table attribute const defaultChartDef = tableMeta[MetaConst.DEFAULT_CHART_DEF]; const defaultChartProps = defaultChartDef && JSON.parse(defaultChartDef); if (defaultChartProps && defaultChartProps.data) { defaultChartProps.data.forEach((e) => e['tbl_id'] = tbl_id); return defaultChartProps; } // for catalogs use lon and lat columns const centerColumns = findTableCenterColumns(tblModel); let isCatalog = get(tblModel, 'totalRows') && centerColumns; let xCol = undefined, yCol = undefined; if (isCatalog) { xCol = colWithName(tableData.columns, get(centerColumns, 'lonCol')); yCol = colWithName(tableData.columns, get(centerColumns, 'latCol')); if (!xCol || !yCol) { isCatalog = false; } } //otherwise use the first one-two numeric columns if (!isCatalog) { const numericCols = getNumericCols(tableData.columns); if (numericCols.length > 1) { xCol = numericCols[0]; yCol = numericCols[1]; } else if (numericCols.length > 0) { xCol = numericCols[0]; yCol = numericCols[0]; } } if (xCol && yCol) { // non-alphanumeric column names should be quoted in expressions const xColName = quoteNonAlphanumeric(xCol.name); const yColName = quoteNonAlphanumeric(yCol.name); if (xColName === yColName) { // if only one numeric column is available, do histogram const chartData = { data: [{ type: 'fireflyHistogram', firefly: { tbl_id, options: { algorithm: 'fixedSizeBins', fixedBinSizeSelection: 'numBins', numBins: 20, columnOrExpr: `${xColName}` } }, name: `${xColName}` }] }; return chartData; } else { // scatter that converts into heatmap and back depending on the number of points const colorscaleName = 'GreySeq'; const colorscale = colorscaleNameToVal(colorscaleName); const chartData = { data: [{ tbl_id, type: totalRows >= getMinScatterGLRows() ? 'scattergl' : 'scatter', mode: 'markers', x: xCol && `tables::${xColName}`, y: yCol && `tables::${yColName}`, colorscale, firefly: { scatterOrHeatmap: true, colorscale: colorscaleName } }], layout: { xaxis: { autorange: isCatalog ? 'reversed' : 'true' }, yaxis: {showgrid: false} } }; return chartData; } } else { return {}; } }