Skip to content

Commit 66a9a67

Browse files
committed
Dynamic edge tickness, score badges, and long text truncation.
1 parent 2b82511 commit 66a9a67

File tree

2 files changed

+59
-38
lines changed

2 files changed

+59
-38
lines changed

frontend/app/lib/GraphNode.tsx

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,33 @@ export default function GraphNode(props: {
1616
onEdit?: (apiId: number, currentName: string, nodeType: "cc" | "co" | "po") => void;
1717
onDelete?: (apiId: number, currentName: string) => void;
1818
}) {
19-
// Use smaller font for CO/PO if text is longer than 150 chars
20-
const isLongText = props.data.label.length > 150;
19+
// Truncate CO/PO text at 100 chars and show full text on hover
2120
const isOutcome = props.data.nodeType === "co" || props.data.nodeType === "po";
22-
const useSmallFont = isLongText && isOutcome;
21+
const shouldTruncate = isOutcome && props.data.label.length > 85;
22+
const truncatedLabel = shouldTruncate ? props.data.label.slice(0, 85) + '…' : props.data.label;
2323

2424
return (
2525
<div className="flex flex-col h-full">
2626
{props.targetPosition && (
27-
<Handle type="target" position={props.targetPosition} />
27+
<Handle type="target" position={props.targetPosition} className="h-2! w-2!" />
2828
)}
29-
<div
30-
className={`font-medium mb-1 text-center flex-1 flex items-center justify-center ${useSmallFont ? 'text-xs' : ''}`}
31-
style={{
29+
<div
30+
className="font-medium mb-1 text-center flex-1 flex items-center justify-center"
31+
style={{
3232
wordBreak: 'break-word',
3333
overflowWrap: 'break-word',
3434
hyphens: 'auto',
35-
fontSize: useSmallFont ? '0.9em' : undefined,
36-
lineHeight: useSmallFont ? '1.3' : undefined,
3735
}}
36+
title={shouldTruncate ? props.data.label : undefined}
3837
>
39-
{props.data.label}
38+
{truncatedLabel}
4039
</div>
41-
40+
4241
{(props.data.score !== undefined && props.data.score !== null) && (
43-
<div className="text-xs text-center text-gray-600 mb-2 font-semibold">
44-
Score: {Math.round(props.data.score)}%
42+
<div className="absolute bottom-2 right-2">
43+
<span className="inline-block bg-indigo-800 text-white text-xs font-bold rounded-full px-2 py-0.5 shadow-sm">
44+
{Math.round(props.data.score)}%
45+
</span>
4546
</div>
4647
)}
4748

@@ -69,7 +70,7 @@ export default function GraphNode(props: {
6970
</ActionIcon>
7071
</div>
7172
{props.sourcePosition && (
72-
<Handle type="source" position={props.sourcePosition} />
73+
<Handle type="source" position={props.sourcePosition} className="h-2! w-2!" />
7374
)}
7475
</div>
7576
);

frontend/app/lib/MainGraph.tsx

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,11 @@ const DISTINCT_COLORS = [
4747

4848
// Constants for layout
4949
const NODE_PADDING = 20; // padding inside node
50-
const NODE_MIN_WIDTH = 150;
50+
const NODE_MIN_WIDTH = 180; // 10em assuming 1em = 16px
5151
const NODE_MAX_WIDTH = 350;
5252
const CHAR_WIDTH = 8; // approximate character width for normal font
5353
const CHAR_WIDTH_SMALL = 6; // approximate character width for smaller font (11px)
54-
const NODE_BASE_HEIGHT = 80; // base height for node
55-
const NODE_BASE_HEIGHT_SMALL = 65; // base height for node with small font
54+
const NODE_BASE_HEIGHT = 50; // base height for node
5655
const VERTICAL_GAP = 50; // vertical gap between nodes in same layer
5756
const LAYER_GAP = 225; // horizontal gap between layers (increased for clarity)
5857

@@ -63,12 +62,11 @@ const calculateNodeWidth = (label: string): number => {
6362
};
6463

6564
// Calculate node height (can expand for multi-line text)
66-
// useSmallFont: for CO/PO nodes with text > 150 chars
67-
const calculateNodeHeight = (label: string, width: number, useSmallFont: boolean = false): number => {
68-
const charWidth = useSmallFont ? CHAR_WIDTH_SMALL : CHAR_WIDTH;
69-
const baseHeight = useSmallFont ? NODE_BASE_HEIGHT_SMALL : NODE_BASE_HEIGHT;
70-
const lineHeight = useSmallFont ? 14 : 20; // smaller line height for small font
71-
65+
// If isTruncatedPO is true, use a smaller base height to avoid extra space
66+
const calculateNodeHeight = (label: string, width: number, isTruncatedPO?: boolean): number => {
67+
const charWidth = CHAR_WIDTH;
68+
const baseHeight = isTruncatedPO ? 40 : NODE_BASE_HEIGHT;
69+
const lineHeight = 20;
7270
const charsPerLine = Math.floor((width - NODE_PADDING * 2) / charWidth);
7371
const lines = Math.ceil(label.length / charsPerLine);
7472
return baseHeight + Math.max(0, lines - 1) * lineHeight;
@@ -229,30 +227,31 @@ const convertToNodes = (
229227
// CC nodes: normal font
230228
const ccDimensions = sortedCC.map(node => {
231229
const width = calculateNodeWidth(node.name);
232-
const height = calculateNodeHeight(node.name, width, false);
230+
const height = calculateNodeHeight(node.name, width);
233231
return { width, height };
234232
});
235233

236-
// CO nodes: use small font if text > 150 chars
234+
// CO nodes: use truncated label and reduced base height if text > 85 chars
237235
const coDimensions = sortedCO.map(node => {
238236
const width = calculateNodeWidth(node.name);
239-
const useSmallFont = node.name.length > 150;
240-
const height = calculateNodeHeight(node.name, width, useSmallFont);
237+
const isTruncated = node.name.length > 85;
238+
const displayLabel = isTruncated ? node.name.slice(0, 85) + '…' : node.name;
239+
const height = calculateNodeHeight(displayLabel, width, isTruncated);
241240
return { width, height };
242241
});
243242

244-
// PO nodes: use small font if text > 150 chars
243+
// PO nodes: use truncated label for height if text > 85 chars, and reduce base height for truncated
245244
const poDimensions = sortedPO.map(node => {
246245
const width = calculateNodeWidth(node.name);
247-
const useSmallFont = node.name.length > 150;
248-
const height = calculateNodeHeight(node.name, width, useSmallFont);
246+
const isTruncated = node.name.length > 85;
247+
const displayLabel = isTruncated ? node.name.slice(0, 85) + '…' : node.name;
248+
const height = calculateNodeHeight(displayLabel, width, isTruncated);
249249
return { width, height };
250250
});
251251

252252
// 3. Calculate max width per layer for alignment
253253
const ccMaxWidth = ccDimensions.length > 0 ? Math.max(...ccDimensions.map(d => d.width)) : NODE_MIN_WIDTH;
254254
const coMaxWidth = coDimensions.length > 0 ? Math.max(...coDimensions.map(d => d.width)) : NODE_MIN_WIDTH;
255-
const poMaxWidth = poDimensions.length > 0 ? Math.max(...poDimensions.map(d => d.width)) : NODE_MIN_WIDTH;
256255

257256
// 4. Calculate total height per layer
258257
const ccTotalHeight = ccDimensions.reduce((sum, d) => sum + d.height + VERTICAL_GAP, -VERTICAL_GAP);
@@ -393,6 +392,13 @@ const convertToEdges = (data: GetNodesResponse): Edge[] => {
393392
const color = DISTINCT_COLORS[colorIndex % DISTINCT_COLORS.length];
394393
colorIndex++;
395394

395+
// Make edge thickness depend on weight (e.g. 1-5 maps to 2-6px)
396+
const minStroke = 1.5;
397+
const maxStroke = 4;
398+
const minWeight = 1;
399+
const maxWeight = 5;
400+
const weight = Math.max(minWeight, Math.min(maxWeight, rel.weight));
401+
const strokeWidth = minStroke + ((weight - minWeight) / (maxWeight - minWeight)) * (maxStroke - minStroke);
396402
edges.push({
397403
id: `e-${rel.relation_id}`,
398404
source: srcId,
@@ -402,7 +408,7 @@ const convertToEdges = (data: GetNodesResponse): Edge[] => {
402408
animated: true,
403409
style: {
404410
stroke: color,
405-
strokeWidth: 2,
411+
strokeWidth,
406412
},
407413
labelStyle: {
408414
fill: color,
@@ -729,10 +735,15 @@ export default function MainGraph() {
729735
weightValue
730736
);
731737

738+
// Use same thickness logic as convertToEdges
739+
const minStroke = 1.5;
740+
const maxStroke = 4;
741+
const minWeight = 1;
742+
const maxWeight = 5;
743+
const weight = Math.max(minWeight, Math.min(maxWeight, weightValue));
744+
const strokeWidth = minStroke + ((weight - minWeight) / (maxWeight - minWeight)) * (maxStroke - minStroke);
732745
const sourceLayer = getNodeLayer(pendingConnection.source);
733-
const edgeColor =
734-
sourceLayer === "course_content" ? "#1976d2" : "#388e3c";
735-
746+
const edgeColor = sourceLayer === "course_content" ? "#1976d2" : "#388e3c";
736747
const newEdge: Edge = {
737748
id: `e-${response.relation_id}`,
738749
source: pendingConnection.source,
@@ -745,7 +756,7 @@ export default function MainGraph() {
745756
animated: true,
746757
style: {
747758
stroke: edgeColor,
748-
strokeWidth: 2,
759+
strokeWidth,
749760
},
750761
labelStyle: {
751762
fill: edgeColor,
@@ -757,7 +768,6 @@ export default function MainGraph() {
757768
fillOpacity: 0.9,
758769
},
759770
};
760-
761771
setEdges((eds) => addEdge(newEdge, eds));
762772
closeWeightModal();
763773
} else if (modalMode === "edit" && editingEdge) {
@@ -770,18 +780,28 @@ export default function MainGraph() {
770780

771781
await updateRelation(relationId, weightValue);
772782

783+
// Use same thickness logic as convertToEdges
784+
const minStroke = 1.5;
785+
const maxStroke = 4;
786+
const minWeight = 1;
787+
const maxWeight = 5;
788+
const weight = Math.max(minWeight, Math.min(maxWeight, weightValue));
789+
const strokeWidth = minStroke + ((weight - minWeight) / (maxWeight - minWeight)) * (maxStroke - minStroke);
773790
setEdges((prev) =>
774791
prev.map((e) =>
775792
e.id === editingEdge.id
776793
? {
777794
...e,
778795
label: `${weightValue}`,
779796
data: { ...e.data, weight: weightValue },
797+
style: {
798+
...(e.style || {}),
799+
strokeWidth,
800+
},
780801
}
781802
: e
782803
)
783804
);
784-
785805
closeWeightModal();
786806
}
787807
} catch (err) {

0 commit comments

Comments
 (0)