brain-understanding-visualization
v0.5.0
Published
A reusable React component that renders an MRI/scan-style wireframe brain to visualize concept understanding.
Maintainers
Readme
brain-understanding-visualization
A React component that renders an interactive, MRI/scan-style 3D brain to visualize how well a set of topics is understood. Topics are sectioned into regions on a real (embedded) anatomical brain mesh; learning points glow as colored nodes (red→green by mastery, gray when unassessed); latent-learning freshness renders as hollow rings; hovering shows tooltips and pauses the slow auto-rotation.
Everything is self-contained: the brain mesh ships inside the package — no asset files, no network fetches, no setup beyond installing peers.
Install
npm install brain-understanding-visualizationPeer dependencies (your app provides them — hard requirements):
npm install react@>=19 react-dom@>=19 three@>=0.160 \
@react-three/fiber@>=9 @react-three/drei@>=10 @react-three/postprocessing@>=3React 18 is not supported (fiber 9 / postprocessing 3 require React 19).
⚠️ Bundle size — lazy-load this
This package pulls in a WebGL stack (three.js + react-three-fiber + drei + postprocessing). Expect the chunk containing it to add roughly 0.5–1 MB compressed to your bundle (measure with your bundler — the package itself is small; the peers are the weight). Do not import it into your main bundle. The supported pattern:
- Isolate the import in its own file — this is what forces the code split; nothing else in your app may import this package:
// components/BrainViz.tsx — the ONLY file that imports the package
import { Brain } from "brain-understanding-visualization";
import type { BrainTopic } from "brain-understanding-visualization";
export default function BrainViz({ topics }: { topics: BrainTopic[] }) {
return <Brain percentageComplete={0} topics={topics} width={640} height={640} />;
}- Load it dynamically, client-only, with a spinner.
Next.js (including static export):
"use client";
import dynamic from "next/dynamic";
const BrainViz = dynamic(() => import("@/components/BrainViz"), {
ssr: false, // required: WebGL component, client-only
loading: () => <Spinner />,
});Plain React (Vite/CRA/etc.):
import { lazy, Suspense } from "react";
const BrainViz = lazy(() => import("./components/BrainViz"));
<Suspense fallback={<Spinner />}>
<BrainViz topics={topics} />
</Suspense>;Because the brain mesh is embedded (decoded synchronously on first render, no
.glb/texture fetches), the dynamic-import spinner covers essentially the
whole loading story — there is no second async asset phase. The first frame
may take a beat for shader compilation; that's it.
Optional extras:
- Defer until visible: if the brain is below the fold, gate the dynamic component behind an IntersectionObserver so the chunk isn't fetched until the user scrolls near it.
- Prefetch on intent: call
import("./components/BrainViz")inonMouseEnter/onTouchStartof the link leading to the page, so the chunk is warm before navigation. - Verify the split worked in your build output: the brain page's first-load JS should jump while other routes stay small.
SSR note: render client-only (ssr: false / mount-gated). On a server (or
any WebGL-less environment) the component renders a static placeholder instead
of throwing, but mixing that with hydration is not a supported path.
Usage — the Brain component
The simple product-facing interface. This is the recommended entry point.
import { Brain } from "brain-understanding-visualization";
<Brain
percentageComplete={72}
topics={[
{
id: "synapses", // optional stable id; callbacks fall back to name
name: "Synapses",
averageMastery: 70,
learningPoints: [
{ id: "lp-1", name: "Vesicles", mastery: 85, freshness: "fresh" },
{ id: "lp-2", name: "Receptors", mastery: null, freshness: "stale" },
],
},
{
id: "memory",
name: "Memory",
averageMastery: 33,
learningPoints: [
{ id: "lp-3", name: "Hippocampus", mastery: 40, freshness: "needsRefresh" },
],
},
]}
width={640}
height={640}
background="#1D3E5F" // your page background — rendered flat & exact
onSelectLearningPoint={(id) => openLesson(id)}
onSelectTopic={(id) => setActiveTopic(id)} // region clicks (node clicks win)
selectedTopicId={activeTopic} // controlled highlight + camera focus
emptyMessage={null} // hide the no-data overlay (ghost brain)
/>;The background prop renders as a flat, unmodified color across the whole
canvas, so the component blends seamlessly into your page. (Tone mapping and
vignette — which would shift/darken it — are off by default; both can be
re-enabled via config.toneMapping / config.vignette.)
Topic selection & the camera focus animation
onSelectTopicfires exactly once when a topic's region is clicked (drags don't fire it; clicks that land on a node fire onlyonSelectLearningPoint). A pointer cursor shows over regions when the handler is provided.selectedTopicId(controlled) keeps that region highlighted, pauses auto-rotation, and — whenever it changes to a new non-null id — animates the camera around the brain so the region faces the viewer (~2s eased orbit; never through the brain). Clicking a region re-presents it even if it's already selected; to re-trigger from the prop alone, set it tonulland back. A user drag cancels the animation instantly, andprefers-reduced-motionsnaps without animating.selectedLearningPointId(controlled) renders that node slightly larger at full brightness.- Duration is configurable:
config={{ focusDurationMs: 800 }}(0 = instant).
Data semantics
| Field | Meaning |
|---|---|
| mastery: 0–100 | Node color band: 0–25 red, 25–50 orange, 50–75 yellow, 75–100 green. |
| mastery: null | Not yet assessed → gray point. |
| averageMastery | Authoritative topic score (region tooltip + score-mode colors). It is not recomputed from the points — pass your own weighting. |
| freshness | "fresh" solid glowing node · "needsRefresh" bright ring, faded core · "stale" empty outline ring. Also shown as a tooltip line. Derive it in your app from time-since-last-answer. |
| id (topic / point) | Optional stable identifier; callbacks and the selected*Id props use it. Without it they fall back to name (names should then be unique). |
| emptyMessage | Overlay text for empty/invalid data; null/"" hides the overlay ("ghost brain"). |
| percentageComplete | Overall completion 0–100. Accepted, currently not rendered — reserved. |
Interaction out of the box: orbit by dragging, slow auto-rotate (pauses while
hovering a node or topic region), hover tooltips (name + score + staleness for
nodes; name + topic score for regions), click a node → onSelectLearningPoint
fires once with the point's name.
The component is defensive: malformed/empty data renders a placeholder rather than throwing; scores clamp; invalid config fields fall back individually.
Sizing / responsive
- Explicit
width/height(px) renders at exactly that size — recommended; drive it from your own breakpoints. Square reads best on small screens; in wide banners the brain sits centered with margins. - With no dimensions it fills the container's width at a 1:1 aspect, capped at 300px — usually you want explicit dimensions instead.
- Heads-up for scrolling pages: the canvas captures the mouse wheel for zoom.
Customization (optional props)
config— partialBrainVisualizationConfig; any field ofDEFAULT_VISUAL_CONFIG(exported) can be overridden:background(default#1D3E5F, rendered flat/exact), node size, edge network on/off, region sectioning style + hover opacity, vertex-cloud color mode ("score"colors each region's dot cloud by that topic's average — the default), bloom, optional tone mapping/vignette (off by default), auto-rotation, freshness style ("hollow" | "dim" | "both"),focusDurationMs, and more.palette— partial override of the five colors:red,orange,yellow,green,outOfRange(the gray).surface— your ownBrainSurfacemesh; omit to use the embedded brain.className— applied to the root container (all internal styling is inline and scoped; your global CSS can't break it, and it can't leak out).
Advanced API
BrainUnderstandingVisualization is the underlying component for hosts that
need explicit ids and full control (Concept → Topic[] → LearningPoint[]).
Brain is a thin mapping over it — see the package's .d.ts for both prop
interfaces.
Developing this package (not needed by consumers)
npm run dev opens a playground: leva controls over every config field, four
responsive breakpoint previews, and Export → "copy settings" (copies the live
config as JSON). Verification: npm run typecheck + npm run test. See
CLAUDE.md for architecture and the embedded-surface regeneration pipeline.
Releasing: publishing to npm is automated via GitHub Actions + npm OIDC
Trusted Publishing — never npm publish by hand. Cut a release from main with
npm run release:patch|minor|major (bumps, tags vX.Y.Z, pushes; the tag
triggers the publish), or use the Release workflow button in the Actions
tab. Full process and one-time setup: RELEASING.md.
Changelog
Full history in CHANGELOG.md (shipped with the package). Latest:
0.4.0
- Distinct selected-region opacity: new
config.regionSelectedOpacity(default0.55) makes a selected topic's region less see-through than a transient hover (config.regionHoverOpacity,0.35). Additive — no breaks.
0.3.0
Seamless host-page embedding. All changes are additive props/config fields, plus three visual default changes (no API breaks).
- Flat, exact backgrounds: new
backgroundprop onBrain(wins overconfig.background; default now#1D3E5F). Tone mapping and vignette are now off by default so the background renders exactly as specified with no center-glow/dark-edge gradient — re-enable via the newconfig.toneMappingandconfig.vignette. - Score-colored regions by default:
regionColorModenow defaults to"score", so the region tint — including the hover/selection highlight — matches the topic's average-mastery color (the same encoding as the vertex cloud). The hue-spread identity colors remain viaconfig.regionColorMode: "hue".
0.2.0
Topic selection, camera focus, and host-integration APIs. All additions are optional props — no breaking changes.
- Camera focus animation ("snap to topic"): when a topic becomes selected
— by clicking its region or by the
selectedTopicIdprop changing to a new non-null value — the camera orbits the brain (eased, along the sphere, never through it) until that region faces the viewer. Default 2000 ms via the newconfig.focusDurationMs(0 = instant); a user drag cancels it instantly;prefers-reduced-motionalways snaps without animating. Auto-rotation is suspended during the flight and stays paused while a controlled selection is active. onSelectTopic(both components): fires exactly once when a topic's region is clicked. Node clicks win (they fire onlyonSelectLearningPoint), orbit drags don't fire it, and a pointer cursor shows over regions when the handler is provided. Host callback errors are isolated, likeonSelectLearningPoint.- Controlled selection:
selectedTopicIdpersistently applies the hover-style region highlight (hover wins while active);selectedLearningPointIdrenders that node slightly larger at full brightness. Unknown/stale ids are silently ignored. - Stable ids on the
Brainfacade:BrainTopic.idandBrainLearningPoint.id(optional). Callbacks and theselected*Idprops use the id when supplied and fall back tonameotherwise — existing consumers see no change. emptyMessage: override the empty/invalid-data overlay text, or passnull/""to hide the overlay entirely while the brain mesh still renders (a "ghost brain" placeholder).- Flat, exact backgrounds: new
backgroundprop onBrain(wins overconfig.background; default now#1D3E5F). Tone mapping and vignette are now off by default so the background renders exactly as specified with no center-glow/dark-edge gradient — re-enable via the newconfig.toneMappingandconfig.vignette. - Score-colored regions by default:
regionColorModenow defaults to"score", so the region tint — including the hover/selection highlight — matches the topic's average-mastery color (the same encoding as the vertex cloud). The hue-spread identity colors remain viaconfig.regionColorMode: "hue".
0.1.0
Initial release: MRI/scan-style brain with an embedded ~6.8k-vertex anatomical
mesh (zero assets/fetches), topic region sectioning with hover highlight and
tooltips, mastery-colored learning points (gray when unassessed), per-topic
score-colored vertex clouds, latent-learning freshness (hollow-ring/dim
styles), the simple Brain facade, full visual configuration via
DEFAULT_VISUAL_CONFIG/config, and defensive rendering throughout.
License
MIT. Brain mesh derived from the public-domain MNI152 whole-brain scan.
