Skip to content

Commit

Permalink
Allow selection of a region detail in regulatory activity viewer (#1182)
Browse files Browse the repository at this point in the history
- By clicking on and dragging across the region overview, it is possible to select a subregion
   that shows up in the region details panel below
- The cursor over the svg changes its shape to suggest that a subregion can be selected
- Features (including their fragments) that are outside the selected subregion are greyed out
   in the RegionOverview panel. The greyed-out features are not interactive, and do not respond to mouse hover
   or to clicks
- RegionOverview shows gene extents (dashed lines to either side of a transcript if the transcript is shorter
   than the gene itself)
- Allow display of multiple transcription start sites
- Show a popup ('zmenu"), upon a click on a gene in region overview
- Add inert areas outside the selection in region overview, so that if you click or mouse over a feature
   outside the selected area in the region overview, nothing happens
  • Loading branch information
azangru authored Nov 3, 2024
1 parent b220b8f commit cf1865a
Show file tree
Hide file tree
Showing 24 changed files with 1,192 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

import noop from 'lodash/noop';

import { useAppSelector } from 'src/store';

import { getActiveGenomeId } from 'src/content/app/regulatory-activity-viewer/state/general/generalSelectors';

import { StandardAppLayout } from 'src/shared/components/layout';
import RegionOverview from './components/region-overview/RegionOverview';
import RegionActivitySection from './components/region-activity-section/RegionActivitySection';
Expand All @@ -36,12 +40,19 @@ const ActivityViewer = () => {
};

const MainContent = () => {
const activeGenomeId = useAppSelector(getActiveGenomeId);

if (!activeGenomeId) {
// this will be an interstitial in the future
return null;
}

return (
<div>
Hello activity viewer
<RegionOverview />
<RegionOverview activeGenomeId={activeGenomeId} />
<div style={{ margin: '3rem 0' }} />
<RegionActivitySection />
<RegionActivitySection activeGenomeId={activeGenomeId} />
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.regularRow span + span {
margin-left: 1ch;
}

.strand {
margin-right: 2.5ch;
}

.light {
font-weight: var(--font-weight-light);
}

.strong {
font-weight: var(--font-weight-bold);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { useState, type ReactNode } from 'react';
import classNames from 'classnames';

import PointerBox, {
Position
} from 'src/shared/components/pointer-box/PointerBox';

import pointerBoxStyles from 'src/shared/components/pointer-box/PointerBox.module.css';
import toolboxStyles from 'src/shared/components/toolbox/Toolbox.module.css';

/**
* This component is similar to the "Zmenu" component of the genome browser.
* Since graphics in the regulatory activity viewer tend to be svgs,
* the component adds a tiny rect element as an anchor for the popup.
*/

type Props = {
x: number;
y: number;
children: ReactNode;
onClose: () => void;
};

const ActivityViewerPopup = (props: Props) => {
const { x, y, children, onClose } = props;
const [anchorElement, setAnchorElement] = useState<SVGRectElement | null>(
null
);

const pointerBoxClasses = classNames(
toolboxStyles.toolbox,
pointerBoxStyles.pointerBoxShadow
);

return (
<>
<rect
ref={setAnchorElement}
x={x}
y={y}
width={0}
height={0}
fill="transparent"
/>
{anchorElement && (
<PointerBox
position={Position.RIGHT_BOTTOM}
anchor={anchorElement}
onOutsideClick={onClose}
className={pointerBoxClasses}
>
{children}
</PointerBox>
)}
</>
);
};

export default ActivityViewerPopup;
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { getStrandDisplayName } from 'src/shared/helpers/formatters/strandFormatter';
import { getFormattedLocation } from 'src/shared/helpers/formatters/regionFormatter';

import TextButton from 'src/shared/components/text-button/TextButton';

import { Strand } from 'src/shared/types/core-api/strand';
import type { GeneInRegionOverview } from 'src/content/app/regulatory-activity-viewer/types/regionOverview';

import styles from './AcrivityViewerPopup.module.css';

type GeneField =
| 'stable_id'
| 'symbol'
| 'unversioned_stable_id'
| 'biotype'
| 'strand'
| 'start'
| 'end';

type Props = {
gene: Pick<GeneInRegionOverview, GeneField> & { region_name: string };
onFocus: () => void;
};

const GenePopupContent = (props: Props) => {
const { gene } = props;

const geneSymbolAndIdentifier = gene.symbol ? (
<>
<span>{gene.symbol}</span> <span>{gene.stable_id}</span>
</>
) : (
<span>{gene.stable_id}</span>
);

return (
<div>
<div className={styles.regularRow}>
<span className={styles.light}>Gene </span>
{geneSymbolAndIdentifier}
</div>
<div className={styles.regularRow}>
<span className={styles.light}>Biotype </span>
<span>{gene.biotype}</span>
</div>
<div className={styles.light}>
<span className={styles.strand}>
{/* TODO: Change Strand enum into a union type */}
{getStrandDisplayName(gene.strand as Strand)}{' '}
</span>
<span>
{getFormattedLocation({
chromosome: gene.region_name,
start: gene.start,
end: gene.end
})}
</span>
</div>
<div>
<TextButton onClick={props.onFocus}>Make focus</TextButton>
</div>
</div>
);
};

export default GenePopupContent;
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@
import { useState, useEffect, useRef, useMemo } from 'react';
import classNames from 'classnames';

import { useAppSelector } from 'src/store';

import prepareFeatureTracks from 'src/content/app/regulatory-activity-viewer/helpers/prepare-feature-tracks/prepareFeatureTracks';

import { getRegionDetailSelectedLocation } from 'src/content/app/regulatory-activity-viewer/state/region-detail/regionDetaillSelectors';

import { useRegionOverviewQuery } from 'src/content/app/regulatory-activity-viewer/state/api/activityViewerApiSlice';

import RegionActivitySectionImage from './RegionActivitySectionImage';
Expand All @@ -27,19 +31,18 @@ import RegionActivitySectionImage from './RegionActivitySectionImage';
import regionOverviewStyles from '../region-overview/RegionOverview.module.css';
import styles from './RegionActivitySection.module.css';

/**
* TODO: the name of this component should probably change.
* It will contain the "magnified region image", but also
* the actual activity heatmap.
*
* TODO: This component will need to know the start and the end locations
* of the magnified segment. It will receive those either from the parent,
* or (more likely) from redux
*/
type Props = {
activeGenomeId: string;
};

const RegionActivitySection = () => {
const RegionActivitySection = (props: Props) => {
const { activeGenomeId } = props;
// TODO: think about how best to handle width changes; maybe they should come from the parent
const [width, setWidth] = useState(0);
const regionDetailLocation = useAppSelector((state) =>
getRegionDetailSelectedLocation(state, activeGenomeId)
);

const { currentData } = useRegionOverviewQuery();

const imageContainerRef = useRef<HTMLDivElement>(null);
Expand All @@ -61,24 +64,21 @@ const RegionActivitySection = () => {
// let's consider just a single contiguous slice without "boring" intervals
const location = currentData.locations[0];

// TODO: below are hard-coded start and end of the selected segment of the region.
// When the selection element is implemented, the selected start and end will come from user selection, probably via redux
// REMEMBER to add selectionStart and selectionEnd to the list of dependencies of useMemo, when they start coming from user selection
const locationLength = location.end - location.start + 1;
const selectedStart = location.start + Math.round(0.2 * locationLength);
const selectedEnd = location.start + Math.round(0.4 * locationLength);
const selectedStart = regionDetailLocation?.start ?? location.start;
const selectedEnd = regionDetailLocation?.end ?? location.end;

const featureTracks = prepareFeatureTracks({
data: currentData,
start: selectedStart,
end: selectedEnd
});

return {
featureTracks,
start: selectedStart,
end: selectedEnd
};
}, [currentData]);
}, [currentData, regionDetailLocation]);

const componentClasses = classNames(
styles.section,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ import { scaleLinear } from 'd3';

import RegionDetailImage from './region-detail-image/RegionDetailImage';

import {
GENE_TRACKS_TOP_OFFSET,
GENE_TRACK_HEIGHT,
REGULATORY_FEATURE_TRACKS_TOP_OFFSET,
REGULATORY_FEATURE_TRACK_HEIGHT
} from './region-detail-image/regionDetailConstants';

import type { FeatureTracks } from 'src/content/app/regulatory-activity-viewer/helpers/prepare-feature-tracks/prepareFeatureTracks';
import type { OverviewRegion } from 'src/content/app/regulatory-activity-viewer/types/regionOverview';

Expand All @@ -43,8 +50,15 @@ const RegionActivitySectionImage = (props: Props) => {
.domain([start, end])
.rangeRound([0, Math.floor(width)]);

// FIXME: height should be calculated from data (the number of tracks)
const height = 500;
const height =
GENE_TRACKS_TOP_OFFSET +
(featureTracks.geneTracks.forwardStrandTracks.length +
featureTracks.geneTracks.reverseStrandTracks.length) *
GENE_TRACK_HEIGHT +
REGULATORY_FEATURE_TRACKS_TOP_OFFSET +
featureTracks.regulatoryFeatureTracks.length *
REGULATORY_FEATURE_TRACK_HEIGHT +
50;

return (
<svg
Expand Down
Loading

0 comments on commit cf1865a

Please sign in to comment.