From 83281f07612061268e78c878555e1f97501c8ea2 Mon Sep 17 00:00:00 2001 From: Jacob Filik Date: Wed, 23 Oct 2024 14:51:25 +0100 Subject: [PATCH] Comparison (#27) * change plot to support different x axes * add comparison feature --- src/components/CompareList.tsx | 121 ++++++++++++ src/components/MetadataStack.tsx | 6 +- src/components/MetadataTab.tsx | 8 + src/components/PersistentView.tsx | 105 +++++----- src/components/StandardMetadataCard.tsx | 18 +- src/components/UploadStack.tsx | 6 +- src/components/UploadXDI.tsx | 4 +- src/components/ViewPage.tsx | 12 +- src/components/WelcomePage.tsx | 4 +- src/components/XASChart.tsx | 248 +++++++++++++++++------- src/components/XDIChart.tsx | 16 +- src/components/XDIPage.tsx | 10 +- src/contexts/XDIFileContext.tsx | 21 +- src/models.ts | 1 + src/xdifile.ts | 10 +- 15 files changed, 441 insertions(+), 149 deletions(-) create mode 100644 src/components/CompareList.tsx diff --git a/src/components/CompareList.tsx b/src/components/CompareList.tsx new file mode 100644 index 0000000..2615264 --- /dev/null +++ b/src/components/CompareList.tsx @@ -0,0 +1,121 @@ +import { + Button, + Box, + TableContainer, + Paper, + Table, + TableHead, + TableRow, + TableCell, + TableBody, +} from "@mui/material"; +import { useContext } from "react"; +import { XDIFileContext } from "../contexts/XDIFileContext"; +import XDIFile from "../xdifile"; + +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${tableCellClasses.head}`]: { + backgroundColor: theme.palette.common.black, + color: theme.palette.common.white, + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 14, + textOverflow: "ellipsis", + overflow: "hidden", + whiteSpace: "nowrap", + }, +})); + +import { tableCellClasses } from "@mui/material/TableCell"; + +import { styled } from "@mui/material/styles"; + +const StyledTableRow = styled(TableRow)(({ theme }) => ({ + "&:nth-of-type(odd):not(:hover):not(.activeclicked)": { + backgroundColor: theme.palette.action.selected, + }, + + // hide last border + "&:last-child td, &:last-child th": { + border: 0, + }, +})); + +function CompareMetadata(props: { + key: number; + xdiFile: XDIFile | null; +}): JSX.Element { + // const className = props.xasFile === props.selected ? "activeclicked" : ""; + + return ( + + + {props.xdiFile?.element ?? "\xa0"} + + + {props.xdiFile?.edge ?? ""} + + + {props.xdiFile?.sample?.name ?? ""} + + + ); +} + +export default function CompareList() { + const xdiFileState = useContext(XDIFileContext); + + const store = () => { + const current = xdiFileState.xdiFile; + if ( + current != null && + !xdiFileState.comparisonFiles.some((f) => f.id == current?.id) + ) { + const filesSlice = + xdiFileState.comparisonFiles.length >= 3 + ? xdiFileState.comparisonFiles.slice(1, 3) + : xdiFileState.comparisonFiles; + + const files = [...filesSlice, current]; + xdiFileState.setComparisonFiles(files); + } + }; + + const clear = () => { + xdiFileState.setComparisonFiles([]); + }; + + return ( + + + + + + + + Element + Edge + Name + + + + {xdiFileState.comparisonFiles.map((xdiFile, key) => + CompareMetadata({ + key: key, + xdiFile: xdiFile, + }) + )} + +
+
+
+ ); +} diff --git a/src/components/MetadataStack.tsx b/src/components/MetadataStack.tsx index a2ab827..20d1eb0 100644 --- a/src/components/MetadataStack.tsx +++ b/src/components/MetadataStack.tsx @@ -47,7 +47,11 @@ function MetadataStack(props: { setOffset={setOffset} /> {selectedStandard && ( - + )} ); diff --git a/src/components/MetadataTab.tsx b/src/components/MetadataTab.tsx index 7c98325..79e5327 100644 --- a/src/components/MetadataTab.tsx +++ b/src/components/MetadataTab.tsx @@ -5,6 +5,7 @@ import ReviewTextView from "./XDITextView"; import { useState } from "react"; import { XASStandard } from "../models"; +import CompareList from "./CompareList"; interface TabPanelProps { children?: React.ReactNode; @@ -38,6 +39,7 @@ function a11yProps(index: number) { export default function MetadataTab(props: { standard: XASStandard; showDownload: boolean; + showCompare: boolean; }) { const [value, setValue] = useState(0); @@ -55,6 +57,7 @@ export default function MetadataTab(props: { > + {props.showCompare && } @@ -66,6 +69,11 @@ export default function MetadataTab(props: { + {props.showCompare && ( + + + + )} ); } diff --git a/src/components/PersistentView.tsx b/src/components/PersistentView.tsx index 565abfd..956a95a 100644 --- a/src/components/PersistentView.tsx +++ b/src/components/PersistentView.tsx @@ -1,4 +1,3 @@ - import { useState, useContext } from "react"; import axios from "axios"; @@ -10,59 +9,61 @@ import XDIFile from "../xdifile.ts"; import { XDIFileProvider } from "../contexts/XDIFileContext.tsx"; import XDIChart from "./XDIChart.tsx"; -import { useLocation} from "react-router-dom"; +import { useLocation } from "react-router-dom"; import { useEffect } from "react"; import { MetadataContext } from "../contexts/MetadataContext.tsx"; - - function PersistentView() { - - const location = useLocation(); - const id = location.pathname.slice(5) - - const [xdiFile, setXDIFile] = useState(null); - const allStandards = useContext(MetadataContext); - - console.log(id) - console.log(allStandards[1]) - - const standard = allStandards.find((f) => f.location === id) - - useEffect(() => { - axios.get("/webxdiviewer/xdidata/" + id).then((response) => { - - let xdi = null; - try { - xdi = XDIFile.parseFile(response.data); - - } catch { - console.log("Could not read {}", focus) - - } - - setXDIFile(xdi); - })}, [id]); - - - - // const onClick = getData(); - - return ( - - - - {standard - ? - : Could not find {id} - } - - - - - - - ); - } -export default PersistentView; \ No newline at end of file + const location = useLocation(); + const id = location.pathname.slice(5); + + const [xdiFile, setXDIFile] = useState(null); + const allStandards = useContext(MetadataContext); + + const standard = allStandards.find((f) => f.location === id); + + useEffect(() => { + axios.get("/webxdiviewer/xdidata/" + id).then((response) => { + let xdi = null; + try { + xdi = XDIFile.parseFile(response.data, id); + } catch { + console.log("Could not read {}", focus); + } + + setXDIFile(xdi); + }); + }, [id]); + + // const onClick = getData(); + + return ( + {}, + }} + > + + + {standard ? ( + + ) : ( + Could not find {id} + )} + + + + + + + ); +} +export default PersistentView; diff --git a/src/components/StandardMetadataCard.tsx b/src/components/StandardMetadataCard.tsx index b09b775..42038d8 100644 --- a/src/components/StandardMetadataCard.tsx +++ b/src/components/StandardMetadataCard.tsx @@ -5,6 +5,7 @@ import { Typography, CardActions, Link, + Stack, } from "@mui/material"; function StandardMetadataCard(props: { @@ -36,12 +37,17 @@ function StandardMetadataCard(props: { {props.showDownload && ( - - Download - + + + Download XDI + + + Persistent Link + + )} diff --git a/src/components/UploadStack.tsx b/src/components/UploadStack.tsx index 8782752..5cde2de 100644 --- a/src/components/UploadStack.tsx +++ b/src/components/UploadStack.tsx @@ -13,7 +13,11 @@ function UploadStack() { {xasMetadata && ( - + )} ); diff --git a/src/components/UploadXDI.tsx b/src/components/UploadXDI.tsx index 6fe7265..befff48 100644 --- a/src/components/UploadXDI.tsx +++ b/src/components/UploadXDI.tsx @@ -18,7 +18,7 @@ function UploadXDI(props: { setXASMetadata: (standard: XASStandard) => void }) { const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); axios.get(xdiURL).then((response) => { - const xdi = XDIFile.parseFile(response.data); + const xdi = XDIFile.parseFile(response.data, xdiURL); xdiContext.setXDIFile(xdi); const standard: XASStandard = { @@ -43,7 +43,7 @@ function UploadXDI(props: { setXASMetadata: (standard: XASStandard) => void }) { if (e.target != null && typeof e.target.result === "string") { let xdi: XDIFile; try { - xdi = XDIFile.parseFile(e.target.result); + xdi = XDIFile.parseFile(e.target.result, "localfile"); xdiContext.setXDIFile(xdi); const standard: XASStandard = { diff --git a/src/components/ViewPage.tsx b/src/components/ViewPage.tsx index 1c67e9c..00e65d2 100644 --- a/src/components/ViewPage.tsx +++ b/src/components/ViewPage.tsx @@ -12,13 +12,14 @@ import XDIChart from "./XDIChart.tsx"; function ViewPage() { const [xdiFile, setXDIFile] = useState(null); + const [comparisonFiles, setComparisonFiles] = useState([]); const allStandards = useContext(MetadataContext); function getData() { return (id: string) => { axios.get("/webxdiviewer/xdidata/" + id).then((response) => { - const xdi = XDIFile.parseFile(response.data); + const xdi = XDIFile.parseFile(response.data, id); setXDIFile(xdi); }); }; @@ -27,7 +28,14 @@ function ViewPage() { const onClick = getData(); return ( - + diff --git a/src/components/WelcomePage.tsx b/src/components/WelcomePage.tsx index 111e9b0..71e9f8d 100644 --- a/src/components/WelcomePage.tsx +++ b/src/components/WelcomePage.tsx @@ -4,10 +4,8 @@ import { Container, Typography, Box } from "@mui/material"; const env_repo_location = import.meta.env.VITE_XDI_REPO_LOCATION; function WelcomePage() { + const repo_location = env_repo_location ?? "examplerepo/xdifiles"; - const repo_location = env_repo_location ?? "examplerepo/xdifiles" - - console.log(repo_location) return ( diff --git a/src/components/XASChart.tsx b/src/components/XASChart.tsx index 29c1b28..f87d1a8 100644 --- a/src/components/XASChart.tsx +++ b/src/components/XASChart.tsx @@ -1,35 +1,122 @@ import { - LineVis, + DataCurve, + TooltipMesh, getDomain, Separator, Selector, Toolbar, - ScaleType, CurveType, ToggleBtn, + VisCanvas, getCombinedDomain, + DefaultInteractions, + Domain, + ResetZoomButton, + AxisConfig, + Overlay, } from "@h5web/lib"; import "@h5web/lib/dist/styles.css"; +import { darken, lighten } from "@mui/material"; + +import { ReactElement } from "react"; + import Paper from "@mui/material/Paper"; import { MdGridOn } from "react-icons/md"; import { useTheme, Theme } from "@mui/material"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; -import ndarray from "ndarray"; +import ndarray, { TypedArray } from "ndarray"; import { Box } from "@mui/material"; import { XASData } from "../models"; import { pre_edge } from "../utils"; +interface DisplayData { + label: string; + x: TypedArray; + y: TypedArray; + color: string; +} + export interface XASChartState { showTrans: boolean; showFluor: boolean; showRefer: boolean; } -function XASChart(props: { xasData: XASData | null }) { +function buildDisplayData( + xasData: XASData, + type: Pick, + normalize: boolean, + label: string, + color: string +): DisplayData { + const ydata = xasData[type]; + const xdata = xasData.energy; + + const y = normalize ? pre_edge(xdata, ydata) : ndarray(ydata, [ydata.length]); + + return { x: xdata, y: y, label: label + ":" + type, color: color }; +} + +function createDisplayData( + xasData: XASData | null, + showTrans: boolean, + showFluor: boolean, + showRefer: boolean, + normalize: boolean, + colors: string[] +): DisplayData[] { + const hideAll = !showTrans && !showFluor && !showRefer; + + const alldata: DisplayData[] = []; + + if (hideAll || xasData == null) { + return alldata; + } + + if (showRefer && xasData.murefer) { + alldata.push( + buildDisplayData(xasData, "murefer", normalize, xasData.id, colors[2]) + ); + } + + if (showFluor && xasData.mufluor) { + alldata.push( + buildDisplayData(xasData, "mufluor", normalize, xasData.id, colors[1]) + ); + } + + if (showTrans && xasData.mutrans) { + alldata.push( + buildDisplayData(xasData, "mutrans", normalize, xasData.id, colors[0]) + ); + } + + return alldata; +} + +function displayDataToDataCurve( + displayData: DisplayData, + curveType: CurveType +): JSX.Element { + return ( + + ); +} + +function XASChart(props: { + xasData: XASData | null; + comparisonFiles: XASData[]; +}) { const [chartState, setChartState] = useState({ showTrans: false, showFluor: false, @@ -55,7 +142,6 @@ function XASChart(props: { xasData: XASData | null }) { const theme = useTheme(); const { showTrans, showFluor, showRefer } = chartState; - const xasdata = props.xasData; const contains = [ props.xasData?.mutrans != null, @@ -63,56 +149,65 @@ function XASChart(props: { xasData: XASData | null }) { props.xasData?.murefer != null, ]; - let xdata: ndarray.NdArray = ndarray([0]); - let ydata: ndarray.NdArray = ndarray([0]); + const tooltipText = (x: number, y: number): ReactElement => { + return ( +

+ {x.toPrecision(8)}, {y.toPrecision(8)} +

+ ); + }; - const aux = []; + const dd = createDisplayData( + props.xasData, + showTrans, + showFluor, + showRefer, + useNorm, + [ + darken(theme.palette.primary.dark, 0.3), + darken(theme.palette.success.light, 0.3), + darken(theme.palette.secondary.dark, 0.3), + ] + ); - let ydataLabel = ""; + const filteredComparison: XASData[] = props.comparisonFiles.filter( + (f) => f.id != props.xasData?.id + ); - const hideAll = !showTrans && !showFluor && !showRefer; + const ddcompare: DisplayData[] = filteredComparison + .map((f, i) => { + return createDisplayData(f, showTrans, showFluor, showRefer, useNorm, [ + lighten(theme.palette.primary.dark, i * 0.3), + lighten(theme.palette.success.light, i * 0.3), + lighten(theme.palette.secondary.dark, i * 0.3), + ]); + }) + .flat(); - if (xasdata != null && !hideAll) { - xdata = ndarray(xasdata.energy, [xasdata.energy.length]); - - let primaryFound = false; - - if (showTrans && xasdata.mutrans) { - primaryFound = true; - - ydata = useNorm - ? pre_edge(xasdata.energy, xasdata.mutrans) - : ndarray(xasdata.mutrans, [xasdata.mutrans.length]); - - ydataLabel = "Transmission"; - } - - if (showFluor && xasdata.mufluor) { - const fdata = useNorm - ? pre_edge(xasdata.energy, xasdata.mufluor) - : ndarray(xasdata.mufluor, [xasdata.mufluor.length]); - if (!primaryFound) { - primaryFound = true; - ydata = fdata; - ydataLabel = "Fluorescence"; - } else { - aux.push({ label: "Fluorescence", array: fdata }); - } - } - - if (showRefer && xasdata.murefer) { - const rdata = useNorm - ? pre_edge(xasdata.energy, xasdata.murefer) - : ndarray(xasdata.murefer, [xasdata.murefer.length]); - if (!primaryFound) { - primaryFound = true; - ydata = rdata; - ydataLabel = "Reference"; - } else { - aux.push({ label: "Reference", array: rdata }); - } - } - } + dd.push(...ddcompare); + + const domain: Domain | undefined = getCombinedDomain( + dd.map((a) => getDomain(a.y)) + ); + const xdomain: Domain | undefined = getCombinedDomain( + dd.map((a) => getDomain(a.x)) + ); + + const abscissaConfig: AxisConfig = { + visDomain: xdomain ?? [0, 1], + showGrid: true, + isIndexAxis: false, + label: "Energy (eV)", + }; + + const ordinateConfig: AxisConfig = { + visDomain: domain ?? [0, 1], + showGrid: true, + isIndexAxis: false, + label: useNorm ? "mu(E) (norm)" : "mu(E)", + }; + + const legendColor = theme.palette.action.hover; const toolbarstyle = { "--h5w-toolbar--bgColor": theme.palette.action.hover, @@ -140,10 +235,6 @@ function XASChart(props: { xasData: XASData | null }) { ], } as React.CSSProperties; - const domain = getCombinedDomain( - [getDomain(ydata)].concat(aux.map((a) => getDomain(a.array))) - ); - return ( - + + {dd.map((d) => displayDataToDataCurve(d, curveOption))} + + + + +
+ {dd.reverse().map((d) => ( +
+ + {"\xa0" + d.label} +
+ ))} +
+
+
); diff --git a/src/components/XDIChart.tsx b/src/components/XDIChart.tsx index b528bc0..5394355 100644 --- a/src/components/XDIChart.tsx +++ b/src/components/XDIChart.tsx @@ -7,6 +7,7 @@ function XDIChart() { const xdiFileState = useContext(XDIFileContext); let xasdata: XASData | null = null; + let comparisonFiles: XASData[] = []; if (xdiFileState.xdiFile != null) { const xdi = xdiFileState.xdiFile; @@ -17,14 +18,27 @@ function XDIChart() { const murefer = xdi.muRefer(); xasdata = { + id: xdi.id, energy: energy, mutrans: mutrans, mufluor: mufluor, murefer: murefer, }; + + comparisonFiles = xdiFileState.comparisonFiles.map((f) => { + return { + id: f.id, + energy: f.energy(), + mutrans: f.muTrans(), + mufluor: f.muFluor(), + murefer: f.muRefer(), + }; + }); } - return ; + return ( + + ); } export default XDIChart; diff --git a/src/components/XDIPage.tsx b/src/components/XDIPage.tsx index 3f642e4..0f5bd08 100644 --- a/src/components/XDIPage.tsx +++ b/src/components/XDIPage.tsx @@ -8,8 +8,16 @@ import UploadStack from "./UploadStack"; function XDIPage() { const [xdiFile, setXDIFile] = useState(null); + return ( - + {}, + }} + > diff --git a/src/contexts/XDIFileContext.tsx b/src/contexts/XDIFileContext.tsx index ed00d4b..25aaab9 100644 --- a/src/contexts/XDIFileContext.tsx +++ b/src/contexts/XDIFileContext.tsx @@ -2,16 +2,23 @@ import { createContext } from "react"; import XDIFile from "../xdifile"; interface XDIState { - xdiFile : XDIFile | null, - setXDIFile: (xdiFile: XDIFile) => void + xdiFile: XDIFile | null; + setXDIFile: (xdiFile: XDIFile) => void; + comparisonFiles: XDIFile[]; + setComparisonFiles: (xdiFiles: XDIFile[]) => void; } const XDIFileContext = createContext({ - xdiFile: null, - setXDIFile: () => {} -}) + xdiFile: null, + setXDIFile: () => {}, + comparisonFiles: [], + setComparisonFiles: () => {}, +}); -function XDIFileProvider(props: { children: React.ReactNode, value: XDIState }) { +function XDIFileProvider(props: { + children: React.ReactNode; + value: XDIState; +}) { const { children } = props; return ( @@ -21,4 +28,4 @@ function XDIFileProvider(props: { children: React.ReactNode, value: XDIState }) ); } -export { XDIFileContext, XDIFileProvider }; \ No newline at end of file +export { XDIFileContext, XDIFileProvider }; diff --git a/src/models.ts b/src/models.ts index c87da52..bf5eeaf 100644 --- a/src/models.ts +++ b/src/models.ts @@ -7,6 +7,7 @@ export interface Edge { } export interface XASData { + id: string; energy: Array; mutrans: Array | null; mufluor: Array | null; diff --git a/src/xdifile.ts b/src/xdifile.ts index 07a914f..1c9ae24 100644 --- a/src/xdifile.ts +++ b/src/xdifile.ts @@ -59,6 +59,7 @@ class XDIFile { comments: string | null; data: { [key: string]: number[] }; raw: string; + id: string; constructor( element: string | null, @@ -69,7 +70,8 @@ class XDIFile { columns: string[], comments: string, data: { [key: string]: number[] }, - raw: string + raw: string, + id: string ) { this.element = element; this.edge = edge; @@ -80,11 +82,12 @@ class XDIFile { this.comments = comments; this.data = data; this.raw = raw; + this.id = id; } // Facility, Beamline, Mono, Detector, Sample, Scan, Element, Column - static parseFile(xditext: string) { + static parseFile(xditext: string, id: string) { const lines = xditext.split("\n"); const sample: { [key: string]: string } = {}; @@ -203,7 +206,8 @@ class XDIFile { columns, comment, datamap, - xditext + xditext, + id ); }