From b244ef4a55b7c52671c189a071984aad575a8ba1 Mon Sep 17 00:00:00 2001 From: sehilyi Date: Wed, 17 Jan 2024 17:26:59 -0500 Subject: [PATCH] wip: working on headers --- editor/Editor.tsx | 2 +- editor/example/json-spec/index.ts | 2 +- src/core/gosling-component.tsx | 2 +- .../gosling-component-with-header.css | 28 ++++ .../gosling-component-with-header.tsx | 132 ++++++++++++++++++ src/gosling-component-with-header/index.ts | 1 + src/index.ts | 1 + 7 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 src/gosling-component-with-header/gosling-component-with-header.css create mode 100644 src/gosling-component-with-header/gosling-component-with-header.tsx create mode 100644 src/gosling-component-with-header/index.ts diff --git a/editor/Editor.tsx b/editor/Editor.tsx index 8ce1de99..cc853a6a 100644 --- a/editor/Editor.tsx +++ b/editor/Editor.tsx @@ -1211,7 +1211,7 @@ function Editor(props: RouteComponentProps) { background: isResponsive ? 'white' : 'none' }} > - void +) => void; interface GoslingCompProps { spec?: gosling.GoslingSpec; diff --git a/src/gosling-component-with-header/gosling-component-with-header.css b/src/gosling-component-with-header/gosling-component-with-header.css new file mode 100644 index 00000000..2246c2f3 --- /dev/null +++ b/src/gosling-component-with-header/gosling-component-with-header.css @@ -0,0 +1,28 @@ +#gosling-component-header { + padding: 3px; +} + +#gosling-component-header > select, +#gosling-component-header > button, +#gosling-component-header > input { + margin-right: 4px; +} + +.sel-btn-activated, +.sel-btn-activated:hover { + background-color: orange !important; +} + +#gosling-component-header > select { + min-width: 100px; +} + +#gosling-component-header > button { + cursor: pointer; + border: 1px solid black; + border-radius: 4px; +} + +#gosling-component-header > button:hover { + background-color: #ddd; +} \ No newline at end of file diff --git a/src/gosling-component-with-header/gosling-component-with-header.tsx b/src/gosling-component-with-header/gosling-component-with-header.tsx new file mode 100644 index 00000000..f82282cc --- /dev/null +++ b/src/gosling-component-with-header/gosling-component-with-header.tsx @@ -0,0 +1,132 @@ +import React, { createContext, useContext, useRef, type ComponentPropsWithRef, useLayoutEffect, useState, forwardRef, useMemo, useEffect } from 'react'; +import { GoslingComponent, type GoslingRef } from '../core/gosling-component'; +import type { TrackApiData } from '@gosling-lang/gosling-schema' +import './gosling-component-with-header.css'; + +const ZOOM_DURATION = 300; + +type goslingCompoProps = ComponentPropsWithRef; +const GoslingContext = createContext({}); + +type GoslingComponentWithHeaderProps = goslingCompoProps & { + // ... anything to add? +}; + +// forwardRef((props, ref) => { + // (props: GoslingComponentWithHeaderProps) { +export const GoslingComponentWithHeader = forwardRef((props) => { + const ref = useRef(null); + + const [selection, setSelection] = useState(false); + const [tracks, setTracks] = useState([]); + const [selectedTrackIdx, setSelectedTrackIdx] = useState(0); + + useEffect(() => { + document.addEventListener('keydown', (e) => { + switch(e.key) { + case 'ArrowDown': + setSelectedTrackIdx((selectedTrackIdx + 1) % tracks.length); + event?.preventDefault(); + break; + case 'ArrowUp': + setSelectedTrackIdx((selectedTrackIdx - 1) % tracks.length); + event?.preventDefault(); + break; + } + }); + return document.removeEventListener('keydown', () => {}); + }, []); + + type NavigationType = '<' | '>' | '+' | '-' | string; + const navigationFn = (type: NavigationType) => { + const trackIds = ref.current?.api.getTrackIds(); + + if(!trackIds || trackIds.length === 0) { + return; + } + + const trackId = trackIds[selectedTrackIdx ?? 1]; + + let [start, end] = ref.current?.hgApi.api.getLocation(trackId).xDomain; + const delta = (end - start) / 3.0; + if(type === '+') { + start += delta; + end -= delta; + } else if(type === '-') { + start -= delta; + end += delta; + } else if(type === '<') { + start -= delta; + end -= delta; + } else if(type === '>') { + start += delta; + end += delta; + } else { + if(type.includes('chr')) { + // chr1:100-200 + ref?.current?.api.zoomTo(trackId, type, 0, ZOOM_DURATION); + } else { + // MYC + ref?.current?.api.suggestGene(trackId, type, (genes) => { + if(genes.length > 0) { + ref?.current?.api.zoomToGene(trackId, genes[0].geneName, 0, ZOOM_DURATION); + } + }); + } + } + ref?.current?.api.zoomTo(trackId, `chr1:${start}-${end}`, 0, ZOOM_DURATION); + }; + + const Header = useMemo(() => { + // const { ref } = useContext(GoslingContext); + return ( +
+ {'Experimental! '} + + + { + if(e.key === 'Enter') navigationFn(e.currentTarget.value); + }} + > + {['<', '>', '+', '-'].map((d: NavigationType) => )} + + +
+ ); + }, [tracks, selection]); + + const Selection = useMemo(() => { + if(!selection || !tracks[selectedTrackIdx]) return; + return ( +
+ ); + }, [selection, selectedTrackIdx, tracks]); + + return ( + + {/* XXX: Handle the sizing of Gosling Component */} + {Header} + { + // XXX: Update of track info is one React rendering cycle behind + // Update Track information for API calls from
+ const _tracks = ref.current?.api.getTracks() ?? []; + setTracks(_tracks.filter(d => d.spec.mark !== 'header')); + console.error('compiled'); + // Call this function for the user-defined callback + props.compiled?.(...args); + }}/> + {Selection} + + ); +}); diff --git a/src/gosling-component-with-header/index.ts b/src/gosling-component-with-header/index.ts new file mode 100644 index 00000000..7f153f2e --- /dev/null +++ b/src/gosling-component-with-header/index.ts @@ -0,0 +1 @@ +export { GoslingComponentWithHeader } from './gosling-component-with-header'; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 7a73b029..e608a73b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,5 +12,6 @@ export { init } from './core/init'; export { compile } from './compiler/compile'; export { validateGoslingSpec } from '@gosling-lang/gosling-schema'; export { GoslingComponent } from './core/gosling-component'; +export { GoslingComponentWithHeader } from './gosling-component-with-header'; export type { GoslingRef } from './core/gosling-component'; export { embed } from './core/gosling-embed';