Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions drc-portals/app/data/c2m2/graph/api/pathway/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Session } from "neo4j-driver";
import { NextRequest } from "next/server";

import { UNIQUE_TO_GENERIC_REL } from "@/lib/neo4j/constants";
import {
createPathwaySearchAllPathsCypher,
createUpperPageBoundCypher,
Expand All @@ -19,7 +20,7 @@ import {
RelationshipResult,
TreeParseResult,
} from "@/lib/neo4j/types";
import { parsePathwayTree } from "@/lib/neo4j/utils";
import { isRelationshipResult, parsePathwayTree } from "@/lib/neo4j/utils";

const MAX_LIMIT = 1000;
const MAX_PAGE_SIBLINGS = 9;
Expand Down Expand Up @@ -172,9 +173,20 @@ export async function POST(request: NextRequest) {
);
}),
]);
const paths: PathwaySearchResultRow[] = pathwaySearchResult.map((record) =>
Object.values(record.toObject())
);
const paths: PathwaySearchResultRow[] = pathwaySearchResult
.map((record) => Object.values(record.toObject()))
.map((val) =>
val.map((column) => {
if (isRelationshipResult(column)) {
return {
...column,
type: UNIQUE_TO_GENERIC_REL.get(column.type) || "Unkown", // Transform the unique rels back to their generic format for UI presentation
};
} else {
return column;
}
})
);
const upperPageBound = upperPageBoundResult.toObject().upperPageBound;

// If we reach the end of the table, we can fix the lower bound to maintain a constant number of page items
Expand Down
2 changes: 1 addition & 1 deletion drc-portals/app/data/documentation/markdown/GQI.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ The UI is a clean, intuitive, interactive canvas with minimal control tools. The

<div class="prose my-2" style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
<div><img src="/img/graph/GQI_docs_labeled_interface.png" alt="Labeled Interface"/></div>
<div>*__Figure 5.__ Clean, intuitive, minimalistic graph query user interface. The canvas shows 1. Input field, a collapsible toolbar, including 2. Start over, 3. Export Cypher 4. Download Pathway, 5. Upload Pathway, 6. Help, 7. Zoom In, 8. Zoom Out and 9. Fit Graph.*</div>
<div>*__Figure 5.__ Clean, intuitive, minimalistic graph query user interface. The canvas shows 1. Input field, a collapsible toolbar, including 2. Start over, 2. Stop Loading 4. Export Cypher 5. Download Pathway, 6. Upload Pathway, 7. Export PNG, 8. Help 9. Zoom In, 10. Zoom Out and 11. Fit Graph.*</div>
</div>

The 'Expand' option will cue the user to explore the possible relationships based on the underlying schema and data availability for the entity/search path. With the 'Filter' option, one can subset the data by instantiating a specific value to a class object (ex, asthma for the 'Disease' class). With the 'Prune' option, one can edit the search path by removing a node(s) and their relationship(s).
Expand Down
20 changes: 11 additions & 9 deletions drc-portals/app/data/graph/help/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,33 @@ You can view an interactive version of the C2M2 Graph Schema [here](/data/graph/

## Quick Start Tips

### __1. Start with a Keyword__
### **1. Start with a Keyword**

Enter a keyword (e.g., human, blood, asthma, RNA-seq, TP53, aspirin) to search ontology-encoded metadata, such as species, anatomy, disease/phenotype, gene, compound, and assay, to discover entities like Subject, Biosample, and File.

![Menu Options](/img/graph/GQI_carousel_1.png)

### __2. Build as You Explore__
### **2. Build as You Explore**

Use node menu options (point the cursor/mouse to a node) — __Expand__ (to explore related entities), __Filter__ (to narrow by values), and __Prune__ (to trim paths) — to build your query one step at a time.
Use node menu options (point the cursor/mouse to a node) — **Expand** (to explore related entities), **Filter** (to narrow by values), and **Prune** (to trim paths) — to build your query one step at a time.

![Menu Options](/img/graph/GQI_docs_streamlined.png)
![Menu Options 1](/img/graph/GQI_docs_streamlined_1.png)
![Menu Options 2](/img/graph/GQI_docs_streamlined_2.png)

### __3. View & Download__
### **3. View & Download**

See results in __Tabular__ or __Network View__, and export them as JSON to share, revisit, or analyze further.
See results in **Tabular** or **Network View**, and export them as JSON to share, revisit, or analyze further.

![Tabular View](/img/graph/GQI_docs_tabular_view.png)

<Link href="/data/graph">
<Button
sx={{textTransform: "uppercase"}}
sx={{ textTransform: "uppercase" }}
color="primary"
variant="contained"
startIcon={<Icon path={mdiArrowLeft} size={1} />}>
BACK TO SEARCH
startIcon={<Icon path={mdiArrowLeft} size={1} />}
>
BACK TO SEARCH
</Button>
</Link>

Expand Down
109 changes: 84 additions & 25 deletions drc-portals/components/prototype/components/GraphPathway.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ import {
PathwaySearchContextProps,
} from "../contexts/PathwaySearchContext";
import {
ColumnData,
ConnectionMenuItem,
PathwaySearchNode,
PathwaySearchNodeData,
} from "../interfaces/pathway-search";
import { PathwaySearchElement } from "../types/pathway-search";
import {
Expand Down Expand Up @@ -245,7 +247,35 @@ export default function GraphPathway() {

const handleReturnBtnClick = () => {
setShowResults(false);
};
}

const handleVisibilityChange = useCallback((columns: ColumnData[]) => {
if (tree !== undefined) {
const newElements = [
...searchElements.map((element) => {
if (isPathwaySearchEdgeElement(element)) {
return deepCopyPathwaySearchEdge(element);
} else {
for (let col of columns) {
if (col.key === element.data.id) {
return deepCopyPathwaySearchNode(
element,
{ visible: col.visible },
[col.visible ? "solid" : "transparent"],
[col.visible ? "transparent" : "solid"]
);
}
}
// This should never happen
console.warn(`Could not find corresponding column for node element with ID: ${element.data.id}`)
return deepCopyPathwaySearchNode(element);
}
}),
];
setSearchElements(newElements);
setTree(createTree(newElements));
}
}, [tree]);

const handleSearchBarSubmit = (cvTerm: NodeResult) => {
// TODO: Direct node results *should* always have at least one label since they are required on all Neo4j nodes, so maybe this check
Expand All @@ -263,16 +293,14 @@ export default function GraphPathway() {
return;
}

const initialNode = createPathwaySearchNode(
{
id: cvTerm.uuid,
dbLabel: cvTerm.labels[0],
props: {
name: [cvTerm.properties.name],
},
const initialNode = createPathwaySearchNode({
id: cvTerm.uuid,
dbLabel: cvTerm.labels[0],
visible: true,
props: {
name: [cvTerm.properties.name],
},
["path-element"]
);
});

let initialTree: PathwayNode;
try {
Expand Down Expand Up @@ -368,13 +396,11 @@ export default function GraphPathway() {

const candidateSearchElements = [
// The newly connected node
createPathwaySearchNode(
{
id: item.nodeId,
dbLabel: item.label,
},
["path-element"]
),
createPathwaySearchNode({
id: item.nodeId,
dbLabel: item.label,
visible: true,
}),
...fallbackElements,
// And the newly connected edge
createPathwaySearchEdge(
Expand All @@ -386,9 +412,7 @@ export default function GraphPathway() {
item.direction === Direction.OUTGOING ? item.target : item.source,
type: item.type,
},
item.direction === Direction.INCOMING
? ["source-arrow-only", "path-element"]
: ["path-element"]
item.direction === Direction.INCOMING ? ["source-arrow-only"] : []
),
];
updateCounts(candidateSearchElements, loadingElements, fallbackElements);
Expand Down Expand Up @@ -531,6 +555,37 @@ export default function GraphPathway() {
}
}, [searchElements]);

const handleShowHide = useCallback(
(node: NodeSingular) => {
const nodeData: PathwaySearchNodeData = node.data();
const nodeClasses = node.classes();
const newVisibility = !nodeData.visible;
const newNode: PathwaySearchNode = {
classes: nodeClasses
// Don't duplicate the solid/transparent classes, and remove the "hovered" class so the node immediately becomes transparent if hidden
.filter(cls => !["solid", "transparent", "hovered"].includes(cls))
.concat([newVisibility ? "solid" : "transparent"]),
data: {
...nodeData,
visible: newVisibility,
},
};
const newElements = [
newNode,
...searchElements
.filter((el) => el.data.id !== nodeData.id)
.map((element) =>
isPathwaySearchEdgeElement(element)
? deepCopyPathwaySearchEdge(element)
: deepCopyPathwaySearchNode(element)
),
];
setSearchElements(newElements);
setTree(createTree(newElements));
},
[searchElements]
);

// On the initial load of the page, populate the pathway with an initial node if one exists in the query params
useEffect(() => {
const id = searchParams.get("id");
Expand All @@ -556,14 +611,17 @@ export default function GraphPathway() {
}, []);

return (
<Box sx={{
height: "640px",
width: "100%",
position: "relative",
}}>
<Box
sx={{
height: "670px",
width: "100%",
position: "relative",
}}
>
{showResults && tree !== undefined ? (
<GraphPathwayResults
tree={tree}
onVisibilityChange={handleVisibilityChange}
onReturnBtnClick={handleReturnBtnClick}
/>
) : (
Expand All @@ -576,6 +634,7 @@ export default function GraphPathway() {
onPruneSelected={handlePruneSelected}
onPruneConfirm={handlePruneConfirm}
onPruneCancel={handlePruneCancel}
onShowHideSelected={handleShowHide}
onDownload={handleExport}
onUpload={handleImport}
onCopyCypher={handleCopyCypher}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@ import TableViewSkeleton from "./PathwayResults/TableViewSkeleton";

interface GraphPathwayResultsProps {
tree: PathwayNode;
onVisibilityChange: (columns: ColumnData[]) => void;
onReturnBtnClick: () => void;
}

export default function GraphPathwayResults(
cmpProps: GraphPathwayResultsProps
) {
const { tree, onReturnBtnClick } = cmpProps;
const { tree, onVisibilityChange, onReturnBtnClick } = cmpProps;
const [paths, setPaths] = useState<PathwaySearchResultRow[]>([]);
const [page, setPage] = useState<number>(1);
const [lowerPageBound, setLowerPageBound] = useState<number>(Math.max(page - 5, 1));
Expand Down Expand Up @@ -79,6 +80,10 @@ export default function GraphPathwayResults(
return { data: await response.json(), status: response.status };
};

const handleReturnBtnClick = () => {
onReturnBtnClick()
}

const handlePageChange = useCallback(
async (newPage: number) => {
setLoading(true);
Expand Down Expand Up @@ -167,7 +172,7 @@ export default function GraphPathwayResults(
[tree, limit, page, columns]
);

const handleColumnChange = useCallback(
const handleColumnPropertyChange = useCallback(
async (changedColumn: number, changes: Partial<ColumnData>) => {
try {
const newColumns = columns.map((col, idx) =>
Expand Down Expand Up @@ -200,6 +205,10 @@ export default function GraphPathwayResults(
[tree, limit, page, columns, orderBy, order]
);

const handleColumnVisibilityChange = (columns: ColumnData[]) => {
onVisibilityChange(columns);
}

const handleDownloadAllClicked = useCallback(async () => {
try {
const columnData = orderBy === undefined ? undefined : columns[orderBy];
Expand Down Expand Up @@ -233,10 +242,13 @@ export default function GraphPathwayResults(
};

useEffect(() => {
const newColumns = getColumnDataFromTree(tree)
const columnData =
orderBy === undefined ? undefined : newColumns[orderBy];
setLoading(true);
setColumns(getColumnDataFromTree(tree));
setColumns(newColumns);
Promise.all([
getPathwaySearchResults(tree, page, limit),
getPathwaySearchResults(tree, page, limit, columnData?.key, columnData?.displayProp, order),
]).then(([searchResult]) => {
setLoading(false);
setPaths(searchResult.data.paths);
Expand Down Expand Up @@ -266,7 +278,7 @@ export default function GraphPathwayResults(
lowerPageBound={lowerPageBound}
upperPageBound={upperPageBound}
columns={columns}
onReturnBtnClick={onReturnBtnClick}
onReturnBtnClick={handleReturnBtnClick}
/>
) : (
<TableView
Expand All @@ -278,11 +290,12 @@ export default function GraphPathwayResults(
order={order}
orderBy={orderBy}
columns={columns}
onReturnBtnClick={onReturnBtnClick}
onReturnBtnClick={handleReturnBtnClick}
onPageChange={handlePageChange}
onLimitChange={handleLimitChange}
onOrderByChange={handleOrderByChange}
onColumnChange={handleColumnChange}
onColumnPropertyChange={handleColumnPropertyChange}
onColumnVisibilityChange={handleColumnVisibilityChange}
onDownloadAll={handleDownloadAllClicked}
></TableView>
)}
Expand All @@ -291,7 +304,7 @@ export default function GraphPathwayResults(
<PathwayResultTabPanel value={1}>
<GraphView
paths={paths}
onReturnBtnClick={onReturnBtnClick}
onReturnBtnClick={handleReturnBtnClick}
></GraphView>
</PathwayResultTabPanel>
</Tabs>
Expand Down
Loading