-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
64bb0df
commit 2e3e282
Showing
17 changed files
with
1,038 additions
and
189 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { Table, TableBody, TableCell, TableRow, Tooltip, Typography } from "@mui/material"; | ||
|
||
export interface CalculationsTableProps { | ||
properties: { name: string; value: string | undefined }[]; | ||
tableWidth?: number; | ||
calcs?: { [key: string]: string }; | ||
blacklist?: string[]; | ||
fontSize?: number | string; | ||
} | ||
|
||
const CalculationsTable = ({ | ||
calcs, | ||
blacklist = [], | ||
properties, | ||
fontSize, | ||
tableWidth, | ||
}: CalculationsTableProps) => { | ||
return ( | ||
<Table> | ||
<TableBody> | ||
{properties | ||
.filter((property) => !blacklist.includes(property.name)) | ||
.map(({ name, value = "" }, index) => { | ||
// TODO: round number-like value to 2 sf | ||
const displayName = calcs?.[name] ?? name; | ||
return ( | ||
<TableRow key={index}> | ||
<TableCell | ||
component="th" | ||
style={{ maxWidth: tableWidth ? `calc(${tableWidth - 32}px - 35px)` : "5rem" }} | ||
sx={{ | ||
fontSize, | ||
paddingY: 0.5, | ||
paddingX: 0, | ||
overflow: "hidden", | ||
textOverflow: "ellipsis", | ||
}} | ||
> | ||
<Tooltip arrow title={displayName}> | ||
<span>{displayName}</span> | ||
</Tooltip> | ||
</TableCell> | ||
<TableCell | ||
align="left" | ||
sx={{ fontSize, paddingY: 0.5, paddingX: 0, width: "100%", pl: 1 }} | ||
> | ||
<Tooltip arrow title={value}> | ||
<Typography noWrap sx={{ fontSize, width: 32, minWidth: "100%" }}> | ||
{value} | ||
</Typography> | ||
</Tooltip> | ||
</TableCell> | ||
</TableRow> | ||
); | ||
})} | ||
</TableBody> | ||
</Table> | ||
); | ||
}; | ||
|
||
export default CalculationsTable; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { styled } from "@mui/styles"; | ||
|
||
export interface DepictMoleculeProps { | ||
smiles: string; | ||
width?: number; | ||
height?: number; | ||
margin?: number; | ||
expand?: boolean; | ||
background?: string; | ||
colorScheme?: string; | ||
explicitHOnly?: boolean; | ||
highlightAtoms?: number[]; | ||
highlightColor?: string; | ||
outerGlow?: boolean; | ||
mcs?: string; | ||
mcsColor?: string; | ||
noStereo?: boolean; | ||
alt?: string; | ||
fragnet_server?: string; | ||
depict_route?: string; | ||
} | ||
|
||
export const DepictMolecule = (props: DepictMoleculeProps) => { | ||
const { | ||
smiles, | ||
width, | ||
height, | ||
margin = 0, | ||
expand = true, | ||
noStereo = false, | ||
mcs = "", | ||
mcsColor = "0xFFAAAAAA", | ||
fragnet_server = "https://squonk.informaticsmatters.org", | ||
depict_route = "/fragnet-depict-api/fragnet-depict/moldepict", | ||
} = props; | ||
|
||
const params = { | ||
mol: smiles, | ||
m: String(margin), | ||
expand: String(expand), | ||
mcs: String(mcs), | ||
noStereo: String(noStereo), | ||
mcsColor, | ||
}; | ||
const searchParams = Object.keys(params).map( | ||
(key) => `${key}=${encodeURIComponent(params[key as keyof typeof params])}`, | ||
); | ||
|
||
return ( | ||
<Image | ||
alt={smiles || "invalid smiles"} | ||
height={height} | ||
loading="lazy" | ||
src={smiles && `${fragnet_server}${depict_route}?${searchParams.join("&")}`} | ||
width={width} | ||
/> | ||
); | ||
}; | ||
|
||
const Image = styled("img")({ | ||
overflow: "hidden", | ||
display: "inline-block", | ||
maxWidth: "100%", | ||
height: "auto", | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { Children, useState } from "react"; | ||
|
||
import type { CardActionsProps, CardProps } from "@mui/material"; | ||
import { Card, CardActions, CardContent } from "@mui/material"; | ||
|
||
import { DepictMolecule } from "./DepictMolecule"; | ||
|
||
export interface MolCardProps extends CardProps { | ||
smiles: string; | ||
children?: React.ReactNode; | ||
depictNoStereo?: boolean; | ||
depictWidth?: number; | ||
depictHeight?: number; | ||
depictmcs?: string; | ||
bgColor?: string; | ||
actions?: (hover?: boolean) => React.ReactNode; | ||
actionsProps?: CardActionsProps; | ||
onClick?: () => void; | ||
} | ||
|
||
/* Generic card rendering molecule depiction with optional content and actions. */ | ||
export const MolCard = ({ | ||
children, | ||
smiles, | ||
depictNoStereo = false, | ||
depictWidth, | ||
depictHeight, | ||
depictmcs, | ||
bgColor, | ||
actions = () => undefined, | ||
actionsProps, | ||
onClick, | ||
...cardProps | ||
}: MolCardProps) => { | ||
const [hover, setHover] = useState<boolean>(false); | ||
|
||
return ( | ||
<Card | ||
{...cardProps} | ||
sx={{ bgcolor: bgColor, cursor: onClick ? "pointer" : undefined }} | ||
onClick={onClick} | ||
onMouseEnter={() => setHover(true)} | ||
onMouseLeave={() => setHover(false)} | ||
> | ||
<CardContent> | ||
<DepictMolecule | ||
height={depictHeight} | ||
mcs={depictmcs} | ||
noStereo={depictNoStereo} | ||
smiles={smiles} | ||
width={depictWidth} | ||
/> | ||
{Children.only(children)} | ||
</CardContent> | ||
<CardActions {...actionsProps} disableSpacing> | ||
{actions(hover)} | ||
</CardActions> | ||
</Card> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./MolCard"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
import type { Dispatch, SetStateAction } from "react"; | ||
import { useState } from "react"; | ||
import type { PlotParams } from "react-plotly.js"; | ||
|
||
import { | ||
Box, | ||
CircularProgress, | ||
MenuItem, | ||
Switch, | ||
TextField, | ||
Tooltip, | ||
Typography, | ||
} from "@mui/material"; | ||
import dynamic from "next/dynamic"; | ||
import type { PlotDatum } from "plotly.js-basic-dist"; | ||
|
||
import type { Molecule } from "../../features/SDFViewer"; | ||
|
||
const Plot = dynamic<PlotParams>( | ||
() => import("../../components/viz/Plot").then((mod) => mod.Plot), | ||
{ | ||
ssr: false, // Plotly only works when browser APIs are in scope | ||
loading: () => <CircularProgress size="1rem" />, | ||
}, | ||
); | ||
|
||
// Utils | ||
|
||
export const isNumber = (x: unknown): x is number => typeof x === "number"; | ||
|
||
const getPropArrayFromMolecules = (molecules: Molecule[], prop: string | null) => { | ||
if (prop === "id") { | ||
return molecules.map((molecule) => molecule.id); | ||
} | ||
return molecules.map((molecule) => (prop ? molecule.properties[prop] ?? null : null)); | ||
}; | ||
|
||
type AxisSeries = ReturnType<typeof getPropArrayFromMolecules>; | ||
|
||
const scaleToSize = (sizeaxis: AxisSeries) => { | ||
const sx = sizeaxis | ||
.filter((value): value is string => value !== null) | ||
.map((value) => Number.parseFloat(value)); | ||
const min = Math.min(...sx); | ||
const max = Math.max(...sx); | ||
|
||
const scaledSizes = sx.map((v) => (45 * (v - min)) / max + 5); | ||
|
||
if (min >= max) { | ||
return { sizes: 10 }; | ||
} | ||
|
||
return { sizes: scaledSizes, min, max }; | ||
}; | ||
|
||
const validateColours = (colouraxis: AxisSeries) => { | ||
const cx = colouraxis | ||
.filter((value): value is string => value !== null) | ||
.map((value) => Number.parseFloat(value)); | ||
|
||
const min = Math.min(...cx); | ||
const max = Math.max(...cx); | ||
|
||
if (min >= max) { | ||
return { colours: 1 }; | ||
} | ||
|
||
return { colours: colouraxis, min, max }; | ||
}; | ||
|
||
export interface ScatterPlotProps { | ||
molecules: Molecule[]; | ||
properties: string[]; | ||
selectPoints: (ids: string[]) => void; | ||
width: number; | ||
} | ||
|
||
export const ScatterPlot = ({ molecules, properties, selectPoints, width }: ScatterPlotProps) => { | ||
// const selectedPoints = selection.map((id) => molecules.findIndex((m) => m.id === id)); | ||
|
||
const [showColourBar, setShowColourBar] = useState(true); | ||
|
||
const [xprop, setXprop] = useState(properties[0]); | ||
const [yprop, setYprop] = useState(properties[1]); | ||
const [cprop, setCprop] = useState(properties[2]); | ||
const [sprop, setSprop] = useState(properties[3]); | ||
|
||
const xaxis = getPropArrayFromMolecules(molecules, xprop); | ||
const yaxis = getPropArrayFromMolecules(molecules, yprop); | ||
|
||
const colouraxis = getPropArrayFromMolecules(molecules, cprop); | ||
const sizeaxis = getPropArrayFromMolecules(molecules, sprop); | ||
|
||
const { sizes, ...sizeExtent } = scaleToSize(sizeaxis); | ||
const { colours } = validateColours(colouraxis); | ||
|
||
return ( | ||
<> | ||
<Plot | ||
config={{ | ||
modeBarButtonsToRemove: [ | ||
"resetScale2d", | ||
"hoverClosestCartesian", | ||
"hoverCompareCartesian", | ||
"toImage", | ||
"toggleSpikelines", | ||
], | ||
}} | ||
data={[ | ||
{ | ||
x: xaxis, | ||
y: yaxis, | ||
customdata: molecules.map((m) => m.id), // Add custom data for use in selection | ||
// selectedpoints: selectedPoints.length ? selectedPoints : undefined, // null or undefined? | ||
type: "scatter", | ||
mode: "markers", | ||
marker: { | ||
color: colours, | ||
size: sizes, | ||
colorscale: "Bluered", | ||
colorbar: showColourBar ? {} : undefined, | ||
}, | ||
}, | ||
]} | ||
layout={{ | ||
width, | ||
height: width, | ||
margin: { t: 10, r: 10, b: 50, l: 50 }, | ||
dragmode: "select", | ||
selectionrevision: 1, | ||
hovermode: "closest", | ||
xaxis: { title: xprop }, | ||
yaxis: { title: yprop }, | ||
}} | ||
onDeselect={() => selectPoints([])} | ||
onSelected={(event) => { | ||
// @types is wrong here, we need `?.` as points can be undefined (double click event) | ||
const points = event.points as PlotDatum[] | undefined; | ||
points?.length && selectPoints(points.map((p) => p.customdata) as string[]); | ||
}} | ||
/> | ||
<Box display="flex" gap={2}> | ||
<div> | ||
<Typography gutterBottom component="label" display="block" variant="h5"> | ||
x-axis | ||
</Typography> | ||
<AxisSelector prop={xprop} properties={properties} onPropChange={setXprop} /> | ||
</div> | ||
<div> | ||
<Typography gutterBottom component="label" display="block" variant="h5"> | ||
y-axis | ||
</Typography> | ||
<AxisSelector prop={yprop} properties={properties} onPropChange={setYprop} /> | ||
</div> | ||
<div> | ||
<Typography gutterBottom component="label" display="block" variant="h5"> | ||
colour-axis | ||
</Typography> | ||
<AxisSelector prop={cprop} properties={properties} onPropChange={setCprop} /> | ||
<Tooltip arrow title="Toggle the colour bar"> | ||
<Switch checked={showColourBar} onChange={() => setShowColourBar(!showColourBar)} /> | ||
</Tooltip> | ||
{/* <div> | ||
<em>({colourExtent.min !== undefined && `${colourExtent.min}–${colourExtent.max}`})</em> | ||
</div> */} | ||
</div> | ||
<div> | ||
<Typography gutterBottom component="label" display="block" variant="h5"> | ||
size-axis | ||
</Typography> | ||
<AxisSelector prop={sprop} properties={properties} onPropChange={setSprop} /> | ||
<div> | ||
<em>({sizeExtent.min !== undefined && `${sizeExtent.min}–${sizeExtent.max}`})</em> | ||
</div> | ||
</div> | ||
</Box> | ||
</> | ||
); | ||
}; | ||
|
||
interface AxisSelectorProps { | ||
properties: string[]; | ||
prop: string; | ||
onPropChange: Dispatch<SetStateAction<string>>; | ||
} | ||
|
||
const AxisSelector = ({ properties, prop = "", onPropChange }: AxisSelectorProps) => { | ||
return ( | ||
<TextField select value={prop} onChange={(event) => onPropChange(event.target.value)}> | ||
{properties.map((property) => ( | ||
<MenuItem key={property} value={property}> | ||
{property} | ||
</MenuItem> | ||
))} | ||
</TextField> | ||
); | ||
}; |
Oops, something went wrong.