Skip to content

Commit

Permalink
Prevent exploratory heatmaps that are too large from rendering (#671)
Browse files Browse the repository at this point in the history
* Catch exploratory heatmaps that are too large before further computation is performed

* updated to work more like visualization guards

* fix class names

Co-authored-by: Andy Shapiro <shapiromatron@gmail.com>
  • Loading branch information
rabstejnek and shapiromatron authored Jul 26, 2022
1 parent afeaba4 commit 5302377
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 23 deletions.
35 changes: 28 additions & 7 deletions frontend/summary/summary/ExploreHeatmap.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import HeatmapDatastore from "./heatmap/HeatmapDatastore";
const startupHeatmapAppRender = function(el, settings, datastore, options) {
const store = new HeatmapDatastore(settings, datastore, options);
try {
if (store.withinRenderableBounds) {
store.initialize();
}
ReactDOM.render(
<Provider store={store}>
<ExploreHeatmapComponent options={options} />
Expand All @@ -40,8 +43,7 @@ const startupHeatmapAppRender = function(el, settings, datastore, options) {
class ExploreHeatmapComponent extends Component {
componentDidMount() {
const {store} = this.props,
{settings} = store,
id = h.hashString(JSON.stringify(settings)),
id = store.settingsHash,
el = document.getElementById(id),
tooltipEl = document.getElementById(`tooltip-${id}`);

Expand All @@ -51,12 +53,31 @@ class ExploreHeatmapComponent extends Component {
}
render() {
const {store} = this.props,
{dataset, settings} = store,
id = h.hashString(JSON.stringify(settings)),
hasFilters = settings.filter_widgets.length > 0;
id = store.settingsHash,
hasFilters = store.settings.filter_widgets.length > 0;

if (dataset === null || dataset.length === 0) {
return <div className="alert alert-danger">No data are available.</div>;
if (!store.hasDataset) {
return (
<div className="alert alert-danger">
<p className="mb-0">
<i className="fa fa-exclamation-circle"></i>&nbsp;No data are available.
</p>
</div>
);
}

if (!store.withinRenderableBounds) {
const {n_rows, n_cols, n_cells, maxCells} = this.props.store;
return (
<div className="alert alert-danger" role="alert">
<p className="mb-0">
<i className="fa fa-exclamation-circle"></i>&nbsp;This heatmap is too large
and cannot be rendered. Using the settings specified, the current heatmap
will have {n_rows} rows, {n_cols} columns, and {n_cells} cells. Please
change the settings to have fewer than {maxCells} cells.
</p>
</div>
);
}

return (
Expand Down
60 changes: 46 additions & 14 deletions frontend/summary/summary/heatmap/HeatmapDatastore.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class HeatmapDatastore {
colorScale = null;
maxValue = null;
extensions = null;
maxCells = 3000;

@observable dataset = null;
@observable settings = null;
Expand All @@ -27,20 +28,25 @@ class HeatmapDatastore {
@observable tableDataFilters = new Set();

constructor(settings, dataset) {
this.getDetailUrl = this.getDetailUrl.bind(this);
this.modal = new HAWCModal();
// basic initialization, enough for bound checking
this.settings = settings;
this.dataset = applyRowFilters(
dataset,
settings.filters,
settings.filtersLogic,
settings.filtersQuery
);
this.dpe = new DataPivotExtension();
this.intersection = this.setIntersection();
this.filterWidgetState = this.setFilterWidgetState();
this.scales = this.setScales();
this.totals = this.setTotals();
}

initialize() {
// further initialization for full store use
this.getDetailUrl = this.getDetailUrl.bind(this);
this.modal = new HAWCModal();
this.dpe = new DataPivotExtension();
this.filterWidgetState = this.setFilterWidgetState();
this.extensions = this.setDataExtensions();
this.setColorScale();
}
Expand Down Expand Up @@ -223,13 +229,42 @@ class HeatmapDatastore {
.range(this.settings.color_range);
}

@computed
get getFilterHash() {
@computed get settingsHash() {
return h.hashString(JSON.stringify(this.settings));
}

@computed get hasDataset() {
return this.dataset !== null && this.dataset.length > 0;
}

@computed get n_rows() {
return this.scales.y.filter((d, i) =>
this.settings.compress_y ? this.totals.y[i] > 0 : true
).length;
}

@computed get n_cols() {
return this.scales.x.filter((d, i) =>
this.settings.compress_x ? this.totals.x[i] > 0 : true
).length;
}

@computed get n_cells() {
return this.n_rows * this.n_cols;
}

@computed get withinRenderableBounds() {
// ensure that the heatmap being generate is of a reasonable size that it could
// potentially be calculated. In some cases users may configure settings that generate
// a heatmap so large it becomes too large to reasonably compute.
return this.n_cells <= this.maxCells;
}

@computed get getFilterHash() {
return h.hashString(JSON.stringify(toJS(this.filterWidgetState)));
}

@computed
get rowsRemovedByFilters() {
@computed get rowsRemovedByFilters() {
// returns a Set of row indexes to remove
let removedRows = [];
const {intersection} = this;
Expand All @@ -248,8 +283,7 @@ class HeatmapDatastore {
return new Set(removedRows);
}

@computed
get usableRows() {
@computed get usableRows() {
/*
Return a Set of row indices which should be presented in heatmap.
If `settings.show_null`, use all row indices. If false, filter rows which are non-null for
Expand All @@ -274,8 +308,7 @@ class HeatmapDatastore {
}
}

@computed
get matrixDataset() {
@computed get matrixDataset() {
// build the dataset required to generate the matrix
const hash = this.getFilterHash;
if (this.matrixDatasetCache[hash]) {
Expand Down Expand Up @@ -369,8 +402,7 @@ class HeatmapDatastore {
return xy_map;
}

@computed
get getTableData() {
@computed get getTableData() {
let rows, data;
if (this.tableDataFilters.size > 0) {
let filtered_rows = [...this.tableDataFilters].map(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,14 @@ class VisualCustomizationPanel extends Component {

<div className="card">
<div className="card-body">
<h4 className="card-title">X fields</h4>
<h4 className="card-title">Columns</h4>
<AxisLabelTable settingsKey={"x_fields"} />
</div>
</div>

<div className="card">
<div className="card-body">
<h4 className="card-title">Y fields</h4>
<h4 className="card-title">Rows</h4>
<AxisLabelTable settingsKey={"y_fields"} />
</div>
</div>
Expand Down

0 comments on commit 5302377

Please sign in to comment.