diff --git a/.gitignore b/.gitignore
index a547bf3..117dc86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,4 @@ dist-ssr
*.njsproj
*.sln
*.sw?
+.claude
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..8c6d234
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,71 @@
+# binding-sim-edu — Claude Code Guidelines
+
+An educational web app for exploring molecular binding simulations, built for the Allen Institute for Cell Science. Runs interactive binding simulations via the Simularium viewer, guides students through experiments, and displays analysis results (concentration plots, equilibrium graphs).
+
+---
+
+## Quick Reference
+
+```
+binding-sim-edu/
+├── CLAUDE.md <- this file
+├── CONTEXT_ORGANIZATION.md <- context/state architecture docs
+├── src/
+│ ├── components/ <- UI components
+│ ├── hooks/ <- React hooks (incl. useSimulationContext.ts)
+│ ├── simulation/ <- simulation engine logic
+│ ├── constants/ <- app constants
+│ ├── content/ <- module content / copy
+│ ├── types/ <- shared TypeScript types
+│ └── utils/ <- pure utility functions
+└── .claude/skills/ <- Claude Code skills
+```
+
+---
+
+## State Architecture
+
+Context is split into three providers — see `CONTEXT_ORGANIZATION.md` for full details:
+
+- **SimulariumUiContext** — page, module, section, viewport type, quiz, progression, completed modules
+- **SimulariumSimulationContext** — playback, controller, trajectory, concentrations, agents, handlers
+- **SimulariumAnalysisContext** — recorded concentrations, analysis reset
+
+Always import via the typed hooks, not `useContext` directly:
+```tsx
+import { useSimulariumUi, useSimulariumSimulation, useSimulariumAnalysis } from "../hooks/useSimulationContext";
+```
+
+---
+
+## Build Commands
+
+| What | Command |
+| ---------- | ------------------- |
+| Dev server | `bun run dev` |
+| Build | `bun run build` |
+| Lint | `bun run lint` |
+| Type check | `bunx tsc --noEmit` |
+
+---
+
+## Code Standards
+
+### General
+- Prefer editing existing files over creating new ones
+- No speculative abstractions — build for what's needed now
+- No commented-out code
+- No `console.log` / debug prints left in production code
+- Destructured keys in alphabetical order (imports, params, props)
+
+### TypeScript / React
+- All public APIs must have explicit types
+- Prefer `interface` over `type` for object shapes
+- No `any` — use `unknown` + narrowing if type is genuinely unknown
+- Components own their props types — don't scatter them into shared types files
+- Custom hooks for shared stateful logic, plain functions for shared pure logic
+- Co-locate component, styles, and tests in the same directory
+
+### Import order
+External libs → internal aliases → relative imports, each group alphabetical.
+
diff --git a/docs/CONTEXT_ORGANIZATION.md b/docs/CONTEXT_ORGANIZATION.md
new file mode 100644
index 0000000..96bf4b4
--- /dev/null
+++ b/docs/CONTEXT_ORGANIZATION.md
@@ -0,0 +1,80 @@
+# Context Organization
+
+The app context has been organized into three separate contexts for better separation of concerns and easier state management.
+
+## Contexts
+
+### SimulariumUiContext
+Contains UI-related state:
+- `page`, `setPage` - Current page number
+- `module`, `setModule` - Current module
+- `progressionElement` - Current progression element for hints
+- `quizQuestion` - Current quiz question
+- `section` - Current section
+- `viewportType`, `setViewportType` - Lab vs Simulation view
+- `completedModules`, `addCompletedModule` - Track module completion
+- `resetAllState()` - Reset all application state
+
+### SimulariumSimulationContext
+Contains simulation-related state:
+- `isPlaying`, `setIsPlaying` - Playback state
+- `simulariumController` - Viewer controller
+- `trajectoryName` - Current trajectory
+- `timeFactor`, `timeUnit` - Time settings
+- `viewportSize`, `setViewportSize` - Viewer dimensions
+- `productName`, `adjustableAgentName` - Agent info
+- `currentProductionConcentration`, `fixedAgentStartingConcentration`, `maxConcentration` - Concentration values
+- `getAgentColor()` - Color mapping
+- `handleMixAgents()`, `handleStartExperiment()`, `handleTimeChange()`, `handleTrajectoryChange()` - Handlers
+
+### SimulariumAnalysisContext
+Contains analysis and recorded data:
+- `recordedConcentrations` - User-recorded concentration values
+- `resetAnalysisState()` - Clear recorded data without resetting UI
+
+## Usage
+
+### Using the Hooks
+
+Import the hooks instead of using `useContext` directly:
+
+```tsx
+import {
+ useSimulariumUi,
+ useSimulariumSimulation,
+ useSimulariumAnalysis,
+} from "../hooks/useSimulationContext";
+
+const MyComponent = () => {
+ const { page, setPage } = useSimulariumUi();
+ const { isPlaying, setIsPlaying } = useSimulariumSimulation();
+ const { recordedConcentrations } = useSimulariumAnalysis();
+
+ // Use the values...
+};
+```
+
+### Using the Reset Button
+
+A `ResetButton` component is available for resetting state:
+
+```tsx
+import ResetButton from "../shared/ResetButton";
+
+// Reset all state (UI + simulation + analysis)
+
+
+// Reset only analysis data
+
+
+// Custom label
+Start Over
+```
+
+## Benefits
+
+1. **Clearer separation of concerns** - UI, simulation, and analysis state are separate
+2. **Easier to reset** - Individual contexts can be reset without affecting others
+3. **Better performance** - Components only re-render when their specific context changes
+4. **Type safety** - Each context has its own strongly-typed interface
+5. **Cleaner imports** - Use hooks instead of context + useContext everywhere
diff --git a/src/App.tsx b/src/App.tsx
index e44214d..307ab41 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -33,9 +33,17 @@ import RightPanel from "./components/main-layout/RightPanel";
import ReactionDisplay from "./components/main-layout/ReactionDisplay";
import ContentPanelTimer from "./components/main-layout/ContentPanelTimer";
import content, { FIRST_PAGE, moduleNames } from "./content";
-import { DEFAULT_VIEWPORT_SIZE, LIVE_SIMULATION_NAME } from "./constants";
+import {
+ DEFAULT_VIEWPORT_SIZE,
+ LIVE_SIMULATION_NAME,
+ ProgressionElement,
+} from "./constants";
import CenterPanel from "./components/main-layout/CenterPanel";
-import { SimulariumContext } from "./simulation/context";
+import {
+ SimulariumAnalysisContext,
+ SimulariumSimulationContext,
+ SimulariumUiContext,
+} from "./simulation/context";
import NavPanel from "./components/main-layout/NavPanel";
import AdminUI from "./components/AdminUi";
import { ProductOverTimeTrace } from "./components/plots/types";
@@ -261,6 +269,9 @@ function App() {
if (isLastFrame) {
simulariumController.gotoTime(0);
}
+ setCurrentProductConcentrationArray((prev) =>
+ prev.length === 0 ? [0] : prev,
+ );
simulariumController.resume();
} else {
simulariumController.pause();
@@ -322,7 +333,7 @@ function App() {
[currentProductConcentrationArray, productOverTimeTraces],
);
- const setExperiment = () => {
+ const setExperiment = useCallback(() => {
setIsPlaying(false);
setCurrentView(ViewType.Simulation);
const activeAgents = simulationData.getActiveAgents(currentModule);
@@ -335,7 +346,7 @@ function App() {
setTimeFactor(LiveSimulationData.INITIAL_TIME_FACTOR);
setInputConcentration(concentrations);
setLiveConcentration(concentrations);
- };
+ }, [simulationData, currentModule, clientSimulator]);
const handleMixAgents = useCallback(() => {
if (clientSimulator) {
@@ -512,7 +523,8 @@ function App() {
clearAllAnalysisState,
]);
- const { section } = content[currentModule][page];
+ const pageContent = content[currentModule][page];
+ const { section } = pageContent;
useEffect(() => {
if (section === Section.Experiment) {
setTimeFactor(LiveSimulationData.DEFAULT_TIME_FACTOR);
@@ -523,93 +535,112 @@ function App() {
// User input handlers
- const addCompletedModule = (module: Module) => {
+ const addCompletedModule = useCallback((module: Module) => {
setCompletedModules((prev: Set) => new Set(prev).add(module));
- };
+ }, []);
- const setModule = (module: Module) => {
- setPage(FIRST_PAGE[module]);
- clearAllAnalysisState();
- setCurrentModule(module);
- setIsPlaying(false);
- // the first module is the only one that starts with the lab view
- if (module === Module.A_B_AB) {
- setCurrentView(ViewType.Lab);
- } else {
- setCurrentView(ViewType.Simulation);
- }
- };
+ const setModule = useCallback(
+ (module: Module) => {
+ setPage(FIRST_PAGE[module]);
+ clearAllAnalysisState();
+ setCurrentModule(module);
+ setIsPlaying(false);
+ // the first module is the only one that starts with the lab view
+ if (module === Module.A_B_AB) {
+ setCurrentView(ViewType.Lab);
+ } else {
+ setCurrentView(ViewType.Simulation);
+ }
+ },
+ [clearAllAnalysisState],
+ );
- const handleStartExperiment = () => {
+ const handleStartExperiment = useCallback(() => {
clearAllAnalysisState();
setExperiment();
setPage(page + 1);
- };
+ }, [clearAllAnalysisState, page, setExperiment]);
// trigger when the trajectory data has been sent by the viewer
- const handleTrajectoryChange = (trajectoryInfo: TrajectoryFileInfo) => {
- setTrajectoryName(trajectoryInfo.trajectoryTitle || "");
- if (trajectoryInfo.trajectoryTitle === LIVE_SIMULATION_NAME) {
- // 2d trajectory
- // switch to orthographic camera
- simulariumController.setCameraType(true);
- setPreComputedTrajectoryPlotData(undefined);
- setFinalTime(-1);
- } else {
- // 3d trajectory
- // switch to perspective camera
- simulariumController.setCameraType(false);
- setTimeFactor(trajectoryInfo.timeStepSize);
- setFinalTime(
- trajectoryInfo.totalSteps * trajectoryInfo.timeStepSize,
- );
- }
- };
+ const handleTrajectoryChange = useCallback(
+ (trajectoryInfo: TrajectoryFileInfo) => {
+ setTrajectoryName(trajectoryInfo.trajectoryTitle || "");
+ if (trajectoryInfo.trajectoryTitle === LIVE_SIMULATION_NAME) {
+ // 2d trajectory
+ // switch to orthographic camera
+ simulariumController.setCameraType(true);
+ setPreComputedTrajectoryPlotData(undefined);
+ setFinalTime(-1);
+ } else {
+ // 3d trajectory
+ // switch to perspective camera
+ simulariumController.setCameraType(false);
+ setTimeFactor(trajectoryInfo.timeStepSize);
+ setFinalTime(
+ trajectoryInfo.totalSteps * trajectoryInfo.timeStepSize,
+ );
+ }
+ },
+ [simulariumController],
+ );
- const handleTimeChange = (timeData: TimeData) => {
- const { time } = timeData;
- setTime(time);
- // can't use isLastFrame here because the time is not updated
- // in state yet
- if (finalTime > 0 && time >= finalTime - timeFactor && isPlaying) {
- setIsPlaying(false);
- }
- let concentrations: CurrentConcentration = {};
- let previousData = currentProductConcentrationArray;
-
- if (preComputedPlotDataManager) {
- if (timeData.time === 0) {
- // for the 3D trajectory,
- // we want to reset the data when we loop
- previousData = [];
+ const handleTimeChange = useCallback(
+ (timeData: TimeData) => {
+ const { time } = timeData;
+ setTime(time);
+ // can't use isLastFrame here because the time is not updated
+ // in state yet
+ if (finalTime > 0 && time >= finalTime - timeFactor && isPlaying) {
+ setIsPlaying(false);
}
- preComputedPlotDataManager.update(timeData.time);
- concentrations =
- preComputedPlotDataManager.getCurrentConcentrations();
- } else if (clientSimulator) {
- concentrations = clientSimulator.getCurrentConcentrations(
- productName,
- ) as CurrentConcentration;
- }
- const productConcentration = concentrations[productName];
- if (productConcentration !== undefined) {
- const newData = [...previousData, productConcentration];
- setCurrentProductConcentrationArray(newData);
- }
- setLiveConcentration(concentrations);
- if (timeData.time % 10 === 0 && clientSimulator) {
- const { numberBindEvents, numberUnBindEvents } =
- clientSimulator.getEvents();
- setBindingEventsOverTime([
- ...bindingEventsOverTime,
- numberBindEvents,
- ]);
- setUnBindingEventsOverTime([
- ...unBindingEventsOverTime,
- numberUnBindEvents,
- ]);
- }
- };
+ let concentrations: CurrentConcentration = {};
+ let previousData = currentProductConcentrationArray;
+
+ if (preComputedPlotDataManager) {
+ if (timeData.time === 0) {
+ // for the 3D trajectory,
+ // we want to reset the data when we loop
+ previousData = [];
+ }
+ preComputedPlotDataManager.update(timeData.time);
+ concentrations =
+ preComputedPlotDataManager.getCurrentConcentrations();
+ } else if (clientSimulator) {
+ concentrations = clientSimulator.getCurrentConcentrations(
+ productName,
+ ) as CurrentConcentration;
+ }
+ const productConcentration = concentrations[productName];
+ if (productConcentration !== undefined && isPlaying) {
+ const newData = [...previousData, productConcentration];
+ setCurrentProductConcentrationArray(newData);
+ }
+ setLiveConcentration(concentrations);
+ if (timeData.time % 10 === 0 && clientSimulator) {
+ const { numberBindEvents, numberUnBindEvents } =
+ clientSimulator.getEvents();
+ setBindingEventsOverTime([
+ ...bindingEventsOverTime,
+ numberBindEvents,
+ ]);
+ setUnBindingEventsOverTime([
+ ...unBindingEventsOverTime,
+ numberUnBindEvents,
+ ]);
+ }
+ },
+ [
+ finalTime,
+ timeFactor,
+ isPlaying,
+ currentProductConcentrationArray,
+ preComputedPlotDataManager,
+ clientSimulator,
+ productName,
+ bindingEventsOverTime,
+ unBindingEventsOverTime,
+ ],
+ );
const handleFinishInputConcentrationChange = (
name: string,
@@ -635,11 +666,11 @@ function App() {
}, 3000);
};
- const handleSwitchView = () => {
+ const handleSwitchView = useCallback(() => {
setCurrentView((prevView) =>
prevView === ViewType.Lab ? ViewType.Simulation : ViewType.Lab,
);
- };
+ }, []);
const handleRecordEquilibrium = () => {
if (!clientSimulator) {
@@ -708,148 +739,207 @@ function App() {
const { totalMainContentPages } = useModule(currentModule);
const lastPageOfExperiment = page === totalMainContentPages;
+ const uiContextValue = useMemo(
+ () => ({
+ addCompletedModule,
+ completedModules,
+ module: currentModule,
+ page,
+ progressionElement: (pageContent.progressionElement ??
+ "") as ProgressionElement,
+ quizQuestion: pageContent.quizQuestion || "",
+ resetAllState: totalReset,
+ section: pageContent.section,
+ setModule,
+ setPage,
+ setViewportType: handleSwitchView,
+ viewportType: currentView,
+ }),
+ [
+ addCompletedModule,
+ completedModules,
+ currentModule,
+ currentView,
+ handleSwitchView,
+ page,
+ pageContent,
+ setModule,
+ setPage,
+ totalReset,
+ ],
+ );
+
+ const simulationContextValue = useMemo(
+ () => ({
+ adjustableAgentName,
+ currentProductionConcentration: liveConcentration[productName] || 0,
+ fixedAgentStartingConcentration:
+ inputConcentration[AgentName.A] || 0,
+ getAgentColor: simulationData.getAgentColor,
+ handleMixAgents,
+ handleStartExperiment,
+ handleTimeChange,
+ handleTrajectoryChange,
+ isPlaying,
+ maxConcentration: simulationData.getMaxConcentration(currentModule),
+ productName,
+ setIsPlaying,
+ setViewportSize,
+ simulariumController,
+ timeFactor,
+ timeUnit: simulationData.timeUnit,
+ trajectoryName,
+ viewportSize,
+ }),
+ [
+ adjustableAgentName,
+ currentModule,
+ handleMixAgents,
+ handleStartExperiment,
+ handleTimeChange,
+ handleTrajectoryChange,
+ inputConcentration,
+ isPlaying,
+ liveConcentration,
+ productName,
+ setIsPlaying,
+ setViewportSize,
+ simulariumController,
+ simulationData,
+ timeFactor,
+ trajectoryName,
+ viewportSize,
+ ],
+ );
+
+ const analysisContextValue = useMemo(
+ () => ({
+ recordedConcentrations: recordedInputConcentration,
+ resetAnalysisState: clearAllAnalysisState,
+ }),
+ [clearAllAnalysisState, recordedInputConcentration],
+ );
+
return (
<>
-
-
- }
- content={
-
- }
- header={
-
- }
- landingPage={
-
- }
- leftPanel={
-
+
+
+
}
- handleFinishInputConcentrationChange={
- handleFinishInputConcentrationChange
+ content={
+
}
- bindingEventsOverTime={bindingEventsOverTime}
- unbindingEventsOverTime={
- unBindingEventsOverTime
+ header={
+
}
- adjustableAgent={adjustableAgentName}
- />
- }
- reactionPanel={
-
- }
- rightPanel={
-
}
- productOverTimeTraces={productOverTimeTraces}
- currentProductConcentrationArray={
- currentProductConcentrationArray
+ leftPanel={
+
}
- handleRecordEquilibrium={
- handleRecordEquilibrium
+ reactionPanel={
+
}
- currentAdjustableAgentConcentration={
- inputConcentration[adjustableAgentName] || 0
+ rightPanel={
+
}
- equilibriumData={{
- inputConcentrations:
- recordedInputConcentration,
- reactantConcentrations:
- recordedReactantConcentrations,
- productConcentrations:
- productEquilibriumConcentrations,
- timeToEquilibrium: timeToReachEquilibrium,
- colors: dataColors,
- kd: simulationData.getKd(currentModule),
- }}
- equilibriumFeedback={equilibriumFeedback}
+ section={pageContent.section}
+ layout={pageContent.layout}
+ />
+
- }
- section={content[currentModule][page].section}
- layout={content[currentModule][page].layout}
- />
-
-
+
+
+
>
);
diff --git a/src/components/AdminUi.tsx b/src/components/AdminUi.tsx
index 93bf59c..9d97233 100644
--- a/src/components/AdminUi.tsx
+++ b/src/components/AdminUi.tsx
@@ -1,8 +1,8 @@
-import React, { useContext, useEffect } from "react";
+import React, { useEffect } from "react";
import Slider from "./shared/Slider";
import { BG_DARK, LIGHT_GREY } from "../constants/colors";
-import { SimulariumContext } from "../simulation/context";
+import { useSimulariumUi } from "../hooks/useSimulationContext";
import { InputNumber, SliderSingleProps } from "antd";
import { zStacking } from "../constants/z-stacking";
import { Module } from "../types";
@@ -18,7 +18,7 @@ const AdminUI: React.FC = ({
setTimeFactor,
totalPages,
}) => {
- const { page, setPage, module, setModule } = useContext(SimulariumContext);
+ const { page, setPage, module, setModule } = useSimulariumUi();
const [visible, setVisible] = React.useState(false);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
diff --git a/src/components/LabView.tsx b/src/components/LabView.tsx
index 8137709..70a1fbe 100644
--- a/src/components/LabView.tsx
+++ b/src/components/LabView.tsx
@@ -1,6 +1,9 @@
import Rainbow from "rainbowvis.js";
-import { useContext, useMemo } from "react";
-import { SimulariumContext } from "../simulation/context";
+import { useMemo } from "react";
+import {
+ useSimulariumSimulation,
+ useSimulariumUi,
+} from "../hooks/useSimulationContext";
import Cuvette from "./icons/Cuvette";
import styles from "./labview.module.css";
import classNames from "classnames";
@@ -12,11 +15,10 @@ const LabView: React.FC = () => {
const {
currentProductionConcentration,
maxConcentration,
- page,
getAgentColor,
productName,
- module,
- } = useContext(SimulariumContext);
+ } = useSimulariumSimulation();
+ const { page, module } = useSimulariumUi();
const color = getAgentColor(productName);
const colorGradient = useMemo(() => {
const rainbow = new Rainbow();
diff --git a/src/components/MixButton.tsx b/src/components/MixButton.tsx
index 430448d..f7cb79a 100644
--- a/src/components/MixButton.tsx
+++ b/src/components/MixButton.tsx
@@ -1,13 +1,13 @@
-import React, { useContext } from "react";
+import React from "react";
-import { SimulariumContext } from "../simulation/context";
+import { useSimulariumSimulation } from "../hooks/useSimulationContext";
import { TertiaryButton } from "./shared/ButtonLibrary";
import { MIX_AGENTS_ID } from "../constants";
import ProgressionControl from "./shared/ProgressionControl";
import style from "./start-experiment.module.css";
const MixButton: React.FC = () => {
- const { handleMixAgents } = useContext(SimulariumContext);
+ const { handleMixAgents } = useSimulariumSimulation();
return (
diff --git a/src/components/PageIndicator.tsx b/src/components/PageIndicator.tsx
index aa27216..617cfd1 100644
--- a/src/components/PageIndicator.tsx
+++ b/src/components/PageIndicator.tsx
@@ -5,7 +5,7 @@ import classNames from "classnames";
import { moduleNames } from "../content";
import styles from "./page-indicator.module.css";
-import { SimulariumContext } from "../simulation/context";
+import { useSimulariumUi } from "../hooks/useSimulationContext";
interface PageIndicatorProps {
title: string;
@@ -18,8 +18,7 @@ const PageIndicator: React.FC = ({
page,
total,
}) => {
- const { module, setModule, completedModules } =
- React.useContext(SimulariumContext);
+ const { module, setModule, completedModules } = useSimulariumUi();
const indexOfActiveModule: number = useMemo(() => {
let toReturn = -1;
map(moduleNames, (name, index) => {
@@ -86,7 +85,7 @@ const PageIndicator: React.FC = ({
size={["100%", 4]}
percent={getModulePercent(
isActiveModule,
- moduleIndex
+ moduleIndex,
)}
showInfo={false}
/>
diff --git a/src/components/PlayButton.tsx b/src/components/PlayButton.tsx
index e29bc8b..1aa5afc 100644
--- a/src/components/PlayButton.tsx
+++ b/src/components/PlayButton.tsx
@@ -1,7 +1,7 @@
-import React, { useContext } from "react";
+import React from "react";
import { CaretRightOutlined, PauseOutlined } from "@ant-design/icons";
-import { SimulariumContext } from "../simulation/context";
+import { useSimulariumSimulation } from "../hooks/useSimulationContext";
import ProgressionControl from "./shared/ProgressionControl";
import VisibilityControl from "./shared/VisibilityControl";
import { OverlayButton } from "./shared/ButtonLibrary";
@@ -9,7 +9,7 @@ import { Module } from "../types";
import { PLAY_BUTTON_ID } from "../constants";
const PlayButton: React.FC = () => {
- const { isPlaying, setIsPlaying } = useContext(SimulariumContext);
+ const { isPlaying, setIsPlaying } = useSimulariumSimulation();
const handleClick = () => {
setIsPlaying(!isPlaying);
diff --git a/src/components/ScaleBar.tsx b/src/components/ScaleBar.tsx
index 7300644..e34fc33 100644
--- a/src/components/ScaleBar.tsx
+++ b/src/components/ScaleBar.tsx
@@ -1,15 +1,15 @@
-import React, { useContext } from "react";
+import React from "react";
import styles from "./scalebar.module.css";
import { MICRO } from "../constants";
-import { SimulariumContext } from "../simulation/context";
+import { useSimulariumSimulation } from "../hooks/useSimulationContext";
interface ScaleBarProps {
productColor: string;
}
const ScaleBar: React.FC = ({ productColor }) => {
- const { maxConcentration } = useContext(SimulariumContext);
+ const { maxConcentration } = useSimulariumSimulation();
const labelArray = [];
const interval = maxConcentration / 5;
for (let i = maxConcentration; i >= 0; i = i - interval) {
diff --git a/src/components/StartExperiment.tsx b/src/components/StartExperiment.tsx
index 20dc924..64931bd 100644
--- a/src/components/StartExperiment.tsx
+++ b/src/components/StartExperiment.tsx
@@ -1,13 +1,13 @@
-import React, { useContext } from "react";
+import React from "react";
import classNames from "classnames";
-import { SimulariumContext } from "../simulation/context";
+import { useSimulariumSimulation } from "../hooks/useSimulationContext";
import { TertiaryButton } from "./shared/ButtonLibrary";
import style from "./start-experiment.module.css";
const StartExperiment: React.FC = () => {
- const { handleStartExperiment } = useContext(SimulariumContext);
+ const { handleStartExperiment } = useSimulariumSimulation();
const [initial, setIsInitial] = React.useState(true);
const handleClick = () => {
if (initial) {
diff --git a/src/components/ViewSwitch.tsx b/src/components/ViewSwitch.tsx
index 21d03c0..71ff600 100644
--- a/src/components/ViewSwitch.tsx
+++ b/src/components/ViewSwitch.tsx
@@ -1,7 +1,10 @@
-import React, { useContext } from "react";
+import React from "react";
import Viewer from "./Viewer";
-import { SimulariumContext } from "../simulation/context";
+import {
+ useSimulariumSimulation,
+ useSimulariumUi,
+} from "../hooks/useSimulationContext";
import ProgressionControl from "./shared/ProgressionControl";
import PlayButton from "./PlayButton";
import { OverlayButton } from "./shared/ButtonLibrary";
@@ -14,10 +17,9 @@ import { FIRST_PAGE } from "../content";
import { VIEW_SWITCH_ID } from "../constants";
const ViewSwitch: React.FC = () => {
- const { viewportType, setViewportType } = useContext(SimulariumContext);
-
- const { page, isPlaying, setIsPlaying, handleTimeChange, module } =
- useContext(SimulariumContext);
+ const { viewportType, setViewportType, page, module } = useSimulariumUi();
+ const { isPlaying, setIsPlaying, handleTimeChange } =
+ useSimulariumSimulation();
const isFirstPageOfFirstModule =
page === FIRST_PAGE[module] + 1 && module === Module.A_B_AB;
diff --git a/src/components/Viewer.tsx b/src/components/Viewer.tsx
index 804cd2a..9beab24 100644
--- a/src/components/Viewer.tsx
+++ b/src/components/Viewer.tsx
@@ -1,11 +1,4 @@
-import {
- ReactNode,
- useCallback,
- useContext,
- useEffect,
- useRef,
- useState,
-} from "react";
+import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
import classNames from "classnames";
import SimulariumViewer, {
RenderStyle,
@@ -13,7 +6,10 @@ import SimulariumViewer, {
} from "@aics/simularium-viewer";
import "@aics/simularium-viewer/style/style.css";
-import { SimulariumContext } from "../simulation/context";
+import {
+ useSimulariumSimulation,
+ useSimulariumUi,
+} from "../hooks/useSimulationContext";
import styles from "./viewer.module.css";
import useWindowResize from "../hooks/useWindowResize";
import { LIVE_SIMULATION_NAME } from "../constants";
@@ -43,8 +39,8 @@ export default function Viewer({ handleTimeChange }: ViewerProps): ReactNode {
simulariumController,
handleTrajectoryChange,
trajectoryName,
- page,
- } = useContext(SimulariumContext);
+ } = useSimulariumSimulation();
+ const { page } = useSimulariumUi();
const setViewportToContainerSize = useCallback(() => {
if (container.current) {
diff --git a/src/components/concentration-display/Concentration.tsx b/src/components/concentration-display/Concentration.tsx
index b288a24..51d8491 100644
--- a/src/components/concentration-display/Concentration.tsx
+++ b/src/components/concentration-display/Concentration.tsx
@@ -1,4 +1,4 @@
-import React, { useContext, useState } from "react";
+import React, { useState } from "react";
import { map } from "lodash";
import { Flex } from "antd";
import classNames from "classnames";
@@ -10,7 +10,10 @@ import {
Section,
UiElement,
} from "../../types";
-import { SimulariumContext } from "../../simulation/context";
+import {
+ useSimulariumSimulation,
+ useSimulariumUi,
+} from "../../hooks/useSimulationContext";
import LiveConcentrationDisplay from "./LiveConcentrationDisplay";
import ConcentrationSlider from "./ConcentrationSlider";
import { MICRO, CHANGE_CONCENTRATION_ID } from "../../constants";
@@ -42,20 +45,16 @@ const Concentration: React.FC = ({
liveConcentration,
onChangeComplete,
}) => {
- const {
- isPlaying,
- maxConcentration,
- getAgentColor,
- section,
- progressionElement,
- } = useContext(SimulariumContext);
+ const { isPlaying, maxConcentration, getAgentColor } =
+ useSimulariumSimulation();
+ const { section, progressionElement } = useSimulariumUi();
const [width, setWidth] = useState(0);
const MARGINS = 64.2;
// on super small screens this can result in a negative number
const widthMinusMargins = Math.max(width - MARGINS, 0);
const [highlightState, setHighlightState] = useState(
- HighlightState.Initial
+ HighlightState.Initial,
);
if (
@@ -75,7 +74,7 @@ const Concentration: React.FC = ({
const getComponent = (
agent: AgentName,
- currentConcentrationOfAgent: number
+ currentConcentrationOfAgent: number,
) => {
if (adjustableAgent === agent && !isPlaying) {
return (
@@ -175,7 +174,7 @@ const Concentration: React.FC = ({
>
{getComponent(
agent,
- agentLiveConcentration
+ agentLiveConcentration,
)}
{MICRO}M
@@ -183,7 +182,7 @@ const Concentration: React.FC = ({
);
- }
+ },
)}
>
diff --git a/src/components/concentration-display/ConcentrationSlider.tsx b/src/components/concentration-display/ConcentrationSlider.tsx
index 4574b38..f195e0f 100644
--- a/src/components/concentration-display/ConcentrationSlider.tsx
+++ b/src/components/concentration-display/ConcentrationSlider.tsx
@@ -1,8 +1,8 @@
-import React, { useContext, useEffect, useMemo, useRef } from "react";
+import React, { useEffect, useMemo, useRef } from "react";
import { SliderSingleProps } from "antd";
import Slider from "../shared/Slider";
-import { SimulariumContext } from "../../simulation/context";
+import { useSimulariumAnalysis } from "../../hooks/useSimulationContext";
import styles from "./concentration-slider.module.css";
import classNames from "classnames";
@@ -20,7 +20,7 @@ const Mark: React.FC<{
disabledNumbers: number[];
onMouseUp: () => void;
}> = ({ index, disabledNumbers, onMouseUp }) => {
- const { recordedConcentrations } = useContext(SimulariumContext);
+ const { recordedConcentrations } = useSimulariumAnalysis();
const ref = useRef(null);
useEffect(() => {
diff --git a/src/components/concentration-display/LiveConcentrationDisplay.tsx b/src/components/concentration-display/LiveConcentrationDisplay.tsx
index 9734a1a..1800976 100644
--- a/src/components/concentration-display/LiveConcentrationDisplay.tsx
+++ b/src/components/concentration-display/LiveConcentrationDisplay.tsx
@@ -1,9 +1,9 @@
import { Flex, Progress } from "antd";
-import React, { useContext } from "react";
+import React from "react";
import { AgentName } from "../../types";
import styles from "./live-concentration-display.module.css";
-import { SimulariumContext } from "../../simulation/context";
+import { useSimulariumSimulation } from "../../hooks/useSimulationContext";
interface LiveConcentrationDisplayProps {
concentration: number;
@@ -16,7 +16,7 @@ const LiveConcentrationDisplay: React.FC = ({
concentration,
width,
}) => {
- const { maxConcentration, getAgentColor } = useContext(SimulariumContext);
+ const { maxConcentration, getAgentColor } = useSimulariumSimulation();
// the steps have a 2px gap, so we are adjusting the
// size of the step based on the total number we want
const steps = maxConcentration;
diff --git a/src/components/main-layout/ContentPanelTimer.tsx b/src/components/main-layout/ContentPanelTimer.tsx
index 81f7cbf..8eedf24 100644
--- a/src/components/main-layout/ContentPanelTimer.tsx
+++ b/src/components/main-layout/ContentPanelTimer.tsx
@@ -1,8 +1,8 @@
-import React, { useContext, useEffect, useRef, useState } from "react";
+import React, { useEffect, useRef, useState } from "react";
import classNames from "classnames";
import { isEqual } from "lodash";
-import { SimulariumContext } from "../../simulation/context";
+import { useSimulariumUi } from "../../hooks/useSimulationContext";
import { PageContent, Module } from "../../types";
import ContentPanel from "./ContentPanel";
@@ -29,7 +29,7 @@ const ContentPanelTimer: React.FC = ({
const previousContentRef = useRef(pageContent);
const contentJustChanged = !isEqual(
previousContentRef.current.content,
- pageContent.content
+ pageContent.content,
);
useEffect(() => {
@@ -44,7 +44,7 @@ const ContentPanelTimer: React.FC = ({
// must be the same as the css transition time
const FADE_TIME = 150;
const updateRenderState = (
- currentRenderState: RenderState
+ currentRenderState: RenderState,
): NodeJS.Timeout | null => {
previousContentRef.current = pageContent;
setRenderState(currentRenderState);
@@ -52,7 +52,7 @@ const ContentPanelTimer: React.FC = ({
if (currentRenderState <= RenderState.FullyVisible) {
return setTimeout(
() => updateRenderState(currentRenderState),
- FADE_TIME
+ FADE_TIME,
);
} else {
return null;
@@ -60,7 +60,7 @@ const ContentPanelTimer: React.FC = ({
};
const timer = setTimeout(
() => updateRenderState(RenderState.NoRender),
- FADE_TIME
+ FADE_TIME,
);
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -72,7 +72,7 @@ const ContentPanelTimer: React.FC = ({
? previousContentRef.current
: pageContent;
- const { page } = useContext(SimulariumContext);
+ const { page } = useSimulariumUi();
const pageNumber = contentJustChanged ? page - 1 : page;
const containerClassNames = classNames([
styles.contentPanelWrapper,
diff --git a/src/components/main-layout/LeftPanel.tsx b/src/components/main-layout/LeftPanel.tsx
index 851f368..acc3ed9 100644
--- a/src/components/main-layout/LeftPanel.tsx
+++ b/src/components/main-layout/LeftPanel.tsx
@@ -6,6 +6,7 @@ import {
InputConcentration,
Module,
} from "../../types";
+import { useSimulariumUi } from "../../hooks/useSimulationContext";
import VisibilityControl from "../shared/VisibilityControl";
import EventsOverTimePlot from "../plots/EventsOverTimePlot";
import Concentration from "../concentration-display/Concentration";
@@ -29,6 +30,8 @@ const LeftPanel: React.FC = ({
unbindingEventsOverTime,
adjustableAgent,
}) => {
+ const { module } = useSimulariumUi();
+
const concentrationExcludedPages = {
[Module.A_B_AB]: [0, 1],
[Module.A_C_AC]: [],
@@ -39,6 +42,10 @@ const LeftPanel: React.FC = ({
[Module.A_B_AB]: [0, 1, 2],
[Module.A_C_AC]: [],
};
+
+ // Don't show events over time plot in the competitive binding module
+ const showEventsOverTime = module !== Module.A_B_D_AB;
+
return (
<>
@@ -52,7 +59,7 @@ const LeftPanel: React.FC = ({
= ({ title, page, total }) => {
- const { setPage } = useContext(SimulariumContext);
+ const { setPage } = useSimulariumUi();
const helpMenuItems = [
{
key: "1",
diff --git a/src/components/main-layout/RightPanel.tsx b/src/components/main-layout/RightPanel.tsx
index b061b55..aaed196 100644
--- a/src/components/main-layout/RightPanel.tsx
+++ b/src/components/main-layout/RightPanel.tsx
@@ -1,4 +1,4 @@
-import React, { ReactNode, useContext, useState } from "react";
+import React, { ReactNode, useState } from "react";
import VisibilityControl from "../shared/VisibilityControl";
import ProductConcentrationPlot from "../plots/ProductConcentrationPlot";
@@ -9,7 +9,7 @@ import { ProductOverTimeTrace } from "../plots/types";
import styles from "./layout.module.css";
import { AB, AC } from "../agent-symbols";
import ResizeContainer from "../shared/ResizeContainer";
-import { SimulariumContext } from "../../simulation/context";
+import { useSimulariumSimulation } from "../../hooks/useSimulationContext";
import HelpPopup from "../HelpPopup";
import InfoText from "../shared/InfoText";
import { ProductName, UiElement } from "../../types";
@@ -40,7 +40,7 @@ const RightPanel: React.FC = ({
currentAdjustableAgentConcentration,
showHelpPanel,
}) => {
- const { productName } = useContext(SimulariumContext);
+ const { productName } = useSimulariumSimulation();
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);
let data = productOverTimeTraces;
diff --git a/src/components/plots/EquilibriumPlot.tsx b/src/components/plots/EquilibriumPlot.tsx
index 861d0b4..b4f7a54 100644
--- a/src/components/plots/EquilibriumPlot.tsx
+++ b/src/components/plots/EquilibriumPlot.tsx
@@ -1,4 +1,4 @@
-import React, { useContext, useMemo } from "react";
+import React, { useMemo } from "react";
import regression, { DataPoint } from "regression";
import Plot from "react-plotly.js";
@@ -8,7 +8,10 @@ import {
CONFIG,
GRAY_COLOR,
} from "./constants";
-import { SimulariumContext } from "../../simulation/context";
+import {
+ useSimulariumSimulation,
+ useSimulariumUi,
+} from "../../hooks/useSimulationContext";
import { AGENT_A_COLOR, AGENT_AB_COLOR } from "../../constants/colors";
import { MICRO } from "../../constants";
@@ -38,8 +41,8 @@ const EquilibriumPlot: React.FC = ({
productName,
getAgentColor,
adjustableAgentName,
- module,
- } = useContext(SimulariumContext);
+ } = useSimulariumSimulation();
+ const { module } = useSimulariumUi();
const xMax = Math.max(...x);
const xAxisMax = Math.max(kd * 2, xMax * 1.1);
@@ -63,7 +66,8 @@ const EquilibriumPlot: React.FC = ({
// ln(halfMax / a) = b*x
// x = ln(halfMax / a) / b
value =
- Math.log(halfMax / fitResult.equation[0]) / fitResult.equation[1];
+ Math.log(halfMax / fitResult.equation[0]) /
+ fitResult.equation[1];
} else {
fitResult = regression.logarithmic(regressionData);
@@ -226,7 +230,7 @@ const EquilibriumPlot: React.FC = ({
color: getAgentColor(adjustableAgentName),
},
tickmode: bestFitVisible ? ("array" as const) : ("auto" as const),
- tickvals: [...xAxisTicks, bestFit.value],
+ tickvals: [...xAxisTicks, Number(bestFit.value.toFixed(1))],
},
yaxis: {
...AXIS_SETTINGS,
diff --git a/src/components/plots/EventsOverTimePlot.tsx b/src/components/plots/EventsOverTimePlot.tsx
index da57e5c..558bb31 100644
--- a/src/components/plots/EventsOverTimePlot.tsx
+++ b/src/components/plots/EventsOverTimePlot.tsx
@@ -1,4 +1,4 @@
-import React, { useContext, useRef, useState } from "react";
+import React, { useRef, useState } from "react";
import { Flex } from "antd";
import Plot from "react-plotly.js";
@@ -8,7 +8,10 @@ import {
BASE_PLOT_LAYOUT,
CONFIG,
} from "./constants";
-import { SimulariumContext } from "../../simulation/context";
+import {
+ useSimulariumSimulation,
+ useSimulariumUi,
+} from "../../hooks/useSimulationContext";
import { A, B, AB, C, AC } from "../agent-symbols";
import { MICRO } from "../../constants";
@@ -27,13 +30,14 @@ const EventsOverTimePlot: React.FC = ({
bindingEventsOverTime,
unbindingEventsOverTime,
}) => {
- const { timeFactor, module } = useContext(SimulariumContext);
+ const { timeFactor } = useSimulariumSimulation();
+ const { module } = useSimulariumUi();
const [width, setWidth] = useState(0);
// the two arrays will always be the same length
// so this time calculation only needs to happen once
const time = bindingEventsOverTime.map(
- (_, i) => (i * 10 * timeFactor) / 1000
+ (_, i) => (i * 10 * timeFactor) / 1000,
);
const max = useRef(0);
diff --git a/src/components/plots/ProductConcentrationPlot.tsx b/src/components/plots/ProductConcentrationPlot.tsx
index 7484d0f..9f42380 100644
--- a/src/components/plots/ProductConcentrationPlot.tsx
+++ b/src/components/plots/ProductConcentrationPlot.tsx
@@ -1,5 +1,5 @@
import { PlotData } from "plotly.js";
-import React, { useContext, useRef } from "react";
+import React, { useRef } from "react";
import Plot from "react-plotly.js";
import {
@@ -11,7 +11,7 @@ import {
PLOT_COLORS,
} from "./constants";
import { ProductOverTimeTrace } from "./types";
-import { SimulariumContext } from "../../simulation/context";
+import { useSimulariumSimulation } from "../../hooks/useSimulationContext";
import { MICRO } from "../../constants";
import { getColorIndex, indexToTime } from "../../utils";
@@ -41,7 +41,7 @@ const ProductConcentrationPlot: React.FC = ({
productName,
adjustableAgentName,
getAgentColor,
- } = useContext(SimulariumContext);
+ } = useSimulariumSimulation();
const hasData = useRef(false);
if (data.length === 0) {
hasData.current = false;
@@ -70,7 +70,7 @@ const ProductConcentrationPlot: React.FC = ({
}
const timeArray = productConcentrations.map((_, i) =>
- indexToTime(i, timeFactor, timeUnit)
+ indexToTime(i, timeFactor, timeUnit),
);
return {
x: timeArray,
diff --git a/src/components/quiz-questions/EquilibriumQuestion.tsx b/src/components/quiz-questions/EquilibriumQuestion.tsx
index e4633e8..7d3f8bb 100644
--- a/src/components/quiz-questions/EquilibriumQuestion.tsx
+++ b/src/components/quiz-questions/EquilibriumQuestion.tsx
@@ -1,14 +1,14 @@
-import React, { useContext, useRef, useState } from "react";
+import React, { useRef, useState } from "react";
import QuizForm from "./QuizForm";
import VisibilityControl from "../shared/VisibilityControl";
import { FormState } from "./types";
import RadioComponent from "../shared/Radio";
import { EQUILIBRIUM_QUIZ_ID } from "../../constants";
-import { SimulariumContext } from "../../simulation/context";
+import { useSimulariumUi } from "../../hooks/useSimulationContext";
import { Module } from "../../types";
const EquilibriumQuestion: React.FC = () => {
- const { page, quizQuestion, module } = useContext(SimulariumContext);
+ const { page, quizQuestion, module } = useSimulariumUi();
const [selectedAnswer, setSelectedAnswer] = useState("");
const [formState, setFormState] = useState(FormState.Clear);
const firstVisiblePage = useRef<{ page: number; module: Module }>({
diff --git a/src/components/quiz-questions/KdQuestion.tsx b/src/components/quiz-questions/KdQuestion.tsx
index 9a02d63..4074dae 100644
--- a/src/components/quiz-questions/KdQuestion.tsx
+++ b/src/components/quiz-questions/KdQuestion.tsx
@@ -1,4 +1,4 @@
-import React, { useContext, useEffect, useState } from "react";
+import React, { useEffect, useState } from "react";
import { valueType } from "antd/es/statistic/utils";
import { Flex } from "antd";
@@ -8,7 +8,7 @@ import InputNumber from "../shared/InputNumber";
import { FormState } from "./types";
import styles from "./popup.module.css";
import { MICRO } from "../../constants";
-import { SimulariumContext } from "../../simulation/context";
+import { useSimulariumUi } from "../../hooks/useSimulationContext";
interface KdQuestionProps {
kd: number;
@@ -19,7 +19,7 @@ const KdQuestion: React.FC = ({ kd, canAnswer }) => {
const [selectedAnswer, setSelectedAnswer] = useState(null);
const [formState, setFormState] = useState(FormState.Clear);
- const { module, addCompletedModule } = useContext(SimulariumContext);
+ const { module, addCompletedModule } = useSimulariumUi();
useEffect(() => {
setSelectedAnswer(null);
@@ -95,8 +95,8 @@ const KdQuestion: React.FC = ({ kd, canAnswer }) => {
where half of the binding sites of A are occupied.
- If you're not sure, look at where the line crosses the 50%
- mark on the Equilibrium concentration plot.
+ If you're not sure, look at where the line crosses the 50% mark
+ on the Equilibrium concentration plot.
Kd = ?
diff --git a/src/components/shared/BackButton.tsx b/src/components/shared/BackButton.tsx
index a14baa7..c05cf42 100644
--- a/src/components/shared/BackButton.tsx
+++ b/src/components/shared/BackButton.tsx
@@ -1,9 +1,8 @@
-import { useContext } from "react";
-import { SimulariumContext } from "../../simulation/context";
+import { useSimulariumUi } from "../../hooks/useSimulationContext";
import { SecondaryButton } from "./ButtonLibrary";
const BackButton = () => {
- const { page, setPage } = useContext(SimulariumContext);
+ const { page, setPage } = useSimulariumUi();
return (
setPage(page - 1)}>
diff --git a/src/components/shared/NextButton.tsx b/src/components/shared/NextButton.tsx
index 225c0b3..e542b75 100644
--- a/src/components/shared/NextButton.tsx
+++ b/src/components/shared/NextButton.tsx
@@ -1,5 +1,4 @@
-import { useContext } from "react";
-import { SimulariumContext } from "../../simulation/context";
+import { useSimulariumUi } from "../../hooks/useSimulationContext";
import { PrimaryButton } from "./ButtonLibrary";
import useModule from "../../hooks/useModule";
@@ -8,7 +7,7 @@ interface NextButtonProps {
}
const NextButton = ({ text }: NextButtonProps) => {
- const { page, setPage, module, setModule } = useContext(SimulariumContext);
+ const { page, setPage, module, setModule } = useSimulariumUi();
const { totalPages } = useModule(module);
if (page + 1 > totalPages) {
diff --git a/src/components/shared/ProgressionControl.tsx b/src/components/shared/ProgressionControl.tsx
index 6216a3d..18b5c53 100644
--- a/src/components/shared/ProgressionControl.tsx
+++ b/src/components/shared/ProgressionControl.tsx
@@ -1,5 +1,5 @@
-import React, { useContext } from "react";
-import { SimulariumContext } from "../../simulation/context";
+import React from "react";
+import { useSimulariumUi } from "../../hooks/useSimulationContext";
import { BaseHandler, ProgressionControlEvent } from "../../types";
import styles from "./progression-control.module.css";
@@ -24,7 +24,7 @@ const ProgressionControl: React.FC = ({
children,
elementId,
}) => {
- const { page, setPage, progressionElement } = useContext(SimulariumContext);
+ const { page, setPage, progressionElement } = useSimulariumUi();
const shouldProgress = progressionElement === elementId;
const progress = () => {
if (shouldProgress) {
@@ -37,7 +37,7 @@ const ProgressionControl: React.FC = ({
const mergeHandlers = (baseHandler: BaseHandler) => {
return (
event: ProgressionControlEvent,
- optionalValue?: string | number | string[] | number[]
+ optionalValue?: string | number | string[] | number[],
) => {
const returnValue = baseHandler(event, optionalValue);
// generally, all handlers are going to return undefined
diff --git a/src/components/shared/ResetButton.tsx b/src/components/shared/ResetButton.tsx
new file mode 100644
index 0000000..cd1bef2
--- /dev/null
+++ b/src/components/shared/ResetButton.tsx
@@ -0,0 +1,40 @@
+import React from "react";
+import {
+ useSimulariumAnalysis,
+ useSimulariumUi,
+} from "../../hooks/useSimulationContext";
+import { SecondaryButton } from "./ButtonLibrary";
+
+interface ResetButtonProps {
+ /** If true, resets all state including UI. If false, only resets analysis data */
+ resetAll?: boolean;
+ children?: React.ReactNode;
+}
+
+/**
+ * Button that resets either all application state or just analysis state
+ * based on the resetAll prop
+ */
+const ResetButton: React.FC = ({
+ resetAll = false,
+ children,
+}) => {
+ const { resetAllState } = useSimulariumUi();
+ const { resetAnalysisState } = useSimulariumAnalysis();
+
+ const handleClick = () => {
+ if (resetAll) {
+ resetAllState();
+ } else {
+ resetAnalysisState();
+ }
+ };
+
+ return (
+
+ {children || (resetAll ? "Reset All" : "Clear Data")}
+
+ );
+};
+
+export default ResetButton;
diff --git a/src/components/shared/VisibilityControl.tsx b/src/components/shared/VisibilityControl.tsx
index 2eaf598..46d0c4d 100644
--- a/src/components/shared/VisibilityControl.tsx
+++ b/src/components/shared/VisibilityControl.tsx
@@ -1,5 +1,5 @@
-import React, { useContext } from "react";
-import { SimulariumContext } from "../../simulation/context";
+import React from "react";
+import { useSimulariumUi } from "../../hooks/useSimulationContext";
import { Module, Section } from "../../types";
interface VisibilityControlProps {
@@ -21,7 +21,7 @@ const VisibilityControl: React.FC = ({
notInIntroduction,
startPage,
}) => {
- const { page, section, module } = useContext(SimulariumContext);
+ const { page, section, module } = useSimulariumUi();
if (conditionalRender === false) {
return null;
}
diff --git a/src/hooks/useSimulationContext.ts b/src/hooks/useSimulationContext.ts
new file mode 100644
index 0000000..68ee299
--- /dev/null
+++ b/src/hooks/useSimulationContext.ts
@@ -0,0 +1,30 @@
+import { useContext } from "react";
+import {
+ SimulariumAnalysisContext,
+ SimulariumAnalysisContextType,
+ SimulariumSimulationContext,
+ SimulariumSimulationContextType,
+ SimulariumUiContext,
+ SimulariumUiContextType,
+} from "../simulation/context";
+
+/**
+ * Hook to access UI-related context (page, module, view type, etc.)
+ */
+export const useSimulariumUi = (): SimulariumUiContextType => {
+ return useContext(SimulariumUiContext);
+};
+
+/**
+ * Hook to access simulation-related context (playback, viewport, trajectory, etc.)
+ */
+export const useSimulariumSimulation = (): SimulariumSimulationContextType => {
+ return useContext(SimulariumSimulationContext);
+};
+
+/**
+ * Hook to access analysis-related context (recorded data, reset functions, etc.)
+ */
+export const useSimulariumAnalysis = (): SimulariumAnalysisContextType => {
+ return useContext(SimulariumAnalysisContext);
+};
diff --git a/src/simulation/context.tsx b/src/simulation/context.tsx
index 82cb55d..c5e7c8e 100644
--- a/src/simulation/context.tsx
+++ b/src/simulation/context.tsx
@@ -12,7 +12,22 @@ import {
} from "../constants";
import { AgentName, Module, ProductName, Section, ViewType } from "../types";
-interface SimulariumContextType {
+export interface SimulariumUiContextType {
+ addCompletedModule: (value: Module) => void;
+ completedModules: Set;
+ module: Module;
+ page: number;
+ progressionElement: ProgressionElement | "";
+ quizQuestion: string;
+ resetAllState: () => void;
+ section: Section;
+ setModule: (value: Module) => void;
+ setPage: (value: number) => void;
+ setViewportType: () => void;
+ viewportType: ViewType;
+}
+
+export interface SimulariumSimulationContextType {
adjustableAgentName: AgentName;
currentProductionConcentration: number;
fixedAgentStartingConcentration: number;
@@ -23,57 +38,60 @@ interface SimulariumContextType {
handleTrajectoryChange: (value: TrajectoryFileInfo) => void;
isPlaying: boolean;
maxConcentration: number;
- module: Module;
- page: number;
productName: ProductName;
- progressionElement: ProgressionElement | "";
- quizQuestion: string;
- recordedConcentrations: number[];
- section: Section;
setIsPlaying: (value: boolean) => void;
- setModule: (value: Module) => void;
- setPage: (value: number) => void;
setViewportSize: (value: { width: number; height: number }) => void;
- setViewportType: () => void;
simulariumController: SimulariumController | null;
timeFactor: number;
timeUnit: string;
trajectoryName: string;
viewportSize: { width: number; height: number };
- viewportType: ViewType;
- addCompletedModule: (value: Module) => void;
- completedModules: Set;
}
-export const SimulariumContext = createContext({
- adjustableAgentName: AgentName.B,
- currentProductionConcentration: 0,
- fixedAgentStartingConcentration: 0,
- getAgentColor: () => "",
- handleMixAgents: () => {},
- handleStartExperiment: () => {},
- handleTimeChange: () => {},
- handleTrajectoryChange: () => {},
- isPlaying: false,
- maxConcentration: 10,
+export interface SimulariumAnalysisContextType {
+ recordedConcentrations: number[];
+ resetAnalysisState: () => void;
+}
+
+export const SimulariumUiContext = createContext({
+ addCompletedModule: () => {},
+ completedModules: new Set(),
module: Module.A_B_AB,
page: 0,
- productName: ProductName.AB,
progressionElement: "",
quizQuestion: "",
- recordedConcentrations: [],
+ resetAllState: () => {},
section: Section.Introduction,
- setIsPlaying: () => {},
setModule: () => {},
setPage: () => {},
- setViewportSize: () => {},
setViewportType: () => {},
- simulariumController: null,
- timeFactor: 30,
- timeUnit: NANO,
- trajectoryName: LIVE_SIMULATION_NAME,
- viewportSize: DEFAULT_VIEWPORT_SIZE,
viewportType: ViewType.Lab,
- addCompletedModule: () => {},
- completedModules: new Set(),
-} as SimulariumContextType);
+});
+
+export const SimulariumSimulationContext =
+ createContext({
+ adjustableAgentName: AgentName.B,
+ currentProductionConcentration: 0,
+ fixedAgentStartingConcentration: 0,
+ getAgentColor: () => "",
+ handleMixAgents: () => {},
+ handleStartExperiment: () => {},
+ handleTimeChange: () => {},
+ handleTrajectoryChange: () => {},
+ isPlaying: false,
+ maxConcentration: 10,
+ productName: ProductName.AB,
+ setIsPlaying: () => {},
+ setViewportSize: () => {},
+ simulariumController: null,
+ timeFactor: 30,
+ timeUnit: NANO,
+ trajectoryName: LIVE_SIMULATION_NAME,
+ viewportSize: DEFAULT_VIEWPORT_SIZE,
+ });
+
+export const SimulariumAnalysisContext =
+ createContext({
+ recordedConcentrations: [],
+ resetAnalysisState: () => {},
+ });