From a7a51cf130794193c205ab48e0b11a5fbeaaafa0 Mon Sep 17 00:00:00 2001 From: Ossama Rafique Date: Wed, 3 May 2023 17:38:53 -0700 Subject: [PATCH 1/2] Perform data indexing and computations on-demand when required --- app/scripts/index.js | 5 +- cypress/integration/data-processor.spec.js | 60 ++++++++++++++-------- src/epiviz.gl/data-processor-worker.js | 44 ++++++++++------ src/epiviz.gl/data-processor.js | 44 ++++++++++------ src/epiviz.gl/utilities.js | 7 +++ src/epiviz.gl/webgl-vis.js | 50 ++++++++++++------ 6 files changed, 140 insertions(+), 70 deletions(-) diff --git a/app/scripts/index.js b/app/scripts/index.js index c537f4d..be6116d 100644 --- a/app/scripts/index.js +++ b/app/scripts/index.js @@ -27,6 +27,9 @@ class App { this.visualization.addEventListener("pointHovered", (event) => console.log("pointHovered", event) ); + this.visualization.addEventListener("pointClicked", (event) => + console.log("pointClicked", event) + ); this.visualization.addEventListener("pan", (event) => console.log("pan", event) ); @@ -46,7 +49,7 @@ class App { let selem = document.getElementById("specification-select"); selem.value = "tsne-10th"; - selem.dispatchEvent(new Event('change')); + selem.dispatchEvent(new Event("change")); document.getElementById("refresh-specification").click(); } diff --git a/cypress/integration/data-processor.spec.js b/cypress/integration/data-processor.spec.js index 6b3340b..3214b83 100644 --- a/cypress/integration/data-processor.spec.js +++ b/cypress/integration/data-processor.spec.js @@ -188,7 +188,8 @@ describe("Box selection", () => { ); cy.wrap(dataProcessor) - .should("have.property", "index") + .should("have.property", "specificationHelper") + .then(() => dataProcessor.indexDataIfNotAlreadyIndexed()) .then(() => { const allPoints = dataProcessor.selectBox([1, 1, 7, 7]); expect(allPoints).to.have.lengthOf( @@ -215,7 +216,8 @@ describe("Box selection", () => { ); cy.wrap(dataProcessor) - .should("have.property", "index") + .should("have.property", "specificationHelper") + .then(() => dataProcessor.indexDataIfNotAlreadyIndexed()) .then(() => { const allPoints = dataProcessor.selectBox([1, 1, 7, 7]); expect(allPoints).to.have.lengthOf( @@ -261,7 +263,8 @@ describe("Box selection", () => { ); cy.wrap(dataProcessor) - .should("have.property", "index") + .should("have.property", "specificationHelper") + .then(() => dataProcessor.indexDataIfNotAlreadyIndexed()) .then(() => { const allPoints = dataProcessor.selectBox([1, 1, 7, 7]); expect(allPoints).to.have.lengthOf( @@ -297,7 +300,8 @@ describe("Box selection", () => { ); cy.wrap(dataProcessor) - .should("have.property", "index") + .should("have.property", "specificationHelper") + .then(() => dataProcessor.indexDataIfNotAlreadyIndexed()) .then(() => { const allPoints = dataProcessor.selectBox([1, 1, 7, 7]); expect(allPoints).to.have.lengthOf( @@ -331,7 +335,8 @@ describe("Box selection", () => { ); cy.wrap(dataProcessor) - .should("have.property", "index") + .should("have.property", "specificationHelper") + .then(() => dataProcessor.indexDataIfNotAlreadyIndexed()) .then(() => { const allPoints = dataProcessor.selectBox([1, 1, 7, 7]); expect(allPoints).to.have.lengthOf( @@ -367,7 +372,8 @@ describe("Box selection", () => { ); cy.wrap(dataProcessor) - .should("have.property", "index") + .should("have.property", "specificationHelper") + .then(() => dataProcessor.indexDataIfNotAlreadyIndexed()) .then(() => { const allPoints = dataProcessor.selectBox([ testGenomeScale.toClipSpaceFromString("chr1:1"), @@ -446,7 +452,8 @@ describe("Box selection", () => { ); cy.wrap(dataProcessor) - .should("have.property", "index") + .should("have.property", "specificationHelper") + .then(() => dataProcessor.indexDataIfNotAlreadyIndexed()) .then(() => { const allPoints = dataProcessor.selectBox([ testGenomeScale.toClipSpaceFromString("chr1:1"), @@ -525,7 +532,8 @@ describe("Box selection", () => { ); cy.wrap(dataProcessor) - .should("have.property", "index") + .should("have.property", "specificationHelper") + .then(() => dataProcessor.indexDataIfNotAlreadyIndexed()) .then(() => { const allPoints = dataProcessor.selectBox([ testGenomeScale.toClipSpaceFromString("chr1:1"), @@ -612,7 +620,8 @@ describe("Lasso selection", () => { ); cy.wrap(dataProcessor) - .should("have.property", "index") + .should("have.property", "specificationHelper") + .then(() => dataProcessor.indexDataIfNotAlreadyIndexed()) .then(() => { expect( dataProcessor.selectLasso([1, 1, 2, 1, 3, 2, 2, 2.5]) @@ -626,7 +635,8 @@ describe("Lasso selection", () => { ); cy.wrap(dataProcessor) - .should("have.property", "index") + .should("have.property", "specificationHelper") + .then(() => dataProcessor.indexDataIfNotAlreadyIndexed()) .then(() => { expect( dataProcessor.selectLasso([1, 1, 2, 1, 3, 2, 2, 2.5]) @@ -640,7 +650,8 @@ describe("Lasso selection", () => { ); cy.wrap(dataProcessor) - .should("have.property", "index") + .should("have.property", "specificationHelper") + .then(() => dataProcessor.indexDataIfNotAlreadyIndexed()) .then(() => { expect( dataProcessor.selectLasso([1, 1, 2, 1, 3, 2, 2, 2.5]) @@ -654,7 +665,8 @@ describe("Lasso selection", () => { ); cy.wrap(dataProcessor) - .should("have.property", "index") + .should("have.property", "specificationHelper") + .then(() => dataProcessor.indexDataIfNotAlreadyIndexed()) .then(() => { expect( dataProcessor.selectLasso([1, 1, 2, 1, 3, 2, 2, 2.5]) @@ -668,7 +680,8 @@ describe("Lasso selection", () => { ); cy.wrap(dataProcessor) - .should("have.property", "index") + .should("have.property", "specificationHelper") + .then(() => dataProcessor.indexDataIfNotAlreadyIndexed()) .then(() => { expect( dataProcessor.selectLasso([1, 1, 2, 1, 3, 2, 2, 2.5]) @@ -682,7 +695,8 @@ describe("Lasso selection", () => { ); cy.wrap(dataProcessor) - .should("have.property", "index") + .should("have.property", "specificationHelper") + .then(() => dataProcessor.indexDataIfNotAlreadyIndexed()) .then(() => { expect( dataProcessor.selectLasso([ @@ -709,7 +723,8 @@ describe("Lasso selection", () => { ); cy.wrap(dataProcessor) - .should("have.property", "index") + .should("have.property", "specificationHelper") + .then(() => dataProcessor.indexDataIfNotAlreadyIndexed()) .then(() => { expect( dataProcessor.selectLasso([ @@ -736,7 +751,8 @@ describe("Lasso selection", () => { ); cy.wrap(dataProcessor) - .should("have.property", "index") + .should("have.property", "specificationHelper") + .then(() => dataProcessor.indexDataIfNotAlreadyIndexed()) .then(() => { expect( dataProcessor.selectLasso([ @@ -768,7 +784,8 @@ describe("Get closest point", () => { ); cy.wrap(dataProcessor) - .should("have.property", "index") + .should("have.property", "specificationHelper") + .then(() => dataProcessor.indexDataIfNotAlreadyIndexed()) .then(() => { let closest = dataProcessor.getClosestPoint([1.1, 1.1]).closestPoint; expect(closest.category).to.eq("a"); @@ -789,7 +806,8 @@ describe("Get closest point", () => { ); cy.wrap(dataProcessor) - .should("have.property", "index") + .should("have.property", "specificationHelper") + .then(() => dataProcessor.indexDataIfNotAlreadyIndexed()) .then(() => { let closest = dataProcessor.getClosestPoint([1, 1]); expect(closest.closestPoint.category).to.eq("a"); @@ -817,7 +835,8 @@ describe("Get closest point", () => { ); cy.wrap(dataProcessor) - .should("have.property", "index") + .should("have.property", "specificationHelper") + .then(() => dataProcessor.indexDataIfNotAlreadyIndexed()) .then(() => { let closest = dataProcessor.getClosestPoint([1.1, 1.1]); expect(closest.closestPoint.category).to.eq("a"); @@ -845,7 +864,8 @@ describe("Get closest point", () => { ); cy.wrap(dataProcessor) - .should("have.property", "index") + .should("have.property", "specificationHelper") + .then(() => dataProcessor.indexDataIfNotAlreadyIndexed()) .then(() => { let closest = dataProcessor.getClosestPoint([ testGenomeScale.toClipSpaceFromString("chr1:101"), diff --git a/src/epiviz.gl/data-processor-worker.js b/src/epiviz.gl/data-processor-worker.js index c668b8f..a69e441 100644 --- a/src/epiviz.gl/data-processor-worker.js +++ b/src/epiviz.gl/data-processor-worker.js @@ -9,31 +9,41 @@ import DataProcessor from "./data-processor"; self.onmessage = (message) => { + function handleSelection(type, points) { + const selectionMethod = type === "selectBox" ? "selectBox" : "selectLasso"; + const selection = self.processor[selectionMethod](points); + postMessage({ + type, + selection, + bounds: points, + }); + } + + function handlePoint(type, point) { + const result = self.processor.getClosestPoint(point); + postMessage({ + type, + ...result, + }); + } + switch (message.data.type) { case "init": self.processor = new DataProcessor(message.data.specification); break; case "selectBox": - postMessage({ - type: message.data.type, - selection: self.processor.selectBox(message.data.points), - bounds: message.data.points, - }); - break; case "selectLasso": - postMessage({ - type: message.data.type, - selection: self.processor.selectLasso(message.data.points), - bounds: message.data.points, - }); - break; case "getClosestPoint": case "getClickPoint": - const result = self.processor.getClosestPoint(message.data.point); - postMessage({ - type: message.data.type, - ...result, - }); + self.processor.indexDataIfNotAlreadyIndexed(); + if ( + message.data.type === "selectBox" || + message.data.type === "selectLasso" + ) { + handleSelection(message.data.type, message.data.points); + } else { + handlePoint(message.data.type, message.data.point); + } break; default: console.error(`Received unknown message type: ${message.type}`); diff --git a/src/epiviz.gl/data-processor.js b/src/epiviz.gl/data-processor.js index cfbcf23..017708f 100644 --- a/src/epiviz.gl/data-processor.js +++ b/src/epiviz.gl/data-processor.js @@ -17,7 +17,10 @@ class DataProcessor { console.log("Loading data..."); - new SpecificationProcessor(specification, this.indexData.bind(this)); + new SpecificationProcessor( + specification, + this.specificationCallback.bind(this) + ); } /** @@ -25,7 +28,20 @@ class DataProcessor { * * @param {SpecificationProcessor} specificationHelper that is built in the constructor */ - indexData(specificationHelper) { + + specificationCallback(specificationHelper) { + this.specificationHelper = specificationHelper; + } + + /** + * Indexes the data in the specificationHelper and stores it in the data and index fields of the DataProcessor + * object. + */ + indexDataIfNotAlreadyIndexed() { + // If the data has already been indexed or specificationHelper hasn't been built yet, do nothing + if (this.index || !this.specificationHelper) return; + // Otherwise, index the data + const specificationHelper = this.specificationHelper; let totalPoints = 0; for (const track of specificationHelper.tracks) { @@ -114,15 +130,14 @@ class DataProcessor { * @returns closest point or undefined */ getClosestPoint(point) { - let indices = this.index.neighbors(point[0], point[1], 1, 0) - let pointToReturn = - this.data[indices]; + let indices = this.index.neighbors(point[0], point[1], 1, 0); + let pointToReturn = this.data[indices]; let distance = 0; let isInside = true; if (pointToReturn === undefined) { - indices = this.index.neighbors(point[0], point[1], 1, 5) - if(indices.length === 0) { - indices = this.index.neighbors(point[0], point[1], 1) + indices = this.index.neighbors(point[0], point[1], 1, 5); + if (indices.length === 0) { + indices = this.index.neighbors(point[0], point[1], 1); } pointToReturn = this.data[indices]; distance = Math.sqrt( @@ -146,12 +161,11 @@ class DataProcessor { const largerX = Math.max(points[0], points[2]); const largerY = Math.max(points[1], points[3]); - let indices = this.index - .search(smallerX, smallerY, largerX, largerY) - - let tpoints = indices.map((i) => this.data[i]); + let indices = this.index.search(smallerX, smallerY, largerX, largerY); + + let tpoints = indices.map((i) => this.data[i]); - return {indices, "points": tpoints}; + return { indices, points: tpoints }; } /** @@ -199,12 +213,12 @@ class DataProcessor { simplifiedBoundingPolygon ); - if (tbool) findices.push(candidatePoints.indices[i]) + if (tbool) findices.push(candidatePoints.indices[i]); return tbool; }); - return {"indices": findices, "points": fpoints} + return { indices: findices, points: fpoints }; } } diff --git a/src/epiviz.gl/utilities.js b/src/epiviz.gl/utilities.js index 35a952e..9ddf244 100644 --- a/src/epiviz.gl/utilities.js +++ b/src/epiviz.gl/utilities.js @@ -264,6 +264,10 @@ const getQuadraticBezierCurveForPoints = (P0, P1, P2) => { return (t) => [x(t), y(t)]; }; +const POINT_HOVERED_EVENT_NAME = "pointHovered"; +const POINT_CLICKED_EVENT_NAME = "pointClicked"; +const ON_SELECTION_END_EVENT_NAME = "onSelectionEnd"; + export { scale, rgbToHex, @@ -275,4 +279,7 @@ export { getQuadraticBezierCurveForPoints, DEFAULT_WIDTH, DEFAULT_HEIGHT, + ON_SELECTION_END_EVENT_NAME, + POINT_HOVERED_EVENT_NAME, + POINT_CLICKED_EVENT_NAME, }; diff --git a/src/epiviz.gl/webgl-vis.js b/src/epiviz.gl/webgl-vis.js index 0c6a2fb..f5655c1 100644 --- a/src/epiviz.gl/webgl-vis.js +++ b/src/epiviz.gl/webgl-vis.js @@ -5,10 +5,12 @@ import { getDimAndMarginStyleForSpecification, DEFAULT_HEIGHT, DEFAULT_WIDTH, + POINT_CLICKED_EVENT_NAME, + POINT_HOVERED_EVENT_NAME, + ON_SELECTION_END_EVENT_NAME, } from "./utilities"; class WebGLVis { - /** * A class meant to display a visualization based off a given specification using webgl. * @@ -33,6 +35,9 @@ class WebGLVis { "currentXRange", "currentYRange", ]); + + // This is a map of event listeners that are subscribed + this.eventListentersMap = new Map(); } /** @@ -106,21 +111,21 @@ class WebGLVis { return; } this.parent.dispatchEvent( - new CustomEvent("pointHovered", { detail: message }) + new CustomEvent(POINT_HOVERED_EVENT_NAME, { detail: message }) ); } else if (message.data.type === "getClickPoint") { if (message.data.closestPoint === undefined) { return; } this.parent.dispatchEvent( - new CustomEvent("pointClicked", { detail: message }) + new CustomEvent(POINT_CLICKED_EVENT_NAME, { detail: message }) ); } else if ( message.data.type === "selectBox" || message.data.type === "selectLasso" ) { this.parent.dispatchEvent( - new CustomEvent("onSelectionEnd", { detail: message }) + new CustomEvent(ON_SELECTION_END_EVENT_NAME, { detail: message }) ); this.dataWorkerStream.push(message); console.log(this.dataWorkerStream); @@ -228,10 +233,13 @@ class WebGLVis { * using points as a polygon */ selectPoints(points) { - if (points.length === 4) { - this.dataWorker.postMessage({ type: "selectBox", points }); - } else if (points.length >= 6) { - this.dataWorker.postMessage({ type: "selectLasso", points }); + // Only send the points if there is a listener for it + if (this.eventListentersMap.get(ON_SELECTION_START_EVENT_NAME)) { + if (points.length === 4) { + this.dataWorker.postMessage({ type: "selectBox", points }); + } else if (points.length >= 6) { + this.dataWorker.postMessage({ type: "selectLasso", points }); + } } } @@ -242,23 +250,29 @@ class WebGLVis { * @param {Array} point to get closest point to */ getClosestPoint(point) { - this.dataWorker.postMessage({ - type: "getClosestPoint", - point, - }); + //Only send the point if there is a listener for it + if (this.eventListentersMap.get(POINT_HOVERED_EVENT_NAME)) { + this.dataWorker.postMessage({ + type: "getClosestPoint", + point, + }); + } } - /** + /** * Utility method to have data worker call {@link DataProcessor#getClosestPoint}. * Does not return, posts result to this.dataWorkerStream. * * @param {Array} point to get closest point to */ getClickPoint(point) { - this.dataWorker.postMessage({ - type: "getClickPoint", - point, - }); + //Only send the point if there is a listener for it + if (this.eventListentersMap.get(POINT_CLICKED_EVENT_NAME)) { + this.dataWorker.postMessage({ + type: "getClickPoint", + point, + }); + } } /** @@ -285,6 +299,7 @@ class WebGLVis { * "onSelection": fires while user is changing the selection box/lasso * "onSelectionEnd": fires when a selection has been completed and the results are in the dataWorkerStream * "pointHovered": fires when pointer hovers over a datapoint + * "pointClicked": fires when pointer clicks on a datapoint * * For information on the parameters and functionality see: * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener @@ -294,6 +309,7 @@ class WebGLVis { * @param {Object} options */ addEventListener(type, listener, options) { + this.eventListentersMap.set(type, true); this.parent.addEventListener(type, listener, options); } From 6b9b039dc7c75fc6f9c06d9158338be55cbac3d3 Mon Sep 17 00:00:00 2001 From: Ossama Rafique Date: Wed, 3 May 2023 17:51:04 -0700 Subject: [PATCH 2/2] Typo Fixed --- src/epiviz.gl/webgl-vis.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/epiviz.gl/webgl-vis.js b/src/epiviz.gl/webgl-vis.js index f5655c1..703a98f 100644 --- a/src/epiviz.gl/webgl-vis.js +++ b/src/epiviz.gl/webgl-vis.js @@ -234,7 +234,7 @@ class WebGLVis { */ selectPoints(points) { // Only send the points if there is a listener for it - if (this.eventListentersMap.get(ON_SELECTION_START_EVENT_NAME)) { + if (this.eventListentersMap.get(ON_SELECTION_END_EVENT_NAME)) { if (points.length === 4) { this.dataWorker.postMessage({ type: "selectBox", points }); } else if (points.length >= 6) {