-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
DM-10830: Optimize the performance of multi-trace chart updates #397
Changes from 1 commit
c8743ad
d76da65
eadf0f8
f1e8e66
f266326
ab8f390
464634f
313a4f5
99d2125
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,7 @@ | |
* Utilities related to charts | ||
* Created by tatianag on 3/17/16. | ||
*/ | ||
import {get, uniqueId, isUndefined, omitBy, zip, isEmpty, range, set, isObject, isPlainObject, pick, cloneDeep, merge} from 'lodash'; | ||
import {get, uniqueId, isUndefined, omitBy, zip, isEmpty, range, set, isObject, pick, cloneDeep, merge} from 'lodash'; | ||
import shallowequal from 'shallowequal'; | ||
|
||
import {flux} from '../Firefly.js'; | ||
|
@@ -278,7 +278,7 @@ export function getOptionsUI(chartId) { | |
const {data, fireflyData, activeTrace=0} = getChartData(chartId); | ||
const type = get(data, [activeTrace, 'type'], 'scatter'); | ||
const dataType = get(fireflyData, [activeTrace, 'dataType'], ''); | ||
if (type === 'scatter') { | ||
if (type.includes('scatter')) { | ||
return ScatterOptions; | ||
} else if (type === 'histogram') { | ||
return HistogramOptions; | ||
|
@@ -292,7 +292,7 @@ export function getOptionsUI(chartId) { | |
export function getToolbarUI(chartId, activeTrace=0) { | ||
const {data, fireflyData} = getChartData(chartId); | ||
const type = get(data, [activeTrace, 'type'], ''); | ||
if (type === 'scatter') { | ||
if (type.includes('scatter')) { | ||
return ScatterToolbar; | ||
} else { | ||
return BasicToolbar; | ||
|
@@ -327,7 +327,7 @@ export function newTraceFrom(data, selIndexes, newTraceProps) { | |
}); | ||
} | ||
deepReplace(sdata); | ||
const flatprops = flattenObject(newTraceProps, isPlainObject); | ||
const flatprops = flattenObject(newTraceProps); | ||
Object.entries(flatprops).forEach(([k,v]) => set(sdata, k, v)); | ||
return sdata; | ||
} | ||
|
@@ -474,7 +474,7 @@ function makeTableSources(chartId, data=[], fireflyData=[]) { | |
const currentData = (data.length < fireflyData.length) ? fireflyData : data; | ||
|
||
return currentData.map((d, traceNum) => { | ||
const ds = data[traceNum] ? convertToDS(flattenObject(data[traceNum], isPlainObject)) : {}; //avoid flattening arrays | ||
const ds = data[traceNum] ? convertToDS(flattenObject(data[traceNum])) : {}; //avoid flattening arrays | ||
if (!ds.tbl_id) { | ||
// table id can be a part of fireflyData | ||
const tbl_id = get(fireflyData, `${traceNum}.tbl_id`); | ||
|
@@ -548,6 +548,19 @@ export function applyDefaults(chartData={}) { | |
chartData.layout = merge(defaultLayout, chartData.layout); | ||
} | ||
|
||
const TRACE_COLORS = ['lightblue', 'green', 'purple', 'brown', 'yellow', 'red']; | ||
export function getNewTraceDefaults(type='', traceNum=0) { | ||
if (type.includes(SCATTER)) { | ||
return { | ||
[`data.${traceNum}.type`] : type, //make sure trace type is set | ||
[`data.${traceNum}.marker.color`]: TRACE_COLORS[traceNum] || undefined, | ||
[`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 | ||
}; | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I noticed that the defaults behave differently for scatter and scattergl. Also, in scattergl, the spikes (vertical and horizontal droplines meeting at the point) are shown by default, while they are not shown by default in scatter plot. For crowded charts these lines are convenient, but since the spikes do not extend all the way, the direction is wrong when axes are on the opposite sides. None of the spike attributes listed in the documentation take effect for scattergl at the moment. mirror axis attribute does not work either. So the workaround would be to remove the options for opposite axis location. (As well as for reverse.) |
||
|
||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,9 +4,10 @@ | |
|
||
import React, {Component} from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import {get,debounce} from 'lodash'; | ||
import {get, debounce, isEmpty, set, omit} from 'lodash'; | ||
import {getPlotLy} from '../PlotlyConfig.js'; | ||
import {logError} from '../../util/WebUtil.js'; | ||
import {getChartData} from '../ChartsCntlr.js'; | ||
import {logError, deltas, flattenObject} from '../../util/WebUtil.js'; | ||
import BrowserInfo from '../../util/BrowserInfo.js'; | ||
import Enum from 'enum'; | ||
|
||
|
@@ -166,24 +167,58 @@ export class PlotlyWrapper extends Component { | |
return true; | ||
} | ||
|
||
optimize(graphDiv, renderType, {chartId, data, layout, dataUpdate, layoutUpdate, dataUpdateTraces, ...rest}) { | ||
const {lastInputTime} = layout; | ||
if (lastInputTime && renderType === RenderType.NEW_PLOT && graphDiv.data) { | ||
// omitting 'firefly' from data[*] for now | ||
data = data.map((d) => omit(d, 'firefly')); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the mismatch in clicked and highlighted point when highlighting a selected point is caused by not passing firefly rowIdx array in plotly data. (For selected points I get row idx from plotly div, because we do not save them in data.) I think, we should omit firefly object for the purposes of calculating the difference, but it should not be dropped completely. We should still pass it with the partial or full update. Alternatively, we should store selected and highlighted traces in our store. |
||
layout = omit(layout, 'lastInputTime'); | ||
|
||
const dataDelta = deltas(data, graphDiv.data || {}); | ||
const layoutDelta = flattenObject(deltas(layout, graphDiv.layout || {}, false)); | ||
|
||
const hasLayout = !isEmpty(layoutDelta); | ||
const hasData = !isEmpty(dataDelta); | ||
if(hasData) { | ||
dataUpdate = Object.values(dataDelta).map((d) => flattenObject(d)); | ||
dataUpdateTraces = Object.keys(dataDelta).map((k) => parseInt(k)); | ||
renderType = RenderType.RESTYLE; | ||
} | ||
if (hasLayout) { | ||
layoutUpdate = layoutDelta; | ||
renderType = RenderType.RELAYOUT; | ||
} | ||
if (hasData && hasLayout) { | ||
renderType = RenderType.RESTYLE_AND_RELAYOUT; | ||
} | ||
} | ||
|
||
return {renderType, chartId, data, layout, dataUpdate, layoutUpdate, dataUpdateTraces, ...rest}; | ||
} | ||
|
||
draw() { | ||
const renderType = this.renderType; | ||
let renderType = this.renderType; | ||
if (renderType===RenderType.PAUSE_DRAWING) return; | ||
|
||
const {data,layout, config= defaultConfig, newPlotCB, dataUpdate, layoutUpdate, dataUpdateTraces}= this.props; | ||
|
||
getPlotLy().then( (Plotly) => { | ||
|
||
const optimized = sessionStorage.getItem('chartRedraw') ? Object.assign({renderType}, this.props) : | ||
(this.optimize(this.div || {}, renderType, this.props)); | ||
|
||
const {chartId, data,layout, config= defaultConfig, newPlotCB, dataUpdate, layoutUpdate, dataUpdateTraces} = optimized; | ||
renderType = optimized.renderType; | ||
|
||
if (this.div) { // make sure the div is still there | ||
const now = Date.now(); | ||
switch (renderType) { | ||
case RenderType.RESTYLE: | ||
Plotly.restyle(this.div, dataUpdate, dataUpdateTraces); | ||
this.restyle(this.div, Plotly, dataUpdate, dataUpdateTraces); | ||
break; | ||
case RenderType.RELAYOUT: | ||
Plotly.relayout(this.div, layoutUpdate); | ||
break; | ||
case RenderType.RESTYLE_AND_RELAYOUT: | ||
Plotly.restyle(this.div, dataUpdate, dataUpdateTraces); | ||
this.restyle(this.div, Plotly, dataUpdate, dataUpdateTraces); | ||
Plotly.relayout(this.div, layoutUpdate); | ||
break; | ||
case RenderType.RESIZE: | ||
|
@@ -202,6 +237,7 @@ export class PlotlyWrapper extends Component { | |
chart.on('plotly_relayout', () => this.showMask(false)); | ||
chart.on('plotly_restyle', () => this.showMask(false)); | ||
chart.on('plotly_redraw', () => this.showMask(false)); | ||
chart.on('plotly_relayout', (changes) => this.syncLayout(chartId, changes)); | ||
} | ||
else { | ||
this.showMask(false); | ||
|
@@ -210,11 +246,48 @@ export class PlotlyWrapper extends Component { | |
|
||
break; | ||
} | ||
this.syncLayout(chartId, {lastInputTime: Date.now()}); | ||
console.log(`redraw elapsed: ${Date.now() - now}`); | ||
} | ||
} ).catch( (e) => { | ||
console.log('Plotly not loaded',e); | ||
}); | ||
} | ||
|
||
/** | ||
* This function sync the div.layout with chart's layout. | ||
* Will use direct object update instead of dispatch chart update to avoid | ||
* unneeded render/comparison. | ||
* @param chartId | ||
* @param changes | ||
*/ | ||
syncLayout(chartId, changes) { | ||
const {layout} = getChartData(chartId) || {}; | ||
if (layout) { | ||
Object.entries(changes).forEach( ([k, v]) => { | ||
if (k === 'xaxis' && Array.isArray(v)) { | ||
set(layout, 'xaxis.range', v); | ||
set(layout, 'xaxis.autorange', false); | ||
} else if (k === 'yaxis' && Array.isArray(v)) { | ||
set(layout, 'yaxis.range', v); | ||
set(layout, 'yaxis.autorange', false); | ||
} else { | ||
set(layout, k, v); | ||
} | ||
}); | ||
} | ||
} | ||
|
||
restyle(div, Plotly, dataUpdate, dataUpdateTraces) { | ||
if (Array.isArray(dataUpdate)) { | ||
dataUpdate.forEach((v,idx) => this.restyle(div, Plotly, v, dataUpdateTraces[idx])); | ||
} else { | ||
if (dataUpdateTraces >= get(div, 'data.length', 0)) { | ||
Plotly.addTraces(div, dataUpdate, dataUpdateTraces); | ||
} else { | ||
Plotly.restyle(div, dataUpdate, dataUpdateTraces); | ||
} | ||
} | ||
} | ||
|
||
refUpdate(ref) { | ||
|
@@ -279,7 +352,7 @@ PlotlyWrapper.defaultProps = { | |
maskOnLayout : true, | ||
maskOnRestyle : false, | ||
maskOnResize : true, | ||
maskOnNewPlot : true, | ||
maskOnNewPlot : false, | ||
|
||
autoSizePlot : false, | ||
autoDetectResizing : false, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think, when the type is not SCATTER this function returns undefined, which is causing the type error, which I mentioned before (when we are trying to use the result as an array).