stillerman commited on
Commit
6beea84
·
1 Parent(s): cb1d20a

better styles

Browse files
src/components/force-directed-graph.tsx CHANGED
@@ -7,23 +7,37 @@ import * as d3 from "d3";
7
  // This is a placeholder component for the force-directed graph
8
  // In a real implementation, you would use a library like D3.js or react-force-graph
9
 
 
 
 
 
 
 
 
 
 
 
 
10
  interface ForceDirectedGraphProps {
11
  runId: number | null;
12
  runs: Run[];
13
  }
14
 
15
  export default function ForceDirectedGraph({runs, runId }: ForceDirectedGraphProps) {
16
- const [graphData, setGraphData] = useState<{nodes: {id: string, color?: string}[], links: {source: string, target: string, color?: string}[]}>({nodes: [], links: []});
17
  const [dimensions, setDimensions] = useState({ width: 800, height: 800 });
18
  const containerRef = useRef<HTMLDivElement>(null);
19
  const graphRef = useRef<ForceGraphMethods<NodeObject<{ id: string; color?: string; }>, LinkObject<{ id: string; color?: string; }, { source: string; target: string; color?: string; }>>>(null);
 
20
  useEffect(() => {
21
-
22
- const newGraphData: {nodes: {id: string, color?: string}[], links: {source: string, target: string, color?: string}[]} = {nodes: [], links: []};
23
  const nodesSet: Set<string> = new Set();
24
  const mainNodeSet: Set<string> = new Set();
25
 
26
  if(runs) {
 
 
 
27
  runs.forEach((run, runIndex) => {
28
  // add in src and dst to nodes
29
  const isSelectedRun = runId === runIndex;
@@ -43,10 +57,14 @@ export default function ForceDirectedGraph({runs, runId }: ForceDirectedGraphPro
43
  nodesSet.add(nextStep.article);
44
  }
45
 
 
 
 
 
46
  newGraphData.links.push({
47
  source: step.article,
48
  target: nextStep.article,
49
- color: isSelectedRun ? '#ff6b6b' : 'gray'
50
  });
51
  }
52
 
@@ -61,12 +79,26 @@ export default function ForceDirectedGraph({runs, runId }: ForceDirectedGraphPro
61
  id,
62
  fx: centerX + radius * Math.cos(angle),
63
  fy: centerY + radius * Math.sin(angle),
64
- color: isSelectedRun && (id === run.start_article || id === run.destination_article) ? '#ff6b6b' : 'red'
 
 
 
65
  };
66
  });
 
 
 
 
 
 
 
 
67
  newGraphData.nodes.push(...Array.from(nodesSet).map((id) => ({
68
  id,
69
- color: isSelectedRun && run.steps.some(step => step.article === id) ? '#ff6b6b' : 'gray'
 
 
 
70
  })));
71
  });
72
 
@@ -82,19 +114,15 @@ export default function ForceDirectedGraph({runs, runId }: ForceDirectedGraphPro
82
  const chargeStrength = -100; // Increase repulsion more
83
  const COLLISION_PADDING = 3;
84
 
85
-
86
- // graphRef.current.centerAt(400, 400);
87
  graphRef.current.zoomToFit();
88
 
89
  graphRef.current.d3Force("link", d3.forceLink(graphData.links).id((d) => d.id).distance(linkDistance).strength(0.9));
90
  graphRef.current.d3Force("charge", d3.forceManyBody().strength(chargeStrength));
91
  graphRef.current.d3Force("radial", d3.forceRadial(radialTargetRadius, 0, 0).strength(radialForceStrength));
92
  graphRef.current.d3Force("collide", d3.forceCollide().radius((d) => d.radius + COLLISION_PADDING));
93
-
94
  }
95
  }, [graphRef]);
96
 
97
-
98
  return (
99
  <div className="w-full h-full flex items-center justify-center">
100
  <div ref={containerRef} className="w-full h-full">
@@ -105,39 +133,70 @@ export default function ForceDirectedGraph({runs, runId }: ForceDirectedGraphPro
105
  linkLabel="id"
106
  nodeColor="color"
107
  linkColor="color"
 
 
 
 
 
108
  nodeCanvasObject={(node, ctx, globalScale) => {
109
- const label = node.id;
110
- const fontSize = 12/globalScale;
111
- ctx.font = `${fontSize}px Sans-Serif`;
112
- const textWidth = ctx.measureText(label).width;
113
- const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.2);
114
-
115
- // Draw node circle
116
- ctx.beginPath();
117
- ctx.arc(node.x!, node.y!, 5, 0, 2 * Math.PI);
118
- ctx.fillStyle = node.color || 'gray';
119
- ctx.fill();
120
-
121
- if(node.color === "red") {
122
- // Draw label background
123
- ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  ctx.fillRect(
125
- node.x! - bckgDimensions[0] / 2,
126
- node.y! + 8,
127
- bckgDimensions[0],
128
- bckgDimensions[1]
129
  );
130
 
131
  // Draw label text
132
- ctx.textAlign = 'center';
133
- ctx.textBaseline = 'middle';
134
- ctx.fillStyle = 'black';
135
  ctx.fillText(
136
- label,
137
- node.x!,
138
- node.y! + 8 + fontSize / 2
139
- );
140
- }
141
  }}
142
  width={dimensions.width}
143
  height={dimensions.height}
 
7
  // This is a placeholder component for the force-directed graph
8
  // In a real implementation, you would use a library like D3.js or react-force-graph
9
 
10
+ // CSS variables for styling
11
+ const STYLES = {
12
+ fixedNodeColor: '#e63946', // Red
13
+ fluidNodeColor: '#457b9d', // Steel Blue
14
+ linkColor: '#adb5bd', // Grey
15
+ highlightColor: '#fca311', // Orange/Yellow
16
+ successColor: '#2a9d8f', // Teal
17
+ minNodeOpacity: 0.3,
18
+ minLinkOpacity: 0.15,
19
+ };
20
+
21
  interface ForceDirectedGraphProps {
22
  runId: number | null;
23
  runs: Run[];
24
  }
25
 
26
  export default function ForceDirectedGraph({runs, runId }: ForceDirectedGraphProps) {
27
+ const [graphData, setGraphData] = useState<{nodes: {id: string, color?: string, type?: string, radius?: number, baseOpacity?: number}[], links: {source: string, target: string, color?: string}[]}>({nodes: [], links: []});
28
  const [dimensions, setDimensions] = useState({ width: 800, height: 800 });
29
  const containerRef = useRef<HTMLDivElement>(null);
30
  const graphRef = useRef<ForceGraphMethods<NodeObject<{ id: string; color?: string; }>, LinkObject<{ id: string; color?: string; }, { source: string; target: string; color?: string; }>>>(null);
31
+
32
  useEffect(() => {
33
+ const newGraphData: {nodes: {id: string, color?: string, type?: string, radius?: number, baseOpacity?: number}[], links: {source: string, target: string, color?: string}[]} = {nodes: [], links: []};
 
34
  const nodesSet: Set<string> = new Set();
35
  const mainNodeSet: Set<string> = new Set();
36
 
37
  if(runs) {
38
+ // Create a map to track node degrees (number of connections)
39
+ const nodeDegrees: Map<string, number> = new Map();
40
+
41
  runs.forEach((run, runIndex) => {
42
  // add in src and dst to nodes
43
  const isSelectedRun = runId === runIndex;
 
57
  nodesSet.add(nextStep.article);
58
  }
59
 
60
+ // Increment degree count for both nodes
61
+ nodeDegrees.set(step.article, (nodeDegrees.get(step.article) || 0) + 1);
62
+ nodeDegrees.set(nextStep.article, (nodeDegrees.get(nextStep.article) || 0) + 1);
63
+
64
  newGraphData.links.push({
65
  source: step.article,
66
  target: nextStep.article,
67
+ color: isSelectedRun ? STYLES.highlightColor : STYLES.linkColor
68
  });
69
  }
70
 
 
79
  id,
80
  fx: centerX + radius * Math.cos(angle),
81
  fy: centerY + radius * Math.sin(angle),
82
+ type: 'fixed',
83
+ radius: 7, // Larger radius for fixed nodes
84
+ color: isSelectedRun && (id === run.start_article || id === run.destination_article) ? STYLES.highlightColor : STYLES.fixedNodeColor,
85
+ baseOpacity: 1.0 // Fixed nodes are always fully visible
86
  };
87
  });
88
+
89
+ // Create opacity scale based on node degrees
90
+ const maxDegree = Math.max(...Array.from(nodeDegrees.values()));
91
+ const opacityScale = d3.scaleLinear()
92
+ .domain([1, Math.max(1, maxDegree)])
93
+ .range([STYLES.minNodeOpacity, 1.0])
94
+ .clamp(true);
95
+
96
  newGraphData.nodes.push(...Array.from(nodesSet).map((id) => ({
97
  id,
98
+ type: 'fluid',
99
+ radius: 5, // Smaller radius for fluid nodes
100
+ color: isSelectedRun && run.steps.some(step => step.article === id) ? STYLES.highlightColor : STYLES.fluidNodeColor,
101
+ baseOpacity: opacityScale(nodeDegrees.get(id) || 1)
102
  })));
103
  });
104
 
 
114
  const chargeStrength = -100; // Increase repulsion more
115
  const COLLISION_PADDING = 3;
116
 
 
 
117
  graphRef.current.zoomToFit();
118
 
119
  graphRef.current.d3Force("link", d3.forceLink(graphData.links).id((d) => d.id).distance(linkDistance).strength(0.9));
120
  graphRef.current.d3Force("charge", d3.forceManyBody().strength(chargeStrength));
121
  graphRef.current.d3Force("radial", d3.forceRadial(radialTargetRadius, 0, 0).strength(radialForceStrength));
122
  graphRef.current.d3Force("collide", d3.forceCollide().radius((d) => d.radius + COLLISION_PADDING));
 
123
  }
124
  }, [graphRef]);
125
 
 
126
  return (
127
  <div className="w-full h-full flex items-center justify-center">
128
  <div ref={containerRef} className="w-full h-full">
 
133
  linkLabel="id"
134
  nodeColor="color"
135
  linkColor="color"
136
+ linkWidth={link => link.source.id === runId || link.target.id === runId ? 2.5 : 1}
137
+ linkOpacity={0.6}
138
+ nodeRelSize={1}
139
+ linkDirectionalParticles={link => link.source.id === runId || link.target.id === runId ? 4 : 0}
140
+ linkDirectionalParticleWidth={2}
141
  nodeCanvasObject={(node, ctx, globalScale) => {
142
+ const label = node.id;
143
+ const fontSize = 12 / globalScale;
144
+ ctx.font = `${fontSize}px Sans-Serif`;
145
+ const textWidth = ctx.measureText(label).width;
146
+ const bckgDimensions = [textWidth, fontSize].map(
147
+ (n) => n + fontSize * 0.2
148
+ );
149
+
150
+ // Apply opacity based on node type and properties
151
+ const opacity =
152
+ node.baseOpacity !== undefined
153
+ ? node.baseOpacity
154
+ : node.type === "fixed"
155
+ ? 1.0
156
+ : STYLES.minNodeOpacity;
157
+
158
+ // Draw node circle with appropriate styling
159
+ const radius =
160
+ node.radius || (node.type === "fixed" ? 7 : 5);
161
+ ctx.beginPath();
162
+ ctx.arc(node.x!, node.y!, radius, 0, 2 * Math.PI);
163
+ ctx.fillStyle = node.color || STYLES.fluidNodeColor;
164
+ ctx.fill();
165
+
166
+ // Add white stroke around nodes
167
+ ctx.strokeStyle = "#fff";
168
+ ctx.globalAlpha = opacity;
169
+ ctx.lineWidth = 1;
170
+ ctx.stroke();
171
+
172
+ // Draw label with background for better visibility
173
+ const shouldShowLabel =
174
+ node.type === "fixed" ||
175
+ (runId !== null &&
176
+ node.id === runs[runId]?.start_article) ||
177
+ (runId !== null &&
178
+ node.id === runs[runId]?.destination_article);
179
+
180
+ if (shouldShowLabel) {
181
+ // Draw label background
182
+ ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
183
  ctx.fillRect(
184
+ node.x! - bckgDimensions[0] / 2,
185
+ node.y! + 8,
186
+ bckgDimensions[0],
187
+ bckgDimensions[1]
188
  );
189
 
190
  // Draw label text
191
+ ctx.textAlign = "center";
192
+ ctx.textBaseline = "middle";
193
+ ctx.fillStyle = "black";
194
  ctx.fillText(
195
+ label,
196
+ node.x!,
197
+ node.y! + 8 + fontSize / 2
198
+ );
199
+ }
200
  }}
201
  width={dimensions.width}
202
  height={dimensions.height}