Architecture Overview
VectorScope follows a client-server architecture with a clear separation between the computation backend and visualization frontend.
System Architecture
┌─────────────────────────────────────────────────────────────┐
│ Frontend (React) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────────────┐ │
│ │Viewport │ │ Graph │ │ Config │ │ State Management │ │
│ │(Plotly) │ │ Editor │ │ Panels │ │ (Zustand) │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ └──────────┬──────────┘ │
│ │ │ │ │ │
│ └───────────┴───────────┴──────────────────┘ │
│ │ │
└────────────────────────────┼──────────────────────────────────┘
│ REST API (Vite Proxy /api → :8001)
┌────────────────────────────┼──────────────────────────────────┐
│ Backend (FastAPI) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Routers │ │ Services │ │ Models │ │
│ │ (API) │ │ (Logic) │ │ (Pydantic) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────────┘ │
│ │ │ │ │
│ ┌──────┴───────────────┴────────────────────┴──────────────┐ │
│ │ In-Memory Storage │ │
│ │ DataStore TransformEngine ProjectionEngine │ │
│ └───────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
Backend Architecture
The Python backend is organized into three main layers:
Models (backend/models/)
Pydantic models defining data structures:
# layer.py
class Layer(BaseModel):
id: UUID
name: str
dimensionality: int
point_count: int
is_derived: bool
column_names: Optional[list[str]]
feature_columns: Optional[list[str]]
label_column: Optional[str]
class Point(BaseModel):
id: UUID
vector: list[float]
label: Optional[str]
metadata: dict
is_virtual: bool
# transformation.py
class TransformationType(str, Enum):
scaling = "scaling"
rotation = "rotation"
affine = "affine"
linear = "linear"
class Transformation(BaseModel):
id: UUID
type: TransformationType
source_layer_id: UUID
target_layer_id: Optional[UUID]
parameters: dict
is_invertible: bool
# projection.py
class ProjectionType(str, Enum):
pca = "pca"
tsne = "tsne"
custom_axes = "custom_axes"
class Projection(BaseModel):
id: UUID
type: ProjectionType
layer_id: UUID
dimensions: int
parameters: dict
random_seed: Optional[int]
Services (backend/services/)
Business logic and state management:
- DataStore (
data_store.py) In-memory storage for layers and points. Singleton pattern.
class DataStore: _layers: dict[UUID, Layer] _points: dict[UUID, dict[UUID, Point]] _raw_data: dict[UUID, dict] # For column reconfiguration def create_layer(...) -> Layer def add_point(layer_id, point_data) -> Point def get_points(layer_id) -> list[Point] def get_vectors_as_array(layer_id) -> np.ndarray
- TransformEngine (
transform_engine.py) Applies transformations to create derived layers.
class TransformEngine: def create_transformation(source_layer_id, type, params) -> Transformation def apply_transformation(transformation, vectors) -> np.ndarray def update_transformation(id, params) -> Transformation
- ProjectionEngine (
projection_engine.py) Computes projections using sklearn.
class ProjectionEngine: def create_projection(layer_id, type, params) -> Projection def get_projected_coordinates(projection_id) -> list[ProjectedPoint] def _compute_projection(projection) -> list[ProjectedPoint]
Routers (backend/routers/)
FastAPI route handlers:
layers.py- CRUD for layers, file uploads, synthetic generationtransformations.py- CRUD for transformationsprojections.py- CRUD for projectionsscenarios.py- Save/load sessions
Frontend Architecture
The React frontend uses Zustand for state management.
Components (frontend/src/components/)
- App.tsx
Main application component. Handles toolbar, dialogs, and view switching.
- GraphEditor.tsx
ReactFlow-based DAG editor. Renders nodes for layers, transformations, projections.
- ConfigPanel.tsx
Right-side panel for editing selected node properties.
- Viewport.tsx
Plotly scatter plot for displaying projections. Handles selection.
- ViewportGrid.tsx
Multi-viewport layout for comparing projections.
State Management (frontend/src/stores/appStore.ts)
Zustand store containing:
interface AppState {
// Data
layers: Layer[];
projections: Projection[];
transformations: Transformation[];
projectedPoints: Record<string, ProjectedPoint[]>;
// Selection
selectedPointIds: Set<string>;
// Viewports
viewports: ViewportConfig[];
// Actions
loadLayers: () => Promise<void>;
createProjection: (params) => Promise<Projection | null>;
updateTransformation: (id, updates) => Promise<Transformation | null>;
// ... many more
}
API Client (frontend/src/api/client.ts)
REST API client wrapping fetch:
export const api = {
layers: {
list: () => fetchJson<Layer[]>('/layers'),
createSynthetic: (params) => fetchJson<Layer>('/layers/synthetic', ...),
upload: (file) => { /* FormData upload */ },
},
projections: {
create: (params) => fetchJson<Projection>('/projections', ...),
getCoordinates: (id) => fetchJson<ProjectedPoint[]>(`/projections/${id}/coordinates`),
},
// ...
};
Data Flow
Creating a Projection
User clicks “Add View” in ConfigPanel
createProjectionaction called in appStorePOST
/projectionssent to backendBackend creates Projection, computes coordinates
Frontend adds projection to state
Frontend fetches coordinates
Viewport renders with Plotly
Updating a Transformation
User adjusts slider in ConfigPanel
updateTransformationaction calledPATCH
/transformations/{id}sentBackend updates transformation, recomputes derived layer
Backend recomputes dependent projections
Frontend reloads layers, projections
Frontend clears projection cache
Active viewports fetch new coordinates
Vite Proxy Configuration
The frontend proxies API calls to the backend:
// vite.config.ts
server: {
proxy: {
'/api': {
target: 'http://localhost:8001',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
}
This means /api/layers → http://localhost:8001/layers.
Singleton Pattern
The backend services use singleton instances:
_data_store: Optional[DataStore] = None
def get_data_store() -> DataStore:
global _data_store
if _data_store is None:
_data_store = DataStore()
return _data_store
This ensures all routes share the same in-memory state.
Error Handling
Backend
FastAPI HTTPException for API errors:
if layer is None:
raise HTTPException(status_code=404, detail="Layer not found")
Frontend
Try-catch with state-based error display:
try {
const layer = await api.layers.get(id);
set({ layers: [...state.layers, layer] });
} catch (e) {
set({ error: (e as Error).message });
}