Harshith Mullapudi 4882f227d2
Feat: clusters (#37)
* Feat: clustering fact statements

* Feat: cluster drift

* Feat: add recall count and model to search

* Feat: Github integration

* Fix: clustering UI

* Improve graph

* Bump: new version

---------

Co-authored-by: Manoj K <saimanoj58@gmail.com>
2025-08-05 15:31:15 +05:30

154 lines
4.1 KiB
TypeScript

import type {
Node,
Edge,
GraphNode,
GraphEdge,
RawTriplet,
GraphTriplet,
} from "./type";
export function toGraphNode(node: Node): GraphNode {
const primaryLabel =
node.labels?.find((label) => label != "Entity") || "Entity";
return {
id: node.uuid,
value: node.name,
uuid: node.uuid,
name: node.name,
createdAt: node.createdAt,
attributes: node.attributes,
summary: node.summary,
labels: node.labels,
primaryLabel,
clusterId: node?.clusterId, // Extract cluster ID from attributes
};
}
export function toGraphEdge(edge: Edge): GraphEdge {
return {
id: edge.uuid,
value: edge.type,
...edge,
};
}
export function toGraphTriplet(triplet: RawTriplet): GraphTriplet {
return {
source: toGraphNode(triplet.sourceNode),
relation: toGraphEdge(triplet.edge),
target: toGraphNode(triplet.targetNode),
};
}
export function toGraphTriplets(triplets: RawTriplet[]): GraphTriplet[] {
return triplets.map(toGraphTriplet);
}
export function drawRoundRect(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
width: number,
height: number,
radius: number,
): void {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
}
const TEXT_COLOR = "#000000";
export function drawHover(
context: CanvasRenderingContext2D,
data: any,
settings: any,
) {
const size = settings.labelSize;
const font = settings.labelFont;
const weight = settings.labelWeight;
const subLabelSize = size - 2;
const label = data.label;
const subLabel = data.tag !== "unknown" ? data.tag : "";
const entityLabel = data.nodeData.attributes.nodeType;
// Simulate the --shadow-1 Tailwind shadow:
// lch(0 0 0 / 0.022) 0px 3px 6px -2px, lch(0 0 0 / 0.044) 0px 1px 1px;
// Canvas only supports a single shadow, so we approximate with the stronger one.
// lch(0 0 0 / 0.044) is roughly rgba(0,0,0,0.044)
context.beginPath();
context.fillStyle = "#fff";
context.shadowOffsetX = 0;
context.shadowOffsetY = 1;
context.shadowBlur = 1;
context.shadowColor = "rgba(0,0,0,0.044)";
context.font = `${weight} ${size}px ${font}`;
const labelWidth = context.measureText(label).width;
context.font = `${weight} ${subLabelSize}px ${font}`;
const subLabelWidth = subLabel ? context.measureText(subLabel).width : 0;
context.font = `${weight} ${subLabelSize}px ${font}`;
const entityLabelWidth = entityLabel
? context.measureText(entityLabel).width
: 0;
const textWidth = Math.max(labelWidth, subLabelWidth, entityLabelWidth);
const x = Math.round(data.x);
const y = Math.round(data.y);
const w = Math.round(textWidth + size / 2 + data.size + 3);
const hLabel = Math.round(size / 2 + 4);
const hSubLabel = subLabel ? Math.round(subLabelSize / 2 + 9) : 0;
const hentityLabel = Math.round(subLabelSize / 2 + 9);
drawRoundRect(
context,
x,
y - hSubLabel - 12,
w,
hentityLabel + hLabel + hSubLabel + 12,
5,
);
context.closePath();
context.fill();
// Remove shadow for text
context.shadowOffsetX = 0;
context.shadowOffsetY = 0;
context.shadowBlur = 0;
context.shadowColor = "transparent";
// And finally we draw the labels
context.fillStyle = TEXT_COLOR;
context.font = `${weight} ${size}px ${font}`;
context.fillText(label, data.x + data.size + 3, data.y + size / 3);
if (subLabel) {
context.fillStyle = TEXT_COLOR;
context.font = `${weight} ${subLabelSize}px ${font}`;
context.fillText(
subLabel,
data.x + data.size + 3,
data.y - (2 * size) / 3 - 2,
);
}
context.fillStyle = data.color;
context.font = `${weight} ${subLabelSize}px ${font}`;
context.fillText(
entityLabel,
data.x + data.size + 3,
data.y + size / 3 + 3 + subLabelSize,
);
}