Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add back in the motif viz #43

Merged
merged 5 commits into from
Sep 3, 2024
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
64 changes: 60 additions & 4 deletions motifstudio-web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions motifstudio-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@
"@headlessui/react": "^1.7.17",
"@monaco-editor/react": "^4.6.0",
"axios": "^1.6.2",
"color-hash": "^2.0.2",
"cytoscape": "^3.30.2",
"cytoscape-cose-bilkent": "^4.1.0",
"next": "^14.2.5",
"react": "^18.3.1",
"react-cytoscapejs": "^2.0.0",
"react-dom": "^18.3.1",
"swr": "^2.2.4"
},
Expand Down
119 changes: 119 additions & 0 deletions motifstudio-web/src/app/MotifVisualizer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import Cytoscape from "cytoscape";
import CytoscapeComponent from "react-cytoscapejs";
import COSEBilkent from "cytoscape-cose-bilkent";
import { useThrottle } from "./useDebounce";
import useSWR from "swr";
import { BASE_URL, bodiedFetcher } from "./api";
import { useRef } from "react";
import ColorHash from "color-hash";

Cytoscape.use(COSEBilkent);

export const MotifVisualizer = ({ motifSource }: { motifSource: string }) => {
// Construct the motif graph:
const debouncedQuery = useThrottle(motifSource, 1000);
let elements = useRef([]);

const colorhash = new ColorHash({
lightness: 0.5,
});

function hexHash(item: { id?: string }, opts = { seed: 0, without: [] }) {
const nodeWithoutID = { ...item, __seed: opts.seed };
delete nodeWithoutID.id;
opts.without.forEach((key) => {
delete nodeWithoutID[key];
});
return colorhash.hex(JSON.stringify(nodeWithoutID));
}

const {
data: queryData,
error: queryError,
isLoading: queryIsLoading,
} = useSWR(
[`${BASE_URL}/queries/motifs/_parse`, "", debouncedQuery],
() => bodiedFetcher(`${BASE_URL}/queries/motifs/_parse`, { host_id: "", query: debouncedQuery }),
{
onSuccess: (data) => {
// Construct the motif graph:
const motifGraph = JSON.parse(data?.motif_nodelink_json || "{}");
elements.current = [
...(motifGraph?.nodes || []).map((node) => {
return {
data: {
...node,
label: node.id,
color: hexHash(node),
},
};
}),
...(motifGraph?.links || []).map((link) => {
return {
data: {
...link,
color: link.exists ? hexHash(link, { without: ["source", "target"] }) : "#fcc",
},
directed: true,
};
}),
];
},
}
);
if (queryError) {
return <div>Error loading motif</div>;
}

if (queryIsLoading) {
return <div>Loading motif...</div>;
}

if (!queryData) {
return <div>No motif data</div>;
}

return (
<CytoscapeComponent
layout={{
name: "cose-bilkent",
animate: false,
}}
elements={[...elements.current]}
style={{ width: "100%", height: "100%", minHeight: "400px" }}
stylesheet={[
{
selector: "node",
style: {
label: "data(label)",
"text-valign": "center",
"text-halign": "center",
"background-color": "data(color)",
color: "white",
"text-outline-width": 1,
"text-outline-color": "#11479e",
shape: "roundrectangle",
width: "label",
height: "label",
// Margin:
padding: "4",
},
},
{
selector: "edge",
style: {
"curve-style": "bezier",
"target-arrow-shape": "triangle",
"target-arrow-color": "#9dbaea",
// "line-color": "#9dbaea",
"line-color": "data(color)",
"text-outline-width": 2,
"text-outline-color": "#9dbaea",
"font-size": "10px",
label: "data(label)",
},
},
]}
/>
);
};
2 changes: 1 addition & 1 deletion motifstudio-web/src/app/ResultsFetcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export function ResultsFetcher({ graph, query }: { graph: HostListing | null; qu
) : (
<div>
{queryData?.error ? (
<div className="text-red-500">{errorText}</div>
<div className="text-red-500">{errorText.toString()}</div>
) : (
<div>No results</div>
)}
Expand Down
6 changes: 3 additions & 3 deletions motifstudio-web/src/app/api.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Base URL based on dev status:
// @ts-ignore
// export const BASE_URL =
// process.env.NODE_ENV === "development" ? "http://localhost:5000" : "https://api.motifstudio.bossdb.org";
export const BASE_URL =
process.env.NODE_ENV === "development" ? "http://localhost:8000" : "https://api.motifstudio.bossdb.org";

export const BASE_URL = "https://api.motifstudio.bossdb.org";
// export const BASE_URL = "https://api.motifstudio.bossdb.org";

export const neuroglancerUrlFromHostVolumetricData = (
segmentationUri: string,
Expand Down
8 changes: 8 additions & 0 deletions motifstudio-web/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { HostListing } from "./api";
import { GraphStats } from "./GraphStats";
import { ResultsWrapper } from "./ResultsWrapper";
import { getQueryParams, updateQueryParams } from "./queryparams";
import { MotifVisualizer } from "./MotifVisualizer";

/**
* The main page of the application.
Expand Down Expand Up @@ -56,6 +57,13 @@ export default function Home() {
<GraphForm startValue={currentGraph} onGraphChange={setSelectedGraph} />
</div>
<div className="div flex w-full flex-col py-4 gap-4">
{motif ? (
<MotifVisualizer
motifSource={motif}
// graph={currentGraph}
// entities={entities}
/>
) : null}
{currentGraph ? <GraphStats graph={currentGraph} onAttributesLoaded={setEntities} /> : null}
{currentGraph ? <ResultsWrapper graph={currentGraph} query={queryText} /> : null}
</div>
Expand Down
23 changes: 22 additions & 1 deletion motifstudio-web/src/app/useDebounce.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import { useState } from "react";
import { useRef, useState } from "react";
import { useEffect } from "react";

export function useDebounce(value: any, delay: number) {
Expand All @@ -17,3 +17,24 @@ export function useDebounce(value: any, delay: number) {

return debouncedValue;
}

export function useThrottle(value: any, delay: number) {
const [throttledValue, setThrottledValue] = useState(value);
const lastValue = useRef(value);

useEffect(() => {
const handler = setTimeout(() => {
setThrottledValue(lastValue.current);
}, delay);

return () => {
clearTimeout(handler);
};
}, [value, delay]);

useEffect(() => {
lastValue.current = value;
}, [value]);

return throttledValue;
}
23 changes: 23 additions & 0 deletions server/src/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Models for the motif studio database and API."""

from typing import Any, Literal
from pydantic import BaseModel, Field

Expand Down Expand Up @@ -96,6 +97,28 @@ class EdgeCountQueryResponse(_QueryResponseBase):
edge_count: int


class MotifParseQueryRequest(_QueryRequestBase):
"""A request to parse a motif query."""

query: str = Field(
...,
description="The motif query to execute, in the DotMotif query language",
)


class MotifParseQueryResponse(_QueryResponseBase):
"""A response with the motif parse results for a host graph."""

query: str
motif_entities: list[str]
motif_edges: list[list[str]]
motif_nodelink_json: str
error: str | None = Field(
None,
description="If an error occurred, a message describing the error.",
)


class MotifCountQueryRequest(_QueryRequestBase):
"""A request to count the number of motifs in a host graph."""

Expand Down
Loading
Loading