{cart[bedId || ''] ? (
@@ -167,12 +173,12 @@ export const Bed2BedSearchResultsTable = (props: Props) => {
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
-
+
if (bedId === undefined) {
toast.error('No bed ID found', { position: 'top-center' });
return;
}
-
+
const bedItem = {
id: bedId,
name: rowData.metadata?.name || 'No name',
@@ -183,7 +189,7 @@ export const Bed2BedSearchResultsTable = (props: Props) => {
description: rowData.metadata?.description || '',
assay: rowData.metadata?.annotation?.assay || 'N/A',
};
-
+
addBedToCart(bedItem);
}}
>
diff --git a/ui/src/components/search/search-bar.tsx b/ui/src/components/search/search-bar.tsx
index 2538b429..ddee1842 100644
--- a/ui/src/components/search/search-bar.tsx
+++ b/ui/src/components/search/search-bar.tsx
@@ -74,14 +74,16 @@ export const SearchBar = (props: Props) => {
{searchView === 't2b' && (
)}
{showOptions && (
diff --git a/ui/src/components/search/search-selector.tsx b/ui/src/components/search/search-selector.tsx
index 41cef61c..64cd1bc5 100644
--- a/ui/src/components/search/search-selector.tsx
+++ b/ui/src/components/search/search-selector.tsx
@@ -13,39 +13,39 @@ export const SearchSelector = (props: Props) => {
const [params, setParams] = useSearchParams();
return (
-
+
+ {
+ params.set('view', 't2b');
+ params.delete('q');
+ setParams(params);
+ setView('t2b');
+ }}
+ >
+ Text-to-BED
+
+ {
+ params.set('view', 'b2b');
+ params.delete('q');
+ setParams(params);
+ setView('b2b');
+ }}
+ >
+ BED-to-BED
+
+ {
+ params.set('view', 't2bs');
+ params.delete('q');
+ setParams(params);
+ setView('t2bs');
+ }}
+ >
+ Text-to-BEDset
+
+
);
diff --git a/ui/src/components/search/text2bed/t2b-search-results-table.tsx b/ui/src/components/search/text2bed/t2b-search-results-table.tsx
index 4bdbfcbc..7da0fac5 100644
--- a/ui/src/components/search/text2bed/t2b-search-results-table.tsx
+++ b/ui/src/components/search/text2bed/t2b-search-results-table.tsx
@@ -1,4 +1,4 @@
-import { ProgressBar } from 'react-bootstrap';
+// import { ProgressBar } from 'react-bootstrap';
import { components } from '../../../../bedbase-types';
import { roundToTwoDecimals } from '../../../utils';
import { useBedCart } from '../../../contexts/bedcart-context';
@@ -117,12 +117,16 @@ export const Text2BedSearchResultsTable = (props: Props) => {
-
+ {
+ // const scoreValue = (result.score ?? 0) * 100;
+ // if (scoreValue >= 80) return 'text-success';
+ // if (scoreValue >= 60) return 'text-warning';
+ // if (scoreValue >= 40) return 'text-info';
+ // return 'text-danger';
+ return 'text-success';
+ })()}`}>
+ {roundToTwoDecimals((result.score ?? 0) * 100)}%
+
|
{cart[result?.metadata?.id || ''] ? (
@@ -149,7 +153,7 @@ export const Text2BedSearchResultsTable = (props: Props) => {
toast.error('No bed ID found', { position: 'top-center' });
return;
}
-
+
// Create the simplified bed item object
const bedItem = {
id: result.metadata.id,
@@ -161,7 +165,7 @@ export const Text2BedSearchResultsTable = (props: Props) => {
description: result.metadata.description || '',
assay: result.metadata.annotation?.assay || 'N/A',
};
-
+
addBedToCart(bedItem);
}}
>
diff --git a/ui/src/components/search/text2bedset.tsx b/ui/src/components/search/text2bedset.tsx
index a0213bce..0fedb944 100644
--- a/ui/src/components/search/text2bedset.tsx
+++ b/ui/src/components/search/text2bedset.tsx
@@ -75,7 +75,8 @@ export const Text2BedSet = () => {
) : (
- Try searching for some BEDsets! e.g. K562 or excluderanges
+ Find collections of BED files by entering a text query above.
+ For example, try searching for "excluderanges" or "a562".
)}
diff --git a/ui/src/components/umap/atlas-tooltip.tsx b/ui/src/components/umap/atlas-tooltip.tsx
new file mode 100644
index 00000000..d0329962
--- /dev/null
+++ b/ui/src/components/umap/atlas-tooltip.tsx
@@ -0,0 +1,93 @@
+import { createRoot, Root } from 'react-dom/client';
+
+interface TooltipProps {
+ tooltip?: {
+ text: string;
+ identifier: string;
+ fields?: {
+ Assay: string;
+ 'Cell Line': string;
+ Description: string;
+ };
+ category?: number;
+ x?: number;
+ y?: number;
+ };
+ showLink?: boolean;
+}
+
+const TooltipContent = ({ tooltip, showLink }: { tooltip: TooltipProps['tooltip']; showLink?: boolean }) => {
+ if (!tooltip) return null;
+ return (
+
+
+ {tooltip.text || 'Unnamed BED'}
+ {tooltip.fields && (
+ <>
+ {tooltip.fields.Description || 'No description available'}
+
+ {tooltip.identifier !== 'custom_point' && (
+ <>
+
+ cell_line: {tooltip.fields['Cell Line'] || 'N/A'}
+
+
+ assay: {tooltip.fields.Assay || 'N/A'}
+
+
+ id: {tooltip.identifier || 'N/A'}
+
+ >
+ )}
+
+ x: {tooltip.x ? tooltip.x.toFixed(6) : 'N/A'}
+
+
+ y: {tooltip.y ? tooltip.y.toFixed(6) : 'N/A'}
+
+
+ >
+ )}
+ {showLink && tooltip.identifier !== 'custom_point' && (
+
+ Go!
+
+ )}
+
+
+ );
+};
+
+export class AtlasTooltip {
+ private root: Root;
+ private showLink: boolean;
+
+ constructor(target: HTMLElement, props: TooltipProps) {
+ // Create a React root and render your component
+ this.root = createRoot(target);
+ this.showLink = props.showLink || false;
+ this.update(props);
+ }
+
+ update(props: TooltipProps) {
+ // Re-render with new props
+ this.root.render();
+ }
+
+ destroy() {
+ // Unmount the React component
+ this.root.unmount();
+ }
+}
diff --git a/ui/src/components/umap/bed-embedding-atlas.tsx b/ui/src/components/umap/bed-embedding-atlas.tsx
new file mode 100644
index 00000000..4e020751
--- /dev/null
+++ b/ui/src/components/umap/bed-embedding-atlas.tsx
@@ -0,0 +1,86 @@
+import { EmbeddingAtlas } from 'embedding-atlas/react';
+import { useEffect, useState } from 'react';
+import { useMosaicCoordinator } from '../../contexts/mosaic-coordinator-context';
+
+type Props = {
+ bedId?: string;
+ neighbors?: any;
+ container?: boolean;
+ width?: string;
+ height?: string;
+};
+
+export const BEDEmbeddingAtlas = (props: Props) => {
+ const { container, width, height } = props;
+
+ const [isReady, setIsReady] = useState(false);
+ const { coordinator, initializeData } = useMosaicCoordinator();
+
+ useEffect(() => {
+ initializeData().then(() => setIsReady(true));
+ }, []);
+
+ return container ? (
+ <>
+ {isReady ? (
+
+ ) : (
+
+ )}
+ >
+ ) : (
+ <>
+ {isReady ? (
+
+
+ console.log(e)}
+ // onExportSelection={async (predicate, format) => {
+ // console.log('Export selection:', predicate, format);
+ // }}
+ />
+
+
+ ) : (
+
+ )}
+ >
+ );
+};
diff --git a/ui/src/components/umap/bed-embedding-plot.tsx b/ui/src/components/umap/bed-embedding-plot.tsx
new file mode 100644
index 00000000..6377a9fe
--- /dev/null
+++ b/ui/src/components/umap/bed-embedding-plot.tsx
@@ -0,0 +1,207 @@
+import { EmbeddingViewMosaic } from 'embedding-atlas/react';
+import { useEffect, useState, useRef, useMemo, forwardRef, useImperativeHandle } from 'react';
+import toast from 'react-hot-toast';
+import * as vg from '@uwdata/vgplot';
+
+import { tableau20 } from '../../utils';
+import { AtlasTooltip } from './atlas-tooltip';
+import { useMosaicCoordinator } from '../../contexts/mosaic-coordinator-context';
+
+type Props = {
+ bedIds?: string[];
+ height?: number;
+ preselectPoint?: boolean;
+ stickyBaseline?: boolean;
+ customCoordinates?: number[] | null;
+ customFilename?: string;
+};
+
+export interface BEDEmbeddingPlotRef {
+ centerOnBedId: (bedId: string, scale?: number) => Promise;
+ handleFileRemove: () => Promise;
+}
+
+export const BEDEmbeddingPlot = forwardRef((props, ref) => {
+ const { bedIds, height, preselectPoint, stickyBaseline, customCoordinates, customFilename } = props;
+ const { coordinator, initializeData, addCustomPoint, deleteCustomPoint, webglStatus } = useMosaicCoordinator();
+
+ const containerRef = useRef(null);
+ const baselinePointsRef = useRef([]);
+
+ const [containerWidth, setContainerWidth] = useState(900);
+ const [isReady, setIsReady] = useState(false);
+ const [selectedPoints, setSelectedPoints] = useState([]);
+ const [tooltipPoint, setTooltipPoint] = useState(null);
+ const [colorGrouping] = useState('cell_line_category');
+ const [viewportState, setViewportState] = useState(null);
+
+ const filter = useMemo(() => vg.Selection.intersect(), []);
+
+ const centerOnPoint = (point: any, scale: number = 1) => {
+ setTooltipPoint(point);
+ setViewportState({
+ x: point.x,
+ y: point.y,
+ scale: scale,
+ });
+ };
+
+ const centerOnBedId = async (bedId: string, scale: number = 1) => {
+ if (!isReady) return;
+
+ const bedData: any = await coordinator.query(
+ `SELECT
+ x, y,
+ ${colorGrouping} as category,
+ name as text,
+ id as identifier,
+ {'Description': description, 'Assay': assay, 'Cell Line': cell_line} as fields
+ FROM data
+ WHERE id = '${bedId}'`,
+ { type: 'json' },
+ );
+
+ if (bedData && bedData.length > 0) {
+ centerOnPoint(bedData[0], scale);
+ setSelectedPoints([bedData[0]]);
+ } else {
+ toast.error('Error: BED file not present in embeddings.');
+ }
+ };
+
+ const handleFileRemove = async () => {
+ try {
+ await deleteCustomPoint();
+ coordinator.clear();
+ } catch (error) {
+ console.error('Error removing file');
+ }
+ };
+
+ useImperativeHandle(ref, () => ({
+ centerOnBedId,
+ handleFileRemove,
+ }));
+
+ useEffect(() => {
+ // initialize data
+ initializeData().then(async () => {
+ if (!!customCoordinates) {
+ await addCustomPoint(customCoordinates[0], customCoordinates[1], customFilename);
+ coordinator.clear();
+ }
+ setIsReady(true);
+ });
+ }, []);
+
+ useEffect(() => {
+ // resize width of view
+ if (containerRef.current) {
+ setContainerWidth(containerRef.current.offsetWidth);
+ }
+ }, [isReady]);
+
+ useEffect(() => {
+ // fetch provided bed ids
+ if (isReady && bedIds && bedIds.length > 0) {
+ setTimeout(async () => {
+ const idsToQuery = customCoordinates ? ['custom_point', ...bedIds] : bedIds;
+ const currentBed: any = await coordinator.query(
+ `SELECT
+ x, y,
+ ${colorGrouping} as category,
+ name as text,
+ id as identifier,
+ {'Description': description, 'Assay': assay, 'Cell Line': cell_line} as fields
+ FROM data
+ WHERE id IN ('${idsToQuery.join("','")}')`,
+ { type: 'json' },
+ );
+ if (!currentBed || currentBed.length === 0) return;
+ if (preselectPoint) {
+ const pointToSelect = customCoordinates
+ ? currentBed.find((bed: any) => bed.identifier === 'custom_point') || currentBed[0]
+ : currentBed[0];
+ if (!!customCoordinates) {
+ centerOnPoint(pointToSelect);
+ } else {
+ setTooltipPoint(pointToSelect);
+ }
+ }
+ baselinePointsRef.current = currentBed;
+ setSelectedPoints(currentBed);
+ }, 200);
+ }
+ }, [isReady, bedIds, coordinator, colorGrouping, customCoordinates]);
+
+ return (
+ <>
+ {webglStatus.error ? (
+
+ {webglStatus.error}
+
+ ) : (
+ <>
+ {isReady ? (
+
+ {
+ if (!dataPoints || (dataPoints.length === 0 && stickyBaseline)) {
+ setTimeout(() => {
+ if (baselinePointsRef.current.length > 0) {
+ setSelectedPoints([...baselinePointsRef.current]);
+ }
+ }, 0);
+ return;
+ }
+ setSelectedPoints(dataPoints);
+ }}
+ theme={{
+ statusBar: false,
+ }}
+ />
+
+ ) : (
+
+ Loading...
+
+ )}
+ >
+ )}
+ >
+ );
+});
diff --git a/ui/src/components/umap/bed-embedding-view.tsx b/ui/src/components/umap/bed-embedding-view.tsx
new file mode 100644
index 00000000..1e5ee3db
--- /dev/null
+++ b/ui/src/components/umap/bed-embedding-view.tsx
@@ -0,0 +1,611 @@
+import { EmbeddingViewMosaic } from 'embedding-atlas/react';
+import { useEffect, useState, useRef, useMemo } from 'react';
+import * as vg from '@uwdata/vgplot';
+
+import { isPointInPolygon, tableau20 } from '../../utils';
+import { useBedCart } from '../../contexts/bedcart-context';
+import { components } from '../../../bedbase-types';
+import { AtlasTooltip } from './atlas-tooltip';
+import { useMosaicCoordinator } from '../../contexts/mosaic-coordinator-context';
+import { useBedUmap } from '../../queries/useBedUmap';
+
+type SearchResponse = components['schemas']['BedListSearchResult'];
+
+type Props = {
+ bedId?: string;
+ neighbors?: SearchResponse;
+ showNeighbors?: boolean;
+ enableUpload?: boolean;
+};
+
+export const BEDEmbeddingView = (props: Props) => {
+ const { bedId, neighbors, showNeighbors, enableUpload } = props;
+ const { coordinator, initializeData, addCustomPoint, deleteCustomPoint, webglStatus } = useMosaicCoordinator();
+ const { addMultipleBedsToCart } = useBedCart();
+ const { mutateAsync: getUmapCoordinates } = useBedUmap();
+
+ const containerRef = useRef(null);
+ const fileInputRef = useRef(null);
+
+ const [containerWidth, setContainerWidth] = useState(900);
+ const [embeddingHeight, setEmbeddingHeight] = useState(500);
+ const [isReady, setIsReady] = useState(false);
+ const [colorGrouping, setColorGrouping] = useState('cell_line_category');
+ const [selectedPoints, setSelectedPoints] = useState([]);
+ const [initialPoint, setInitialPoint] = useState(null);
+ const [viewportState, setViewportState] = useState(null);
+ const [legendItems, setLegendItems] = useState([]);
+ const [filterSelection, setFilterSelection] = useState(null);
+ const [addedToCart, setAddedToCart] = useState(false);
+ const [tooltipPoint, setTooltipPoint] = useState(null);
+ const [uploadedFilename, setUploadedFilename] = useState('');
+ const [dataVersion, setDataVersion] = useState(0);
+ const [pendingSelection, setPendingSelection] = useState(null);
+ const [uploadButtonText, setUploadButtonText] = useState('Upload BED');
+
+ const filter = useMemo(() => vg.Selection.intersect(), []);
+ const legendFilterSource = useMemo(() => ({}), []);
+ const neighborIDs = useMemo(() => neighbors?.results?.map((result) => result.id), [neighbors]);
+
+ const centerOnPoint = (point: any, scale: number = 1) => {
+ setTooltipPoint(point);
+ setViewportState({
+ x: point.x,
+ y: point.y,
+ scale: scale,
+ });
+ };
+
+ const handleFileRemove = async () => {
+ try {
+ await deleteCustomPoint();
+ setUploadedFilename('');
+
+ coordinator.clear();
+ const updatedLegend = await fetchLegendItems(coordinator);
+ setLegendItems(updatedLegend);
+
+ // Prepare selection without custom point
+ const newSelection = selectedPoints.filter((p: any) => p.identifier !== 'custom_point');
+ setPendingSelection(newSelection);
+
+ // Force remount to remove point from map
+ setDataVersion((v) => v + 1);
+ } catch (error) {
+ console.error('Error deleting file');
+ }
+ };
+
+ const handleFileUpload = async (event: React.ChangeEvent) => {
+ const file = event.target.files?.[0];
+ if (!file) return;
+
+ setUploadButtonText('Uploading...');
+
+ try {
+ const coordinates = await getUmapCoordinates(file);
+
+ if (coordinates.length >= 2) {
+ await addCustomPoint(coordinates[0], coordinates[1]);
+ setUploadedFilename(file.name);
+
+ // Clear coordinator cache and refresh legend
+ coordinator.clear();
+ const updatedLegend = await fetchLegendItems(coordinator);
+ setLegendItems(updatedLegend);
+
+ // await new Promise(resolve => setTimeout(resolve, 150));
+
+ // Query the uploaded point
+ const customPoint: any = await coordinator.query(
+ `SELECT
+ x, y,
+ ${colorGrouping} as category,
+ name as text,
+ id as identifier,
+ {'Description': description, 'Assay': assay, 'Cell Line': cell_line} as fields
+ FROM data
+ WHERE id = 'custom_point'`,
+ { type: 'json' },
+ );
+
+ if (customPoint && customPoint.length > 0) {
+ // console.log('Custom point queried:', customPoint[0]);
+
+ // Prepare selection to apply after remount
+ const newSelection = selectedPoints.filter((p: any) => p.identifier !== 'custom_point');
+ newSelection.push(customPoint[0]);
+ setPendingSelection(newSelection);
+
+ // Force remount to show new point
+ setDataVersion((v) => v + 1);
+ }
+ }
+ } catch (error) {
+ console.error('Error getting UMAP coordinates:', error);
+ } finally {
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ setUploadButtonText('Upload BED');
+ }
+ };
+
+ const handleUploadClick = () => {
+ fileInputRef.current?.click();
+ };
+
+ const getSortedSelectedPoints = () => {
+ if (!selectedPoints || selectedPoints.length === 0) return [];
+
+ const currentBed = selectedPoints.find((p: any) => p.identifier === bedId);
+ const others = selectedPoints.filter((p: any) => p.identifier !== bedId);
+
+ if (neighbors?.results && neighbors.results.length > 0) {
+ const scoreMap = new Map(neighbors.results.map((n) => [n.id, n.score]));
+ others.sort((a: any, b: any) => {
+ const scoreA = scoreMap.get(a.identifier) || 0;
+ const scoreB = scoreMap.get(b.identifier) || 0;
+ return scoreB - scoreA; // Descending order
+ });
+ }
+
+ return currentBed ? [currentBed, ...others] : others;
+ };
+
+ const handleLegendClick = (item: any) => {
+ if (filterSelection?.category === item.category) {
+ setFilterSelection(null);
+ filter.update({
+ source: legendFilterSource, // memoized so that mosaic can keep track of source and clear previous selection
+ value: null,
+ predicate: null,
+ });
+ } else {
+ setFilterSelection(item);
+ filter.update({
+ source: legendFilterSource,
+ value: item.category,
+ predicate: vg.eq(colorGrouping, item.category),
+ });
+ }
+ };
+
+ const handlePointSelection = (dataPoints: any[] | null) => {
+ // console.log('Selection changed via onSelection callback:', dataPoints);
+ const points = dataPoints || [];
+ const hasInitialPoint = points.some((p) => p.identifier === initialPoint?.identifier);
+ let finalPoints = hasInitialPoint ? points : initialPoint ? [initialPoint, ...points] : points;
+
+ if (showNeighbors && neighborIDs && neighborIDs.length > 0) {
+ const selectedIds = new Set(finalPoints.map((p: any) => p.identifier));
+ const missingNeighborIds = neighborIDs.filter((id) => !selectedIds.has(id));
+
+ if (missingNeighborIds.length > 0) {
+ coordinator
+ .query(
+ `SELECT
+ x, y,
+ ${colorGrouping} as category,
+ name as text,
+ id as identifier,
+ {'Description': description, 'Assay': assay, 'Cell Line': cell_line} as fields
+ FROM data
+ WHERE id IN (${missingNeighborIds.map((id) => `'${id}'`).join(',')})`,
+ { type: 'json' },
+ )
+ .then((neighborPoints: any) => {
+ setSelectedPoints([...finalPoints, ...neighborPoints]);
+ });
+ return;
+ }
+ }
+
+ // setTooltipPoint(finalPoints.slice(-1)[0])
+ setSelectedPoints(finalPoints);
+ };
+
+ // Range selection (for rectangle/lasso)
+ const handleRangeSelection = async (coordinator: any, value: any) => {
+ // console.log('Range selection updated:', value);
+
+ if (!value) {
+ return;
+ }
+
+ let result;
+
+ // filter clause prevents selecting points that are not within a selected legend category
+ const filterClause = filterSelection ? ` AND ${colorGrouping} = '${filterSelection.category}'` : '';
+
+ // Check if rectangle selection (bounding box)
+ if (typeof value === 'object' && 'xMin' in value) {
+ result = (await coordinator.query(
+ `SELECT
+ x, y,
+ ${colorGrouping} as category,
+ name as text,
+ id as identifier,
+ {'Description': description, 'Assay': assay, 'Cell Line': cell_line} as fields
+ FROM data
+ WHERE x >= ${value.xMin} AND x <= ${value.xMax} AND y >= ${value.yMin} AND y <= ${value.yMax}${filterClause}`,
+ { type: 'json' },
+ )) as any[];
+ }
+ // Check if lasso selection (array of points)
+ else if (Array.isArray(value) && value.length > 0) {
+ // First get points within bounding box (optimization)
+ const xCoords = value.map((p: any) => p.x);
+ const yCoords = value.map((p: any) => p.y);
+ const xMin = Math.min(...xCoords);
+ const xMax = Math.max(...xCoords);
+ const yMin = Math.min(...yCoords);
+ const yMax = Math.max(...yCoords);
+
+ // Only fetch x, y, identifier for filtering, then get full data for matches
+ const candidates: any = await coordinator.query(
+ `SELECT x, y, id as identifier FROM data
+ WHERE x >= ${xMin} AND x <= ${xMax} AND y >= ${yMin} AND y <= ${yMax}${filterClause}`,
+ { type: 'json' },
+ );
+
+ // Filter to points inside polygon
+ const filteredIds = candidates
+ .filter((point: any) => isPointInPolygon(point, value))
+ .map((p: any) => `'${p.identifier}'`)
+ .join(',');
+
+ if (filteredIds) {
+ result = (await coordinator.query(
+ `SELECT
+ x, y,
+ ${colorGrouping} as category,
+ name as text,
+ id as identifier,
+ {'Description': description, 'Assay': assay, 'Cell Line': cell_line} as fields
+ FROM data
+ WHERE id IN (${filteredIds})${filterClause}`,
+ { type: 'json' },
+ )) as any[];
+ } else {
+ result = [];
+ }
+ }
+
+ const resultArray = result || [];
+ const hasInitialPoint =
+ resultArray.length > 0 && resultArray.some((p: any) => p.identifier === initialPoint?.identifier);
+ let finalPoints = hasInitialPoint ? resultArray : initialPoint ? [initialPoint, ...resultArray] : resultArray;
+
+ if (showNeighbors && neighborIDs && neighborIDs.length > 0) {
+ const selectedIds = new Set(finalPoints.map((p: any) => p.identifier));
+ const missingNeighborIds = neighborIDs.filter((id) => !selectedIds.has(id));
+
+ if (missingNeighborIds.length > 0) {
+ // fetch missing neighbor points
+ const neighborPoints = (await coordinator.query(
+ `SELECT
+ x, y,
+ ${colorGrouping} as category,
+ name as text,
+ id as identifier,
+ {'Description': description, 'Assay': assay, 'Cell Line': cell_line} as fields
+ FROM data
+ WHERE id IN (${missingNeighborIds.map((id) => `'${id}'`).join(',')})`,
+ { type: 'json' },
+ )) as any[];
+ finalPoints = [...finalPoints, ...neighborPoints];
+ }
+ }
+
+ setSelectedPoints(finalPoints);
+ };
+
+ const fetchLegendItems = async (coordinator: any) => {
+ const query = `SELECT DISTINCT
+ ${colorGrouping.replace('_category', '')} as name,
+ ${colorGrouping} as category
+ FROM data
+ ORDER BY ${colorGrouping}`;
+
+ const result = (await coordinator.query(query, { type: 'json' })) as any[];
+ return result;
+ };
+
+ useEffect(() => {
+ // initialize data
+ initializeData().then(() => {
+ setIsReady(true);
+ });
+ }, []);
+
+ useEffect(() => {
+ // resize width and height of view based on window size
+ const updateDimensions = () => {
+ if (containerRef.current) {
+ setContainerWidth(containerRef.current.offsetWidth);
+ }
+ // Calculate height: window height minus approximate offset for header/footer/margins
+ const calculatedHeight = Math.max(400, window.innerHeight * 0.6);
+ setEmbeddingHeight(calculatedHeight);
+ };
+
+ updateDimensions();
+ window.addEventListener('resize', updateDimensions);
+
+ return () => window.removeEventListener('resize', updateDimensions);
+ }, [isReady]);
+
+ useEffect(() => {
+ // set legend items
+ if (isReady) {
+ fetchLegendItems(coordinator).then((result) => {
+ setLegendItems(result);
+ });
+ }
+ }, [isReady, colorGrouping]);
+
+ useEffect(() => {
+ // apply pending selection after dataVersion change
+ if (pendingSelection !== null) {
+ setTimeout(() => {
+ setSelectedPoints(pendingSelection);
+ // If there's an uploaded file, center on it and set tooltip
+ if (uploadedFilename) {
+ const uploadedPoint = pendingSelection.find((point: any) => point.identifier === 'custom_point');
+ if (uploadedPoint) {
+ centerOnPoint(uploadedPoint, 0.3);
+ }
+ }
+ setPendingSelection(null);
+ }, 200);
+ }
+ }, [dataVersion, pendingSelection]);
+
+ useEffect(() => {
+ // fetch initial bed id and neighbors
+ if (isReady && !!bedId) {
+ setTimeout(async () => {
+ const currentBed: any = await coordinator.query(
+ `SELECT
+ x, y,
+ ${colorGrouping} as category,
+ name as text,
+ id as identifier,
+ {'Description': description, 'Assay': assay, 'Cell Line': cell_line} as fields
+ FROM data
+ WHERE id = '${bedId}'`,
+ { type: 'json' },
+ );
+ if (!currentBed || currentBed.length === 0) return;
+ setInitialPoint(currentBed[0]);
+ setTooltipPoint(currentBed[0]);
+
+ if (showNeighbors && neighborIDs && neighborIDs.length > 0) {
+ const neighborPoints: any = await coordinator.query(
+ `SELECT
+ x, y,
+ ${colorGrouping} as category,
+ name as text,
+ id as identifier,
+ {'Description': description, 'Assay': assay, 'Cell Line': cell_line} as fields
+ FROM data
+ WHERE id IN (${neighborIDs.map((id) => `'${id}'`).join(',')})`,
+ { type: 'json' },
+ );
+ setSelectedPoints([currentBed[0], ...neighborPoints]);
+ } else if (showNeighbors && enableUpload) {
+ setSelectedPoints([currentBed[0]]);
+ } else {
+ setSelectedPoints([currentBed[0]]);
+ }
+ }, 200);
+ }
+ }, [isReady, bedId, coordinator, colorGrouping, showNeighbors, neighborIDs]);
+
+ return (
+ <>
+
+ {enableUpload && (
+
+ )}
+
+ {isReady ? (
+
+
+
+
+ Region Embeddings
+
+
+ {!!uploadedFilename && (
+
+ {uploadedFilename}
+
+
+ )}
+
+
+
+ {webglStatus.error ? (
+
+ {webglStatus.error}
+
+ ) : (
+ handleRangeSelection(coordinator, e)}
+ />
+ )}
+
+
+
+
+
+
+
+
+ | BED Name |
+ Assay |
+ Cell Line |
+ Description |
+
+
+
+ {getSortedSelectedPoints().map((point: any, index: number) => (
+ centerOnPoint(point, 0.3)}
+ key={point.identifier + '_' + index}
+ >
+ | {point.text} |
+ {point.fields.Assay} |
+ {point.fields['Cell Line']} |
+ {point.fields.Description} |
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+ {legendItems?.map((item: any) => (
+ handleLegendClick(item)}
+ key={item.category}
+ >
+ |
+
+
+ {item.name}
+
+ {filterSelection?.category === item.category && (
+
+ )}
+ |
+
+ ))}
+
+
+
+
+
+
+ ) : (
+
+ )}
+ >
+ );
+};
diff --git a/ui/src/const.ts b/ui/src/const.ts
index 93e20b4c..418a036b 100644
--- a/ui/src/const.ts
+++ b/ui/src/const.ts
@@ -4,6 +4,8 @@ const API_BASE = import.meta.env.VITE_API_BASE || '';
const EXAMPLE_URL = `${API_BASE}/bed/example`;
+export const UMAP_URL = 'https://huggingface.co/databio/bedbase-umap/resolve/main/hg38_umap.json';
+
export const BEDBASE_PYTHON_CODE_MD = `
\`\`\`python
import requests
@@ -127,6 +129,18 @@ print(bed_granges)
\`\`\`
`;
+export const CLIENT_RUST_CODE_RAW = `
+\`\`\`python
+use gtars::bbclient::BBClient;
+
+let mut bbc = BBClient::new(Some(cache_folder.clone()), None).expect("Failed to create BBClient");
+
+let bed_id: String = bbc
+ .add_local_bed_to_cache(PathBuf::from(_path/to.bed.gz), None)
+ .unwrap();
+\`\`\`
+`;
+
export const BBCONF_SNIPPETS = [
{
@@ -139,4 +153,9 @@ export const BBCONF_SNIPPETS = [
code: CLIENT_R_CODE_RAW,
raw: CLIENT_R_CODE_RAW,
},
+ {
+ language: 'Rust',
+ code: CLIENT_RUST_CODE_RAW,
+ raw: CLIENT_RUST_CODE_RAW,
+ },
];
\ No newline at end of file
diff --git a/ui/src/contexts/mosaic-coordinator-context.tsx b/ui/src/contexts/mosaic-coordinator-context.tsx
new file mode 100644
index 00000000..2cce6343
--- /dev/null
+++ b/ui/src/contexts/mosaic-coordinator-context.tsx
@@ -0,0 +1,167 @@
+import { createContext, useContext, useMemo, useRef, ReactNode, useState, useEffect } from 'react';
+import * as vg from '@uwdata/vgplot';
+import { UMAP_URL } from '../const.ts'
+
+interface MosaicCoordinatorContextType {
+ getCoordinator: () => vg.Coordinator;
+ initializeData: () => Promise;
+ addCustomPoint: (x: number, y: number, description?: string) => Promise;
+ deleteCustomPoint: () => Promise;
+ webglStatus: { checking: boolean; webgl2: boolean; error: string | null };
+}
+
+const MosaicCoordinatorContext = createContext(null);
+
+export const MosaicCoordinatorProvider = ({ children }: { children: ReactNode }) => {
+ const coordinatorRef = useRef(null);
+ const dataInitializedRef = useRef(false);
+ const [webglStatus, setWebglStatus] = useState<{ checking: boolean; webgl2: boolean; error: string | null }>({
+ checking: true,
+ webgl2: false,
+ error: null,
+ });
+
+ const getCoordinator = () => {
+ if (!coordinatorRef.current) {
+ coordinatorRef.current = new vg.Coordinator(vg.wasmConnector());
+ }
+ return coordinatorRef.current;
+ };
+
+ const initializeData = async () => {
+ if (dataInitializedRef.current) {
+ return; // Already initialized
+ }
+
+ const coordinator = getCoordinator();
+
+
+ await coordinator.exec([
+ vg.sql`CREATE OR REPLACE TABLE data AS
+ SELECT
+ unnest(nodes, recursive := true)
+ FROM read_json_auto('${UMAP_URL}')`,
+ vg.sql`CREATE OR REPLACE TABLE data AS
+ SELECT
+ *,
+ (DENSE_RANK() OVER (ORDER BY assay) - 1)::INTEGER AS assay_category,
+ (DENSE_RANK() OVER (ORDER BY cell_line) - 1)::INTEGER AS cell_line_category
+ FROM data` as any,
+ ]);
+
+ dataInitializedRef.current = true;
+ };
+
+ const deleteCustomPoint = async () => {
+ const coordinator = getCoordinator();
+
+ await coordinator.exec([vg.sql`DELETE FROM data WHERE id = 'custom_point'` as any]);
+ };
+
+ const addCustomPoint = async (x: number, y: number, description: string = 'User uploaded BED file') => {
+ const coordinator = getCoordinator();
+
+ await coordinator.exec([vg.sql`DELETE FROM data WHERE id = 'custom_point'` as any]);
+
+ // Get max category indices for uploaded points (after deletion to ensure clean state)
+ const maxCategories = (await coordinator.query(
+ `SELECT
+ MAX(assay_category) as max_assay_category,
+ MAX(cell_line_category) as max_cell_line_category
+ FROM data`,
+ { type: 'json' },
+ )) as any[];
+
+ const assayCategory = (maxCategories[0]?.max_assay_category ?? -1) + 1;
+ const cellLineCategory = (maxCategories[0]?.max_cell_line_category ?? -1) + 1;
+
+ await coordinator.exec([
+ vg.sql`INSERT INTO data VALUES (
+ ${x},
+ ${y},
+ 0,
+ 'custom_point',
+ 'Your uploaded file',
+ '${description}',
+ 'Uploaded BED',
+ 'Uploaded BED',
+ ${assayCategory},
+ ${cellLineCategory}
+ )` as any,
+ ]);
+ };
+
+ useEffect(() => {
+ const checkGraphicsSupport = async () => {
+ let webgpuAvailable = false;
+ let webgl2Available = false;
+
+ // Check WebGPU
+ if ('gpu' in navigator) {
+ try {
+ const adapter = await (navigator as any).gpu.requestAdapter();
+ webgpuAvailable = !!adapter;
+ } catch (error) {
+ // console.error('WebGPU check failed:', error);
+ webgpuAvailable = false;
+ }
+ }
+
+ // Check WebGL2
+ const canvas = document.createElement('canvas');
+ const gl = canvas.getContext('webgl2');
+ webgl2Available = !!gl;
+
+ if (gl) {
+ gl.getExtension('WEBGL_lose_context')?.loseContext();
+ }
+
+ // Force WebGL by hiding WebGPU if not available
+ if (!webgpuAvailable && webgl2Available) {
+ // console.log('WebGPU not available, forcing WebGL2 fallback');
+ if ('gpu' in navigator) {
+ Object.defineProperty(navigator, 'gpu', {
+ get: () => undefined,
+ configurable: true,
+ });
+ }
+ }
+
+ if (!webgpuAvailable && !webgl2Available) {
+ setWebglStatus({
+ checking: false,
+ webgl2: false,
+ error: 'WebGL2 is unavailable. Please enable it or use a different browser to use the Embedding Atlas.',
+ });
+ } else {
+ setWebglStatus({
+ checking: false,
+ webgl2: webgl2Available,
+ error: null,
+ });
+ }
+ };
+
+ checkGraphicsSupport();
+ }, []);
+
+ const value = useMemo(
+ () => ({ getCoordinator, initializeData, addCustomPoint, deleteCustomPoint, webglStatus }),
+ [webglStatus],
+ );
+ return {children};
+};
+
+export const useMosaicCoordinator = () => {
+ const context = useContext(MosaicCoordinatorContext);
+ if (!context) {
+ throw new Error('useMosaicCoordinator must be used within MosaicCoordinatorProvider');
+ }
+ return {
+ coordinator: context.getCoordinator(),
+ initializeData: context.initializeData,
+ addCustomPoint: context.addCustomPoint,
+ deleteCustomPoint: context.deleteCustomPoint,
+ webglStatus: context.webglStatus,
+ };
+};
diff --git a/ui/src/custom.scss b/ui/src/custom.scss
index 4c40686a..3bfa979f 100644
--- a/ui/src/custom.scss
+++ b/ui/src/custom.scss
@@ -1,16 +1,6 @@
-@import 'bootstrap/scss/functions';
-@import 'bootstrap/scss/variables';
-
-$primary: #008080;
-$secondary: #f97316;
-
-// merge with existing $theme-colors map
-$theme-colors: map-merge(
- $theme-colors,
- (
- 'primary': $primary,
- 'secondary': $secondary,
- )
+// Import Bootstrap with @use instead of @import
+@use 'bootstrap/scss/bootstrap' as * with (
+ $primary: #008080 // $secondary: #f97316,
);
// override btn-outline-primary hover bg-color
@@ -22,8 +12,9 @@ $theme-colors: map-merge(
}
}
-// set changes
-@import 'bootstrap';
+body {
+ background-color: var(--bs-body-tertiary-bg) !important;
+}
a {
color: $primary;
@@ -98,15 +89,15 @@ a {
}
.bed-splash-genomic-feature-bar-height {
- height: 27rem;
+ height: 26.375rem;
}
.bedset-splash-stat-card-height {
- min-height: 7rem;
+ min-height: 9rem;
}
.bedset-splash-genomic-feature-bar-height {
- height: 24rem;
+ height: 24.875rem;
}
.min-h-screen {
@@ -149,7 +140,9 @@ a {
white-space: nowrap; /* Keeps the content on a single line */
margin: 0 auto; /* Gives that scrolling effect as the typing happens */
letter-spacing: 0.15em; /* Adjust as needed */
- animation: typing 3.5s steps(40, end), blink-caret 0.75s step-end infinite;
+ animation:
+ typing 3.5s steps(40, end),
+ blink-caret 0.75s step-end infinite;
}
.truncate {
@@ -317,6 +310,31 @@ td {
outline: 1px solid $primary !important;
}
+.btn-group-xs > .btn {
+ padding: 0.125rem 0.6rem;
+ font-size: 0.7rem;
+ line-height: 1.2;
+ border-radius: 0.25rem;
+}
+
+.btn-card {
+ transition:
+ color 0.15s ease-in-out,
+ background-color 0.15s ease-in-out,
+ border-color 0.15s ease-in-out,
+ box-shadow 0.15s ease-in-out;
+}
+
+.table-transparent {
+ background-color: transparent; /* Makes the table background completely transparent */
+ --bs-table-bg: transparent;
+}
+
+.table-transparent tbody tr {
+ background-color: transparent;
+ --bs-table-bg: transparent;
+}
+
.transparent-btn {
//pointer-events: none;
cursor: default;
@@ -340,5 +358,17 @@ td {
}
.metric-plot-height {
- height: 450px;
-}
\ No newline at end of file
+ height: auto;
+}
+
+.btn-xs {
+ padding: 0.125rem 0.6rem !important;
+ font-size: 0.7rem !important;
+ line-height: 1.2 !important;
+ border-radius: 0.25rem !important;
+}
+
+.btn-xs > .bi {
+ font-size: 0.7rem;
+ vertical-align: -0.125em;
+}
diff --git a/ui/src/main.tsx b/ui/src/main.tsx
index df411949..5ebc938e 100644
--- a/ui/src/main.tsx
+++ b/ui/src/main.tsx
@@ -8,8 +8,11 @@ import { BedCartProvider } from './contexts/bedcart-context.tsx';
import toast, { Toaster } from 'react-hot-toast';
import { Home } from './pages/home.tsx';
import { Metrics } from './pages/metrics.tsx';
-import { UMAPGraph } from './pages/visualization.tsx';
+import { BEDUmap } from './pages/bed-umap.tsx';
+import { BEDAnalytics } from './pages/bed-analytics.tsx';
+import init from '@databio/gtars';
import { HelmetProvider } from 'react-helmet-async';
+import { MosaicCoordinatorProvider } from './contexts/mosaic-coordinator-context.tsx';
// css stuff
import 'bootstrap/dist/css/bootstrap.min.css';
@@ -31,17 +34,20 @@ const queryClient = new QueryClient({
},
},
queryCache: new QueryCache({
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
onError: (error: any) => {
if (error.response && error.response.status === 413) {
toast.error(`${error.response.data.detail}`);
- return;}
+ return;
+ }
if (error.response && error.response.status === 415) {
toast.error(`${error.response.data.detail}`);
- return;}
+ return;
+ }
//
// console.error(error);
// toast.error(`Something went wrong: ${error.message}`);
- }
+ },
}),
});
@@ -77,10 +83,17 @@ const router = createBrowserRouter([
},
{
path: '/umap',
- element: ,
+ element: ,
+ },
+ {
+ path: '/analyze',
+ element: ,
},
]);
+// initialize gtars:
+init();
+
// entry point
ReactDOM.createRoot(document.getElementById('root')!).render(
@@ -88,8 +101,10 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
-
-
+
+
+
+
diff --git a/ui/src/motions/landing-animations.tsx b/ui/src/motions/landing-animations.tsx
index b714e2f7..6397e4fa 100644
--- a/ui/src/motions/landing-animations.tsx
+++ b/ui/src/motions/landing-animations.tsx
@@ -1,14 +1,14 @@
import { motion } from 'framer-motion';
import { PRIMARY_COLOR } from '../const';
-const STROKE_WIDTH = 2;
+const STROKE_WIDTH = 2.5;
const STROKE_SPEAD = 0;
export const InPaths = () => {
return (
{
- {
y2="50%"
strokeWidth={STROKE_WIDTH}
animate={{
- strokeDashoffset: [100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 0, -10, -20],
- }}
- strokeDasharray="6,6"
- transition={{
- duration: STROKE_SPEAD,
- repeat: Infinity,
- repeatType: 'loop',
- ease: 'linear',
- }}
- stroke="#000"
- />
- {
repeatType: 'loop',
ease: 'linear',
}}
+ stroke={PRIMARY_COLOR}
/>
{
{
{
+ const [rs, setRs] = useState(null);
+ const [loadingRS, setLoadingRS] = useState(false);
+ const [totalProcessingTime, setTotalProcessingTime] = useState(null);
+ const [selectedFile, setSelectedFile] = useState(null);
+ const [inputMode, setInputMode] = useState<'file' | 'url'>('file');
+ const [bedUrl, setBedUrl] = useState('');
+ const [triggerSearch, setTriggerSearch] = useState(0);
+ const [searchParams] = useSearchParams();
+ const navigate = useNavigate();
+
+ const fileInputRef = useRef(null);
+
+ useEffect(() => {
+ const urlParam = searchParams.get('bedUrl');
+ if (urlParam) {
+ setInputMode('url');
+ setBedUrl(urlParam);
+ }
+ }, [searchParams]);
+
+ useEffect(() => {
+ initializeRegionSet();
+ }, [triggerSearch]);
+
+ const fetchBedFromUrl = async (url: string): Promise => {
+ // console.log(`${url[0]}, ${url[1]}, ${url}`);
+ const fetchUrl =
+ url.length === 32 && !url.startsWith('http')
+ ? `https://api.bedbase.org/v1/files/files/${url[0]}/${url[1]}/${url}.bed.gz`
+ : url;
+ // console.log(`${fetchUrl}`);
+ const response = await fetch(fetchUrl);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch BED file: ${response.statusText}`);
+ }
+ const blob = await response.blob();
+ const fileName = fetchUrl.split('/').pop() || 'remote-bed-file.bed';
+ return new File([blob], fileName, { type: 'text/plain' });
+ };
+
+ const initializeRegionSet = async () => {
+ let fileToProcess: File | null = null;
+
+ if (inputMode === 'file' && selectedFile) {
+ fileToProcess = selectedFile;
+ } else if (inputMode === 'url' && bedUrl.trim()) {
+ try {
+ fileToProcess = await fetchBedFromUrl(bedUrl.trim());
+ } catch (error) {
+ console.error('Error fetching URL:', error);
+ return;
+ }
+ }
+
+ if (fileToProcess) {
+ setLoadingRS(true);
+ setTotalProcessingTime(null);
+
+ try {
+ const startTime = performance.now();
+
+ const syntheticEvent = {
+ target: { files: [fileToProcess] },
+ } as unknown as Event;
+
+ await handleBedFileInput(syntheticEvent, (entries) => {
+ setTimeout(() => {
+ const rs = new RegionSet(entries);
+ const endTime = performance.now();
+ const totalTimeMs = endTime - startTime;
+
+ setRs(rs);
+ setTotalProcessingTime(totalTimeMs);
+ setLoadingRS(false);
+ }, 10);
+ });
+ } catch (error) {
+ setLoadingRS(false);
+ console.error('Error loading file:', error);
+ }
+ }
+ };
+
+ const unloadFile = () => {
+ setRs(null);
+ setTotalProcessingTime(null);
+ setSelectedFile(null);
+ setBedUrl('');
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ };
+
+ const handleOnKeyDown = (e: React.KeyboardEvent): void => {
+ if (e.key === 'Enter') {
+ if (inputMode === 'url') {
+ const params = new URLSearchParams(searchParams);
+ params.set('bedUrl', bedUrl);
+ navigate(`?${params.toString()}`);
+ }
+ setTriggerSearch(triggerSearch + 1);
+ }
+ };
+
+ return (
+
+ BED analyzer
+
+
+
+
+
+
+
+ {loadingRS && (
+
+
+ Loading...
+
+
+ Loading and analyzing...
+
+
+ )}
+
+ {rs && !loadingRS && (
+
+ )}
+
+
+ {rs && (
+
+
+
+
+
+ | Identifier |
+ {rs.identifier} |
+
+
+ | Mean region width |
+ {rs.meanRegionWidth} |
+
+
+ | Total number of regions |
+ {rs.numberOfRegions} |
+
+
+ | Total number of nucleotides |
+ {rs.nucleotidesLength} |
+
+
+
+
+
+ Interval chromosome length statistics
+ {rs && (
+
+ )}
+
+
+ {rs && (
+ //
+ // Region Distribution Data
+ //
+ // {JSON.stringify(rs.calculateRegionDistribution(300), null, 2)}
+ //
+ //
+
+
+
+ )}
+
+
+ )}
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/ui/src/pages/bed-umap.tsx b/ui/src/pages/bed-umap.tsx
new file mode 100644
index 00000000..645ec58e
--- /dev/null
+++ b/ui/src/pages/bed-umap.tsx
@@ -0,0 +1,18 @@
+import { useSearchParams } from 'react-router-dom';
+import { Layout } from '../components/layout';
+// import { BEDEmbeddingAtlas } from '../components/umap/bed-embedding-atlas';
+import { BEDEmbeddingView } from '../components/umap/bed-embedding-view';
+
+export const BEDUmap: React.FC = () => {
+ const [searchParams] = useSearchParams();
+
+ const bedId = searchParams.get('searchId');
+ // console.log(bedId);
+
+ return (
+
+ {/* */}
+
+
+ );
+};
diff --git a/ui/src/pages/home.tsx b/ui/src/pages/home.tsx
index de80c4b6..6767d614 100644
--- a/ui/src/pages/home.tsx
+++ b/ui/src/pages/home.tsx
@@ -8,25 +8,37 @@ import rehypeHighlight from 'rehype-highlight';
import { CODE_SNIPPETS } from '../const';
import { BBCONF_SNIPPETS } from '../const';
// import { useExampleBed } from '../queries/useExampleBed';
+import toast from 'react-hot-toast';
import { useExampleBedSet } from '../queries/useExampleBedSet';
import { useStats } from '../queries/useStats.ts';
+import { motion } from 'framer-motion';
-type FileBadgeProps = {
- children?: React.ReactNode;
-};
+// type FileBadgeProps = {
+// children?: React.ReactNode;
+// };
-const FileBadge = (props: FileBadgeProps) => {
- const { children } = props;
- return {children} ;
-};
+// const FileBadge = (props: FileBadgeProps) => {
+// const { children } = props;
+// return {children} ;
+// };
export const Home = () => {
const [searchTerm, setSearchTerm] = useState('');
const [searchTermSmall, setSearchTermSmall] = useState('Kidney cancer in humans');
const [copied, setCopied] = useState(false);
+ const [searchType, setSearchType] = useState('t2b');
const navigate = useNavigate();
+ const handleSearch = () => {
+ if (!searchTerm) {
+ toast.error('Please enter a search term.');
+ return;
+ }
+ navigate(`/search?q=${searchTerm}&view=${searchType}`);
+ };
+
+
// const { data: exampleBedMetadata } = useExampleBed(); # if example will be dynamic again
const { data: exampleBedSetMetadata } = useExampleBedSet();
const { data: bedbaseStats } = useStats();
@@ -49,132 +61,165 @@ export const Home = () => {
{/* */}
{/* */}
{/**/}
- Welcome to BEDbase
-
+ BEDbase
+ Find, analyze, and understand genomic region data - all in one
+ place
+
+
-
- BEDbase is a unified platform for aggregating, analyzing, and serving genomic region data. BEDbase redefines
- the way to manage genomic region data and allows users to search for BED files of interest and create
- collections tailored to research needs. BEDbase is composed of a web server and an API. Users can explore
+
+ BEDbase is a unified platform for searching, analyzing, visualizing and serving genomic region data.
+ BEDbase redefines the way to manage genomic region data and allows users to search for BED files of
+ interest, visualize them, and create
+ collections tailored to research needs. Users can explore
comprehensive descriptions of specific BED files via a user-oriented web interface and programmatically
interact with the data via an OpenAPI-compatible API.
-
- setSearchTerm(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === 'Enter') {
- if (searchTerm.length === 0) {
- return;
- }
- navigate(`/search?q=${searchTerm}`);
- }
- }}
- />
- |