Skip to content

Add first version of neuroglancer embedded as a library #1639

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

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
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
4 changes: 4 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,9 @@
},
"engines": {
"pnpm": "8.10.5"
},
"resolutions": {
"react": "18.2.0",
"react-dom": "18.2.0"
}
}
2 changes: 1 addition & 1 deletion frontend/packages/data-portal/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ FROM node:20.8-alpine as build
COPY . /app

WORKDIR /app
RUN --mount=type=cache,id=pnpm,target=/pnpm/store npm -g i [email protected] && pnpm i --frozen-lockfile
RUN --mount=type=cache,id=pnpm,target=/pnpm/store npm -g i [email protected] && pnpm i --frozen-lockfile --ignore-scripts

WORKDIR /app/packages/data-portal
RUN pnpm build
Expand Down
78 changes: 78 additions & 0 deletions frontend/packages/data-portal/app/components/Run/ViewerPage.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
.main-container {
margin: 0;
padding: 0;
height: 100vh;
width: 100vw;
display: flex;
flex-direction: column;
overflow: hidden;
}

.main-header {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #000;
border-bottom: 1px solid #333;
box-sizing: border-box;
flex-shrink: 0;
}

.portal-title {
margin: 0;
color: #fff;
font-size: 0.875rem;
}

.iframe-container {
height: "90vh";
width: "100%";
flex-grow: 1;
overflow: "hidden";
}

.neuroglancer-iframe {
width: 100%;
height: 100%;
border: none;
display: block;
}

.cryoet-doc-button {
min-height: 2.875rem;
background-color: transparent;
padding-left: 1rem;
padding-right: 1rem;
border: none;
font-size: 0.875rem;
font-weight: 600;
color: #fff;
}

.cryoet-doc-button:hover {
text-decoration: underline;
cursor: pointer;
}

.button-group {
display: flex;
align-items: center;
gap: 0.25rem;
margin-right: 1rem;
}

.toggle-button {
border-radius: 100px;
background: #0b68f8;
padding: 6px 12px;
border: none;
color: #fff;
font-size: 13px;
font-weight: 500;
}

.toggle-button:hover {
background-color: #0033cc;
cursor: pointer;
}
91 changes: 91 additions & 0 deletions frontend/packages/data-portal/app/components/Run/ViewerPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import "./ViewerPage.css";
import { useLocation } from "@remix-run/react";
import { currentState, NeuroglancerWrapper, updateState } from "neuroglancer";
import { useState } from "react";


// Button action for toggling layers visibility
const isAnnotation = (layer: any) => layer.type === "annotation" || layer.type === "segmentation";
const toggleVisibility = (layer: any) => !(layer.visible === undefined || layer.visible);

const toggleLayersVisibility = () => {
updateState((state) => {
for (const layer of state.neuroglancer.layers) {
layer.visible = toggleVisibility(layer);
}
return state;
})
};



const toggleAnnotations = () => {
updateState((state) => {
for (const layer of state.neuroglancer.layers) {
if (isAnnotation(layer)) {
layer.visible = toggleVisibility(layer);
}
}
return state
})
}

const hasAnnotationLayers = (state: any) => {
const root = state.neuroglancer || state;
return root.layers.some(isAnnotation);
}

const CRYOET_PORTAL_DOC_URL = "https://chanzuckerberg.github.io/cryoet-data-portal/neuroglancer_quickstart.html#neuroglancer-quickstart"
const EXAMPLE_CRYOET_HASH = "#!%7B%22dimensions%22:%7B%22x%22:%5B4.99e-10%2C%22m%22%5D%2C%22y%22:%5B4.99e-10%2C%22m%22%5D%2C%22z%22:%5B4.99e-10%2C%22m%22%5D%7D%2C%22position%22:%5B632%2C632%2C185%5D%2C%22crossSectionScale%22:3.16%2C%22projectionOrientation%22:%5B0.3826834261417389%2C0%2C0%2C0.9238795042037964%5D%2C%22projectionScale%22:1390.4%2C%22layers%22:%5B%7B%22type%22:%22image%22%2C%22source%22:%22zarr://https://files.cryoetdataportal.cziscience.com/10444/24may06a_Position_1_2/Reconstructions/VoxelSpacing4.990/Tomograms/100/24may06a_Position_1_2.zarr%22%2C%22tab%22:%22rendering%22%2C%22opacity%22:0.51%2C%22shader%22:%22#uicontrol%20invlerp%20contrast%5Cn#uicontrol%20bool%20invert_contrast%20checkbox%5Cn%5Cnfloat%20get_contrast%28%29%20%7B%5Cn%20%20return%20invert_contrast%20?%201.0%20-%20contrast%28%29%20:%20contrast%28%29%3B%5Cn%7D%5Cn%5Cnvoid%20main%28%29%20%7B%5Cn%20%20float%20outputValue%3B%5Cn%20%20outputValue%20=%20get_contrast%28%29%3B%5Cn%20%20emitGrayscale%28outputValue%29%3B%5Cn%7D%22%2C%22shaderControls%22:%7B%22contrast%22:%7B%22range%22:%5B-0.000008560898862697286%2C0.000008186889914441053%5D%2C%22window%22:%5B-0.00001023567774041112%2C0.000009861668792154887%5D%7D%7D%2C%22name%22:%2224may06a_Position_1_2%22%7D%2C%7B%22type%22:%22segmentation%22%2C%22source%22:%7B%22url%22:%22precomputed://https://files.cryoetdataportal.cziscience.com/10444/24may06a_Position_1_2/Reconstructions/VoxelSpacing4.990/NeuroglancerPrecompute/100-membrane-1.0_segmentationmask%22%2C%22transform%22:%7B%22outputDimensions%22:%7B%22x%22:%5B4.99e-10%2C%22m%22%5D%2C%22y%22:%5B4.99e-10%2C%22m%22%5D%2C%22z%22:%5B4.99e-10%2C%22m%22%5D%7D%2C%22inputDimensions%22:%7B%22x%22:%5B4.99e-10%2C%22m%22%5D%2C%22y%22:%5B4.99e-10%2C%22m%22%5D%2C%22z%22:%5B4.99e-10%2C%22m%22%5D%7D%7D%7D%2C%22tab%22:%22rendering%22%2C%22selectedAlpha%22:1%2C%22hoverHighlight%22:false%2C%22segments%22:%5B%221%22%5D%2C%22segmentDefaultColor%22:%22#a66120%22%2C%22name%22:%22100%20membrane%20segmentation%22%7D%5D%2C%22selectedLayer%22:%7B%22visible%22:true%2C%22layer%22:%2224may06a_Position_1_2%22%7D%2C%22crossSectionBackgroundColor%22:%22#000000%22%2C%22layout%22:%224panel%22%7D"


const ViewerPage = () => {
const cryoetUrl = CRYOET_PORTAL_DOC_URL;
const exampleHash = EXAMPLE_CRYOET_HASH;

const providerUrl = useLocation();
const runId = providerUrl.pathname.toString().match(/.*\/(\d+)\/$/)![1];

const [hasAnnotations, setHasAnnotations] = useState(hasAnnotationLayers(currentState()));

const updateButtons = (state: any) => {
setHasAnnotations(hasAnnotationLayers(state))
}

return (
<div className="main-container">
<header className="main-header">
<a href={cryoetUrl} target="_blank" rel="noopener noreferrer">
<button className="cryoet-doc-button">View documentation</button>
</a>
<p className="portal-title">CryoET data portal neuroglancer coming from {providerUrl.pathname.toString()}</p>
<div className="button-group">
<a href={`/runs/${runId}`}>
<button className="toggle-button"> Go back to run</button>
</a>
<button
className="toggle-button"
onClick={() => {
window.location.hash = exampleHash;
}}
>
Load example data
</button>
<button className="toggle-button" onClick={toggleLayersVisibility}>
Toggle layers visibility
</button>
{hasAnnotations &&
<button className="toggle-button" onClick={toggleAnnotations}>
Toggle annotations
</button>
}
</div>
</header>
<div className="iframe-container">
<NeuroglancerWrapper onStateChange={updateButtons}/>
</div>
</div>
);
};

export default ViewerPage;
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getNeuroglancerUrl } from 'app/utils/url'

import { Link } from './Link'
import { Tooltip } from './Tooltip'
import { useLocation } from '@remix-run/react'

export interface ViewTomogramButtonProps {
tomogramId?: string
Expand All @@ -15,7 +16,7 @@ export interface ViewTomogramButtonProps {
event: EventPayloads[Events.ViewTomogram]
neuroglancerConfig: string | null | undefined
tooltipPlacement: TooltipProps['placement']
setIsHoveringOver?: (isHoveringOver: boolean) => void
setIsHoveringOver?: (isHoveringOver: boolean) => void,
}

export function ViewTomogramButton({
Expand All @@ -34,7 +35,6 @@ export function ViewTomogramButton({
}

const enabled = tomogramId !== undefined && neuroglancerConfig != null

return (
<Tooltip
tooltip={
Expand Down Expand Up @@ -66,7 +66,8 @@ export function ViewTomogramButton({
className="min-w-[144px]"
>
<Button
href={enabled ? getNeuroglancerUrl(neuroglancerConfig) : undefined}
// href={enabled ? getNeuroglancerUrl(neuroglancerConfig) : undefined}
href={enabled ? `/view/runs/${event.runId}/#!${encodeURIComponent(neuroglancerConfig)}` : undefined}
disabled={!enabled}
LinkComponent={Link}
{...buttonProps}
Expand Down
113 changes: 113 additions & 0 deletions frontend/packages/data-portal/app/routes/view.runs.$id.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/* eslint-disable @typescript-eslint/no-throw-literal */

// import { ShouldRevalidateFunctionArgs } from '@remix-run/react'
// import { json, LoaderFunctionArgs } from '@remix-run/server-runtime'
// import { startCase, toNumber } from 'lodash-es'
// import { match } from 'ts-pattern'

// import { apolloClient, apolloClientV2 } from 'app/apollo.server'
// import { AnnotationFilter } from 'app/components/AnnotationFilter/AnnotationFilter'
// import { DepositionFilterBanner } from 'app/components/DepositionFilterBanner'
// import { DownloadModal } from 'app/components/Download'
// import { NoFilteredResults } from 'app/components/NoFilteredResults'
// import { NoTotalResults } from 'app/components/NoTotalResults'
// import { RunHeader } from 'app/components/Run'
// import { AnnotationDrawer } from 'app/components/Run/AnnotationDrawer'
// import { AnnotationTable } from 'app/components/Run/AnnotationTable'
// import { RunMetadataDrawer } from 'app/components/Run/RunMetadataDrawer'
// import { TomogramMetadataDrawer } from 'app/components/Run/TomogramMetadataDrawer'
// import { TomogramsTable } from 'app/components/Run/TomogramTable'
// import { TablePageLayout } from 'app/components/TablePageLayout'
// import { QueryParams } from 'app/constants/query'
// import { getRunById } from 'app/graphql/getRunById.server'
// import { logIfHasDiff } from 'app/graphql/getRunByIdDiffer'
// import { getRunByIdV2 } from 'app/graphql/getRunByIdV2.server'
// import { useDownloadModalQueryParamState } from 'app/hooks/useDownloadModalQueryParamState'
// import { useI18n } from 'app/hooks/useI18n'
// import { useQueryParam } from 'app/hooks/useQueryParam'
// import { useRunById } from 'app/hooks/useRunById'
// import { DownloadConfig } from 'app/types/download'
// import { shouldRevalidatePage } from 'app/utils/revalidate'

// export async function loader({ request, params }: LoaderFunctionArgs) {
// const id = params.id ? +params.id : NaN

// if (Number.isNaN(+id)) {
// throw new Response(null, {
// status: 400,
// statusText: 'ID is not defined',
// })
// }

// const url = new URL(request.url)
// const annotationsPage = +(
// url.searchParams.get(QueryParams.AnnotationsPage) ?? '1'
// )
// const depositionId = Number(url.searchParams.get(QueryParams.DepositionId))

// const [{ data: responseV1 }, { data: responseV2 }] = await Promise.all([
// getRunById({
// id,
// annotationsPage,
// depositionId: Number.isNaN(depositionId) ? undefined : depositionId,
// client: apolloClient,
// params: url.searchParams,
// }),
// getRunByIdV2({
// client: apolloClientV2,
// id,
// annotationsPage,
// params: url.searchParams,
// depositionId: Number.isNaN(depositionId) ? undefined : depositionId,
// }),
// ])

// if (responseV1.runs.length === 0) {
// throw new Response(null, {
// status: 404,
// statusText: `Run with ID ${id} not found`,
// })
// }

// try {
// logIfHasDiff(request.url, responseV1, responseV2)
// } catch (error) {
// // eslint-disable-next-line no-console, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
// console.log(`DIFF ERROR: ${(error as any)?.stack}`)
// }

// return json({
// v1: responseV1,
// v2: responseV2,
// })
// }

// export function shouldRevalidate(args: ShouldRevalidateFunctionArgs) {
// return shouldRevalidatePage({
// ...args,
// paramsToRefetch: [
// QueryParams.AuthorName,
// QueryParams.AuthorOrcid,
// QueryParams.ObjectName,
// QueryParams.ObjectId,
// QueryParams.ObjectShapeType,
// QueryParams.MethodType,
// QueryParams.AnnotationSoftware,
// QueryParams.AnnotationsPage,
// QueryParams.DepositionId,
// ],
// })
// }


import { lazy, Suspense } from 'react';

const ViewerPage = lazy(() => import("app/components/Run/ViewerPage"));

export default function RunByIdViewerPage() {
return (
<Suspense fallback={<div>Loading...</div>}>
<ViewerPage />
</Suspense>
);
}
3 changes: 3 additions & 0 deletions frontend/packages/data-portal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"clean": "rm -rf build public/build .cache/ node_modules/.cache/ app/__generated__/",
"clean:modules": "rm -rf node_modules",
"dev": "pnpm build:codegen && run-p -l 'dev:*'",
"dev:neuroglancer": "pnpm --filter neuroglancer build",
"dev:codegen": "pnpm build:codegen -w",
"dev:remix": "remix dev --manual -c 'node --loader ts-node/esm --watch-path server.ts --watch server.ts'",
"dev:tcm": "pnpm build:tcm -w",
Expand Down Expand Up @@ -74,8 +75,10 @@
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"morgan": "^1.10.0",
"neuroglancer": "workspace:*",
"next-mdx-remote": "^4.4.1",
"number-to-words": "^1.2.4",
"pako": "^2.1.0",
"pretty-bytes": "^6.1.1",
"prop-types": "^15.8.1",
"react": "^18.2.0",
Expand Down
2 changes: 2 additions & 0 deletions frontend/packages/data-portal/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import compression from 'compression'
import express, { NextFunction, Request, Response } from 'express'
import fs from 'fs'
import morgan from 'morgan'
import path from 'path';
import sourceMapSupport from 'source-map-support'

import { ServerContext } from 'app/types/context'
Expand Down Expand Up @@ -86,6 +87,7 @@ async function main() {
// Everything else (like favicon.ico) is cached for an hour. You may want to be
// more aggressive with this caching.
app.use(express.static('public', { maxAge: '1h' }))
app.use("/neuroglancer", express.static(path.join('..', 'neuroglancer', 'dist')))

app.use(morgan('tiny'))

Expand Down
5 changes: 4 additions & 1 deletion frontend/packages/data-portal/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
"strict": true,
"target": "esnext",
"incremental": true,
"types": ["@testing-library/jest-dom"]
"types": ["@testing-library/jest-dom"],
"paths": {
"neuroglancer": ["../neuroglancer/dist"]
}
},
"include": ["*.d.ts", "*.ts", "app/**/*.ts", "app/**/*.tsx", "e2e/**/*.ts"],
"exclude": ["node_modules"]
Expand Down
Loading