diff --git a/ui/bedbase-types.d.ts b/ui/bedbase-types.d.ts index bece0579..6c275770 100644 --- a/ui/bedbase-types.d.ts +++ b/ui/bedbase-types.d.ts @@ -66,7 +66,7 @@ export interface paths { cookie?: never; }; /** - * Get summary statistics for the DRS object store + * Get summary statistics for BEDbase platform * @description Returns statistics */ get: operations["get_bedbase_db_stats_v1_stats_get"]; @@ -78,6 +78,26 @@ export interface paths { patch?: never; trace?: never; }; + "/v1/detailed-stats": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get detailed statistics for BEDbase platform, including number of files for each genome + * @description Returns detailed statistics + */ + get: operations["get_detailed_stats_v1_detailed_stats_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v1/genomes": { parameters: { query?: never; @@ -118,6 +138,23 @@ export interface paths { patch?: never; trace?: never; }; + "/v1/files/{file_path}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Redirect To Download */ + get: operations["redirect_to_download_v1_files__file_path__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v1/bed/example": { parameters: { query?: never; @@ -971,7 +1008,7 @@ export interface components { * @description Library source (e.g. genomic, transcriptomic) * @default */ - library_source: string; + assay: string; /** * Genotype * @description Genotype of the sample @@ -1066,7 +1103,7 @@ export interface components { * @description Library source (e.g. genomic, transcriptomic) * @default */ - library_source: string; + assay: string; /** * Genotype * @description Genotype of the sample @@ -1313,6 +1350,21 @@ export interface components { /** Access Methods */ access_methods?: components["schemas"]["AccessMethod"][]; }; + /** FileStats */ + FileStats: { + /** File Type */ + file_type: { + [key: string]: number; + }; + /** File Format */ + file_format: { + [key: string]: number; + }; + /** File Genome */ + file_genome: { + [key: string]: number; + }; + }; /** HTTPValidationError */ HTTPValidationError: { /** Detail */ @@ -1397,7 +1449,7 @@ export interface components { * @description Name of species. e.g. Homo sapiens. * @default */ - species_name: string; + organism: string; /** * Species Id * @default @@ -1443,13 +1495,13 @@ export interface components { * @description Library source (e.g. genomic, transcriptomic) * @default */ - library_source: string; + assay: string; /** * Exp Protocol * @description Experimental protocol (e.g. ChIP-seq) * @default */ - assay: string; + exp_protocol: string; /** * Antibody * @description Antibody used in the assay @@ -1633,6 +1685,26 @@ export interface operations { }; }; }; + get_detailed_stats_v1_detailed_stats_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FileStats"]; + }; + }; + }; + }; get_bedbase_db_stats_v1_genomes_get: { parameters: { query?: never; @@ -1673,6 +1745,37 @@ export interface operations { }; }; }; + redirect_to_download_v1_files__file_path__get: { + parameters: { + query?: never; + header?: never; + path: { + file_path: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; get_example_bed_record_v1_bed_example_get: { parameters: { query?: never; @@ -2100,7 +2203,7 @@ export interface operations { text_to_bed_search_v1_bed_search_text_post: { parameters: { query: { - query: unknown; + query: string; limit?: number; offset?: number; }; diff --git a/ui/package-lock.json b/ui/package-lock.json index 88a9059b..703a86c8 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -17,6 +17,7 @@ "bootstrap-icons": "^1.11.3", "chart.js": "^4.4.2", "chartjs-chart-error-bars": "^4.4.0", + "chartjs-plugin-datalabels": "^2.2.0", "framer-motion": "^11.0.20", "js-yaml": "^4.1.0", "react": "^18.2.0", @@ -2102,6 +2103,15 @@ "chart.js": "^4.1.0" } }, + "node_modules/chartjs-plugin-datalabels": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-2.2.0.tgz", + "integrity": "sha512-14ZU30lH7n89oq+A4bWaJPnAG8a7ZTk7dKf48YAzMvJjQtjrgg5Dpk9f+LbjCF6bpx3RAGTeL13IXpKQYyRvlw==", + "license": "MIT", + "peerDependencies": { + "chart.js": ">=3.0.0" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -2191,9 +2201,9 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -3958,12 +3968,12 @@ ] }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -4010,9 +4020,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, "funding": [ { @@ -5121,9 +5131,9 @@ } }, "node_modules/vite": { - "version": "5.4.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.9.tgz", - "integrity": "sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg==", + "version": "5.4.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", + "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", "dev": true, "dependencies": { "esbuild": "^0.21.3", diff --git a/ui/package.json b/ui/package.json index 79afb5d9..503e3d5c 100644 --- a/ui/package.json +++ b/ui/package.json @@ -21,6 +21,7 @@ "bootstrap-icons": "^1.11.3", "chart.js": "^4.4.2", "chartjs-chart-error-bars": "^4.4.0", + "chartjs-plugin-datalabels": "^2.2.0", "framer-motion": "^11.0.20", "js-yaml": "^4.1.0", "react": "^18.2.0", diff --git a/ui/src/components/bed-splash-components/charts/genomic-feature-bar.tsx b/ui/src/components/bed-splash-components/charts/genomic-feature-bar.tsx index 33ac2caf..59342828 100644 --- a/ui/src/components/bed-splash-components/charts/genomic-feature-bar.tsx +++ b/ui/src/components/bed-splash-components/charts/genomic-feature-bar.tsx @@ -20,6 +20,9 @@ const chartOptions = { legend: { position: 'top' as const, }, + datalabels: { + display: false, + }, }, }; @@ -95,7 +98,7 @@ export const GenomicFeatureBar = (props: Props) => { } return ( -
+

Genomic Features

diff --git a/ui/src/components/bed-splash-components/header.tsx b/ui/src/components/bed-splash-components/header.tsx index 9e8015cd..2d2aedf2 100644 --- a/ui/src/components/bed-splash-components/header.tsx +++ b/ui/src/components/bed-splash-components/header.tsx @@ -54,7 +54,7 @@ export const BedSplashHeader = (props: Props) => {
@@ -194,7 +194,6 @@ export const BedSplashHeader = (props: Props) => { className="badge bg-primary border-start border-light rounded-start-0" role="button" onClick={() => { - // conditional required to prevent double click if (showRefGenomeModal !== true) { setShowRefGenomeModal(true); } diff --git a/ui/src/components/bed-splash-components/refgenome-modal.tsx b/ui/src/components/bed-splash-components/refgenome-modal.tsx index a57f8ee9..633428dc 100644 --- a/ui/src/components/bed-splash-components/refgenome-modal.tsx +++ b/ui/src/components/bed-splash-components/refgenome-modal.tsx @@ -24,12 +24,12 @@ export const RefGenomeModal = (props: Props) => { Reference Genome Compatibility

- Note: Below is a ranking of the compatibility various reference genomes to this BED file (rank 1 is best). + Note: Below is a ranking of the compatibility various reference genomes to this BED file (tier 1 is best). The ranking is based on the following metrics:

  • XS (eXtra Sequences): the proportion of shared regions in both this BED file and reference genome over the total number of regions in this BED file [recall].
  • -
  • OOBR (Out Of Bounds Regions): The proportion of shared regions from this BED file that do not exceed the bounds of the corresponding shared region in the reference genome.
  • +
  • OOBR (Out Of Bounds Regions): The proportion of shared regions from this BED file that do not exceed the bounds of the corresponding shared region in the reference genome. OOBR is only calculated if XS is 100%.
  • SF (Sequence Fit): the proportion of shared region lengths in both this BED file and reference genome over the total number of region lengths in the reference genome [precision].
@@ -40,7 +40,7 @@ export const RefGenomeModal = (props: Props) => {

XS

OOBR

SF

-

Rank

+

Tier

@@ -60,27 +60,60 @@ export const RefGenomeModal = (props: Props) => {

{genome.compared_genome}

- 30 ? 'text-white' : 'text-dark'}`}> - {((genome.xs || 0) * 100).toFixed(2) + '%'} - -
+ { genome.xs ? + <> + 30 ? 'text-white' : 'text-dark'}`}> + {((genome.xs || 0) * 100).toFixed(2) + '%'} + +
+ + : + <> + + N/A + +
+ + }
- 30 ? 'text-white' : 'text-dark'}`}> - {((genome.oobr || 0) * 100).toFixed(2) + '%'} - -
+ { genome.oobr ? + <> + 30 ? 'text-white' : 'text-dark'}`}> + {((genome.oobr || 0) * 100).toFixed(2) + '%'} + +
+ + : + <> + + N/A + +
+ + }
- 30 ? 'text-white' : 'text-dark'}`}> - {((genome.sequence_fit || 0) * 100).toFixed(2) + '%'} - -
+ { genome.sequence_fit ? + <> + 30 ? 'text-white' : 'text-dark'}`}> + {((genome.sequence_fit || 0) * 100).toFixed(2) + '%'} + +
+ + : + <> + + N/A + +
+ + }
-

Rank {genome.tier_ranking}

+

Tier {genome.tier_ranking}

diff --git a/ui/src/components/bedset-splash-components/charts/genomic-feature-bar.tsx b/ui/src/components/bedset-splash-components/charts/genomic-feature-bar.tsx index 3c75491b..f55d33af 100644 --- a/ui/src/components/bedset-splash-components/charts/genomic-feature-bar.tsx +++ b/ui/src/components/bedset-splash-components/charts/genomic-feature-bar.tsx @@ -29,6 +29,9 @@ const chartOptions = { legend: { position: 'top' as const, }, + datalabels: { + display: false, + }, }, }; @@ -185,7 +188,7 @@ export const GenomicFeatureBar = (props: Props) => { } return ( -
+

Genomic Features

diff --git a/ui/src/components/metrics/metric-plot.tsx b/ui/src/components/metrics/metric-plot.tsx new file mode 100644 index 00000000..88884720 --- /dev/null +++ b/ui/src/components/metrics/metric-plot.tsx @@ -0,0 +1,124 @@ +import { Bar, Pie } from 'react-chartjs-2'; +import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, ArcElement } from 'chart.js'; +import ChartDataLabels from 'chartjs-plugin-datalabels'; + +ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + ArcElement, + Title, + Tooltip, + Legend, + ChartDataLabels +); + +export type MetricPlotType = 'bar' | 'pie'; + +type Props = { + type: MetricPlotType; + data: [string, number][]; + dataLabel?: string; + backgroundColor: string[]; + borderWidth: number; + sliceIndex: number; + plotRef?: any; +}; + +export const MetricPlot = (props: Props) => { + const { type, data, dataLabel, backgroundColor, borderWidth, sliceIndex, plotRef } = props; + + const sortedData = data.sort((a, b) => b[1] - a[1]).slice(0, sliceIndex); + const labels = sortedData.map(entry => entry[0]); + const values = sortedData.map(entry => entry[1]); + + // Function to generate a color palette with the same length as the data + const generateColorPalette = (dataLength: number): string[] => { + + const baseColors = [ + 'rgba(255, 99, 132, 0.6)', // red + 'rgba(54, 162, 235, 0.6)', // blue + 'rgba(255, 206, 86, 0.6)', // yellow + 'rgba(51,193,43,0.6)', // teal + 'rgba(153, 102, 255, 0.6)', // purple + 'rgba(255, 159, 64, 0.6)', // orange + 'rgba(199, 199, 199, 0.6)', // gray + 'rgba(83, 102, 255, 0.6)', // indigo + 'rgba(255, 99, 255, 0.6)', // pink + 'rgb(6,80,49)', // light green + ]; + + let colorPalette = []; + + // If we need more colors than in our base palette, + // we'll cycle through with different opacities + const cycles = Math.ceil((dataLength - colorPalette.length) / baseColors.length); + + for (let cycle = 0; cycle < cycles; cycle++) { + // For each cycle, adjust opacity slightly + const opacity = 0.6 - (cycle * 0.1); + + for (let i = 0; i < baseColors.length; i++) { + if (colorPalette.length >= dataLength) break; + + // Create a new color with adjusted opacity + const baseColor = baseColors[i]; + const rgbPart = baseColor.substring(0, baseColor.lastIndexOf(',')); + const newColor = `${rgbPart}, ${opacity})`; + + colorPalette.push(newColor); + } + } + + return colorPalette; + }; + + const ensureColorPalette = (data: [string, number][]): string[] => { + return generateColorPalette(data.length); + }; + + const plotData = { + labels: labels, + datasets: [ + { + label: dataLabel, + data: values, + backgroundColor: type === 'bar' ? backgroundColor : ensureColorPalette(data), + borderWidth: borderWidth, + }, + ], + }; + + const plotOptions = { + responsive: true, + maintainAspectRatio: true, + plugins: { + datalabels: { + font: { + size: 10, + }, + align: 'end' as const, + offset: 2, + }, + }, + }; + + if (type === 'bar') { + return ( + + ); + } else if (type === 'pie') { + return ( + + )} + + return null; +}; diff --git a/ui/src/components/modals/endpoints-modal.tsx b/ui/src/components/modals/endpoints-modal.tsx new file mode 100644 index 00000000..931a8657 --- /dev/null +++ b/ui/src/components/modals/endpoints-modal.tsx @@ -0,0 +1,47 @@ +import { Modal } from 'react-bootstrap'; + +type Props = { + titles: string[]; + endpoints: string[], + show: boolean; + onHide: () => void; +}; + +export const EndpointsModal = (props: Props) => { + const { titles, endpoints, show, onHide } = props; + + return ( + onHide()} + size='lg' + aria-labelledby='contained-modal-title-vcenter' + centered + > + API Endpoints + +
+ + + + + + + + + {titles.map((title, index) => ( + + + + + ))} + +
ServiceEndpoint
{title} + {endpoints[index]} +
+
+
+
+ ); +}; diff --git a/ui/src/components/modals/metric-modal.tsx b/ui/src/components/modals/metric-modal.tsx new file mode 100644 index 00000000..66dd7173 --- /dev/null +++ b/ui/src/components/modals/metric-modal.tsx @@ -0,0 +1,84 @@ +import { useState } from 'react'; +import { Modal } from 'react-bootstrap'; +import { MetricPlot, MetricPlotType } from '../metrics/metric-plot'; +import { useRef } from 'react'; + +type Props = { + title: string; + type: MetricPlotType, + data: [string, number][]; + dataLabel?: string | undefined; + backgroundColor: string[]; + borderWidth: number; + sliceIndex: number; + show: boolean; + onHide: () => void; +}; + +export const MetricModal = (props: Props) => { + const { title, type, data, dataLabel, backgroundColor, borderWidth, sliceIndex, show, onHide } = props; + const plotRef = useRef<{ toBase64Image: () => string } | null>(null); + + const [plotType, setPlotType] = useState(type); + + const checkHandler = (value: MetricPlotType) => { + setPlotType(value); + }; + + const handleDownload = () => { + if (plotRef.current) { + const link = document.createElement('a'); + link.download = (props.title ? props.title : 'metric_plot') + '.png'; + link.href = plotRef.current.toBase64Image(); + link.click(); + } + }; + + return ( + onHide()} + size='lg' + aria-labelledby='contained-modal-title-vcenter' + centered + > + {title} + +
+ +
+ +
+ + + + + +
+ ); +}; diff --git a/ui/src/components/nav/nav-desktop.tsx b/ui/src/components/nav/nav-desktop.tsx index e47f2281..0e711bca 100644 --- a/ui/src/components/nav/nav-desktop.tsx +++ b/ui/src/components/nav/nav-desktop.tsx @@ -2,7 +2,6 @@ import { useBedCart } from '../../contexts/bedcart-context'; const API_BASE = import.meta.env.VITE_API_BASE || ''; - export const NavDesktop = () => { const { cart } = useBedCart(); return ( diff --git a/ui/src/components/nav/nav-mobile.tsx b/ui/src/components/nav/nav-mobile.tsx index 4248f17b..1ed31734 100644 --- a/ui/src/components/nav/nav-mobile.tsx +++ b/ui/src/components/nav/nav-mobile.tsx @@ -19,6 +19,10 @@ export const MobileNav = () => { Docs + + + Metrics + Cart diff --git a/ui/src/custom.scss b/ui/src/custom.scss index d5c409e1..014f0cdd 100644 --- a/ui/src/custom.scss +++ b/ui/src/custom.scss @@ -313,6 +313,6 @@ td { } .genome-card:hover { - border: 1px solid $primary; - filter: brightness(98%); + border: 1px solid $primary !important; + outline: 1px solid $primary !important; } diff --git a/ui/src/main.tsx b/ui/src/main.tsx index eefdbabe..b0ee442e 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -7,6 +7,7 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { BedCartProvider } from './contexts/bedcart-context.tsx'; import { Toaster } from 'react-hot-toast'; import { Home } from './pages/home.tsx'; +import { Metrics } from './pages/metrics.tsx'; import { HelmetProvider } from 'react-helmet-async'; // css stuff @@ -52,6 +53,10 @@ const router = createBrowserRouter([ path: '/search', element: , }, + { + path: '/metrics', + element: , + }, { path: '*', element:
Not Found
, diff --git a/ui/src/pages/bed-splash.tsx b/ui/src/pages/bed-splash.tsx index d7fd5cbc..d9d59f1f 100644 --- a/ui/src/pages/bed-splash.tsx +++ b/ui/src/pages/bed-splash.tsx @@ -2,7 +2,7 @@ import { useParams } from 'react-router-dom'; import { useBedMetadata } from '../queries/useBedMetadata'; import { useBedGenomeStats } from '../queries/useBedGenomeStats'; import { Layout } from '../components/layout'; -import { Col, Row } from 'react-bootstrap'; +import { Col, Container, Row } from 'react-bootstrap'; import { BedSplashHeader } from '../components/bed-splash-components/header'; import { CardSkeleton } from '../components/skeletons/card-skeleton'; import { ErrorPage } from '../components/common/error-page'; @@ -135,7 +135,7 @@ export const BedSplash = () => { } else { return ( -
+ {metadata !== undefined && genomeStats !== undefined ? : null} @@ -277,7 +277,7 @@ export const BedSplash = () => {

Statistics

{metadata && ( - + @@ -304,7 +304,7 @@ export const BedSplash = () => {
)} -
+
); } diff --git a/ui/src/pages/bedset-splash.tsx b/ui/src/pages/bedset-splash.tsx index 8cdf0fca..bd129979 100644 --- a/ui/src/pages/bedset-splash.tsx +++ b/ui/src/pages/bedset-splash.tsx @@ -1,6 +1,6 @@ import { useParams } from 'react-router-dom'; import { Layout } from '../components/layout'; -import { Col, Row } from 'react-bootstrap'; +import { Col, Container, Row } from 'react-bootstrap'; import { useBedsetMetadata } from '../queries/useBedsetMetadata'; import { CardSkeleton } from '../components/skeletons/card-skeleton'; import { BedsetSplashHeader } from '../components/bedset-splash-components/header'; @@ -103,7 +103,7 @@ export const BedsetSplash = () => { } else { return ( -
+ {metadata !== undefined ? : null} @@ -111,7 +111,7 @@ export const BedsetSplash = () => {

Statistics

{metadata && ( - + @@ -146,7 +146,7 @@ export const BedsetSplash = () => { )}
-
+
); } diff --git a/ui/src/pages/home.tsx b/ui/src/pages/home.tsx index e3bb3323..e35f1caa 100644 --- a/ui/src/pages/home.tsx +++ b/ui/src/pages/home.tsx @@ -336,6 +336,10 @@ export const Home = () => {

Data Availability Summary

+

+ Comprehensive metrics about BEDbase file statistics is available on the {' '} + metrics page. +

diff --git a/ui/src/pages/metrics.tsx b/ui/src/pages/metrics.tsx new file mode 100644 index 00000000..63dc18f8 --- /dev/null +++ b/ui/src/pages/metrics.tsx @@ -0,0 +1,231 @@ +import { useState } from 'react'; +import { Layout } from '../components/layout.tsx'; +import { Col, Row, Container } from 'react-bootstrap'; + +import { useStats } from '../queries/useStats.ts'; +import { useDetailedStats } from '../queries/useDetailedStats.ts'; +import { MetricPlot, MetricPlotType } from '../components/metrics/metric-plot.tsx'; +import { MetricModal } from '../components/modals/metric-modal.tsx'; +import { CardSkeleton } from '../components/skeletons/card-skeleton'; +import { EndpointsModal } from '../components/modals/endpoints-modal.tsx'; + +const API_BASE = import.meta.env.VITE_API_BASE || ''; +export const PRIMARY_COLOR = 'rgba(0, 128, 128,0.6)'; + +interface MetricModalProps { + title: string; + type: MetricPlotType; + data: [string, number][]; + dataLabel?: string; + backgroundColor: string[]; + borderWidth: number; + sliceIndex: number; +} + +export const Metrics = () => { + const [showMetricModal, setShowMetricModal] = useState(false); + const [metricModalTitle, setMetricModalTitle] = useState(''); + const [metricModalType, setMetricModalType] = useState('bar'); + const [metricModalData, setMetricModalData] = useState<[string, number][]>([]); + const [metricModalDataLabel, setMetricModalDataLabel] = useState(''); + const [metricModalBackgroundColor, setMetricModalBackgroundColor] = useState([]); + const [metricModalBorderWidth, setMetricModalBorderWidth] = useState(0); + const [metricModalSliceIndex, setMetricModalSliceIndex] = useState(0); + const [endpointsModalShow, setEndpointsModalShow] = useState(false); + + const setMetricModalProps = ({ title, type, data, dataLabel = '', backgroundColor, borderWidth, sliceIndex }: MetricModalProps): void => { + setMetricModalTitle(title); + setMetricModalType(type); + setMetricModalData(data); + setMetricModalDataLabel(dataLabel); + setMetricModalBackgroundColor(backgroundColor); + setMetricModalBorderWidth(borderWidth); + setMetricModalSliceIndex(sliceIndex); + setShowMetricModal(true); + }; + + const { data: bedbaseStats } = useStats(); + const { data: detailedStats, isLoading } = useDetailedStats(); + + const sliceIndex = 5; + + if (isLoading) { + return ( + +
+ + +
+ +
+ +
+ + +
+ +
+
+ +
+ + + + +
+
+
+ ); + }; + + return ( + + + + +

Metrics

+ + + + + + +
+ + +
BEDbase File Statistics
+ +
    +
  • + Number of bed files available: + {(bedbaseStats?.bedfiles_number || 0).toLocaleString()} +
  • +
  • + Number of bed sets available: + {(bedbaseStats?.bedsets_number || 0).toLocaleString()} +
  • +
  • + Number of genomes available: + {(bedbaseStats?.genomes_number || 0).toLocaleString()} +
  • +
+ +
+ + {detailedStats && ( + + +
setMetricModalProps({ + title: 'BED Files by Genome', + type: 'bar', + data: Object.entries(detailedStats?.file_genome || {}), + dataLabel: 'Number of BED files', + backgroundColor: [PRIMARY_COLOR], + + borderWidth: 1, + sliceIndex: Object.entries(detailedStats?.file_genome || {}).length + })} + > +
BED Files by Genome
+ +
+
setMetricModalProps({ + title: 'BED Files by Type', + type: 'bar', + data: Object.entries(detailedStats?.file_type || {}), + dataLabel: 'Number of BED files', + backgroundColor: [PRIMARY_COLOR], + borderWidth: 1, + sliceIndex: Object.entries(detailedStats?.file_type || {}).length + })} + > +
BED Files by Type
+ +
+ + + +
setMetricModalProps({ + title: 'BED Files by Format', + type: 'pie', + data: Object.entries(detailedStats?.file_format || {}), + dataLabel: 'Number of BED files', + backgroundColor: [PRIMARY_COLOR], + borderWidth: 1, + sliceIndex: Object.entries(detailedStats?.file_format || {}).length + })} + > +
BED Files by Format
+
+ +
+
+ +
+ )} + + +
BEDbase Usage Statistics
+ +

Coming Soon..

+ +
+ + {showMetricModal && ( + setShowMetricModal(false)} + /> + )} + + setEndpointsModalShow(false)} + /> + +
+
+ ); +}; diff --git a/ui/src/queries/useDetailedStats.ts b/ui/src/queries/useDetailedStats.ts new file mode 100644 index 00000000..f3047e9c --- /dev/null +++ b/ui/src/queries/useDetailedStats.ts @@ -0,0 +1,16 @@ +import {useQuery} from '@tanstack/react-query'; +import {components} from '../../bedbase-types'; +import {useBedbaseApi} from '../contexts/api-context'; + +type BEDBASEStatistics = components['schemas']['FileStats']; + +export const useDetailedStats = () => { + const {api} = useBedbaseApi(); + return useQuery({ + queryKey: ['detailed-stats'], + queryFn: async () => { + const {data} = await api.get('/detailed-stats'); + return data; + }, + }); +};