Skip to content

Commit

Permalink
Merge pull request #68 from zilliztech/federTest
Browse files Browse the repository at this point in the history
Improve code structure
  • Loading branch information
shanghaikid committed Sep 30, 2022
2 parents 5a5b6ad + f9d34d9 commit 22c19bd
Show file tree
Hide file tree
Showing 20 changed files with 302 additions and 353 deletions.
3 changes: 2 additions & 1 deletion dev/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ window.addEventListener('DOMContentLoaded', async () => {
const testSearchParams = {
k: 4,
ef: 6,
nprobe: 4,
nprobe: 6,
};

const mediaCallback = await getRowId2imgUrl();
const viewParams = {
width: 800,
height: 480,
canvasScale: 1,
projectParams: { projectSeed: 12315 },
mediaType: 'img',
mediaCallback,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TLayoutParamsHnsw } from 'Types/visData';
export const defaultHnswLayoutParams = {
width: 800,
height: 480,
canvasScale: 2,
canvasScale: 1,
targetOrigin: [0, 0],
numForceIterations: 100,
padding: [80, 200, 60, 220],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { EProjectMethod } from "Types";

export const defaultLayoutParamsIvfflat = {
numForceIterations: 100,
width: 800,
height: 480,
canvasScale: 1,
coarseSearchWithProjection: true,
fineSearchWithProjection: true,
projectMethod: EProjectMethod.umap,
projectParams: {},
polarOriginBias: 0.15,
polarRadiusUpperBound: 0.97,
nonTopkNodeR: 3,
minVoronoiRadius: 5,
projectPadding: [20, 30, 20, 30],
staticPanelWidth: 240,
};
23 changes: 4 additions & 19 deletions federjs/FederLayout/visDataHandler/ivfflat/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EProjectMethod, EViewType } from 'Types';
import { EViewType } from 'Types';
import { TIndexMetaIvfflat } from 'Types/indexMeta';
import { TSearchRecords } from 'Types/searchRecords';
import {
Expand All @@ -8,6 +8,7 @@ import {
TVisDataIvfflatSearchView,
} from 'Types/visData';
import { TFederLayoutHandler } from '../../FederLayoutHandler';
import { defaultLayoutParamsIvfflat } from './defaultLayoutParamsIvfflat';
import IvfflatOverviewLayout from './overview';
import IvfflatSearchViewLayout from './search';

Expand All @@ -19,22 +20,6 @@ const searchViewLayoutFuncMap = {
[EViewType.default]: IvfflatSearchViewLayout,
};

const layoutParamsIvfflatDefault = {
numForceIterations: 100,
width: 800,
height: 480,
canvasScale: 2,
coarseSearchWithProjection: true,
fineSearchWithProjection: true,
projectMethod: EProjectMethod.umap,
projectParams: {},
polarOriginBias: 0.15,
polarRadiusUpperBound: 0.97,
nonTopkNodeR: 3,
minVoronoiRadius: 5,
projectPadding: [20, 30, 20, 30],
staticPanelWidth: 240,
};
export default class FederLayoutIvfflat implements TFederLayoutHandler {
overviewLayoutParams: TLayoutParamsIvfflat = {};
overviewClusters: TVisDataIvfflatOverviewCluster;
Expand All @@ -45,7 +30,7 @@ export default class FederLayoutIvfflat implements TFederLayoutHandler {
): Promise<TVisDataIvfflatOverview> {
const layoutParams = Object.assign(
{},
layoutParamsIvfflatDefault,
defaultLayoutParamsIvfflat,
_layoutParams
);
this.overviewLayoutParams = layoutParams;
Expand All @@ -63,7 +48,7 @@ export default class FederLayoutIvfflat implements TFederLayoutHandler {
): Promise<TVisDataIvfflatSearchView> {
const layoutParams = Object.assign(
{},
layoutParamsIvfflatDefault,
defaultLayoutParamsIvfflat,
_layoutParams
);
const searchViewLayoutFunc = searchViewLayoutFuncMap[viewType];
Expand Down
240 changes: 19 additions & 221 deletions federjs/FederView/hnswView/HnswOverview/index.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
import * as d3 from 'd3';
import {
getNodeIdWithLevel,
parseNodeIdWidthLevel,
} from 'FederLayout/visDataHandler/hnsw/utils';
import clearCanvas from 'FederView/clearCanvas';
import InfoPanel, { TInfoPanelContentItem } from 'FederView/InfoPanel';
import InfoPanel from 'FederView/InfoPanel';
import initCanvas from 'FederView/initCanvas';
import initEventListener from 'FederView/initEventListener';
import TViewHandler from 'FederView/types';
import { EMediaType, TCoord, TD3Link, TId } from 'Types';
import { TCoord, TId } from 'Types';
import {
TViewParamsHnsw,
TVisDataHnswGraph,
TVisDataHnswGraphNode,
TVisDataHnswOverview,
} from 'Types/visData';
import { getDisL2Square, vecAdd, vecMultiply } from 'Utils/distFunc';
import { getDisL2Square } from 'Utils/distFunc';
import defaultViewParamsHnsw from '../defaultViewParamsHnsw';
import renderLayer from '../HnswSearchView/renderLayer';
import initPanels from '../initPanels';
import renderTipLine from '../renderTipLine';
import renderLinks from './renderLinks';
import renderNodes from './renderNodes';
import updateClickedPanel from './updateClickedPanel';
import updateStaticPanel from './updateStaticPanel';
import renderView from './renderView';
export default class HnswOverview implements TViewHandler {
node: HTMLElement;
staticPanel: InfoPanel;
Expand Down Expand Up @@ -59,8 +55,8 @@ export default class HnswOverview implements TViewHandler {
}
init(): void {
this.initIdWithLevel2node();
this.initCanvas();
this.initEventListener();
initCanvas.call(this);
initEventListener.call(this);
initPanels.call(this);
}
initIdWithLevel2node() {
Expand All @@ -72,126 +68,14 @@ export default class HnswOverview implements TViewHandler {
);
this.idWithLevel2node = idWithLevel2node;
}
initCanvas() {
const { width, height, canvasScale } = this.viewParams;
const divD3 = d3
.create('div')
.style('width', `${width}px`)
.style('height', `${height}px`)
.style('position', 'relative');
this.node = divD3.node();
const canvasD3 = divD3
.append('canvas')
.attr('width', width)
.attr('height', height);
this.ctx = canvasD3.node().getContext('2d');
this.ctx.scale(1 / canvasScale, 1 / canvasScale);
}
render(): void {
this.initView();
}
async updateStaticPanel() {
this.staticPanel.setContent({
themeColor: '#FFFFFF',
hasBorder: true,
content: [
{
title: 'HNSW',
},
{ text: `M = ${this.M}, ef_construction = ${this.efConstruction}` },
{
text: `${this.ntotal} vectors, ${this.nlevels}-layer hierarchical graph (only visual the top-${this.overviewNodesLevels.length} layers).`,
},
...this.nodesCount
.map((c, level) => {
return {
title: `Level ${level}`,
text: `${c} vectors, ${this.linksCount[level]} links`,
};
})
.reverse(),
],
});
}
async updateClickedPanel() {
const node = this.clickedNode;
if (!node) {
this.clickedPanel.setContent({ content: [] });
return;
}

const mediaContent = {} as TInfoPanelContentItem;
if (this.viewParams.mediaType === EMediaType.image)
mediaContent.image = this.viewParams.mediaContent(node.id);
else if (this.viewParams.mediaType === EMediaType.text)
mediaContent.text = this.viewParams.mediaContent(node.id);

const pathFromEntryTexts = this.overviewNodesLevels
.filter((_, level) => {
return level >= this.clickedLevel;
})
.map(
({ level }) =>
`level ${level}: ` +
node.pathFromEntry
.filter(
(idWithLevel) => parseNodeIdWidthLevel(idWithLevel)[0] === level
)
.map((idWithLevel) => parseNodeIdWidthLevel(idWithLevel)[1])
.join(' => ')
)
.reverse();

const linkedNodeText = node.links.join(', ');
this.clickedPanel.setContent({
themeColor: '#FFFC85',
hasBorder: true,
content: [
{ title: `Level ${node.level}` },
{ title: `Row No. ${node.id}` },
mediaContent,
{ title: `Shortest path from the entry:` },
...pathFromEntryTexts.map((text) => ({ text })),
{ title: `Linked vectors:` },
{ text: linkedNodeText },
],
});
}
async updateHoveredPanel(hoveredPanelPos: TCoord, reverse = false) {
if (!hoveredPanelPos) {
this.hoveredPanel.setContent({ content: [] });
return;
}
if (reverse)
this.hoveredPanel.setPosition({
left: null,
right: `${this.viewParams.width - hoveredPanelPos[0]}px`,
top: `${hoveredPanelPos[1] - 4}px`,
});
else
this.hoveredPanel.setPosition({
left: `${hoveredPanelPos[0]}px`,
top: `${hoveredPanelPos[1] - 4}px`,
});

const mediaContent = {} as TInfoPanelContentItem;
if (this.viewParams.mediaType === EMediaType.image)
mediaContent.image = this.viewParams.mediaContent(this.hoveredNode.id);
else if (this.viewParams.mediaType === EMediaType.text)
mediaContent.text = this.viewParams.mediaContent(this.hoveredNode.id);

this.hoveredPanel.setContent({
themeColor: '#FFFC85',
hasBorder: false,
flex: true,
flexDirection: reverse ? 'row-reverse' : 'row',
content: [{ title: `No. ${this.hoveredNode.id}` }, mediaContent],
});
}
initView() {
// event;
this.renderView();
this.updateStaticPanel();
renderView.call(this);
updateStaticPanel.call(this);

const mouse2level = (x: number, y: number) =>
this.overviewLayerPosLevels.findIndex((points) =>
Expand All @@ -214,13 +98,13 @@ export default class HnswOverview implements TViewHandler {
const clickedNode = mouse2node(x, y, this.clickedLevel);
if (clickedNode != this.clickedNode) {
this.clickedNode = clickedNode;
this.renderView();
this.updateClickedPanel();
renderView.call(this);
updateClickedPanel.call(this);
}
} else {
this.clickedNode = null;
this.renderView();
this.updateClickedPanel();
renderView.call(this);
updateClickedPanel.call(this);
}
};
this.mouseMoveHandler = ({ x, y }: { x: number; y: number }) => {
Expand All @@ -229,104 +113,18 @@ export default class HnswOverview implements TViewHandler {
const hoveredNode = mouse2node(x, y, this.hoveredLevel);
if (hoveredNode != this.hoveredNode) {
this.hoveredNode = hoveredNode;
this.renderView();
renderView.call(this);
}
} else {
this.hoveredLevel = -1;
this.hoveredNode = null;
this.renderView();
renderView.call(this);
}
};
this.mouseLeaveHandler = () => {
this.hoveredLevel = -1;
this.hoveredNode = null;
this.renderView();
renderView.call(this);
};
}
initEventListener() {
const { canvasScale } = this.viewParams;
this.node.addEventListener('mousemove', (e) => {
const { offsetX, offsetY } = e;
const x = offsetX * canvasScale;
const y = offsetY * canvasScale;
this.mouseMoveHandler && this.mouseMoveHandler({ x, y });
});
this.node.addEventListener('click', (e) => {
const { offsetX, offsetY } = e;
const x = offsetX * canvasScale;
const y = offsetY * canvasScale;
this.mouseClickHandler && this.mouseClickHandler({ x, y });
});
this.node.addEventListener('mouseleave', () => {
this.mouseLeaveHandler && this.mouseLeaveHandler();
});
}
renderView() {
clearCanvas.call(this);

const highlightNode = this.clickedNode || this.hoveredNode;

for (let i = 0; i < this.overviewNodesLevels.length; i++) {
const { nodes, level } = this.overviewNodesLevels[i];

const baseLinks =
i > 1
? nodes.reduce(
(acc, node) =>
acc.concat(
node.links.map((targetId) => ({
source: getNodeIdWithLevel(node.id, level),
target: getNodeIdWithLevel(targetId, level),
}))
),
[] as TD3Link[]
)
: [];
const pathFromEntryLinks =
(highlightNode?.pathFromEntry
.map((idWithLevel, k) => {
const [_level, id] = parseNodeIdWidthLevel(idWithLevel);
if (k > 0 && _level === level) {
return {
source: highlightNode.pathFromEntry[k - 1],
target: idWithLevel,
};
}
return null;
})
.filter((a) => a) as TD3Link[]) || [];
const path2NeighborLinks =
highlightNode && level === highlightNode.level
? highlightNode.links.map(
(neighborId) =>
({
source: highlightNode.idWithLevel,
target: getNodeIdWithLevel(neighborId, highlightNode.level),
} as TD3Link)
)
: [];

renderLayer.call(this, this.overviewLayerPosLevels[i]);
renderLinks.call(this, baseLinks, pathFromEntryLinks, path2NeighborLinks);
}

renderNodes.call(this);

if (!!this.hoveredNode) {
const nodePos = this.hoveredNode.overviewPos;
const origin = vecMultiply(
vecAdd(
this.overviewLayerPosLevels[0][0],
this.overviewLayerPosLevels[0][2]
),
0.5
);
const reverse = this.hoveredNode.overviewPos[0] < origin[0];
const tooltipPos = renderTipLine.call(this, nodePos, reverse);
this.updateHoveredPanel(
vecMultiply(tooltipPos, 1 / this.viewParams.canvasScale) as TCoord,
reverse
);
} else this.updateHoveredPanel(null);
}
}
Loading

0 comments on commit 22c19bd

Please sign in to comment.