diff --git a/api/schemas/FilterSchemas.yaml b/api/schemas/FilterSchemas.yaml index 7506523..bf857f2 100644 --- a/api/schemas/FilterSchemas.yaml +++ b/api/schemas/FilterSchemas.yaml @@ -13,7 +13,7 @@ components: enum: [ MS,MS2,MS3,MS4 ] IonMode: type: string - desciption: "enum: [ POSITIVE, NEGATIVE ]" + description: "enum: [ POSITIVE, NEGATIVE ]" CompoundName: type: string ExactMass: @@ -35,7 +35,7 @@ components: description: 'A m/z prefixed with "and" or "or" before a colon, e.g. "or:143.0".' pattern: "(and|or):[0-9]+(\\.[0-9]*)?" Intensity: - description: "Releative intensity for peak search" + description: "Relative intensity for peak search" type: integer default: 100 minimum: 1 diff --git a/cmd/mb3server/src/api-impl.go b/cmd/mb3server/src/api-impl.go index 885f803..e53a2ec 100644 --- a/cmd/mb3server/src/api-impl.go +++ b/cmd/mb3server/src/api-impl.go @@ -6,6 +6,7 @@ import ( "net/http" "net/url" "regexp" + "strconv" "github.com/MassBank/MassBank3/pkg/config" "github.com/MassBank/MassBank3/pkg/database" @@ -309,19 +310,12 @@ func GetRecord(accession string) (*MbRecord, error) { }) } - var mzs = *&record.Peak.Peak.Mz //[]float64{} - var ints = *&record.Peak.Peak.Intensity //[]float64{} - var rels = *&record.Peak.Peak.Rel //[]uint{} - // for _, mz := range *&record.Peak.Peak.Mz { - // mzs = append(mzs, mz) - // } - // for _, int := range *&record.Peak.Peak.Intensity { - // ints = append(ints, int) - // } - // for _, rel := range *&record.Peak.Peak.Rel { - // rels = append(rels, rel) - // } + // insert peak data + result.Peak.Peak.Header = record.Peak.Peak.Header + var mzs = record.Peak.Peak.Mz + var ints = record.Peak.Peak.Intensity + var rels = record.Peak.Peak.Rel for i := 0; i < len(mzs); i++ { result.Peak.Peak.Values = append(result.Peak.Peak.Values, MbRecordPeakPeakValuesInner{ Mz: mzs[i], @@ -330,21 +324,31 @@ func GetRecord(accession string) (*MbRecord, error) { }) } - // var header = []string{} - // for _, h := range *&record.Peak.Annotation.Header { - // fmt.Printf("%v\n", h) - // header = append(header, h) - // } - // result.Peak.Annotation.Header = header + // insert annotation data + if record.Peak.Annotation != nil { - // for _, v := range *&record.Peak.Annotation.Values { - // fmt.Printf("%v\n", v) - // // for _, k := range *&v { - // // fmt.Printf("%v\n", k) - // // } - // } + result.Peak.Annotation.Header = record.Peak.Annotation.Header - // // result.Peak.Annotation.Values = + var annotationValues = [][]string{} + for _, headerKey := range record.Peak.Annotation.Header { + annotationValues = append(annotationValues, []string{}) + + for _, v := range record.Peak.Annotation.Values[headerKey] { + m, ok := v.(float64) + if ok { + s := strconv.FormatFloat(m, 'f', -1, 64) + annotationValues[len(annotationValues)-1] = append(annotationValues[len(annotationValues)-1], s) + } else { + m, ok := v.(string) + if ok { + annotationValues[len(annotationValues)-1] = append(annotationValues[len(annotationValues)-1], m) + } + } + } + } + + result.Peak.Annotation.Values = annotationValues + } return &result, nil diff --git a/web-frontend/package.json b/web-frontend/package.json index 59b773d..2879cd8 100644 --- a/web-frontend/package.json +++ b/web-frontend/package.json @@ -4,7 +4,7 @@ "scripts": { "start": "vite --host localhost --port 3000 --strictPort --open", "build": "rimraf dist && tsc && vite build", - "preview": "vite preview --host 127.0.0.1 --port 5173 -d --strictPort", + "preview": "vite preview --host 0.0.0.0 --port 3000 -d --strictPort", "compile": "tsc", "clean-install": "rm package-lock.json && rm -rf node_modules && npm i", "eslint": "eslint . --ext .js,.jsx,.ts,.tsx", diff --git a/web-frontend/src/elements/basic/Chart.tsx b/web-frontend/src/elements/basic/Chart.tsx index 22640ca..463469d 100644 --- a/web-frontend/src/elements/basic/Chart.tsx +++ b/web-frontend/src/elements/basic/Chart.tsx @@ -1,15 +1,15 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { brushX, scaleLinear, select } from 'd3'; -import PeakData from '../../types/PeakData'; import ChartElement from './ChartElement'; import Button from './Button'; +import Peak from '../../types/peak/Peak'; const MARGIN = { top: 55, right: 30, bottom: 50, left: 70, button: 35 }; type InputProps = { - peakData: PeakData[]; + peakData: Peak[]; // eslint-disable-next-line no-unused-vars - onZoom: (fpd: PeakData[]) => void; + onZoom: (fpd: Peak[]) => void; width?: number; height?: number; }; diff --git a/web-frontend/src/elements/basic/ChartElement.tsx b/web-frontend/src/elements/basic/ChartElement.tsx index e3981ff..0a8a2b1 100644 --- a/web-frontend/src/elements/basic/ChartElement.tsx +++ b/web-frontend/src/elements/basic/ChartElement.tsx @@ -1,12 +1,12 @@ import './ChartElement.scss'; import { MouseEvent, useCallback, useEffect, useMemo, useState } from 'react'; -import PeakData from '../../types/PeakData'; import { ScaleLinear } from 'd3'; import { useHighlight, useHighlightData } from '../../highlight/Index'; +import Peak from '../../types/peak/Peak'; type InputProps = { - pd: PeakData; + pd: Peak; xScale: ScaleLinear; yScale: ScaleLinear; showLabel: boolean; @@ -79,7 +79,7 @@ function ChartElement({ pd, xScale, yScale, showLabel }: InputProps) { yScale(pd.rel || 0) - 10 }) rotate(-30)`} > - {pd.mz} + {pd.mz.toFixed(4)} )} diff --git a/web-frontend/src/elements/record/AnnotationTable.scss b/web-frontend/src/elements/record/AnnotationTable.scss new file mode 100644 index 0000000..4b652b7 --- /dev/null +++ b/web-frontend/src/elements/record/AnnotationTable.scss @@ -0,0 +1,20 @@ +.AnnotationTable { + overflow: scroll; + + table { + width: 100%; + height: 100%; + text-align: center; + + thead th { + position: sticky; + position: -webkit-sticky; + top: 0; + background: lightgrey; + } + + .auto-height { + height: 100%; + } + } +} diff --git a/web-frontend/src/elements/record/AnnotationTable.tsx b/web-frontend/src/elements/record/AnnotationTable.tsx new file mode 100644 index 0000000..87d6e49 --- /dev/null +++ b/web-frontend/src/elements/record/AnnotationTable.tsx @@ -0,0 +1,58 @@ +import './AnnotationTable.scss'; + +import { useMemo } from 'react'; +import PeakAnnotation from '../../types/peak/PeakAnnotation'; + +type InputProps = { + annotation: PeakAnnotation; + width?: number | string; + height?: number | string; +}; + +function AnnotationTable({ + annotation, + width = '100%', + height = 400, +}: InputProps) { + const annotationTable = useMemo(() => { + const numHeaders = annotation.header.length; + const numPeaks = annotation.values[0].length; + + const headerContent = ( + + {annotation.header.map((h) => ( + {h.split('_').join(' ')} + ))} + + ); + + const bodyContent: JSX.Element[] = []; + for (let i = 0; i < numPeaks; i++) { + const rows: JSX.Element[] = []; + for (let h = 0; h < numHeaders; h++) { + rows.push( + {annotation.values[h][i]}, + ); + } + bodyContent.push({rows}); + } + + return ( + + {headerContent} + + {bodyContent} + + +
+ ); + }, [annotation]); + + return ( +
+ {annotationTable} +
+ ); +} + +export default AnnotationTable; diff --git a/web-frontend/src/elements/record/PeakTable.scss b/web-frontend/src/elements/record/PeakTable.scss index 4146b25..6d70d6d 100644 --- a/web-frontend/src/elements/record/PeakTable.scss +++ b/web-frontend/src/elements/record/PeakTable.scss @@ -4,7 +4,6 @@ table { width: 100%; height: 100%; - table-layout: fixed; text-align: center; thead th { @@ -18,10 +17,6 @@ background-color: lightcyan; } - tr { - height: 10px; - } - .auto-height { height: 100%; } diff --git a/web-frontend/src/elements/record/PeakTable.tsx b/web-frontend/src/elements/record/PeakTable.tsx index ebefba0..0295e51 100644 --- a/web-frontend/src/elements/record/PeakTable.tsx +++ b/web-frontend/src/elements/record/PeakTable.tsx @@ -1,18 +1,18 @@ import './PeakTable.scss'; import { useMemo } from 'react'; -import PeakData from '../../types/PeakData'; import PeakTableRow from './PeakTableRow'; +import Peak from '../../types/peak/Peak'; type InputProps = { - pd: PeakData[]; + pd: Peak[]; width: number; height: number; }; function PeakTable({ pd, width, height }: InputProps) { const rows = useMemo( - () => pd.map((r) => ), + () => pd.map((p) => ), [pd], ); @@ -28,7 +28,7 @@ function PeakTable({ pd, width, height }: InputProps) { {rows} - + diff --git a/web-frontend/src/elements/record/PeakTableRow.tsx b/web-frontend/src/elements/record/PeakTableRow.tsx index f325aed..b114f1e 100644 --- a/web-frontend/src/elements/record/PeakTableRow.tsx +++ b/web-frontend/src/elements/record/PeakTableRow.tsx @@ -1,13 +1,13 @@ import { MouseEvent, useCallback, useMemo } from 'react'; import { useHighlight } from '../../highlight/Index'; -import PeakData from '../../types/PeakData'; +import Peak from '../../types/peak/Peak'; type InputProps = { - rowData: PeakData; + peak: Peak; }; -function PeakTableRow({ rowData }: InputProps) { - const highlightRow = useHighlight([rowData.id]); +function PeakTableRow({ peak }: InputProps) { + const highlightRow = useHighlight([peak.id]); const handleOnMouseEnter = useCallback( (e: MouseEvent) => { @@ -38,18 +38,18 @@ function PeakTableRow({ rowData }: InputProps) { backgroundColor: highlightRow.isActive ? 'lightblue' : 'transparent', }} > - {rowData.mz.toFixed(4)} - {rowData.intensity.toFixed(2)} - {rowData.rel || 0} + {peak.mz.toFixed(4)} + {peak.intensity.toFixed(1)} + {peak.rel || 0} ), [ handleOnMouseEnter, handleOnMouseLeave, highlightRow.isActive, - rowData.intensity, - rowData.mz, - rowData.rel, + peak.intensity, + peak.mz, + peak.rel, ], ); diff --git a/web-frontend/src/elements/record/RecordView.tsx b/web-frontend/src/elements/record/RecordView.tsx index b501d80..4771abc 100644 --- a/web-frontend/src/elements/record/RecordView.tsx +++ b/web-frontend/src/elements/record/RecordView.tsx @@ -3,11 +3,12 @@ import './RecordView.scss'; import Record from '../../types/Record'; import useContainerDimensions from '../../utils/useContainerDimensions'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import PeakData from '../../types/PeakData'; import StructureView from '../basic/StructureView'; import Chart from '../basic/Chart'; import PeakTable from './PeakTable'; import { MF } from 'react-mf'; +import Peak from '../../types/peak/Peak'; +import AnnotationTable from './AnnotationTable'; type inputProps = { record: Record; @@ -24,7 +25,7 @@ function RecordView({ record }: inputProps) { const { width: chartContainerWidth } = useContainerDimensions(chartContainerRef); - const chartHeight = useMemo(() => containerHeight * 0.7, [containerHeight]); + const chartHeight = useMemo(() => containerHeight * 0.6, [containerHeight]); const chartWidth = useMemo( () => chartContainerWidth * 0.7, [chartContainerWidth], @@ -34,11 +35,11 @@ function RecordView({ record }: inputProps) { [chartContainerWidth], ); - const [filteredPeakData, setFilteredPeakData] = useState( + const [filteredPeakData, setFilteredPeakData] = useState( record.peak.peak.values, ); - const handleOnZoom = useCallback((fpd: PeakData[]) => { + const handleOnZoom = useCallback((fpd: Peak[]) => { setFilteredPeakData(fpd); }, []); @@ -124,20 +125,33 @@ function RecordView({ record }: inputProps) { - - Mass - {record.compound.mass} - SPLASH {record.peak.splash} + + Mass + {record.compound.mass} + Formula {' '} + + Annotation + + {record.peak.annotation && + Object.keys(record.peak.annotation).length > 0 && ( + + )} + + Names {record.compound.names} @@ -172,11 +186,10 @@ function RecordView({ record }: inputProps) { record.date.created, record.authors, record.license, - record.peak.peak.values, - record.peak.splash, + record.peak, + chartHeight, handleOnZoom, chartWidth, - chartHeight, filteredPeakData, peakTableWidth, ], diff --git a/web-frontend/src/elements/routes/pages/accession/Accession.tsx b/web-frontend/src/elements/routes/pages/accession/Accession.tsx index 4e05628..182fbbd 100644 --- a/web-frontend/src/elements/routes/pages/accession/Accession.tsx +++ b/web-frontend/src/elements/routes/pages/accession/Accession.tsx @@ -13,7 +13,6 @@ import { useSearchParams, } from 'react-router-dom'; import routes from '../../../../constants/routes'; -import PeakData from '../../../../types/PeakData'; const base = 'http://localhost:8081'; @@ -47,12 +46,12 @@ function Accession() { setIsRequesting(true); setRequestedAccession(id); - const rec = await getRecord(base, id); + const rec: Record | undefined = await getRecord(base, id); if (rec) { - rec.peak.peak.values = rec.peak.peak.values.map((v: PeakData) => { - const _v = v; - _v.id = generateID(); - return _v; + rec.peak.peak.values = rec.peak.peak.values.map((p) => { + const _p = p; + _p.id = generateID(); + return _p; }); } setRecord(rec); diff --git a/web-frontend/src/types/Record.d.ts b/web-frontend/src/types/Record.d.ts index 443aa41..23664e9 100644 --- a/web-frontend/src/types/Record.d.ts +++ b/web-frontend/src/types/Record.d.ts @@ -1,4 +1,4 @@ -import PeakData from './PeakData'; +import PeakData from './peak/PeakData'; export default interface Record { accession: string; @@ -26,13 +26,5 @@ export default interface Record { mass_spectrometry: {}; }; mass_spectrometry: {}; - peak: { - splash: string; - annotation: {}; - numPeak: number; - peak: { - header: string[]; - values: PeakData[]; - }; - }; + peak: PeakData; } diff --git a/web-frontend/src/types/PeakData.d.ts b/web-frontend/src/types/peak/Peak.d.ts similarity index 64% rename from web-frontend/src/types/PeakData.d.ts rename to web-frontend/src/types/peak/Peak.d.ts index c72d292..143b85d 100644 --- a/web-frontend/src/types/PeakData.d.ts +++ b/web-frontend/src/types/peak/Peak.d.ts @@ -1,4 +1,4 @@ -export default interface PeakData { +export default interface Peak { mz: number; intensity: number; rel: number; diff --git a/web-frontend/src/types/peak/PeakAnnotation.d.ts b/web-frontend/src/types/peak/PeakAnnotation.d.ts new file mode 100644 index 0000000..3fc375a --- /dev/null +++ b/web-frontend/src/types/peak/PeakAnnotation.d.ts @@ -0,0 +1,4 @@ +export default interface PeakAnnotation { + header: string[]; + values: string[][]; +} diff --git a/web-frontend/src/types/peak/PeakData.d.ts b/web-frontend/src/types/peak/PeakData.d.ts new file mode 100644 index 0000000..2117087 --- /dev/null +++ b/web-frontend/src/types/peak/PeakData.d.ts @@ -0,0 +1,9 @@ +import PeakAnnotation from './PeakAnnotation'; +import PeakPeakData from './PeakPeakData'; + +export default interface PeakData { + splash: string; + numPeak: number; + peak: PeakPeakData; + annotation?: PeakAnnotation; +} diff --git a/web-frontend/src/types/peak/PeakPeakData.d.ts b/web-frontend/src/types/peak/PeakPeakData.d.ts new file mode 100644 index 0000000..eb73403 --- /dev/null +++ b/web-frontend/src/types/peak/PeakPeakData.d.ts @@ -0,0 +1,6 @@ +import Peak from './Peak'; + +export default interface PeakPeakData { + header: string[]; + values: Peak[]; +}