diff --git a/src/core/mark/index.ts b/src/core/mark/index.ts index dcddb4b3..5e229686 100644 --- a/src/core/mark/index.ts +++ b/src/core/mark/index.ts @@ -75,7 +75,6 @@ export function drawMark(HGC: import('@higlass/types').HGC, trackInfo: any, tile model.setChannelScale(d, yScale); }); } - // Size of a track const [trackWidth, trackHeight] = trackInfo.dimensions; diff --git a/src/core/mark/point.ts b/src/core/mark/point.ts index f270d42b..ecaec323 100644 --- a/src/core/mark/point.ts +++ b/src/core/mark/point.ts @@ -5,6 +5,20 @@ import { getValueUsingChannel } from '@gosling-lang/gosling-schema'; import colorToHex from '../utils/color-to-hex'; import { cartesianToPolar } from '../utils/polar'; import type { PIXIVisualProperty } from '../visual-property.schema'; +import { uuid } from '../utils/uuid'; + +function calculateOpacity( + model: GoslingTrackModel, + datum: { + [k: string]: string | number; + }, + radius: number, + zoomLevel: number +) { + const opacity = model.encodedPIXIProperty('opacity', datum); + const alphaTransition = model.markVisibility(datum, { width: radius, zoomLevel }); + return Math.min(alphaTransition, opacity); +} export function drawPoint(track: any, g: PIXI.Graphics, model: GoslingTrackModel) { /* track spec */ @@ -36,7 +50,6 @@ export function drawPoint(track: any, g: PIXI.Graphics, model: GoslingTrackModel /* row separation */ const rowCategories = (model.getChannelDomainArray('row') as string[]) ?? ['___SINGLE_ROW___']; const rowHeight = trackHeight / rowCategories.length; - /* render */ rowCategories.forEach(rowCategory => { const rowPosition = model.encodedValue('row', rowCategory); @@ -46,16 +59,24 @@ export function drawPoint(track: any, g: PIXI.Graphics, model: GoslingTrackModel !getValueUsingChannel(d, spec.row as Channel) || (getValueUsingChannel(d, spec.row as Channel) as string) === rowCategory ).forEach(d => { + // const cx = model.xScale(d.position); const cx = model.encodedPIXIProperty('x-center', d); - const cy = model.encodedPIXIProperty('y-center', d); - const color = model.encodedPIXIProperty('color', d); - const radius = model.encodedPIXIProperty('p-size', d); - const strokeWidth = model.encodedPIXIProperty('strokeWidth', d); - const stroke = model.encodedPIXIProperty('stroke', d); - const opacity = model.encodedPIXIProperty('opacity', d); - - const alphaTransition = model.markVisibility(d, { width: radius, zoomLevel }); - const actualOpacity = Math.min(alphaTransition, opacity); + const cy = d.cy ?? model.encodedPIXIProperty('y-center', d); + const color = (d.color as number) ?? (colorToHex(model.encodedPIXIProperty('color', d)) as number); + const radius = d.radius ?? model.encodedPIXIProperty('p-size', d); + const strokeWidth = d.strokeWidth ?? model.encodedPIXIProperty('strokeWidth', d); + const strokeColor = d.stroke ?? colorToHex(model.encodedPIXIProperty('stroke', d)); + const actualOpacity = d.opacity ?? calculateOpacity(model, d, radius, zoomLevel); + + if (!d.radius) { + d.cy = cy; + d.color = color; + d.radius = radius; + d.strokeWidth = strokeWidth; + d.stroke = strokeColor; + d.opacity = actualOpacity; + d.uuid = uuid(); + } if (radius <= 0.1 || actualOpacity === 0 || cx + radius < 0 || cx - radius > trackWidth) { // Don't draw invisible marks @@ -65,26 +86,25 @@ export function drawPoint(track: any, g: PIXI.Graphics, model: GoslingTrackModel // stroke g.lineStyle( strokeWidth, - colorToHex(stroke), + strokeColor, actualOpacity, // alpha 1 // alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outter) ); + let pos: { x: number; y: number }; if (circular) { const r = trackOuterRadius - ((rowPosition + rowHeight - cy) / trackHeight) * trackRingSize; - const pos = cartesianToPolar(cx, trackWidth, r, tcx, tcy, startAngle, endAngle); - g.beginFill(colorToHex(color), actualOpacity); - g.drawCircle(pos.x, pos.y, radius); - + pos = cartesianToPolar(cx, trackWidth, r, tcx, tcy, startAngle, endAngle); /* Mouse Events */ - model.getMouseEventModel().addPointBasedEvent(d, [pos.x, pos.y, radius]); } else { - g.beginFill(colorToHex(color), actualOpacity); - g.drawCircle(cx, rowPosition + rowHeight - cy, radius); - - /* Mouse Events */ - model.getMouseEventModel().addPointBasedEvent(d, [cx, rowPosition + rowHeight - cy, radius]); + pos = { + x: cx, + y: rowPosition + rowHeight - cy + }; } + g.beginFill(color, actualOpacity); + g.drawCircle(pos.x, pos.y, radius); + model.getMouseEventModel().addPointBasedEvent(d, [pos.x, pos.y, radius], d.uuid); }); }); } @@ -96,7 +116,8 @@ export function pointProperty( ) { const xe = model.visualPropertyByChannel('xe', datum); const x = model.visualPropertyByChannel('x', datum); - const size = model.visualPropertyByChannel('size', datum); + + // console.warn('pointProperty', propertyKey); // priority of channels switch (propertyKey) { @@ -107,8 +128,10 @@ export function pointProperty( const y = model.visualPropertyByChannel('y', datum); return ye ? (ye + y) / 2.0 : y; } - case 'p-size': + case 'p-size': { + const size = model.visualPropertyByChannel('size', datum); return xe && model.spec().stretch ? (xe - x) / 2.0 : size; + } default: return undefined; } diff --git a/src/tracks/gosling-track/gosling-mouse-event/mouse-event-model.ts b/src/tracks/gosling-track/gosling-mouse-event/mouse-event-model.ts index c6f6d54a..52be19b0 100644 --- a/src/tracks/gosling-track/gosling-mouse-event/mouse-event-model.ts +++ b/src/tracks/gosling-track/gosling-mouse-event/mouse-event-model.ts @@ -58,8 +58,8 @@ export class MouseEventModel { /** * Add a new mouse event that is point-based. */ - public addPointBasedEvent(value: Datum, pointAndRadius: [number, number, number]) { - this.data.push({ uid: uuid(), type: 'point', value, polygon: pointAndRadius }); + public addPointBasedEvent(value: Datum, pointAndRadius: [number, number, number], presetUID?: string) { + this.data.push({ uid: presetUID ?? uuid(), type: 'point', value, polygon: pointAndRadius }); } /** diff --git a/src/tracks/gosling-track/gosling-track-model.ts b/src/tracks/gosling-track/gosling-track-model.ts index c89532f8..3cd09b86 100644 --- a/src/tracks/gosling-track/gosling-track-model.ts +++ b/src/tracks/gosling-track/gosling-track-model.ts @@ -71,6 +71,9 @@ export class GoslingTrackModel { /* mouse events */ private mouseEventModel: MouseEventModel; + public xScale: ScaleType | undefined; + public yScale: ScaleType | undefined; + constructor(spec: SingleTrack, data: { [k: string]: number | string }[], theme: Required) { this.id = uuid(); diff --git a/src/tracks/gosling-track/gosling-track.ts b/src/tracks/gosling-track/gosling-track.ts index 598903c4..1d8ad627 100644 --- a/src/tracks/gosling-track/gosling-track.ts +++ b/src/tracks/gosling-track/gosling-track.ts @@ -174,6 +174,8 @@ const factory: PluginTrackFactory = (HGC, context, op #loadingText = new HGC.libraries.PIXI.Text('', loadingTextStyle); prevVisibleAndFetchedTiles?: Tile[]; resolvedTracks: SingleTrack[] | undefined; + // This is used to persist processed tile data across draw() calls. + #processedTileMap: WeakMap = new WeakMap(); /* * * @@ -396,8 +398,6 @@ const factory: PluginTrackFactory = (HGC, context, op drawMark(HGC, this, tile, model); drawPostEmbellishment(HGC, this, tile, model, this.options.theme); }); - - this.forceDraw(); } /** @@ -416,10 +416,12 @@ const factory: PluginTrackFactory = (HGC, context, op this.getResolvedTracks(true); // force update this.clearMouseEventData(); this.textsBeingUsed = 0; + // Without this, tracks with the same ID between specs will not be redrawn + this.#processedTileMap = new WeakMap(); this.processAllTiles(true); this.draw(); - this.forceDraw(); + this.forceAnimate(); } /** * Clears MouseEventModel from each GoslingTrackModel. Must be a public method because it is called from draw() @@ -475,7 +477,7 @@ const factory: PluginTrackFactory = (HGC, context, op * A function to redraw this track. Typically called when an asynchronous event occurs (i.e. tiles loaded) * (Refer to https://github.com/higlass/higlass/blob/54f5aae61d3474f9e868621228270f0c90ef9343/app/scripts/TiledPixiTrack.js#L71) */ - forceDraw() { + forceAnimate() { this.animate(); } @@ -491,13 +493,12 @@ const factory: PluginTrackFactory = (HGC, context, op this.mRangeBrush.updateRange( range ? [newXScale(this._xScale.invert(range[0])), newXScale(this._xScale.invert(range[1]))] : null ); - + console.warn(newXScale === this._xScale); // true this.xScale(newXScale); this.yScale(newYScale); this.refreshTiles(); this.draw(); - this.forceDraw(); // Publish the new genomic axis domain const genomicRange = newXScale @@ -556,6 +557,12 @@ const factory: PluginTrackFactory = (HGC, context, op const tiles = this.visibleAndFetchedTiles(); + // If we have already processed all tiles, we don't need to do anything + // this.#processedTileMap contains all of data needed to draw + if (tiles.every(tile => this.#processedTileMap.get(tile) !== undefined)) { + return; + } + // generated tabular data tiles.forEach(tile => this.#generateTabularData(tile, force)); @@ -571,6 +578,11 @@ const factory: PluginTrackFactory = (HGC, context, op if (flatTileData.length !== 0) { this.options.siblingIds.forEach(id => publish('rawData', { id, data: flatTileData })); } + + // Record processed tiles so that we don't process them again + tiles.forEach(tile => { + this.#processedTileMap.set(tile, true); + }); } /** @@ -609,7 +621,6 @@ const factory: PluginTrackFactory = (HGC, context, op for (const tile of tiles) { const { tileWidth } = this.getTilePosAndDimensions(tile[0], [tile[1], tile[1]]); - this.forceDraw(); if (tileWidth > maxTileWith) { return; } @@ -1250,7 +1261,7 @@ const factory: PluginTrackFactory = (HGC, context, op }); } - this.forceDraw(); + this.forceAnimate(); } /**