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 0d069da..e91fe6d 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 { points: allPoints } = dataProcessor.selectBox([1, 1, 7, 7]); @@ -218,7 +219,8 @@ describe("Box selection", () => { ); cy.wrap(dataProcessor) - .should("have.property", "index") + .should("have.property", "specificationHelper") + .then(() => dataProcessor.indexDataIfNotAlreadyIndexed()) .then(() => { const { points: allPoints } = dataProcessor.selectBox([1, 1, 7, 7]); expect(allPoints).to.have.lengthOf( @@ -266,7 +268,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]).points; expect(allPoints).to.have.lengthOf( @@ -312,7 +315,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]).points; expect(allPoints).to.have.lengthOf( @@ -358,7 +362,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]).points; expect(allPoints).to.have.lengthOf( @@ -396,7 +401,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"), @@ -475,7 +481,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"), @@ -554,7 +561,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"), @@ -641,7 +649,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]).points @@ -655,7 +664,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]).points @@ -669,7 +679,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]).points @@ -683,7 +694,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]).points @@ -697,7 +709,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]).points @@ -711,7 +724,8 @@ describe("Lasso selection", () => { ); cy.wrap(dataProcessor) - .should("have.property", "index") + .should("have.property", "specificationHelper") + .then(() => dataProcessor.indexDataIfNotAlreadyIndexed()) .then(() => { expect( dataProcessor.selectLasso([ @@ -738,7 +752,8 @@ describe("Lasso selection", () => { ); cy.wrap(dataProcessor) - .should("have.property", "index") + .should("have.property", "specificationHelper") + .then(() => dataProcessor.indexDataIfNotAlreadyIndexed()) .then(() => { expect( dataProcessor.selectLasso([ @@ -765,7 +780,8 @@ describe("Lasso selection", () => { ); cy.wrap(dataProcessor) - .should("have.property", "index") + .should("have.property", "specificationHelper") + .then(() => dataProcessor.indexDataIfNotAlreadyIndexed()) .then(() => { expect( dataProcessor.selectLasso([ @@ -797,7 +813,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"); @@ -818,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]); expect(closest.closestPoint.category).to.eq("a"); @@ -846,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([1.1, 1.1]); expect(closest.closestPoint.category).to.eq("a"); @@ -874,7 +893,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 15ede2b..6c0839c 100644 --- a/src/epiviz.gl/webgl-vis.js +++ b/src/epiviz.gl/webgl-vis.js @@ -5,6 +5,9 @@ import { getDimAndMarginStyleForSpecification, DEFAULT_HEIGHT, DEFAULT_WIDTH, + POINT_CLICKED_EVENT_NAME, + POINT_HOVERED_EVENT_NAME, + ON_SELECTION_END_EVENT_NAME, } from "./utilities"; class WebGLVis { @@ -32,6 +35,9 @@ class WebGLVis { "currentXRange", "currentYRange", ]); + + // This is a map of event listeners that are subscribed + this.eventListentersMap = new Map(); } /** @@ -109,21 +115,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); @@ -234,10 +240,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_END_EVENT_NAME)) { + if (points.length === 4) { + this.dataWorker.postMessage({ type: "selectBox", points }); + } else if (points.length >= 6) { + this.dataWorker.postMessage({ type: "selectLasso", points }); + } } } @@ -248,10 +257,13 @@ 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, + }); + } } /** @@ -261,10 +273,13 @@ class WebGLVis { * @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, + }); + } } /** @@ -291,6 +306,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 @@ -300,6 +316,7 @@ class WebGLVis { * @param {Object} options */ addEventListener(type, listener, options) { + this.eventListentersMap.set(type, true); this.parent.addEventListener(type, listener, options); }