Skip to content
Open
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
418 changes: 248 additions & 170 deletions tool/server/server.py

Large diffs are not rendered by default.

215 changes: 124 additions & 91 deletions tool/server/server_utils.py

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions web/src/communication/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,4 +224,23 @@ export function calculateTrainingEvents(
export function testConnection(message: string, options?: NetworkOptions) {
const data = { message };
return basicPostWithJsonResponse('/testConnection', data, options);
}

// new function to fetch all necessary data for a specific epoch in one request
export function getNeighborsForSample(
contentPath: string,
visId: string,
epoch: number,
sampleIndex: number,
options?: NetworkOptions
) {
const data = {
"content_path": contentPath,
"vis_id": visId,
"epoch": epoch,
"sample_index": sampleIndex
};

// request to the backend to fetch both original and projection neighbors for the hovered sample
return basicPostWithJsonResponse('/getNeighborsForSample', data, options);
}
57 changes: 53 additions & 4 deletions web/src/component/chart.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// ChartComponent.tsx
import { memo, useEffect, useMemo, useRef, useState } from 'react';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { EmbeddingView, type EmbeddingViewProps, type DataPoint, type ViewportState } from 'embedding-atlas/react';
import { useDefaultStore } from "../state/state.unified";
import { transferArray2Color } from './utils';
import * as BackendAPI from '../communication/backend';

type EmbeddingData = NonNullable<EmbeddingViewProps['data']>;

Expand All @@ -29,6 +30,10 @@ export const ChartComponent = memo(() => {
const { availableEpochs } = useDefaultStore(["availableEpochs"]);
const { showTrail } = useDefaultStore(["showTrail"]);
const { setSelectedIndices } = useDefaultStore(["setSelectedIndices"]);
// added for on demand calls and cache
const { contentPath, visId, neighborCache, setValue } = useDefaultStore(["contentPath", "visId", "neighborCache", "setValue"]);

const [isFetchingNeighbors, setIsFetchingNeighbors] = useState(false);

const epochData = allEpochData[epoch];

Expand All @@ -37,6 +42,36 @@ export const ChartComponent = memo(() => {
// selection can be added later when needed
let [viewportState, setViewportState] = useState<ViewportState | null>(null);

// define hoveredIndex so it doesnt trigger the call to get neighbors when i hover on a point
useEffect(() => {
setHoveredIndex(undefined);
}, [epochData, contentPath]);

// fetch neighbors when user clicks on a hovered point and not when i just hoiver over it
const handleChartClick = useCallback(() => {
if (!tooltip) return
if (!contentPath || !visId || epoch == null) return
if (!revealOriginalNeighbors && !revealProjectionNeighbors) return

const clickedIndex = tooltip.identifier as number
const cacheKey = `${epoch}-${clickedIndex}`
if (neighborCache[cacheKey]) return

setIsFetchingNeighbors(true)
BackendAPI.getNeighborsForSample(contentPath, visId, epoch, clickedIndex)
.then((result: any) => {
setValue('neighborCache', {
...neighborCache,
[cacheKey]: {
originalNeighbors: result.originalNeighbors || result.neighbors || [],
projectionNeighbors: result.projectionNeighbors || result.projection_neighbors || [],
}
});
})
.catch((err: any) => console.warn('Failed to fetch neighbors:', err))
.finally(() => setIsFetchingNeighbors(false))
}, [tooltip, epoch, contentPath, visId, revealOriginalNeighbors, revealProjectionNeighbors, neighborCache, setValue]);

// observe container size change
useEffect(() => {
const node = atlasRef.current;
Expand Down Expand Up @@ -193,9 +228,12 @@ export const ChartComponent = memo(() => {
if (!prepared || !epochData) return { center: null, original: [], projection: [], dataX: new Float32Array(0), dataY: new Float32Array(0), pointSize, revealOriginalNeighbors, revealProjectionNeighbors } as any;
const idsByPos = prepared.dataPoints.map((p) => p.identifier as number);
if (!tooltip) return { center: null, original: [], projection: [], dataX: prepared.simpleData.x as Float32Array, dataY: prepared.simpleData.y as Float32Array, pointSize, revealOriginalNeighbors, revealProjectionNeighbors, idsByPos, showLabel, showIndex, labelDict, textData, inherentLabelData, viewportState, showTrail, availableEpochs, allEpochData, currentEpoch: epoch, setSelectedIndices, selectedIndices } as any;
// now read from cache
const hoverId = tooltip.identifier as number;
const orig = (epochData.originalNeighbors?.[hoverId] ?? []).filter((nid) => posMap.has(nid));
const proj = (epochData.projectionNeighbors?.[hoverId] ?? []).filter((nid) => posMap.has(nid));
const cacheKey = `${epoch}-${hoverId}`;
const cached = neighborCache[cacheKey];
const orig = (cached?.originalNeighbors ?? []).filter((nid: number) => posMap.has(nid));
const proj = (cached?.projectionNeighbors ?? []).filter((nid: number) => posMap.has(nid));
return {
center: tooltip,
original: orig,
Expand All @@ -219,7 +257,7 @@ export const ChartComponent = memo(() => {
setSelectedIndices,
selectedIndices,
};
}, [prepared, epochData, tooltip, posMap, pointSize, revealOriginalNeighbors, revealProjectionNeighbors, showLabel, showIndex, labelDict, textData, inherentLabelData, viewportState, showTrail, availableEpochs, allEpochData, epoch, trailRefresh, selectedIndices]);
}, [prepared, epochData, tooltip, posMap, pointSize, revealOriginalNeighbors, revealProjectionNeighbors, showLabel, showIndex, labelDict, textData, inherentLabelData, viewportState, showTrail, availableEpochs, allEpochData, epoch, trailRefresh, selectedIndices, neighborCache]);

class NeighborOverlay {
private el: HTMLDivElement | null = null;
Expand Down Expand Up @@ -543,9 +581,20 @@ export const ChartComponent = memo(() => {
width: '100%',
height: '100%',
}}
onClick={handleChartClick}
>
<div style={{ position: 'relative', flex: 1 }}>
{content ?? <div style={{ width: '100%', height: '100%' }} />}

{/* show loading bar when click point and fetch neighbors */}
{isFetchingNeighbors && (
<div className="neighbor-loading-container">
<span className="neighbor-loading-text">Loading neighbors...</span>
<div className="neighbor-loading-bar-container">
<div className="neighbor-loading-bar" />
</div>
</div>
)}
</div>
</div>
);
Expand Down
42 changes: 37 additions & 5 deletions web/src/component/custom/basic-components.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,46 @@
import { Collapse } from "antd";
import { HolderOutlined } from "@ant-design/icons";

// TODO put these blocks to a universal file
// TODO add resize/drag/dock-to mouse interaction
export function FunctionalBlock(props: { label?: string; children?: null | React.ReactNode | React.ReactNode[]; }) {

type FunctionalBlockProps = {
label?: string;
children?: React.ReactNode;
defaultCollapsed?: boolean;
dragHandleProps?: React.HTMLAttributes<HTMLSpanElement>;
};

export function FunctionalBlock(props: FunctionalBlockProps) {
if (!props.label) return <div className="functional-block" style={{ overflow: "visible" }}>{props.children}</div>;

const header = (
<div style={{ display: "flex", alignItems: "center", gap: 6, flex: 1 }}>
{props.dragHandleProps && (
<span
{...props.dragHandleProps}
style={{ cursor: "grab", color: "#aaa", fontSize: 12, lineHeight: 1 }}
onClick={(e) => e.stopPropagation()}
>
<HolderOutlined />
</span>
)}
<span style={{ fontSize: 12, fontWeight: 600, flex: 1 }}>{props.label}</span>
</div>
);

return (
<div className="functional-block" style={{ overflow: 'visible' }}>
{props.label && <div className="functional-block-title">{props.label}</div>}
{props.children}
<div className="functional-block" style={{ overflow: "visible" }}>
<Collapse
size="small"
defaultActiveKey={props.defaultCollapsed ? [] : ["block"]}
items={[{ key: "block", label: header, children: props.children }]}
/>
</div>
);
}
export function ComponentBlock(props: { label?: string; children?: null | React.ReactNode | React.ReactNode[]; }) {

export function ComponentBlock(props: { label?: string; children?: React.ReactNode }) {
return (
<div className="component-block">
{props.label && <div className="label">{props.label}</div>}
Expand Down
Loading