66
77import { Dispatch , SetStateAction , useCallback , useContext , useEffect , useMemo , useRef , useState } from "react"
88import ForceGraph2D from "react-force-graph-2d"
9- import { securedFetch , GraphRef , handleZoomToFit , getTheme , Tab , ViewportState , getNodeDisplayText } from "@/lib/utils"
9+ import { securedFetch , GraphRef , handleZoomToFit , getTheme , Tab , ViewportState , getNodeDisplayText , getContrastTextColor } from "@/lib/utils"
1010import { useToast } from "@/components/ui/use-toast"
1111import * as d3 from "d3"
1212import { useTheme } from "next-themes"
13- import { Link , Node , Relationship , Graph , getLabelWithFewestElements , GraphData } from "../api/graph/model"
13+ import { Link , Node , Relationship , Graph , getLabelWithFewestElements , GraphData , EMPTY_DISPLAY_NAME } from "../api/graph/model"
1414import { BrowserSettingsContext , IndicatorContext } from "./provider"
1515import Spinning from "./ui/spinning"
1616
@@ -294,10 +294,12 @@ export default function ForceGraph({
294294 } ) ;
295295 }
296296
297- // Add collision force to prevent node overlap (scale radius by node degree)
297+ // Add collision force to prevent node overlap (scale radius by node degree and custom size )
298298 chartRef . current . d3Force ( 'collision' , d3 . forceCollide ( ( node : Node ) => {
299299 const degree = nodeDegreeMap . get ( node . id ) || 0 ;
300- return COLLISION_BASE_RADIUS + Math . sqrt ( degree ) * HIGH_DEGREE_PADDING ;
300+ const label = getLabelWithFewestElements ( node . labels . map ( l => graph . LabelsMap . get ( l ) || graph . createLabel ( [ l ] ) [ 0 ] ) ) ;
301+ const customSize = label . style ?. customSize || 1 ;
302+ return ( COLLISION_BASE_RADIUS * customSize ) + Math . sqrt ( degree ) * HIGH_DEGREE_PADDING ;
301303 } ) . strength ( COLLISION_STRENGTH ) . iterations ( 2 ) ) ;
302304
303305 // Center force to keep graph centered
@@ -324,7 +326,8 @@ export default function ForceGraph({
324326 // Clear cached display names when displayTextPriority changes
325327 useEffect ( ( ) => {
326328 data . nodes . forEach ( node => {
327- node . displayName = [ '' , '' ] ;
329+ // eslint-disable-next-line no-param-reassign
330+ node . displayName = [ ...EMPTY_DISPLAY_NAME ] ;
328331 } ) ;
329332 // Force a re-render by reheating the simulation
330333 if ( chartRef . current ) {
@@ -441,9 +444,14 @@ export default function ForceGraph({
441444 nodeLabel = { ( node ) => type === "graph" ? handleGetNodeDisplayText ( node ) : node . labels [ 0 ] }
442445 graphData = { data }
443446 nodeRelSize = { NODE_SIZE }
444- nodeCanvasObjectMode = { ( ) => 'after' }
447+ nodeVal = { ( node ) => {
448+ // Return the square of customSize because library uses Math.sqrt(nodeVal)
449+ const label = getLabelWithFewestElements ( node . labels . map ( l => graph . LabelsMap . get ( l ) || graph . createLabel ( [ l ] ) [ 0 ] ) ) ;
450+ const customSize = label . style ?. customSize || 1 ;
451+ return customSize * customSize ; // Squared because library will sqrt it
452+ } }
453+ nodeCanvasObjectMode = { ( ) => 'replace' }
445454 linkCanvasObjectMode = { ( ) => 'after' }
446- linkDirectionalArrowRelPos = { 1 }
447455 linkDirectionalArrowLength = { ( link ) => {
448456 let length = 0 ;
449457
@@ -453,6 +461,7 @@ export default function ForceGraph({
453461
454462 return length ;
455463 } }
464+ linkDirectionalArrowRelPos = { 1 }
456465 linkDirectionalArrowColor = { ( link ) => link . color }
457466 linkWidth = { ( link ) => isLinkSelected ( link ) ? 2 : 1 }
458467 nodeCanvasObject = { ( node , ctx ) => {
@@ -462,17 +471,26 @@ export default function ForceGraph({
462471 node . y = 0
463472 }
464473
474+ // Get label style customization
475+ const label = getLabelWithFewestElements ( node . labels . map ( l => graph . LabelsMap . get ( l ) || graph . createLabel ( [ l ] ) [ 0 ] ) ) ;
476+ const customSize = label . style ?. customSize || 1 ;
477+ const nodeSize = NODE_SIZE * customSize ;
478+
479+ // Draw the node circle with custom color and size
480+ ctx . fillStyle = node . color ;
481+ ctx . beginPath ( ) ;
482+ ctx . arc ( node . x , node . y , nodeSize , 0 , 2 * Math . PI , false ) ;
483+ ctx . fill ( ) ;
484+
485+ // Draw the border
465486 ctx . lineWidth = ( ( selectedElements . length > 0 && selectedElements . some ( el => el . id === node . id && ! el . source ) ) )
466487 || ( hoverElement && ! hoverElement . source && hoverElement . id === node . id )
467488 ? 1.5 : 0.5
468489 ctx . strokeStyle = foreground ;
469-
470- ctx . beginPath ( ) ;
471- ctx . arc ( node . x , node . y , NODE_SIZE , 0 , 2 * Math . PI , false ) ;
472490 ctx . stroke ( ) ;
473- ctx . fill ( ) ;
474491
475- ctx . fillStyle = 'black' ;
492+ // Set text color based on node background color for better contrast
493+ ctx . fillStyle = getContrastTextColor ( node . color ) ;
476494 ctx . textAlign = 'center' ;
477495 ctx . textBaseline = 'middle' ;
478496 ctx . font = `400 2px SofiaSans` ;
@@ -485,13 +503,27 @@ export default function ForceGraph({
485503 let text = '' ;
486504
487505 if ( type === "graph" ) {
488- text = handleGetNodeDisplayText ( node ) ;
506+ // Check if label has custom caption property
507+ const customCaption = label . style ?. customCaption ;
508+ if ( customCaption ) {
509+ if ( customCaption === "Description" ) {
510+ text = handleGetNodeDisplayText ( node ) ;
511+ } else if ( customCaption === "id" ) {
512+ text = String ( node . id ) ;
513+ } else if ( node . data [ customCaption ] ) {
514+ text = String ( node . data [ customCaption ] ) ;
515+ } else {
516+ text = handleGetNodeDisplayText ( node ) ;
517+ }
518+ } else {
519+ text = handleGetNodeDisplayText ( node ) ;
520+ }
489521 } else {
490- text = getLabelWithFewestElements ( node . labels . map ( label => graph . LabelsMap . get ( label ) || graph . createLabel ( [ label ] ) [ 0 ] ) ) . name ;
522+ text = label . name ;
491523 }
492524
493525 // Calculate text wrapping for circular node
494- const textRadius = NODE_SIZE - PADDING / 2 ; // Leave some padding inside the circle
526+ const textRadius = nodeSize - PADDING / 2 ; // Leave some padding inside the circle
495527 [ line1 , line2 ] = wrapTextForCircularNode ( ctx , text , textRadius ) ;
496528
497529 // Cache the result
0 commit comments