diff --git a/demo/GoslingComponent.tsx b/demo/GoslingComponent.tsx index 429e41e56..a3a931355 100644 --- a/demo/GoslingComponent.tsx +++ b/demo/GoslingComponent.tsx @@ -26,7 +26,7 @@ export function GoslingComponent({ spec, width, height, urlToFetchOptions }: Gos if (!pixiManager) { const canvasWidth = 1000, canvasHeight = 1000; // These initial sizes don't matter because the size will be updated - const pixiManager = new PixiManager(canvasWidth, canvasHeight, plotElement, () => {}); + const pixiManager = new PixiManager(canvasWidth, canvasHeight, plotElement, () => { }); renderGosling(spec, plotElement, pixiManager, urlToFetchOptions); setPixiManager(pixiManager); } else { @@ -37,6 +37,8 @@ export function GoslingComponent({ spec, width, height, urlToFetchOptions }: Gos return
; } + +const baseTheme = getTheme('light'); /** * This is the main function. It takes a Gosling spec and renders it using the PixiManager */ @@ -47,7 +49,12 @@ function renderGosling( urlToFetchOptions?: UrlToFetchOptions ) { // 1. Compile the spec - const compileResult = compile(gs, [], getTheme('light'), { containerSize: { width: 0, height: 0 } }); + const compileResult = compile( + gs, + [], + { ...baseTheme, axis: { ...baseTheme.axis, labelExcludeChrPrefix: true, labelMargin: 1 } }, + { containerSize: { width: 0, height: 0 } } + ); const { trackInfos, gs: processedSpec, theme } = compileResult; console.warn('Spec', processedSpec); // 2. Extract all of the linking information from the spec @@ -93,7 +100,7 @@ function renderGosling( /** Debounces the resize observer */ function debounce(f: (arg0: unknown) => unknown, delay: number) { let timer = 0; - return function (...args: [arg0: unknown]) { + return function(...args: [arg0: unknown]) { clearTimeout(timer); timer = setTimeout(() => f.apply(this, args), delay); }; diff --git a/demo/renderer/dataFetcher.ts b/demo/renderer/dataFetcher.ts index d9235c0e3..218646a01 100644 --- a/demo/renderer/dataFetcher.ts +++ b/demo/renderer/dataFetcher.ts @@ -15,6 +15,9 @@ import type { UrlToFetchOptions } from 'src/compiler/compile'; export function getDataFetcher(spec: ProcessedTrack, urlToFetchOptions?: UrlToFetchOptions) { if (!('data' in spec)) { console.warn('No data in the track spec', spec); + // A data object can be missing in 3D tracks. In this case, just pass an empty data. + // The 3d track can still use the 3D genome model for visual encoding. + return new JsonDataFetcher({ type: 'json', values: [], assembly: spec.assembly }); } const urlFetchOptions = ('url' in spec.data && urlToFetchOptions?.[spec.data.url]) || {}; diff --git a/demo/renderer/main.ts b/demo/renderer/main.ts index eddba36b2..c2fec6fb7 100644 --- a/demo/renderer/main.ts +++ b/demo/renderer/main.ts @@ -13,6 +13,8 @@ import { HeatmapTrack } from '@gosling-lang/heatmap'; import type { PixiManager } from '@pixi-manager'; import { DummyTrack } from '@gosling-lang/dummy-track'; import type { UrlToFetchOptions } from 'src/compiler/compile'; +import { createSpatialTrack } from '../../src/tracks/spatial-track/spatial-track'; +import type { CsvDataFetcherClass } from 'src/data-fetchers/csv/csv-data-fetcher'; /** * Takes a list of track definitions and linkedEncodings and renders them @@ -109,6 +111,21 @@ export function renderTrackDefs( if (type === TrackType.Dummy) { new DummyTrack(options, pixiManager.makeContainer(boundingBox).overlayDiv); } + // Add a new track type for Chromospace + if (type === TrackType.Spatial) { + // Even though Chromospace doesn't use PixiJS, we can use the PixiManager to create a div container that the canvas can be placed into. + // In the final version, we would probably want Chromospace to use an existing canvas element (to limit the creation of new elements). + // But for now this gets the job done. + const container = pixiManager.makeContainer(boundingBox).overlayDiv; + console.log('!@$!#%@#'); + console.log(options.spec); + if (options.spec.data) { + // Ensure to pull all data needed + options.spec.data.sampleLength = 30000; + } + const datafetcher = getDataFetcher(options.spec, urlToFetchOptions); + createSpatialTrack(options, datafetcher as CsvDataFetcherClass, container); + } }); } diff --git a/demo/track-def/axis.ts b/demo/track-def/axis.ts index 120090704..f45caea75 100644 --- a/demo/track-def/axis.ts +++ b/demo/track-def/axis.ts @@ -104,7 +104,7 @@ function getAxisTrackLinearOptions( startAngle: 0, endAngle: 0, layout: 'linear', - assembly: 'hg38', + assembly: track.assembly ?? 'hg38', stroke: 'transparent', // text outline color: theme.axis.labelColor, labelMargin: theme.axis.labelMargin, @@ -165,7 +165,7 @@ function getAxisTrackCircularOptions( height: boundingBox.height, startAngle, endAngle, - assembly: 'hg38', + assembly: track.assembly, stroke: 'transparent', // text outline color: theme.axis.labelColor, labelMargin: theme.axis.labelMargin, diff --git a/demo/track-def/main.ts b/demo/track-def/main.ts index 90a76f8aa..193e538e3 100644 --- a/demo/track-def/main.ts +++ b/demo/track-def/main.ts @@ -6,6 +6,7 @@ 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 type { SpatialTrackOptions } from 'src/tracks/spatial-track/spatial-track'; import { proccessTextHeader } from './text'; import { processHeatmapTrack, isHeatmapTrack } from './heatmap'; @@ -25,9 +26,11 @@ export enum TrackType { Axis, BrushLinear, BrushCircular, - Heatmap + Heatmap, + Spatial } + /** * Associate options to each track type */ @@ -39,6 +42,7 @@ interface TrackOptionsMap { [TrackType.BrushLinear]: BrushLinearTrackOptions; [TrackType.BrushCircular]: BrushCircularTrackOptions; [TrackType.Heatmap]: HeatmapTrackOptions; + [TrackType.Spatial]: SpatialTrackOptions; //~ TODO: add actual options } /** @@ -83,6 +87,21 @@ export function createTrackDefs(trackInfos: TrackInfo[], theme: Required = { + type: TrackType.Spatial, + trackId: track.id, + boundingBox, + options: { + spec: track, + color: track.color ? track.color.value : undefined, + test: track.test, + data3D: track.data3D, + } + }; + trackDefs.push(trackDef); } else { // We have a gosling track const goslingAxisDefs = processGoslingTrack(track, boundingBox, theme); diff --git a/demo/track-def/types.ts b/demo/track-def/types.ts index 740d99a33..b24c8c611 100644 --- a/demo/track-def/types.ts +++ b/demo/track-def/types.ts @@ -13,7 +13,7 @@ import type { DataDeep, Assembly, DummyTrackStyle, Mark, X, Y } from '@gosling-l */ /** A Track after it has been compiled */ -export type ProcessedTrack = ProcessedLinearTrack | ProcessedCircularTrack | ProcessedDummyTrack; +export type ProcessedTrack = ProcessedLinearTrack | ProcessedCircularTrack | ProcessedDummyTrack | ProcessedSpatialTrack; /** All tracks potentially have these properties */ export interface ProcessedTrackBase { id: string; @@ -49,6 +49,12 @@ export type ProcessedCircularTrack = ProcessedTrackBase & { innerRadius: number; }; +export type ProcessedSpatialTrack = ProcessedTrackBase & { + layout: 'spatial'; + test: string; + data3D: string; +}; + export type ProcessedDummyTrack = ProcessedTrackBase & { type?: string; style?: DummyTrackStyle; diff --git a/docs/spatial-layout.md b/docs/spatial-layout.md new file mode 100644 index 000000000..e1a1881f4 --- /dev/null +++ b/docs/spatial-layout.md @@ -0,0 +1,234 @@ +# 3D chromatin structures in Gosling + +The purpose of this file is to document the changes in the specification and +gosling.js to support visualizing 3D structures of chromatin. It should be +merged in the main documentation at some point. + +Minimal specification using 3D data: + +```javascript +const spec = { + views: [{ + layout: "spatial", + tracks: [ + { + data: { + url: "https://...", + }, + }, + ], + }] +} +``` + +## Features + +### Specifying visual channels +```javascript +{ + /* ... */ + views: [{ + layout: "spatial", + tracks: [ + { + data: { + url: "https://...", + }, + color: { + value: "#ff00ff" + }, + scale: { + value: 0.01 + } + }, + ], + }] +} +``` + +### Composing multiple tracks within a spatial view + +To create more complex visualizations, we can compose several tracks within a +single view. Then each track can have different visual encodings, filtering, +etc., that will be composed (overlaid) within a single view + +```javascript +{ + /* ... */ + views: [{ + layout: "spatial", + tracks: [ + { // track 1 + data: { + url: "https://...", + }, + color: { + value: "#ff00ff" + }, + scale: { + value: 0.01 + } + }, + { // track 2 + data: { + url: "https://...", // TODO: could this be too much repetition (if it's the same dataset)? + }, + color: { + value: "#333333" + }, + scale: { + value: 0.001 + } + }, + ], + }] +} +``` + +### Filtering + +In composing several tracks, it might be helpful to filter certain tracks, +either by genomic coordinates, or based on the data. The question is how much +power should this expression have: can be whole SQL. + +```javascript +{ + /* ... */ + views: [{ + layout: "spatial", + tracks: [ + { // track 1 + data: { + url: "https://...", + }, + color: { + value: "#ff00ff" + }, + scale: { + value: 0.01 + } + filter: "chr1", //~ select only part to show + //filter: "chr1:10000-20000", //~ select only part to show + }, + { // track 2 + data: { + url: "https://...", // TODO: could this be too much repetition (if it's the same dataset)? + }, + color: { + value: "#333333" + }, + scale: { + value: 0.001 + } + }, + ], + }] +} +``` + +### Minimal example without any defaults + +```javascript +const spec = { + views: [{ + layout: "spatial", //~ basically, this should switch it into chromospace rendering + tracks: [ + { + data: { + url: "https://.../model.arrow", + }, + x: { + field: "x", //~ this should look into the .arrow file + type: ???, // will need to lift the requirement for x to only be 'genomic' + }, + y: { + field: "y", + type: ???, + }, + z: { + field: "z", + type: ???, + }, + color: { + field: "chr", + type: ???, + }, + }, + ], + }] +} +``` +Constant color: + +```javascript +const spec = { + views: [{ + layout: "spatial", //~ basically, this should switch it into chromospace rendering + tracks: [ + { + data: { + url: "https://.../model.arrow", + }, + x: { + field: "x", //~ this should look into the .arrow file + type: ???, // will need to lift the requirement for x to only be 'genomic' + }, + y: { + field: "y", + type: ???, + }, + z: { + field: "z", + type: ???, + }, + color: { + value: "red", + }, + }, + ], + }] +} +``` + +Designing with the goal of augmenting existing gosling specs + +```javascript +{ + "title": "Visual Linking", + "subtitle": "Change the position and range of brushes to update the detail view on the bottom", + "arrangement": "vertical", + "centerRadius": 0.4, + "views": [ + { + "layout": "linear", + "xDomain": {"chromosome": "chr1", "interval": [160000000, 200000000]}, + "linkingId": "detail", + "tracks": [ + { + "data": { + "url": "https://server.gosling-lang.org/api/v1/tileset_info/?d=cistrome-multivec", + "type": "multivec", + "row": "sample", + "column": "position", + "value": "peak", + "categories": ["sample 1", "sample 2", "sample 3", "sample 4"] + }, + "mark": "bar", + // "spatial": { + // data: "https;", + // x: "x", + // y: "y", + // z: "z", + // } + "x": {"field": "position", "type": "genomic", "axis": "top"}, + "y": {"field": "peak", "type": "quantitative"}, + "row": {"field": "sample", "type": "nominal"}, + "color": {"field": "sample", "type": "nominal"}, + "width": 690, + "height": 200 + } + ] + } + ] +} +``` diff --git a/editor/Editor.tsx b/editor/Editor.tsx index 8a73ffe6b..d632e8a28 100644 --- a/editor/Editor.tsx +++ b/editor/Editor.tsx @@ -588,10 +588,10 @@ function Editor(props: RouteComponentProps) { typeof goslingSpec?.responsiveSize === 'undefined' ? false : typeof goslingSpec?.responsiveSize === 'boolean' - ? goslingSpec?.responsiveSize === true - : typeof goslingSpec?.responsiveSize === 'object' - ? goslingSpec?.responsiveSize.width === true || goslingSpec?.responsiveSize.height === true - : false; + ? goslingSpec?.responsiveSize === true + : typeof goslingSpec?.responsiveSize === 'object' + ? goslingSpec?.responsiveSize.width === true || goslingSpec?.responsiveSize.height === true + : false; if (newIsResponsive !== isResponsive && newIsResponsive) { setScreenSize(undefined); // reset the screen setVisibleScreenSize(undefined); @@ -738,25 +738,25 @@ function Editor(props: RouteComponentProps) { <>
{ - // if (!gosRef.current) return; - // // ! Be aware that the first view is for the title/subtitle track. So navigation API does not work. - // const id = gosRef.current.api.getViewIds()?.[1]; //'view-1'; - // if(id) { - // gosRef.current.api.zoomToExtent(id); - // } - // - // // Static visualization rendered in canvas - // const { canvas } = gosRef.current.api.getCanvas({ - // resolution: 1, - // transparentBackground: true, - // }); - // const testDiv = document.getElementById('preview-container'); - // if(canvas && testDiv) { - // testDiv.appendChild(canvas); - // } - // }} + // To test APIs, uncomment the following code. + // onClick={() => { + // if (!gosRef.current) return; + // // ! Be aware that the first view is for the title/subtitle track. So navigation API does not work. + // const id = gosRef.current.api.getViewIds()?.[1]; //'view-1'; + // if(id) { + // gosRef.current.api.zoomToExtent(id); + // } + // + // // Static visualization rendered in canvas + // const { canvas } = gosRef.current.api.getCanvas({ + // resolution: 1, + // transparentBackground: true, + // }); + // const testDiv = document.getElementById('preview-container'); + // if(canvas && testDiv) { + // testDiv.appendChild(canvas); + // } + // }} >