Skip to content

Commit

Permalink
feat(beacon): add assembly-wise chromosome selector
Browse files Browse the repository at this point in the history
TODO: translation, testing, network
  • Loading branch information
davidlougheed committed Nov 21, 2024
1 parent d8e693b commit 4155e34
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 16 deletions.
20 changes: 17 additions & 3 deletions src/js/components/Beacon/BeaconCommon/VariantInput.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import { Form, Input } from 'antd';
import { Form, Input, Select } from 'antd';
import type { DefaultOptionType } from 'antd/es/select/index';
import { useTranslationFn } from '@/hooks';
import type { FormField } from '@/types/beacon';

const VariantInput = ({ field, disabled }: VariantInputProps) => {
type InputMode = { type: 'input' } | { type: 'select'; options?: DefaultOptionType[] };

const VariantInput = ({ field, disabled, mode }: VariantInputProps) => {
const t = useTranslationFn();
return (
<>
<Form.Item name={field.name} label={t(field.name)} rules={field.rules}>
<Input placeholder={field.placeholder} disabled={disabled} />
{!mode || mode.type === 'input' ? (
<Input placeholder={field.placeholder} disabled={disabled} />
) : (
<Select
placeholder={field.placeholder}
disabled={disabled}
options={mode.options}
showSearch={true}
optionFilterProp="value"
/>
)}
</Form.Item>
</>
);
Expand All @@ -16,6 +29,7 @@ const VariantInput = ({ field, disabled }: VariantInputProps) => {
export interface VariantInputProps {
field: FormField;
disabled: boolean;
mode?: InputMode;
}

export default VariantInput;
90 changes: 77 additions & 13 deletions src/js/components/Beacon/BeaconCommon/VariantsForm.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,60 @@
import type { CSSProperties } from 'react';
import { Col, Row } from 'antd';
import VariantInput from './VariantInput';
import AssemblyIdSelect from './AssemblyIdSelect';
import { type CSSProperties, useEffect, useMemo } from 'react';

import { Col, Form, Row } from 'antd';
import type { DefaultOptionType } from 'antd/es/select/index';

import { useTranslationFn } from '@/hooks';
import { useReference } from '@/features/reference/hooks';
import type { Contig } from '@/features/reference/types';
import type { BeaconAssemblyIds } from '@/types/beacon';

import VariantInput from './VariantInput';
import AssemblyIdSelect from './AssemblyIdSelect';

type ContigOptionType = DefaultOptionType & { value: string };

// form state has to be one of these:
// empty (except for autofilled assemblyID)
// chrom, start, assemblyID, end
// chrom, start, assemblyID, ref, alt

// forgiving chromosome regex
// accepts X, Y, etc. and any one- or two-digit non-zero number
// note that, eg, polar bears have 37 pairs of chromosomes...
const CHROMOSOME_REGEX = /^([1-9][0-9]?|X|x|Y|y|M|m|MT|mt)$/;

const NUCLEOTIDES_REGEX = /^([acgtnACGTN])*$/;
const DIGITS_REGEX = /^[0-9]+$/;
const DIGITS_REGEX = /^\d+$/;

const HUMAN_LIKE_CONTIG_REGEX = /^(?:chr)?(\d+|X|Y|M)$/;
const HUMAN_LIKE_EXCLUDE_CONTIG_REGEX = /^(?:chr)?(\d+|X|Y|M|Un)_.+$/;

const contigToOption = (c: Contig): ContigOptionType => ({ value: c.name });

const contigOptionSort = (a: ContigOptionType, b: ContigOptionType) => {
const aMatch = a.value.match(HUMAN_LIKE_CONTIG_REGEX);
const bMatch = b.value.match(HUMAN_LIKE_CONTIG_REGEX);
if (aMatch) {
if (bMatch) {
const aNoPrefix = aMatch[1];
const bNoPrefix = bMatch[1];
const aNumeric = !!aNoPrefix.match(DIGITS_REGEX);
const bNumeric = !!bNoPrefix.match(DIGITS_REGEX);
if (aNumeric) {
if (bNumeric) {
return parseInt(aNoPrefix, 10) - parseInt(bNoPrefix, 10);
} else {
return -1;
}
} else if (bNumeric) {
return 1;
} else {
return aNoPrefix.localeCompare(bNoPrefix);
}
} else {
// chr## type contigs put before other types
return -1;
}
}
return a.value.localeCompare(b.value);
};

const filterOutHumanLikeExtraContigs = (opt: ContigOptionType) => !opt.value.match(HUMAN_LIKE_EXCLUDE_CONTIG_REGEX);

const FORM_STYLE: CSSProperties = {
display: 'flex',
Expand All @@ -26,12 +64,34 @@ const FORM_STYLE: CSSProperties = {
const FORM_ROW_GUTTER: [number, number] = [12, 0];

const VariantsForm = ({ beaconAssemblyIds }: VariantsFormProps) => {
const { genomesByID } = useReference();

// Pick up form context from outside
const form = Form.useFormInstance();
const currentAssemblyID = Form.useWatch('Assembly ID', form);

const availableContigs = useMemo<ContigOptionType[]>(
() =>
currentAssemblyID && genomesByID[currentAssemblyID]
? genomesByID[currentAssemblyID].contigs
.map(contigToOption)
.sort(contigOptionSort)
.filter(filterOutHumanLikeExtraContigs)
: [],
[currentAssemblyID, genomesByID]
);

useEffect(() => {
// Clear contig value when list of available contigs changes:
form.setFieldValue('Chromosome', '');
}, [form, availableContigs]);

const t = useTranslationFn();
const formFields = {
referenceName: {
name: 'Chromosome',
rules: [{ pattern: CHROMOSOME_REGEX, message: t('Enter a chromosome name, e.g.: "17" or "X"') }],
placeholder: '1-22, X, Y, M',
rules: [{ message: t('Select a chromosome') }],
placeholder: '',
initialValue: '',
},
start: {
Expand Down Expand Up @@ -67,7 +127,11 @@ const VariantsForm = ({ beaconAssemblyIds }: VariantsFormProps) => {
<div style={FORM_STYLE}>
<Row gutter={FORM_ROW_GUTTER}>
<Col span={8}>
<VariantInput field={formFields.referenceName} disabled={variantsError} />
<VariantInput
field={formFields.referenceName}
disabled={variantsError || !currentAssemblyID}
mode={currentAssemblyID ? { type: 'select', options: availableContigs } : { type: 'input' }}
/>
</Col>
<Col span={8}>
<VariantInput field={formFields.start} disabled={variantsError} />
Expand Down
5 changes: 5 additions & 0 deletions src/js/features/reference/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { useEffect, useState } from 'react';
import { useAuthorizationHeader } from 'bento-auth-js';
import { referenceGenomesUrl } from '@/constants/configConstants';
import { useAppSelector } from '@/hooks';
import { RequestStatus } from '@/types/requests';
import type { GenomeFeature } from './types';

export const useReference = () => {
return useAppSelector((state) => state.reference);
};

export const useGeneNameSearch = (referenceGenomeID: string | undefined, nameQuery: string | null | undefined) => {
const authHeader = useAuthorizationHeader();

Expand Down

0 comments on commit 4155e34

Please sign in to comment.