diff --git a/cypress/e2e/figures.cy.ts b/cypress/e2e/figures.cy.ts index 6e57f5c8..726efd66 100644 --- a/cypress/e2e/figures.cy.ts +++ b/cypress/e2e/figures.cy.ts @@ -93,7 +93,7 @@ describe("Figure", function() { data["type_"] = "parallelplot"; const parallelPlot = Figure.fromMultiplot(data, canvas.width, canvas.height, canvasID); - scatter.sendRubberBandsMultiplot([scatter2, parallelPlot]); + // scatter.sendRubberBandsMultiplot([scatter2, parallelPlot]); expect(scatter2.axes[0].rubberBand, "scatter2.axes[0].rubberBand").to.not.deep.equal(scatter.axes[1].rubberBand); expect(scatter2.axes[1].rubberBand, "scatter2.axes[1].rubberBand").to.not.deep.equal(scatter.axes[0].rubberBand); @@ -121,7 +121,7 @@ describe("Figure", function() { [scatter.axes[0].name, scatter.axes[0].rubberBand], [scatter.axes[1].name, scatter.axes[1].rubberBand] ]); - scatter.initRubberBandMultiplot(multiplotRubberBands); + // scatter.initRubberBandMultiplot(multiplotRubberBands); scatter.axes.forEach(axis => { expect(multiplotRubberBands.get(axis.name).minValue, `empty rubberband ${axis.name}.minValue`).to.deep.equal(referenceRubberBands.get(axis.name).minValue); @@ -132,7 +132,7 @@ describe("Figure", function() { scatter.axes[0].rubberBand.maxValue = 4; scatter.axes[1].rubberBand.minValue = 2; scatter.axes[1].rubberBand.maxValue = 5; - scatter.updateRubberBandMultiplot(multiplotRubberBands); + // scatter.updateRubberBandMultiplot(multiplotRubberBands); scatter.axes.forEach(axis => { expect(multiplotRubberBands.get(axis.name).minValue, `edited rubberband ${axis.name}.minValue`).to.deep.equal(referenceRubberBands.get(axis.name).minValue); expect(multiplotRubberBands.get(axis.name).maxValue, `edited rubberband ${axis.name}.minValue`).to.deep.equal(referenceRubberBands.get(axis.name).maxValue); diff --git a/src/axes.ts b/src/axes.ts index c9bf11ea..cdd854cf 100644 --- a/src/axes.ts +++ b/src/axes.ts @@ -4,6 +4,8 @@ import { Vertex, Shape } from "./baseShape" import { Rect, Point } from "./primitives" import { TextParams, Text, RubberBand } from "./shapes" import { EventEmitter } from "events" +import { onAxisSelection, rubberbandsChange } from "./interactions" +import { filter, withLatestFrom } from "rxjs" export class TitleSettings { constructor( @@ -76,6 +78,22 @@ export class Axis extends Shape { this.updateOffsetTicks(); this.offsetTitle = 0; this.title = new Text(this.titleText, new Vertex(0, 0), {}); + + onAxisSelection.pipe( + filter((axis) => this.name !== "number" && this.name === axis.name), + withLatestFrom(rubberbandsChange) + ).subscribe(([axis, rubberbands]) => { + let rubberband = rubberbands.get(this.name); + if (!rubberband) { + rubberband = new RubberBand(axis.name, axis.rubberBand.minValue, axis.rubberBand.maxValue, this.isVertical) + } else { + rubberband.minValue = axis.rubberBand.minValue; + rubberband.maxValue = axis.rubberBand.maxValue; + } + rubberbands.set(axis.name, rubberband) + this.rubberBand.minValue = rubberband.minValue; + this.rubberBand.maxValue = rubberband.maxValue; + }) }; public get drawLength(): number { @@ -233,10 +251,6 @@ export class Axis extends Shape { this.rubberBand.defaultStyle(); } - public sendRubberBand(rubberBands: Map) { this.rubberBand.selfSend(rubberBands) } - - public sendRubberBandRange(rubberBands: Map) { this.rubberBand.selfSendRange(rubberBands) } - private static nearestFive(value: number): number { const tenPower = Math.floor(Math.log10(Math.abs(value))); const normedValue = Math.floor(value / Math.pow(10, tenPower - 2)); @@ -621,7 +635,7 @@ export class Axis extends Shape { this.updateRubberBand(); context.setTransform(canvasMatrix); this.rubberBand.draw(context); - if (this.rubberBand.isClicked) this.emitter.emit("rubberBandChange", this.rubberBand); + if (this.rubberBand.isClicked) onAxisSelection.next(this); } protected mouseTranslate(mouseDown: Vertex, mouseCoords: Vertex): void { } @@ -639,7 +653,7 @@ export class Axis extends Shape { private clickOnArrow(mouseDown: Vertex): void { this.is_drawing_rubberband = true; // OLD this.rubberBand.isHovered ? this.rubberBand.mouseDown(mouseDown) : this.rubberBand.reset(); - this.emitter.emit("rubberBandChange", this.rubberBand); + onAxisSelection.next(this); } private clickOnDrawnPath(mouseDown: Vertex): void { @@ -676,7 +690,7 @@ export class Axis extends Shape { this.title.mouseUp(false); this.title.isClicked = false; this.rubberBand.mouseUp(keepState); - if (this.is_drawing_rubberband) this.emitter.emit("rubberBandChange", this.rubberBand); + if (this.is_drawing_rubberband) onAxisSelection.next(this); this.is_drawing_rubberband = false; } diff --git a/src/figures.ts b/src/figures.ts index 32ddaed7..61f5fecb 100644 --- a/src/figures.ts +++ b/src/figures.ts @@ -5,12 +5,12 @@ import { colorHsl } from "./colors" import { PointStyle } from "./styles" import { Vertex, Shape } from "./baseShape" import { Rect, Point, LineSequence } from "./primitives" -import { ScatterPoint, Bar, RubberBand, SelectionBox } from "./shapes" +import { ScatterPoint, Bar, SelectionBox } from "./shapes" import { Axis, ParallelAxis } from "./axes" import { ShapeCollection, GroupCollection, PointSet } from "./collections" import { RemoteFigure } from "./remoteFigure" import { DataInterface } from "./dataInterfaces" -import { HighlightData } from "./interactions" +import { HighlightData, onAxisSelection } from "./interactions" export class Figure extends RemoteFigure { constructor( @@ -67,32 +67,6 @@ export class Figure extends RemoteFigure { } public receivePointSets(pointSets: PointSet[]): void { this.pointSets = pointSets } - - public initRubberBandMultiplot(multiplotRubberBands: Map): void { - this.axes.forEach(axis => axis.sendRubberBand(multiplotRubberBands)); - } - - public updateRubberBandMultiplot(multiplotRubberBands: Map): void { - this.axes.forEach(axis => axis.sendRubberBandRange(multiplotRubberBands)); - } - - public sendRubberBandsMultiplot(figures: Figure[]): void { - figures.forEach(figure => figure.receiveRubberBandFromFigure(this)); - } - - protected sendRubberBandsInFigure(figure: Figure): void { - figure.axes.forEach(otherAxis => { - this.axes.forEach(thisAxis => { - if (thisAxis.name == otherAxis.name && thisAxis.name != "number") { - otherAxis.rubberBand.minValue = thisAxis.rubberBand.minValue; - otherAxis.rubberBand.maxValue = thisAxis.rubberBand.maxValue; - otherAxis.emitter.emit("rubberBandChange", otherAxis.rubberBand); - } - }) - }) - } - - protected receiveRubberBandFromFigure(figure: Figure): void { figure.sendRubberBandsInFigure(this) } } export class Frame extends Figure { @@ -222,6 +196,8 @@ export class Frame extends Figure { this.axes[1].rubberBand.minValue = Math.min(frameDown.y, frameMouse.y); this.axes[0].rubberBand.maxValue = Math.max(frameDown.x, frameMouse.x); this.axes[1].rubberBand.maxValue = Math.max(frameDown.y, frameMouse.y); + onAxisSelection.next(this.axes[0]); + onAxisSelection.next(this.axes[1]); super.updateSelectionBox(...this.rubberBandsCorners); } @@ -235,9 +211,9 @@ export class Frame extends Figure { return [new Vertex(this.axes[0].rubberBand.minValue, this.axes[1].rubberBand.minValue), new Vertex(this.axes[0].rubberBand.maxValue, this.axes[1].rubberBand.maxValue)] } - public activateSelection(emittedRubberBand: RubberBand, index: number): void { - super.activateSelection(emittedRubberBand, index) - this.selectionBox.rubberBandUpdate(emittedRubberBand, ["x", "y"][index]); + public activateSelection(axis: Axis): void { + super.activateSelection(axis) + this.selectionBox.rubberBandUpdate(axis.rubberBand, ["x", "y"][this.getAxisIndex(axis)]); } } @@ -403,24 +379,6 @@ export class Histogram extends Frame { this.scaleY = 1; super.regulateScale(); } - - public initRubberBandMultiplot(multiplotRubberBands: Map): void { - this.axes[0].sendRubberBand(multiplotRubberBands); - } - - public updateRubberBandMultiplot(multiplotRubberBands: Map): void { - this.axes[0].sendRubberBandRange(multiplotRubberBands); - } - - protected sendRubberBandsInFigure(figure: Figure): void { - figure.axes.forEach(otherAxis => { - if (this.axes[0].name == otherAxis.name) { - otherAxis.rubberBand.minValue = this.axes[0].rubberBand.minValue; - otherAxis.rubberBand.maxValue = this.axes[0].rubberBand.maxValue; - otherAxis.emitter.emit("rubberBandChange", otherAxis.rubberBand); - } - }) - } } export class Scatter extends Frame { @@ -815,14 +773,6 @@ export class Graph2D extends Scatter { public multiplotSelectedIntersection(multiplotSelected: number[], isSelecting: boolean): [number[], boolean] { return [multiplotSelected, isSelecting] } public receivePointSets(pointSets: PointSet[]): void {} - - public initRubberBandMultiplot(multiplotRubberBands: Map): void {} - - public updateRubberBandMultiplot(multiplotRubberBands: Map): void {} - - public sendRubberBandsMultiplot(figures: Figure[]): void {} - - protected receiveRubberBandFromFigure(figure: Figure): void {} } export class ParallelPlot extends Figure { @@ -1177,14 +1127,6 @@ export class Draw extends Frame { public receivePointSets(pointSets: PointSet[]): void {} - public initRubberBandMultiplot(multiplotRubberBands: Map): void {} - - public updateRubberBandMultiplot(multiplotRubberBands: Map): void {} - - public sendRubberBandsMultiplot(figures: Figure[]): void {} - - protected receiveRubberBandFromFigure(figure: Figure): void {} - public highlightFromReferencePath(highlightData: HighlightData) { const highlight = highlightData.highlight; const shapes = this.getShapesFromPath(highlightData.referencePath); diff --git a/src/interactions.ts b/src/interactions.ts index 9c717d51..a003215d 100644 --- a/src/interactions.ts +++ b/src/interactions.ts @@ -1,4 +1,6 @@ -import { Subject } from "rxjs" +import { BehaviorSubject, ReplaySubject, Subject } from "rxjs" +import { Axis } from "./axes"; +import { RubberBand } from "./shapes"; export interface HighlightData { referencePath: string, @@ -7,3 +9,6 @@ export interface HighlightData { } export const highlightShape: Subject = new Subject(); + +export const onAxisSelection: Subject = new Subject(); +export const rubberbandsChange: Subject> = new Subject(); diff --git a/src/multiplot.ts b/src/multiplot.ts index 1fcdb2c5..33c3957a 100644 --- a/src/multiplot.ts +++ b/src/multiplot.ts @@ -5,6 +5,7 @@ import { RubberBand, SelectionBox } from "./shapes" import { SelectionBoxCollection, PointSet } from "./collections" import { Figure, Scatter, Graph2D, ParallelPlot, Draw } from './figures' import { DataInterface, MultiplotDataInterface } from "./dataInterfaces" +import { rubberbandsChange } from "./interactions" /* TODO: Does this inherit from RemoteFigure or the opposite or does this @@ -18,7 +19,7 @@ export class Multiplot { public featureNames: string[]; public nSamples: number; public figures: Figure[]; - public rubberBands: Map; + public rubberBands: Map = new Map(); public figureZones = new SelectionBoxCollection([]); public isSelecting: boolean = false; @@ -46,7 +47,6 @@ export class Multiplot { this.nSamples = this.features.entries().next().value[1].length; this.computeTable(); this.draw(); - this.initRubberBands(); this.mouseListener(); } @@ -176,15 +176,10 @@ export class Multiplot { private activateAxisEvents(figure: Figure): void { figure.axes.forEach(axis => axis.emitter.on('axisStateChange', e => figure.axisChangeUpdate(e))); - figure.axes.forEach((axis, index) => { - axis.emitter.on('rubberBandChange', e => { - figure.activateSelection(e, index); - this.isSelecting = true; - }) - }) } public selectionOn(): void { + // Never called. Is this useful ? this.isSelecting = true; this.figures.forEach(figure => figure.isSelecting = true); this.canvas.style.cursor = 'crosshair'; @@ -200,6 +195,7 @@ export class Multiplot { } public switchSelection(): void { + // Never called. Is this useful ? this.isSelecting ? this.selectionOff() : this.selectionOn(); } @@ -310,19 +306,6 @@ export class Multiplot { public updateHoveredIndices(figure: Figure): void { this.hoveredIndices = figure.sendHoveredIndicesMultiplot() } - public initRubberBands(): void { - this.rubberBands = new Map(); - this.figures.forEach(figure => figure.initRubberBandMultiplot(this.rubberBands)); - } - - public updateRubberBands(currentFigure: Figure): void { - if (this.isSelecting) { - if (!this.rubberBands) this.initRubberBands(); - currentFigure.sendRubberBandsMultiplot(this.figures); - this.figures.forEach(figure => figure.updateRubberBandMultiplot(this.rubberBands)); - } - } - public resetRubberBands(): void { this.rubberBands.forEach(rubberBand => rubberBand.reset()); this.figures.forEach(figure => figure.resetRubberBands()); @@ -332,17 +315,6 @@ export class Multiplot { this.figures.forEach(figure => figure.axes.forEach(axis => axis.emitter.on('axisStateChange', e => figure.axisChangeUpdate(e)))); } - private listenRubberBandChange(): void { - this.figures.forEach(figure => { - figure.axes.forEach((axis, index) => { - axis.emitter.on('rubberBandChange', e => { - figure.activateSelection(e, index); - this.isSelecting = true; - }) - }) - }) - } - private keyDownDrawer(e: KeyboardEvent, ctrlKey: boolean, shiftKey: boolean, spaceKey: boolean): [boolean, boolean, boolean] { if (e.key == "Control") { ctrlKey = true; @@ -423,7 +395,7 @@ export class Multiplot { } else this.resizeWithMouse(absoluteMouse, clickedObject); this.updateHoveredIndices(this.figures[this.hoveredFigureIndex]); - this.updateRubberBands(this.figures[this.hoveredFigureIndex]); + rubberbandsChange.next(this.rubberBands); this.updateSelectedIndices(); return [canvasMouse, frameMouse, absoluteMouse, canvasDown, hasLeftFigure] } @@ -440,7 +412,7 @@ export class Multiplot { if (!(this.figures[this.hoveredFigureIndex] instanceof Graph2D || this.figures[this.hoveredFigureIndex] instanceof Draw)) { this.clickedIndices = this.figures[this.hoveredFigureIndex].clickedIndices; } - this.updateRubberBands(this.figures[this.hoveredFigureIndex]); + rubberbandsChange.next(this.rubberBands); hasLeftFigure = this.resetStateAttributes(shiftKey, ctrlKey); clickedObject = null; this.updateSelectedIndices(); @@ -474,8 +446,6 @@ export class Multiplot { this.listenAxisStateChange(); - this.listenRubberBandChange(); - window.addEventListener('keydown', e => [ctrlKey, shiftKey, spaceKey] = this.keyDownDrawer(e, ctrlKey, shiftKey, spaceKey)); window.addEventListener('keyup', e => [ctrlKey, shiftKey, spaceKey] = this.keyUpDrawer(e, ctrlKey, shiftKey, spaceKey)); diff --git a/src/remoteFigure.ts b/src/remoteFigure.ts index 6998cff2..3b79d087 100644 --- a/src/remoteFigure.ts +++ b/src/remoteFigure.ts @@ -4,10 +4,11 @@ import { colorHsl } from "./colors" import { PointStyle } from "./styles" import { Vertex, Shape } from "./baseShape" import { Rect } from "./primitives" -import { RubberBand, SelectionBox } from "./shapes" +import { SelectionBox } from "./shapes" import { Axis } from "./axes" import { PointSet, ShapeCollection, GroupCollection } from "./collections" import { DataInterface } from "./dataInterfaces" +import { onAxisSelection } from "./interactions" export class RemoteFigure extends Rect { public context: CanvasRenderingContext2D; @@ -80,6 +81,8 @@ export class RemoteFigure extends Rect { this.relativeObjects = new GroupCollection(); this.absoluteObjects = new GroupCollection(); this.setAxisVisibility(data); + + onAxisSelection.subscribe((axis) => this.activateSelection(axis)) } get scale(): Vertex { return new Vertex(this.relativeMatrix.a, this.relativeMatrix.d)} @@ -426,7 +429,7 @@ export class RemoteFigure extends Rect { this.context.restore(); } - public switchSelection(): void { this.isSelecting = !this.isSelecting; this.draw() } + public switchSelection(): void { this.isSelecting = !this.isSelecting; this.draw() } // Never called. Is this useful ? public switchMerge(): void {} @@ -651,7 +654,14 @@ export class RemoteFigure extends Rect { this.translation = translation; } - public activateSelection(emittedRubberBand: RubberBand, index: number): void { this.is_drawing_rubber_band = true } + public activateSelection(axis: Axis): void { + if (this.getAxisIndex(axis) > -1) this.is_drawing_rubber_band = true + } + + protected getAxisIndex(axis): number { + const axisNames = this.axes.map((a) => a.name) + return axisNames.indexOf(axis.name); + } public shiftOnAction(canvas: HTMLElement): void { this.isSelecting = true; @@ -677,8 +687,6 @@ export class RemoteFigure extends Rect { const canvas = document.getElementById(this.canvasID) as HTMLCanvasElement; let ctrlKey = false; let shiftKey = false; let spaceKey = false; - this.axes.forEach((axis, index) => axis.emitter.on('rubberBandChange', e => this.activateSelection(e, index))); - this.axes.forEach(axis => axis.emitter.on('axisStateChange', e => this.axisChangeUpdate(e))); window.addEventListener('keydown', e => { diff --git a/src/shapes.ts b/src/shapes.ts index 3d2ea1b7..1171b354 100644 --- a/src/shapes.ts +++ b/src/shapes.ts @@ -907,8 +907,6 @@ export class RubberBand extends Rect { public get isTranslating(): boolean { return !this.minUpdate && !this.maxUpdate && this.isClicked} - public selfSend(rubberBands: Map): void { rubberBands.set(this.attributeName, new RubberBand(this.attributeName, 0, 0, this.isVertical)) } - public defaultStyle(): void { this.lineWidth = 0.1; this.fillStyle = C.RUBBERBAND_COLOR; @@ -916,11 +914,6 @@ export class RubberBand extends Rect { this.alpha = C.RUBBERBAND_ALPHA; } - public selfSendRange(rubberBands: Map): void { - rubberBands.get(this.attributeName).minValue = this.minValue; - rubberBands.get(this.attributeName).maxValue = this.maxValue; - } - public updateCoords(canvasCoords: Vertex, axisOrigin: Vertex, axisEnd: Vertex): void { const coord = this.isVertical ? "y" : "x"; this.canvasMin = Math.max(canvasCoords.min, axisOrigin[coord]);