From fa89656549fddb70f14b17483a29cdb93cfe34cf Mon Sep 17 00:00:00 2001 From: sehilyi Date: Wed, 12 Feb 2025 14:56:56 -0500 Subject: [PATCH 1/2] move demo/track-def to src --- demo/App.tsx | 4 - demo/GoslingComponent.tsx | 2 +- demo/linking/linkedEncoding.ts | 12 +- demo/renderer/main.ts | 2 +- demo/{track-def => src}/axis.ts | 0 demo/{track-def => src}/brush.ts | 0 demo/{track-def => src}/dummy.ts | 0 demo/{track-def => src}/gosling.ts | 0 demo/{track-def => src}/heatmap.ts | 0 demo/{track-def => src}/main.ts | 0 demo/{track-def => src}/text.ts | 0 demo/{track-def => src}/types.ts | 0 editor/index.css | 13 +- src/compiler/bounding-box.ts | 2 +- src/compiler/compile.test.ts | 2 +- src/compiler/views-and-tracks.ts | 18 +- src/core/utils/assembly.ts | 6 +- src/core/utils/overlay.ts | 4 +- src/data-fetchers/bam/bam-data-fetcher.ts | 4 +- src/data-fetchers/bed/bed-worker.ts | 5 +- src/data-fetchers/utils.ts | 6 +- src/data-fetchers/vcf/vcf-worker.ts | 5 +- src/gosling-schema/gosling.schema.guards.ts | 2 +- src/higlass/higlass-vendored.js | 108 +++++---- src/track-def/axis.ts | 255 ++++++++++++++++++++ src/track-def/brush.ts | 71 ++++++ src/track-def/dummy.ts | 21 ++ src/track-def/gosling.ts | 75 ++++++ src/track-def/heatmap.ts | 83 +++++++ src/track-def/index.ts | 106 ++++++++ src/track-def/text.ts | 71 ++++++ src/track-def/types.ts | 65 +++++ 32 files changed, 858 insertions(+), 84 deletions(-) rename demo/{track-def => src}/axis.ts (100%) rename demo/{track-def => src}/brush.ts (100%) rename demo/{track-def => src}/dummy.ts (100%) rename demo/{track-def => src}/gosling.ts (100%) rename demo/{track-def => src}/heatmap.ts (100%) rename demo/{track-def => src}/main.ts (100%) rename demo/{track-def => src}/text.ts (100%) rename demo/{track-def => src}/types.ts (100%) create mode 100644 src/track-def/axis.ts create mode 100644 src/track-def/brush.ts create mode 100644 src/track-def/dummy.ts create mode 100644 src/track-def/gosling.ts create mode 100644 src/track-def/heatmap.ts create mode 100644 src/track-def/index.ts create mode 100644 src/track-def/text.ts create mode 100644 src/track-def/types.ts diff --git a/demo/App.tsx b/demo/App.tsx index 0338c40cc..2a0d12111 100644 --- a/demo/App.tsx +++ b/demo/App.tsx @@ -16,10 +16,6 @@ import { compile } from '../src/compiler/compile'; import { getTheme } from '../src/core/utils/theme'; import './App.css'; -import { createTrackDefs, renderTrackDefs, showTrackInfoPositions } from './track-def/main'; -import type { TrackInfo } from 'src/compiler/bounding-box'; -import type { GoslingSpec } from 'gosling.js'; -import { getLinkedEncodings } from './linking/linkedEncoding'; import { GoslingComponent } from './GoslingComponent'; function App() { diff --git a/demo/GoslingComponent.tsx b/demo/GoslingComponent.tsx index 429e41e56..92be738fc 100644 --- a/demo/GoslingComponent.tsx +++ b/demo/GoslingComponent.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { PixiManager } from '@pixi-manager'; import { compile, type UrlToFetchOptions } from '../src/compiler/compile'; import { getTheme } from '../src/core/utils/theme'; -import { createTrackDefs } from './track-def/main'; +import { createTrackDefs } from '../src/track-def'; import { renderTrackDefs } from './renderer/main'; import type { TrackInfo } from 'src/compiler/bounding-box'; import type { GoslingSpec } from 'gosling.js'; diff --git a/demo/linking/linkedEncoding.ts b/demo/linking/linkedEncoding.ts index 08ae574cc..c8be5a86b 100644 --- a/demo/linking/linkedEncoding.ts +++ b/demo/linking/linkedEncoding.ts @@ -1,9 +1,15 @@ -import { IsDummyTrack, IsMultipleViews, IsSingleView, type Assembly, type SingleView } from '@gosling-lang/gosling-schema'; +import { + IsDummyTrack, + IsMultipleViews, + IsSingleView, + type Assembly, + type SingleView +} from '@gosling-lang/gosling-schema'; import { GenomicPositionHelper, computeChromSizes } from '../../src/core/utils/assembly'; import { signal, type Signal } from '@preact/signals-core'; import type { GoslingSpec } from 'gosling.js'; -import { TrackType } from '../track-def/main'; -import { isHeatmapTrack } from '../track-def/heatmap'; +import { TrackType } from '../../src/track-def'; +import { isHeatmapTrack } from '../../src/track-def/heatmap'; /** * This is the information needed to link tracks together diff --git a/demo/renderer/main.ts b/demo/renderer/main.ts index eddba36b2..3068572b7 100644 --- a/demo/renderer/main.ts +++ b/demo/renderer/main.ts @@ -5,7 +5,7 @@ import { Signal } from '@preact/signals-core'; import { TextTrack } from '@gosling-lang/text-track'; import { panZoom, panZoomHeatmap } from '@gosling-lang/interactors'; -import { type TrackDefs, TrackType } from '../track-def/main'; +import { type TrackDefs, TrackType } from '../../src/track-def'; import { getDataFetcher } from './dataFetcher'; import type { LinkedEncoding } from '../linking/linkedEncoding'; import { BrushCircularTrack } from '@gosling-lang/brush-circular'; diff --git a/demo/track-def/axis.ts b/demo/src/axis.ts similarity index 100% rename from demo/track-def/axis.ts rename to demo/src/axis.ts diff --git a/demo/track-def/brush.ts b/demo/src/brush.ts similarity index 100% rename from demo/track-def/brush.ts rename to demo/src/brush.ts diff --git a/demo/track-def/dummy.ts b/demo/src/dummy.ts similarity index 100% rename from demo/track-def/dummy.ts rename to demo/src/dummy.ts diff --git a/demo/track-def/gosling.ts b/demo/src/gosling.ts similarity index 100% rename from demo/track-def/gosling.ts rename to demo/src/gosling.ts diff --git a/demo/track-def/heatmap.ts b/demo/src/heatmap.ts similarity index 100% rename from demo/track-def/heatmap.ts rename to demo/src/heatmap.ts diff --git a/demo/track-def/main.ts b/demo/src/main.ts similarity index 100% rename from demo/track-def/main.ts rename to demo/src/main.ts diff --git a/demo/track-def/text.ts b/demo/src/text.ts similarity index 100% rename from demo/track-def/text.ts rename to demo/src/text.ts diff --git a/demo/track-def/types.ts b/demo/src/types.ts similarity index 100% rename from demo/track-def/types.ts rename to demo/src/types.ts diff --git a/editor/index.css b/editor/index.css index f8c154730..38eed6d55 100644 --- a/editor/index.css +++ b/editor/index.css @@ -15,8 +15,17 @@ body { /* Common font styles */ body, button { - font-family: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', - 'Cantarell', 'Droid Sans', sans-serif; + font-family: + 'Helvetica Neue', + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + 'Roboto', + 'Oxygen', + 'Ubuntu', + 'Cantarell', + 'Droid Sans', + sans-serif; font-size: 14px; text-align: left; line-height: 1.42857143; diff --git a/src/compiler/bounding-box.ts b/src/compiler/bounding-box.ts index 408f0e49e..1e555f7f1 100644 --- a/src/compiler/bounding-box.ts +++ b/src/compiler/bounding-box.ts @@ -10,7 +10,7 @@ import { import { resolveSuperposedTracks } from '../core/utils/overlay'; import { traverseTracksAndViews, traverseViewArrangements } from './spec-preprocess'; import type { CompleteThemeDeep } from '../core/utils/theme'; -import type { ProcessedCircularTrack, ProcessedTrack } from '../../demo/track-def/types'; +import type { ProcessedCircularTrack, ProcessedTrack } from '../track-def/types'; export interface Size { width: number; height: number; diff --git a/src/compiler/compile.test.ts b/src/compiler/compile.test.ts index 580130490..c1b15bbee 100644 --- a/src/compiler/compile.test.ts +++ b/src/compiler/compile.test.ts @@ -6,7 +6,7 @@ import { convertToFlatTracks } from './spec-preprocess'; import type { SingleView } from '@gosling-lang/gosling-schema'; import { spreadTracksByData } from '../core/utils/overlay'; import { getDataFetcher } from '../../demo/renderer/dataFetcher'; -import type { ProcessedTrack } from 'demo/track-def/types'; +import type { ProcessedTrack } from '../track-def/types'; // TODO: Move this to dataFetcher.test.ts after we move the /demo/renderer under /src describe('Compiler with UrlToFetchOptions', () => { diff --git a/src/compiler/views-and-tracks.ts b/src/compiler/views-and-tracks.ts index 1456d3411..517217ac8 100644 --- a/src/compiler/views-and-tracks.ts +++ b/src/compiler/views-and-tracks.ts @@ -10,7 +10,7 @@ import type { import type { CompleteThemeDeep } from '../core/utils/theme'; import { getViewApiData } from '../api/api-data'; import { IsDummyTrack } from '@gosling-lang/gosling-schema'; -import type { ProcessedCircularTrack } from 'demo/track-def/types'; +import type { ProcessedCircularTrack } from '../track-def/types'; export function collectViewsAndTracks(spec: GoslingSpec, trackInfos: TrackInfo[], theme: Required) { if (trackInfos.length === 0) { @@ -32,14 +32,14 @@ export function collectViewsAndTracks(spec: GoslingSpec, trackInfos: TrackInfo[] shape: isLinear ? d.boundingBox : { - ...d.boundingBox, - cx: d.boundingBox.x + d.boundingBox.width / 2.0, - cy: d.boundingBox.y + d.boundingBox.height / 2.0, - innerRadius: (d.track as ProcessedCircularTrack).innerRadius!, - outerRadius: (d.track as ProcessedCircularTrack).outerRadius!, - startAngle: (d.track as ProcessedCircularTrack).startAngle!, - endAngle: (d.track as ProcessedCircularTrack).endAngle! - } + ...d.boundingBox, + cx: d.boundingBox.x + d.boundingBox.width / 2.0, + cy: d.boundingBox.y + d.boundingBox.height / 2.0, + innerRadius: (d.track as ProcessedCircularTrack).innerRadius!, + outerRadius: (d.track as ProcessedCircularTrack).outerRadius!, + startAngle: (d.track as ProcessedCircularTrack).startAngle!, + endAngle: (d.track as ProcessedCircularTrack).endAngle! + } }; }); diff --git a/src/core/utils/assembly.ts b/src/core/utils/assembly.ts index 34b544b5d..b5ce5bebb 100644 --- a/src/core/utils/assembly.ts +++ b/src/core/utils/assembly.ts @@ -213,7 +213,11 @@ export function parseGenomicPosition(position: string): { chromosome: string; st * A class that consistently manage and convert genomics positions. */ export class GenomicPositionHelper { - constructor(public chromosome: string, public start?: number, public end?: number) {} + constructor( + public chromosome: string, + public start?: number, + public end?: number + ) {} static fromString(str: string) { const result = parseGenomicPosition(str); return new GenomicPositionHelper(result.chromosome, result.start, result.end); diff --git a/src/core/utils/overlay.ts b/src/core/utils/overlay.ts index 1db0651bf..49c9c093f 100644 --- a/src/core/utils/overlay.ts +++ b/src/core/utils/overlay.ts @@ -126,8 +126,8 @@ export function spreadTracksByData(tracks: Track[]): Track[] { IsSingleTrack(track) && IsChannelDeep(track.y) && !track.y.axis && overlayOnPreviousTrack ? ({ ...track.y, axis: i === 1 ? 'right' : 'none' } as ChannelDeep) : IsSingleTrack(track) - ? track.y - : undefined; + ? track.y + : undefined; if (track.title && i !== arr.length - 1 && arr.length !== 1) { delete track.title; // remove `title` except the last one diff --git a/src/data-fetchers/bam/bam-data-fetcher.ts b/src/data-fetchers/bam/bam-data-fetcher.ts index 59e4da7e8..5ae8bd980 100644 --- a/src/data-fetchers/bam/bam-data-fetcher.ts +++ b/src/data-fetchers/bam/bam-data-fetcher.ts @@ -17,8 +17,8 @@ const DEBOUNCE_TIME = 200; type InferTileType = Config['extractJunction'] extends true ? Junction : Config['loadMates'] extends true - ? SegmentWithMate - : Segment; + ? SegmentWithMate + : Segment; class BamDataFetcher implements TabularDataFetcher> { static config = { type: 'bam' }; diff --git a/src/data-fetchers/bed/bed-worker.ts b/src/data-fetchers/bed/bed-worker.ts index 0ca291d28..2a23544d1 100644 --- a/src/data-fetchers/bed/bed-worker.ts +++ b/src/data-fetchers/bed/bed-worker.ts @@ -51,7 +51,10 @@ class BedFile { #customFields?: string[]; #uid: string; - constructor(public tbi: TabixIndexedFile, uid: string) { + constructor( + public tbi: TabixIndexedFile, + uid: string + ) { this.#uid = uid; } /** diff --git a/src/data-fetchers/utils.ts b/src/data-fetchers/utils.ts index f2212fe21..d4f99bb15 100644 --- a/src/data-fetchers/utils.ts +++ b/src/data-fetchers/utils.ts @@ -17,7 +17,11 @@ export class DataSource { chromInfo: ExtendedChromInfo; tilesetInfo: ReturnType; - constructor(public file: File, chromSizes: ChromSizes, public options: Options) { + constructor( + public file: File, + chromSizes: ChromSizes, + public options: Options + ) { this.chromInfo = sizesToChromInfo(chromSizes); this.tilesetInfo = tilesetInfoFromChromInfo(this.chromInfo); } diff --git a/src/data-fetchers/vcf/vcf-worker.ts b/src/data-fetchers/vcf/vcf-worker.ts index 5289a505d..c03a68871 100644 --- a/src/data-fetchers/vcf/vcf-worker.ts +++ b/src/data-fetchers/vcf/vcf-worker.ts @@ -29,7 +29,10 @@ type VcfFileOptions = { class VcfFile { #parser?: VCF; #uid: string; - constructor(public tbi: TabixIndexedFile, uid: string) { + constructor( + public tbi: TabixIndexedFile, + uid: string + ) { this.#uid = uid; } /** diff --git a/src/gosling-schema/gosling.schema.guards.ts b/src/gosling-schema/gosling.schema.guards.ts index 1b5960b18..ab67b231b 100644 --- a/src/gosling-schema/gosling.schema.guards.ts +++ b/src/gosling-schema/gosling.schema.guards.ts @@ -49,7 +49,7 @@ import { } from 'd3-scale-chromatic'; import { resolveSuperposedTracks } from '../core/utils/overlay'; import type { TabularDataFetcher } from '@data-fetchers'; -import type { ProcessedDummyTrack, ProcessedTrack } from 'demo/track-def/types'; +import type { ProcessedDummyTrack, ProcessedTrack } from '../track-def/types'; export const PREDEFINED_COLOR_STR_MAP: { [k: string]: (t: number) => string } = { viridis: interpolateViridis, diff --git a/src/higlass/higlass-vendored.js b/src/higlass/higlass-vendored.js index 71504eeb9..c30a0f24b 100644 --- a/src/higlass/higlass-vendored.js +++ b/src/higlass/higlass-vendored.js @@ -24,13 +24,13 @@ const isWithin = (x, y, minX, maxX, minY, maxY, is1d = false) => const fakePubSub = { __fake__: true, - publish: () => { }, + publish: () => {}, subscribe: () => ({ event: '', - handler: () => { } + handler: () => {} }), - unsubscribe: () => { }, - clear: () => { } + unsubscribe: () => {}, + clear: () => {} }; /** @@ -124,7 +124,7 @@ class Track { return () => this[prop]; } - getData() { } + getData() {} /** * Capture click events. x and y are relative to the track position @@ -143,7 +143,7 @@ class Track { } /** There was a click event outside the track * */ - clickOutside() { } + clickOutside() {} /** @returns {[number, number]} */ getDimensions() { @@ -263,7 +263,7 @@ class Track { } /** @returns {void} */ - draw() { } + draw() {} /** @returns {[number, number]} */ getPosition() { @@ -285,7 +285,7 @@ class Track { * @param {{}} evt * @returns {void} */ - defaultMouseMoveHandler() { } + defaultMouseMoveHandler() {} /** @returns {void} */ remove() { @@ -298,7 +298,7 @@ class Track { * @param {Options} options * @returns {void} */ - rerender() { } + rerender() {} /** * This function is for seeing whether this track should respond @@ -318,13 +318,13 @@ class Track { * @param {number} kMultiplier * @returns {void} */ - zoomedY() { } + zoomedY() {} /** * @param {number} dY * @returns {void} */ - movedY() { } + movedY() {} } // @ts-nocheck @@ -826,10 +826,10 @@ class PixiTrack extends Track { graphics.drawRect( this.position[0] + (labelLeftMargin || labelTopMargin) + this.labelXOffset, this.position[1] + - this.dimensions[1] - - this.labelText.height - - labelBackgroundMargin - - (labelBottomMargin || labelRightMargin), + this.dimensions[1] - + this.labelText.height - + labelBackgroundMargin - + (labelBottomMargin || labelRightMargin), this.labelText.width + labelBackgroundMargin, this.labelText.height + labelBackgroundMargin ); @@ -846,11 +846,11 @@ class PixiTrack extends Track { graphics.drawRect( this.position[0] + - this.dimensions[0] - - this.labelText.width - - labelBackgroundMargin - - (labelRightMargin || labelBottomMargin) - - this.labelXOffset, + this.dimensions[0] - + this.labelText.width - + labelBackgroundMargin - + (labelRightMargin || labelBottomMargin) - + this.labelXOffset, this.position[1] + (labelTopMargin || labelLeftMargin), this.labelText.width + labelBackgroundMargin, this.labelText.height + labelBackgroundMargin @@ -867,16 +867,16 @@ class PixiTrack extends Track { graphics.drawRect( this.position[0] + - this.dimensions[0] - - this.labelText.width - - labelBackgroundMargin - - labelRightMargin - - this.labelXOffset, + this.dimensions[0] - + this.labelText.width - + labelBackgroundMargin - + labelRightMargin - + this.labelXOffset, this.position[1] + - this.dimensions[1] - - this.labelText.height - - labelBackgroundMargin - - labelBottomMargin, + this.dimensions[1] - + this.labelText.height - + labelBackgroundMargin - + labelBottomMargin, this.labelText.width + labelBackgroundMargin, this.labelText.height + labelBackgroundMargin ); @@ -1008,9 +1008,9 @@ class PixiTrack extends Track { clipPolygon.setAttribute( 'points', `${this.position[0]},${this.position[1]} ` + - `${this.position[0] + this.dimensions[0]},${this.position[1]} ` + - `${this.position[0] + this.dimensions[0]},${this.position[1] + this.dimensions[1]} ` + - `${this.position[0]},${this.position[1] + this.dimensions[1]} ` + `${this.position[0] + this.dimensions[0]},${this.position[1]} ` + + `${this.position[0] + this.dimensions[0]},${this.position[1] + this.dimensions[1]} ` + + `${this.position[0]},${this.position[1] + this.dimensions[1]} ` ); // the clipping area needs to be a clipPath element @@ -2183,9 +2183,9 @@ function workerGetTiles(outUrl, server, theseTileIds, authHeader, done, requestB headers, ...(requestBody && Object.keys(requestBody).length > 0 ? { - method: 'POST', - body: JSON.stringify(requestBody) - } + method: 'POST', + body: JSON.stringify(requestBody) + } : {}) }) .then(response => response.json()) @@ -4091,7 +4091,7 @@ class TiledPixiTrack extends PixiTrack { * Function is called when all tiles that should be visible have * been received. */ - allTilesLoaded() { } + allTilesLoaded() {} minValue(_) { if (_) { @@ -4131,7 +4131,7 @@ class TiledPixiTrack extends PixiTrack { this.scale.maxValue = this.scale.maxRawValue; } - updateTile(/* tile */) { } + updateTile(/* tile */) {} destroyTile(/* tile */) { // remove all data structures needed to draw this tile @@ -4367,7 +4367,7 @@ class TiledPixiTrack extends PixiTrack { /** * Draw a tile on some graphics */ - drawTile(/* tileData, graphics */) { } + drawTile(/* tileData, graphics */) {} calculateMedianVisibleValue() { if (this.areAllVisibleTilesLoaded()) { @@ -5179,17 +5179,17 @@ const setupShowMousePosition = (context, is2d = false, isGlobal = false) => { */ const valueToColor = (valueScale, colorScale, pseudoCounts = 0, eps = 0.000001) => - value => { - let rgbIdx = 255; + value => { + let rgbIdx = 255; - if (value > eps) { - // values less than espilon are considered NaNs and made transparent - // (rgbIdx 255) - rgbIdx = Math.max(0, Math.min(255, Math.floor(valueScale(value + pseudoCounts)))); - } + if (value > eps) { + // values less than espilon are considered NaNs and made transparent + // (rgbIdx 255) + rgbIdx = Math.max(0, Math.min(255, Math.floor(valueScale(value + pseudoCounts)))); + } - return colorScale[rgbIdx]; - }; + return colorScale[rgbIdx]; + }; // @ts-nocheck @@ -6074,9 +6074,9 @@ class HeatmapTiledPixiTrack extends TiledPixiTrack { ) { this.limitedValueScale.domain([ this.valueScale.domain()[0] + - (this.valueScale.domain()[1] - this.valueScale.domain()[0]) * this.options.scaleStartPercent, + (this.valueScale.domain()[1] - this.valueScale.domain()[0]) * this.options.scaleStartPercent, this.valueScale.domain()[0] + - (this.valueScale.domain()[1] - this.valueScale.domain()[0]) * this.options.scaleEndPercent + (this.valueScale.domain()[1] - this.valueScale.domain()[0]) * this.options.scaleEndPercent ]); } @@ -6387,7 +6387,8 @@ class HeatmapTiledPixiTrack extends TiledPixiTrack { this.gColorscaleBrush.attr( 'transform', - `translate(${this.pColorbarArea.x + this.pColorbar.x + COLORBAR_WIDTH + 2},${this.pColorbarArea.y + this.pColorbar.y - 1 + `translate(${this.pColorbarArea.x + this.pColorbar.x + COLORBAR_WIDTH + 2},${ + this.pColorbarArea.y + this.pColorbar.y - 1 })` ); } @@ -6405,7 +6406,8 @@ class HeatmapTiledPixiTrack extends TiledPixiTrack { this.gColorscaleBrush.attr( 'transform', - `translate(${this.pColorbarArea.x + this.pColorbar.x + COLORBAR_WIDTH + BRUSH_COLORBAR_GAP},${this.pColorbarArea.y + this.pColorbar.y - 1 + `translate(${this.pColorbarArea.x + this.pColorbar.x + COLORBAR_WIDTH + BRUSH_COLORBAR_GAP},${ + this.pColorbarArea.y + this.pColorbar.y - 1 })` ); } @@ -6800,8 +6802,8 @@ class HeatmapTiledPixiTrack extends TiledPixiTrack { GLOBALS.PIXI.VERSION[0] === '4' ? GLOBALS.PIXI.Texture.fromCanvas(canvas, GLOBALS.PIXI.SCALE_MODES.NEAREST) : GLOBALS.PIXI.Texture.from(canvas, { - scaleMode: GLOBALS.PIXI.SCALE_MODES.NEAREST - }); + scaleMode: GLOBALS.PIXI.SCALE_MODES.NEAREST + }); const sprite = new GLOBALS.PIXI.Sprite(texture); diff --git a/src/track-def/axis.ts b/src/track-def/axis.ts new file mode 100644 index 000000000..4fa9b738b --- /dev/null +++ b/src/track-def/axis.ts @@ -0,0 +1,255 @@ +import { type AxisTrackOptions } from '@gosling-lang/genomic-axis'; +import { IsChannelDeep, IsDummyTrack, IsTemplateTrack, type AxisPosition } from '@gosling-lang/gosling-schema'; +import type { CompleteThemeDeep } from '../../src/core/utils/theme'; +import { resolveSuperposedTracks } from '../../src/core/utils/overlay'; +import { TrackType, type TrackDef } from './index'; +import type { ProcessedCircularTrack, ProcessedTrack } from './types'; +import { DEFAULT_AXIS_SIZE } from '../../src/compiler/defaults'; + +/** + * Generates the track definition for the axis track + * @param track + * @param boundingBox + * @param theme + */ +export function getAxisTrackDef( + track: ProcessedTrack, + boundingBox: { x: number; y: number; width: number; height: number }, + theme: Required +): [trackBbox: { x: number; y: number; width: number; height: number }, TrackDef[] | undefined] { + const { xAxisPosition, yAxisPosition } = getAxisPositions(track); + // This is a copy of the original bounding box. It will be modified if an axis is added + const trackBbox = { ...boundingBox }; + const trackDefs: TrackDef[] = []; + if (xAxisPosition) { + if (track.layout === 'circular') { + trackDefs.push({ + type: TrackType.Axis, + trackId: track.id, + boundingBox: boundingBox, + options: getAxisTrackCircularOptions(track, boundingBox, xAxisPosition, theme) + }); + } + if (track.layout === 'linear') { + const isHorizontal = track.orientation === 'horizontal'; + const widthOrHeight = isHorizontal ? 'height' : 'width'; + const axisBbox = { ...trackBbox, [widthOrHeight]: DEFAULT_AXIS_SIZE }; + trackBbox[widthOrHeight] -= axisBbox[widthOrHeight]; + if (xAxisPosition === 'top') { + trackBbox.y += axisBbox.height; + } else if (xAxisPosition === 'bottom') { + axisBbox.y = trackBbox.y + trackBbox.height; + } else if (xAxisPosition === 'right') { + axisBbox.x = trackBbox.x + trackBbox.width; + } else if (xAxisPosition === 'left') { + trackBbox.x += axisBbox.width; + } + trackDefs.push({ + type: TrackType.Axis, + trackId: track.id, + boundingBox: axisBbox, + options: getAxisTrackLinearOptions('x', track, axisBbox, xAxisPosition, theme) + }); + } + } + if (yAxisPosition) { + if (track.layout === 'circular') { + console.warn('Error: Circular layout does not support y-axis'); + } + if (track.layout === 'linear') { + if (yAxisPosition === 'top' || yAxisPosition === 'bottom') { + console.warn('Error: Bottom y-axis is not supported. Defaulting to left.'); + } + const isHorizontal = track.orientation === 'horizontal'; + const widthOrHeight = isHorizontal ? 'width' : 'height'; + const axisBbox = { ...trackBbox, [widthOrHeight]: DEFAULT_AXIS_SIZE }; + trackBbox[widthOrHeight] -= axisBbox[widthOrHeight]; + if (yAxisPosition === 'right') { + axisBbox.x = trackBbox.x + trackBbox.width; + } else if (yAxisPosition === 'left' || yAxisPosition === 'bottom' || yAxisPosition === 'top') { + trackBbox.x += axisBbox.width; + } + trackDefs.push({ + type: TrackType.Axis, + trackId: track.id, + boundingBox: axisBbox, + options: getAxisTrackLinearOptions('y', track, axisBbox, yAxisPosition, theme) + }); + } + } + return [trackBbox, trackDefs]; +} + +/** + * Generates options for the linear axis track + * @param boundingBox Bounding box of the track + * @param position "top" | "bottom" | "left" | "right + */ +function getAxisTrackLinearOptions( + encoding: 'x' | 'y', + track: ProcessedTrack, + boundingBox: { x: number; y: number; width: number; height: number }, + position: AxisPosition, + theme: Required +): AxisTrackOptions { + const narrowType = getAxisNarrowType(encoding, track.orientation, boundingBox.width, boundingBox.height); + const options: AxisTrackOptions = { + orientation: getAxisOrientation(encoding, track.orientation), + encoding: encoding, + static: track.static, + innerRadius: 0, + outerRadius: 0, + width: boundingBox.width, + height: boundingBox.height, + startAngle: 0, + endAngle: 0, + layout: 'linear', + assembly: 'hg38', + stroke: 'transparent', // text outline + color: theme.axis.labelColor, + labelMargin: theme.axis.labelMargin, + excludeChrPrefix: theme.axis.labelExcludeChrPrefix, + fontSize: theme.axis.labelFontSize, + fontFamily: theme.axis.labelFontFamily, + fontWeight: theme.axis.labelFontWeight, + tickColor: theme.axis.tickColor, + tickFormat: narrowType === 'narrower' ? 'si' : 'plain', + tickPositions: narrowType === 'regular' ? 'even' : 'ends', + reverseOrientation: position === 'bottom' || position === 'right' ? true : false + }; + return options; +} + +/** + * Determines the orientation of the axis + */ +function getAxisOrientation( + encoding: 'x' | 'y', + trackOrientation: 'horizontal' | 'vertical' +): 'horizontal' | 'vertical' { + if (encoding === 'x') { + return trackOrientation === 'horizontal' ? 'horizontal' : 'vertical'; + } + if (encoding === 'y') { + return trackOrientation === 'horizontal' ? 'vertical' : 'horizontal'; + } + console.warn('Invalid track orientation. Defaulting to horizontal'); + return 'horizontal'; +} + +/** + * Generates options for the circular axis track + */ +function getAxisTrackCircularOptions( + track: ProcessedCircularTrack, + boundingBox: { x: number; y: number; width: number; height: number }, + position: AxisPosition, + theme: Required +): AxisTrackOptions { + const narrowType = getAxisNarrowType('x', 'horizontal', boundingBox.width, boundingBox.height); + const { startAngle, endAngle, outerRadius } = track; + let { innerRadius } = track; + if (position === 'top') { + innerRadius = outerRadius - 30; + } else if (position === 'left' || position === 'right') { + console.error('Axis position left or right is not supported in circular layout'); + } + + const options: AxisTrackOptions = { + layout: 'circular', + encoding: 'x', + static: track.static, + innerRadius, + outerRadius, + width: boundingBox.width, + height: boundingBox.height, + startAngle, + endAngle, + assembly: 'hg38', + stroke: 'transparent', // text outline + color: theme.axis.labelColor, + labelMargin: theme.axis.labelMargin, + excludeChrPrefix: theme.axis.labelExcludeChrPrefix, + fontSize: theme.axis.labelFontSize, + fontFamily: theme.axis.labelFontFamily, + fontWeight: theme.axis.labelFontWeight, + tickColor: theme.axis.tickColor, + tickFormat: narrowType === 'narrower' ? 'si' : 'plain', + tickPositions: narrowType === 'regular' ? 'even' : 'ends', + reverseOrientation: position === 'bottom' || position === 'right' ? true : false + }; + return options; +} + +/** + * Determines the position of the x and y axes for a given track + * @param track + * @returns + */ +function getAxisPositions(track: ProcessedTrack): { + xAxisPosition: AxisPosition | undefined; + yAxisPosition: AxisPosition | undefined; +} { + if (IsTemplateTrack(track) || IsDummyTrack(track)) { + return { xAxisPosition: undefined, yAxisPosition: undefined }; + } + + const resolvedSpecs = resolveSuperposedTracks(track); + const firstResolvedSpec = resolvedSpecs[0]; + + const hasXAxis = + ('x' in firstResolvedSpec && + firstResolvedSpec.x && + 'axis' in firstResolvedSpec.x && + firstResolvedSpec.x.axis !== 'none' && + firstResolvedSpec.x.type === 'genomic') || + false; + const hasYAxis = + ('y' in firstResolvedSpec && + firstResolvedSpec.y && + 'axis' in firstResolvedSpec.y && + firstResolvedSpec.y.axis !== 'none' && + firstResolvedSpec.y.type === 'genomic') || + false; + + const xAxisPosition = + hasXAxis && IsChannelDeep(firstResolvedSpec.x) ? (firstResolvedSpec.x?.axis as AxisPosition) : undefined; + const yAxisPosition = + hasYAxis && IsChannelDeep(firstResolvedSpec.y) ? (firstResolvedSpec.y?.axis as AxisPosition) : undefined; + + return { + xAxisPosition, + yAxisPosition + }; +} + +/** + * Determines the compactness type of an axis considering the size of a track + */ +const getAxisNarrowType = ( + c: 'x' | 'y', + orientation: 'horizontal' | 'vertical' = 'horizontal', + width: number, + height: number +) => { + const narrowSizeThreshold = 400; + const narrowerSizeThreshold = 200; + + if (orientation === 'horizontal') { + if ((c === 'x' && width <= narrowerSizeThreshold) || (c === 'y' && height <= narrowerSizeThreshold)) { + return 'narrower'; + } else if ((c === 'x' && width <= narrowSizeThreshold) || (c === 'y' && height <= narrowSizeThreshold)) { + return 'narrow'; + } else { + return 'regular'; + } + } else { + if ((c === 'x' && height <= narrowerSizeThreshold) || (c === 'y' && width <= narrowerSizeThreshold)) { + return 'narrower'; + } else if ((c === 'x' && height <= narrowSizeThreshold) || (c === 'y' && width <= narrowSizeThreshold)) { + return 'narrow'; + } else { + return 'regular'; + } + } +}; diff --git a/src/track-def/brush.ts b/src/track-def/brush.ts new file mode 100644 index 000000000..9a1cf828a --- /dev/null +++ b/src/track-def/brush.ts @@ -0,0 +1,71 @@ +import type { BrushLinearTrackOptions } from '@gosling-lang/brush-linear'; +import type { BrushCircularTrackOptions } from '@gosling-lang/brush-circular'; +import { type TrackDef, TrackType } from './index'; +import { type ProcessedTrack, type OverlayTrack, type ProcessedCircularTrack } from './types'; + +export function getBrushTrackDefs( + spec: ProcessedTrack, + boundingBox: { x: number; y: number; width: number; height: number } +): TrackDef[] | TrackDef[] { + const trackDefs: TrackDef[] = []; + // We always expect brushes to be overlayed on top of another track + if (!spec._overlay) return []; + + spec._overlay.forEach((overlay: OverlayTrack) => { + // Skip if the overlay is not a brush + if (overlay.mark !== 'brush') return; + + if (spec.layout === 'linear') { + const options = getBrushLinearOptions(spec, overlay); + trackDefs.push({ + type: TrackType.BrushLinear, + trackId: overlay.id, + boundingBox: { ...boundingBox }, + options + }); + } else if (spec.layout === 'circular') { + // If we have a circular layout, we use the BrushCircularTrack + const options = getBrushCircularOptions(spec, overlay); + trackDefs.push({ + type: TrackType.BrushCircular, + trackId: overlay.id, + boundingBox: { ...boundingBox }, + options + }); + } + }); + return trackDefs; +} + +/** + * Get the options for a BrushLinearTrack + */ +function getBrushLinearOptions(spec: ProcessedTrack, overlay: OverlayTrack): BrushLinearTrackOptions { + const options = { + projectionFillColor: overlay.color?.value ?? 'gray', + projectionStrokeColor: spec.stroke?.value ?? 'black', + projectionFillOpacity: spec.opacity?.value ?? 0.3, + projectionStrokeOpacity: spec.opacity?.value ?? 0.3, + strokeWidth: spec.strokeWidth?.value ?? 1 + }; + return options; +} + +/** + * Get the options for a BrushCircularTrack + */ +function getBrushCircularOptions(spec: ProcessedCircularTrack, overlay: OverlayTrack): BrushCircularTrackOptions { + const options = { + projectionFillColor: overlay.color?.value ?? 'gray', + projectionStrokeColor: overlay.stroke?.value ?? 'black', + projectionFillOpacity: 0.3, + projectionStrokeOpacity: 0.3, + strokeWidth: spec.strokeWidth?.value ?? 0.3, + startAngle: spec.startAngle ?? 7.2, + endAngle: spec.endAngle ?? 352.8, + innerRadius: spec.innerRadius ?? 151.08695652173913, + outerRadius: spec.outerRadius ?? 250, + axisPositionHorizontal: 'left' + }; + return options; +} diff --git a/src/track-def/dummy.ts b/src/track-def/dummy.ts new file mode 100644 index 000000000..6531576ed --- /dev/null +++ b/src/track-def/dummy.ts @@ -0,0 +1,21 @@ +import type { DummyTrackOptions } from '@gosling-lang/dummy-track'; +import { type TrackDef, TrackType } from './index'; +import { type ProcessedDummyTrack } from './types'; + +export function processDummyTrack( + track: ProcessedDummyTrack, + boundingBox: { x: number; y: number; width: number; height: number } +): TrackDef[] { + const trackDef: TrackDef = { + type: TrackType.Dummy, + trackId: track.id, + boundingBox, + options: { + width: boundingBox.width, + height: boundingBox.height, + ...track.style, + title: track.title ?? '' + } + }; + return [trackDef]; +} diff --git a/src/track-def/gosling.ts b/src/track-def/gosling.ts new file mode 100644 index 000000000..552118f51 --- /dev/null +++ b/src/track-def/gosling.ts @@ -0,0 +1,75 @@ +import { type AxisTrackOptions } from '@gosling-lang/genomic-axis'; +import type { CompleteThemeDeep } from '../../src/core/utils/theme'; +import type { GoslingTrackOptions } from '@gosling-lang/gosling-track'; +import type { BrushLinearTrackOptions } from '@gosling-lang/brush-linear'; +import { getAxisTrackDef } from './axis'; +import { type TrackDef, TrackType } from './index'; +import { getBrushTrackDefs } from './brush'; +import type { ProcessedTrack } from './types'; + +/** + * A Gosling track, as defined in the schema, can be composed of multiple tracks: + * A GoslingTrack, an AxisTrack, and a BrushTrack. This function processes the spec of a single Gosling track + * and returns the corresponding track definitions. + */ +export function processGoslingTrack( + track: ProcessedTrack, + boundingBox: { x: number; y: number; width: number; height: number }, + theme: Required +): (TrackDef | TrackDef | TrackDef)[] { + const trackDefs: ( + | TrackDef + | TrackDef + | TrackDef + )[] = []; + + // Adds the axis tracks + const [newTrackBbox, axisTrackDefs] = getAxisTrackDef(track, boundingBox, theme); + if (axisTrackDefs) { + // Only add the axis track if it is not overlayed on top of the Gosling track + if (!track.overlayOnPreviousTrack) trackDefs.push(...axisTrackDefs); + // modify the bounding box to exclude the axis track + // warning: there could be some weirdness around overlayOnPreviousTrack here that needs to be tested + boundingBox = newTrackBbox; + } + + // Add the Gosling track + const goslingTrackOptions = getGoslingTrackOptions(track, theme); + trackDefs.push({ + type: TrackType.Gosling, + trackId: track.id, + boundingBox: { ...boundingBox }, + options: goslingTrackOptions + }); + + // Add the brush after Gosling track so that it is on top + const brushTrackDefs = getBrushTrackDefs(track, boundingBox); + brushTrackDefs.forEach(brushTrackDef => { + trackDefs.push(brushTrackDef); + }); + + return trackDefs; +} + +function getGoslingTrackOptions(spec: ProcessedTrack, theme: Required): GoslingTrackOptions { + return { + spec: spec, + id: spec.id, + siblingIds: [], + showMousePosition: true, + mousePositionColor: '#000000', + name: spec.title, + labelPosition: spec.overlayOnPreviousTrack || spec.title === undefined ? 'none' : 'topLeft', + labelShowResolution: false, + labelColor: 'black', + labelBackgroundColor: 'white', + labelBackgroundOpacity: 0.5, + labelTextOpacity: 1, + labelLeftMargin: 1, + labelTopMargin: 1, + labelRightMargin: 0, + labelBottomMargin: 0, + backgroundColor: 'transparent', + theme: theme + }; +} diff --git a/src/track-def/heatmap.ts b/src/track-def/heatmap.ts new file mode 100644 index 000000000..644677cd1 --- /dev/null +++ b/src/track-def/heatmap.ts @@ -0,0 +1,83 @@ +import { type TrackDef, TrackType } from './index'; +import { type HeatmapTrackOptions } from '@gosling-lang/heatmap'; +import type { CompleteThemeDeep } from '../../src/core/utils/theme'; +import { computeChromSizes } from '../../src/core/utils/assembly'; +import { getAxisTrackDef } from './axis'; +import { type AxisTrackOptions } from '@gosling-lang/genomic-axis'; +import type { ProcessedTrack } from './types'; +import { IsChannelDeep, getHiGlassColorRange } from '@gosling-lang/gosling-schema'; + +export function processHeatmapTrack( + track: ProcessedTrack, + boundingBox: { x: number; y: number; width: number; height: number }, + theme: Required +): (TrackDef | TrackDef)[] { + const trackDefs: (TrackDef | TrackDef)[] = []; + + // Adds the axis tracks if needed + const [newTrackBbox, axisTrackDefs] = getAxisTrackDef(track, boundingBox, theme); + if (axisTrackDefs) { + trackDefs.push(...axisTrackDefs); + // modify the bounding box to exclude the axis track + boundingBox = newTrackBbox; + } + + const heatmapOptions = getHeatmapOptions(track, theme); + trackDefs.push({ + type: TrackType.Heatmap, + options: heatmapOptions, + boundingBox, + trackId: track.id + }); + return trackDefs; +} + +export function isHeatmapTrack(track: ProcessedTrack): boolean { + return (track.data && track.data.type === 'matrix') || false; +} + +function getHeatmapOptions(track: ProcessedTrack): HeatmapTrackOptions { + const { assembly } = track; + // Edge case: The first track in a view with "alignment": "overlay" can + // sometimes not have a y encoding but it has a single overlay track which contains the y encoding + // TODO: Should be possible to fix this during when the spec is compiled + const missingX = !('x' in track) || track.x === undefined; + const missingY = !('y' in track) || track.y === undefined; + const hasOverlay = '_overlay' in track && track._overlay && track._overlay.length == 1; + if (missingX && missingY && hasOverlay) track = { ...track, ...track._overlay[0] }; + + // Get color range + const colorStr = + IsChannelDeep(track.color) && typeof track.color.range === 'string' ? track.color.range : 'viridis'; + const colorRange = getHiGlassColorRange(colorStr); + return { + spec: track, + maxDomain: computeChromSizes(assembly).total, + showMousePosition: false, + mousePositionColor: '#000000', + name: track.title, + labelPosition: 'none', + labelShowResolution: false, + labelColor: 'black', + labelBackgroundColor: 'white', + labelBackgroundOpacity: 0.5, + labelTextOpacity: 1, + labelLeftMargin: 1, + labelTopMargin: 1, + labelRightMargin: 0, + labelBottomMargin: 0, + backgroundColor: 'transparent', + trackBorderWidth: 1, + trackBorderColor: 'black', + extent: 'full', + colorbarPosition: 'hidden', + labelShowAssembly: true, + colorbarBackgroundColor: '#ffffff', + minWidth: 100, + minHeight: 100, + heatmapValueScaling: 'log', + showTooltip: false, + zeroValueColor: undefined, + colorRange + }; +} diff --git a/src/track-def/index.ts b/src/track-def/index.ts new file mode 100644 index 000000000..90a76f8aa --- /dev/null +++ b/src/track-def/index.ts @@ -0,0 +1,106 @@ +import type { PixiManager } from '@pixi-manager'; +import { type TextTrackOptions } from '@gosling-lang/text-track'; +import { type DummyTrackOptions } from '@gosling-lang/dummy-track'; +import { type AxisTrackOptions } from '@gosling-lang/genomic-axis'; +import { type BrushLinearTrackOptions } from '@gosling-lang/brush-linear'; +import type { TrackInfo } from '../../src/compiler/bounding-box'; +import type { CompleteThemeDeep } from '../../src/core/utils/theme'; +import type { GoslingTrackOptions } from '../../src/tracks/gosling-track/gosling-track'; + +import { proccessTextHeader } from './text'; +import { processHeatmapTrack, isHeatmapTrack } from './heatmap'; +import { processGoslingTrack } from './gosling'; +import { type BrushCircularTrackOptions } from '@gosling-lang/brush-circular'; +import { type HeatmapTrackOptions } from '@gosling-lang/heatmap'; +import { processDummyTrack } from './dummy'; +import { IsDummyTrack } from '@gosling-lang/gosling-schema'; + +/** + * All the different types of tracks that can be rendered + */ +export enum TrackType { + Text, + Dummy, + Gosling, + Axis, + BrushLinear, + BrushCircular, + Heatmap +} + +/** + * Associate options to each track type + */ +interface TrackOptionsMap { + [TrackType.Gosling]: GoslingTrackOptions; + [TrackType.Text]: TextTrackOptions; + [TrackType.Dummy]: DummyTrackOptions; + [TrackType.Axis]: AxisTrackOptions; + [TrackType.BrushLinear]: BrushLinearTrackOptions; + [TrackType.BrushCircular]: BrushCircularTrackOptions; + [TrackType.Heatmap]: HeatmapTrackOptions; +} + +/** + * This interface contains all of the information needed to render each track type. + */ +export interface TrackDef { + type: TrackType; + trackId: string; + boundingBox: { x: number; y: number; width: number; height: number }; + options: T; +} + +/** + * This is a union of all the different TrackDefs + */ +export type TrackDefs = { + [K in keyof TrackOptionsMap]: TrackDef; +}[keyof TrackOptionsMap]; + +/** + * Takes a list of TrackInfos and returns a list of TrackDefs + * @param trackInfos + * @param pixiManager + * @param theme + * @returns + */ +export function createTrackDefs(trackInfos: TrackInfo[], theme: Required): TrackDefs[] { + const trackDefs: TrackDefs[] = []; + console.warn('trackinfos', trackInfos); + trackInfos.forEach(trackInfo => { + const { track, boundingBox } = trackInfo; + + if (track.mark === '_header') { + // Header marks contain both the title and subtitle + const textTrackDefs = proccessTextHeader(track, boundingBox, theme); + trackDefs.push(...textTrackDefs); + } else if (isHeatmapTrack(track)) { + // We have a heatmap track + const heatmapTrackDefs = processHeatmapTrack(track, boundingBox, theme); + trackDefs.push(...heatmapTrackDefs); + } else if (IsDummyTrack(track)) { + // We have a dummy track + const dummyTrackDefs = processDummyTrack(track, boundingBox); + trackDefs.push(...dummyTrackDefs); + } else { + // We have a gosling track + const goslingAxisDefs = processGoslingTrack(track, boundingBox, theme); + trackDefs.push(...goslingAxisDefs); + } + }); + return trackDefs; +} + +/** + * This function is for internal testing usage only. It will render a red border around each track + */ +export function showTrackInfoPositions(trackInfos: TrackInfo[], pixiManager: PixiManager) { + trackInfos.forEach(trackInfo => { + const { track, boundingBox } = trackInfo; + const div = pixiManager.makeContainer(boundingBox).overlayDiv; + div.style.border = '3px solid red'; + div.innerHTML = track.mark || 'No mark'; + div.style.textAlign = 'left'; + }); +} diff --git a/src/track-def/text.ts b/src/track-def/text.ts new file mode 100644 index 000000000..477f2ca3b --- /dev/null +++ b/src/track-def/text.ts @@ -0,0 +1,71 @@ +import { type TextTrackOptions } from '@gosling-lang/text-track'; +import type { CompleteThemeDeep } from '../../src/core/utils/theme'; +import { TrackType, type TrackDef } from './index'; +import type { ProcessedTrack } from './types'; + +/** + * Separate the the track with mark "_header" into title and subtitle text tracks + * @param track + * @param boundingBox + * @returns + */ +export function proccessTextHeader( + track: ProcessedTrack, + boundingBox: { x: number; y: number; width: number; height: number }, + theme: Required +): TrackDef[] { + let cumHeight = 0; + const trackDefs: TrackDef[] = []; + if (track.title) { + const textTrackOptions = getTextTrackOptions(track, 'title', theme); + const height = textTrackOptions.fontSize + 6; + trackDefs.push({ + type: TrackType.Text, + trackId: 'title', + boundingBox: { ...boundingBox, height }, + options: textTrackOptions + }); + cumHeight += height; + } + if (track.subtitle) { + const textTrackOptions = getTextTrackOptions(track, 'subtitle', theme); + const height = textTrackOptions.fontSize + 6; + trackDefs.push({ + type: TrackType.Text, + trackId: 'subtitle', + boundingBox: { ...boundingBox, y: boundingBox.y + cumHeight, height }, + options: textTrackOptions + }); + } + return trackDefs; +} + +function getTextTrackOptions( + spec: ProcessedTrack, + type: 'title' | 'subtitle', + theme: Required +): TextTrackOptions { + if (type === 'title') { + return { + backgroundColor: theme.root.titleBackgroundColor, + textColor: theme.root.titleColor, + fontSize: theme.root.titleFontSize ?? 18, + fontWeight: theme.root.titleFontWeight, + fontFamily: theme.root.titleFontFamily, + offsetY: 0, + align: theme.root.titleAlign, + text: spec.title + }; + } else { + return { + backgroundColor: theme.root.subtitleBackgroundColor, + textColor: theme.root.subtitleColor, + fontSize: theme.root.subtitleFontSize ?? 18, + fontWeight: theme.root.subtitleFontWeight, + fontFamily: theme.root.subtitleFontFamily, + offsetY: 0, + align: theme.root.subtitleAlign, + text: spec.subtitle + }; + } +} diff --git a/src/track-def/types.ts b/src/track-def/types.ts new file mode 100644 index 000000000..740d99a33 --- /dev/null +++ b/src/track-def/types.ts @@ -0,0 +1,65 @@ +import type { DataDeep, Assembly, DummyTrackStyle, Mark, X, Y } from '@gosling-lang/gosling-schema'; + +/** + * After the Gosling spec is compiled, it is a "processed spec". + * A processed spec has most of the same properties as the original spec, but some properties are + * added or modified during the compilation process. + * + * For example, a valid Gosling spec may have no 'id' property, but a processed spec will always have an 'id' property. + * + * This file contains the types for the processed spec. + * + * TODO: this file is incomplete. It should be updated to include all the properties that a processed spec can have. + */ + +/** A Track after it has been compiled */ +export type ProcessedTrack = ProcessedLinearTrack | ProcessedCircularTrack | ProcessedDummyTrack; +/** All tracks potentially have these properties */ +export interface ProcessedTrackBase { + id: string; + height: number; + width: number; + static: boolean; + mark?: string; + orientation: 'horizontal' | 'vertical'; + title?: string; + subtitle?: string; + data?: DataDeep; + assembly?: Assembly; + overlayOnPreviousTrack?: boolean; + _overlay?: OverlayTrack[]; + color?: { value: string }; + stroke?: { value: string }; + opacity?: { value: number }; + strokeWidth?: { value: number }; + xOffset?: number; + yOffset?: number; +} + +export type ProcessedLinearTrack = ProcessedTrackBase & { + layout: 'linear'; +}; + +export type ProcessedCircularTrack = ProcessedTrackBase & { + id: string; + layout: 'circular'; + startAngle: number; + endAngle: number; + outerRadius: number; + innerRadius: number; +}; + +export type ProcessedDummyTrack = ProcessedTrackBase & { + type?: string; + style?: DummyTrackStyle; +}; + +/** Tracks in the _overlay */ +export interface OverlayTrack { + id: string; + mark?: Mark; + x?: X; + y?: Y; + color?: { value: string }; + stroke?: { value: string }; +} From fffc10ea9520df642c5bd93696578515d25fd274 Mon Sep 17 00:00:00 2001 From: sehilyi Date: Fri, 14 Feb 2025 11:54:08 -0500 Subject: [PATCH 2/2] more ts fix --- src/gosling-schema/validate.ts | 4 ++- src/track-def/axis.ts | 23 +--------------- src/track-def/brush.ts | 2 +- src/track-def/heatmap.ts | 6 ++--- src/track-def/types.ts | 2 ++ src/tracks/genomic-axis/axis-track.ts | 27 ------------------- .../gosling-track/gosling-track-model.ts | 8 ++++-- src/tracks/gosling-track/gosling-track.ts | 22 ++++++++++++--- src/tracks/heatmap/heatmap-plot.ts | 3 +++ src/tracks/text-track/text-track.ts | 2 +- 10 files changed, 38 insertions(+), 61 deletions(-) diff --git a/src/gosling-schema/validate.ts b/src/gosling-schema/validate.ts index 5e8ae7f1d..265682103 100644 --- a/src/gosling-schema/validate.ts +++ b/src/gosling-schema/validate.ts @@ -3,6 +3,7 @@ import type { SingleTrack, ChannelDeep, ChannelTypes, OverlaidTrack, Track } fro import { IsChannelDeep } from './gosling.schema.guards'; import { resolveSuperposedTracks } from '../core/utils/overlay'; import GoslingSchema from './gosling.schema.json'; +import type { ProcessedTrack } from 'src/track-def/types'; export interface Validity { message: string; @@ -37,10 +38,11 @@ export function validateSpec(schema: any, spec: any, silence = false): Validity return { state: valid ? 'success' : 'warn', message, details }; } -export function validateTrack(track: Track) { +export function validateProcessedTrack(track: ProcessedTrack) { let valid = true; const errorMessages: string[] = []; + // @ts-expect-error This function should be re-written const resolvedTrack = resolveSuperposedTracks(track); resolvedTrack.forEach(spec => { diff --git a/src/track-def/axis.ts b/src/track-def/axis.ts index 4fa9b738b..9635a1751 100644 --- a/src/track-def/axis.ts +++ b/src/track-def/axis.ts @@ -94,9 +94,6 @@ function getAxisTrackLinearOptions( ): AxisTrackOptions { const narrowType = getAxisNarrowType(encoding, track.orientation, boundingBox.width, boundingBox.height); const options: AxisTrackOptions = { - orientation: getAxisOrientation(encoding, track.orientation), - encoding: encoding, - static: track.static, innerRadius: 0, outerRadius: 0, width: boundingBox.width, @@ -120,23 +117,6 @@ function getAxisTrackLinearOptions( return options; } -/** - * Determines the orientation of the axis - */ -function getAxisOrientation( - encoding: 'x' | 'y', - trackOrientation: 'horizontal' | 'vertical' -): 'horizontal' | 'vertical' { - if (encoding === 'x') { - return trackOrientation === 'horizontal' ? 'horizontal' : 'vertical'; - } - if (encoding === 'y') { - return trackOrientation === 'horizontal' ? 'vertical' : 'horizontal'; - } - console.warn('Invalid track orientation. Defaulting to horizontal'); - return 'horizontal'; -} - /** * Generates options for the circular axis track */ @@ -157,8 +137,6 @@ function getAxisTrackCircularOptions( const options: AxisTrackOptions = { layout: 'circular', - encoding: 'x', - static: track.static, innerRadius, outerRadius, width: boundingBox.width, @@ -194,6 +172,7 @@ function getAxisPositions(track: ProcessedTrack): { return { xAxisPosition: undefined, yAxisPosition: undefined }; } + // @ts-expect-error this function should be refactored const resolvedSpecs = resolveSuperposedTracks(track); const firstResolvedSpec = resolvedSpecs[0]; diff --git a/src/track-def/brush.ts b/src/track-def/brush.ts index 9a1cf828a..073e09243 100644 --- a/src/track-def/brush.ts +++ b/src/track-def/brush.ts @@ -65,7 +65,7 @@ function getBrushCircularOptions(spec: ProcessedCircularTrack, overlay: OverlayT endAngle: spec.endAngle ?? 352.8, innerRadius: spec.innerRadius ?? 151.08695652173913, outerRadius: spec.outerRadius ?? 250, - axisPositionHorizontal: 'left' + axisPositionHorizontal: 'left' as 'left' | 'right' }; return options; } diff --git a/src/track-def/heatmap.ts b/src/track-def/heatmap.ts index 644677cd1..921e86bb6 100644 --- a/src/track-def/heatmap.ts +++ b/src/track-def/heatmap.ts @@ -22,7 +22,7 @@ export function processHeatmapTrack( boundingBox = newTrackBbox; } - const heatmapOptions = getHeatmapOptions(track, theme); + const heatmapOptions = getHeatmapOptions(track); // TODO: should we consider `theme`? trackDefs.push({ type: TrackType.Heatmap, options: heatmapOptions, @@ -44,7 +44,7 @@ function getHeatmapOptions(track: ProcessedTrack): HeatmapTrackOptions { const missingX = !('x' in track) || track.x === undefined; const missingY = !('y' in track) || track.y === undefined; const hasOverlay = '_overlay' in track && track._overlay && track._overlay.length == 1; - if (missingX && missingY && hasOverlay) track = { ...track, ...track._overlay[0] }; + if (missingX && missingY && hasOverlay) track = { ...track, ...track._overlay![0] }; // Get color range const colorStr = @@ -73,8 +73,6 @@ function getHeatmapOptions(track: ProcessedTrack): HeatmapTrackOptions { colorbarPosition: 'hidden', labelShowAssembly: true, colorbarBackgroundColor: '#ffffff', - minWidth: 100, - minHeight: 100, heatmapValueScaling: 'log', showTooltip: false, zeroValueColor: undefined, diff --git a/src/track-def/types.ts b/src/track-def/types.ts index 740d99a33..8f065f79f 100644 --- a/src/track-def/types.ts +++ b/src/track-def/types.ts @@ -17,6 +17,7 @@ export type ProcessedTrack = ProcessedLinearTrack | ProcessedCircularTrack | Pro /** All tracks potentially have these properties */ export interface ProcessedTrackBase { id: string; + _renderingId?: string; height: number; width: number; static: boolean; @@ -50,6 +51,7 @@ export type ProcessedCircularTrack = ProcessedTrackBase & { }; export type ProcessedDummyTrack = ProcessedTrackBase & { + layout: 'linear'; // Can only be linear type?: string; style?: DummyTrackStyle; }; diff --git a/src/tracks/genomic-axis/axis-track.ts b/src/tracks/genomic-axis/axis-track.ts index 4b83c94ca..6c61ce2fb 100644 --- a/src/tracks/genomic-axis/axis-track.ts +++ b/src/tracks/genomic-axis/axis-track.ts @@ -28,19 +28,12 @@ export type AxisTrackOptions = { layout: 'linear' | 'circular'; labelMargin: number; excludeChrPrefix: boolean; - labelPosition: string; - labelColor: string; - labelTextOpacity: number; - trackBorderWidth: number; - trackBorderColor: string; tickPositions: 'even' | 'ends'; fontSize: number; fontFamily: string; // 'Arial', fontWeight: NonNullable; color: string; stroke: string; - backgroundColor: string; - showMousePosition: boolean; tickColor: number | string; tickFormat?: string; assembly?: Assembly; @@ -81,19 +74,12 @@ const defaultOptions = { layout: 'linear', labelMargin: 5, excludeChrPrefix: false, - labelPosition: 'none', - labelColor: 'black', - labelTextOpacity: 0.4, - trackBorderWidth: 0, - trackBorderColor: 'black', tickPositions: 'even', fontSize: 12, fontFamily: 'sans-serif', // 'Arial', fontWeight: 'normal', color: '#808080', stroke: '#ffffff', - backgroundColor: 'transparent', - showMousePosition: false, tickColor: TICK_COLOR }; @@ -176,10 +162,6 @@ export class AxisTrackClass extends PixiTrack { this.pubSubs = []; - if (this.options.showMousePosition && !this.hideMousePosition) { - this.hideMousePosition = showMousePosition(this, this.is2d, this.isShowGlobalMousePosition()); - } - let chromSizesPath = chromInfoPath; if (!chromSizesPath) { @@ -304,15 +286,6 @@ export class AxisTrackClass extends PixiTrack { } super.rerender(options, force); - - if (this.options.showMousePosition && !this.hideMousePosition) { - this.hideMousePosition = showMousePosition(this, this.is2d, this.isShowGlobalMousePosition()); - } - - if (!this.options.showMousePosition && this.hideMousePosition) { - this.hideMousePosition(); - this.hideMousePosition = undefined; - } } formatTick(pos: number) { diff --git a/src/tracks/gosling-track/gosling-track-model.ts b/src/tracks/gosling-track/gosling-track-model.ts index fb47f9926..b79edc18b 100644 --- a/src/tracks/gosling-track/gosling-track-model.ts +++ b/src/tracks/gosling-track/gosling-track-model.ts @@ -8,7 +8,11 @@ import type { Color, Stroke } from '@gosling-lang/gosling-schema'; -import { validateTrack, getGenomicChannelFromTrack, getGenomicChannelKeyFromTrack } from '@gosling-lang/gosling-schema'; +import { + validateProcessedTrack, + getGenomicChannelFromTrack, + getGenomicChannelKeyFromTrack +} from '@gosling-lang/gosling-schema'; import { type ScaleLinear, scaleLinear, @@ -886,6 +890,6 @@ export class GoslingTrackModel { * Validate the original spec. */ public validateSpec(): { valid: boolean; errorMessages: string[] } { - return validateTrack(this.originalSpec()); + return validateProcessedTrack(this.originalSpec()); } } diff --git a/src/tracks/gosling-track/gosling-track.ts b/src/tracks/gosling-track/gosling-track.ts index 2684bfc39..283eaaa7d 100644 --- a/src/tracks/gosling-track/gosling-track.ts +++ b/src/tracks/gosling-track/gosling-track.ts @@ -21,7 +21,7 @@ import { getTabularData } from './data-abstraction'; import type { CompleteThemeDeep } from '../../core/utils/theme'; import { drawMark, drawPostEmbellishment, drawPreEmbellishment } from '../../core/mark'; import { GoslingTrackModel } from './gosling-track-model'; -import { validateTrack } from '@gosling-lang/gosling-schema'; +import { validateProcessedTrack } from '@gosling-lang/gosling-schema'; import { shareScaleAcrossTracks } from '../../core/utils/scales'; import { resolveSuperposedTracks } from '../../core/utils/overlay'; import colorToHex from '../../core/utils/color-to-hex'; @@ -59,6 +59,7 @@ import { select, type Selection } from 'd3-selection'; import { format } from 'd3-format'; import { calculate1DVisibleTiles } from './utils'; import { DEFAULT_AXIS_SIZE } from '../../compiler/defaults'; +import type { ProcessedTrack } from 'src/track-def/types'; // Set `true` to print in what order each function is called export const PRINT_RENDERING_CYCLE = false; @@ -86,8 +87,23 @@ export interface GoslingTrackOptions { * Track IDs that are superposed with this track, containing the id of this track itself */ siblingIds: string[]; - spec: SingleTrack | OverlaidTrack; + spec: ProcessedTrack; theme: CompleteThemeDeep; + showMousePosition: boolean; + mousePositionColor: string; + name?: string; + // TODO: are these below all really needed? + labelPosition: string; + labelShowResolution: boolean; + labelColor: string; + labelBackgroundColor: string; + labelBackgroundOpacity: number; + labelTextOpacity: number; + labelLeftMargin: number; + labelTopMargin: number; + labelRightMargin: number; + labelBottomMargin: number; + backgroundColor: string; } export type GoslingTrackContext = Context; @@ -192,7 +208,7 @@ export class GoslingTrackClass extends TiledPixiTrack this.fetchedTiles = {}; this.tileSize = this.tilesetInfo?.tile_size ?? 1024; - const { valid, errorMessages } = validateTrack(this.options.spec); + const { valid, errorMessages } = validateProcessedTrack(this.options.spec); if (!valid) { console.warn('The specification of the following track is invalid', errorMessages, this.options.spec); diff --git a/src/tracks/heatmap/heatmap-plot.ts b/src/tracks/heatmap/heatmap-plot.ts index f4f99d211..d10833bd9 100644 --- a/src/tracks/heatmap/heatmap-plot.ts +++ b/src/tracks/heatmap/heatmap-plot.ts @@ -11,6 +11,7 @@ import { scaleLinear } from 'd3-scale'; import { DataFetcher } from '@higlass/datafetcher'; import { signal, type Signal } from '@preact/signals-core'; import type { Tile } from '@gosling-lang/gosling-track'; +import type { ProcessedTrack } from 'src/track-def/types'; export type HeatmapTrackContext = TiledPixiTrackContext & { svgElement: SVGElement; @@ -21,6 +22,7 @@ export type HeatmapTrackContext = TiledPixiTrackContext & { }; export type HeatmapTrackOptions = TiledPixiTrackOptions & { + spec: ProcessedTrack; maxDomain: number; dataTransform?: unknown; extent?: string; @@ -28,6 +30,7 @@ export type HeatmapTrackOptions = TiledPixiTrackOptions & { showTooltip?: boolean; heatmapValueScaling?: string; colorRange?: unknown; + mousePositionColor: string; showMousePosition?: boolean; scaleStartPercent?: unknown; scaleEndPercent?: unknown; diff --git a/src/tracks/text-track/text-track.ts b/src/tracks/text-track/text-track.ts index be0097c65..59ccbc86a 100644 --- a/src/tracks/text-track/text-track.ts +++ b/src/tracks/text-track/text-track.ts @@ -23,7 +23,7 @@ export interface TextTrackOptions { fontWeight: string; align: 'left' | 'right' | 'middle'; offsetY: number; - text: string; + text?: string; } export type TextTrackContext = PixiTrackContext;