2626 width : 100% ;
2727 overflow : hidden;
2828 }
29+ .graph {
30+ width : 80% ;
31+ height : auto;
32+ }
2933
3034 </ style >
3135 < script >
@@ -72,7 +76,9 @@ <h2 style="text-align: center;">
7276 </ h2 >
7377
7478 < div class ="svg-container ">
75- < svg class ="graph ">
79+ < svg class ="graph "
80+ viewBox ="0 0 2000 1200 "
81+ preserveAspectRatio ="xMidYMid meet ">
7682 </ svg >
7783 </ div >
7884 < button id ="editButton " onclick ="toggleEdit() "> Edit </ button >
@@ -92,55 +98,52 @@ <h2 style="text-align: center;">
9298
9399 }
94100
95-
96- const width = 1000 ;
97- const height = 1000 ;
98- const centerX = width / 2 ;
99- const centerY = width / 2 ;
100- const link_stroke_width = 5 ;
101- const arrowhead_size = link_stroke_width * 1.5 ;
102- const circle_radius = 30 ;
101+ const width = 1000 ,
102+ height = 1000 ,
103+ centerX = width / 2 ,
104+ centerY = width / 2 ,
105+ link_stroke_width = 5 ,
106+ arrowhead_size = link_stroke_width * 1.5 ,
107+ circle_radius = { { params . circle_radius } } ,
108+ title_circle_radius = circle_radius * 1.2 ,
109+ node_stroke_width = 1 ,
110+ node_stroke = "gray"
103111
104112 const svg = d3 . select ( "svg" )
105113 . attr ( "width" , width )
106114 . attr ( "height" , width )
107115 . attr ( "viewBox" , [ - width / 2 , - height / 2 , width , height ] ) ;
108-
109-
110116
111117
112118 async function grabData ( ) {
113119 const data = await d3 . json ( "{% url 'studentservices:course_guide_data' %}?groups={{ request.GET.groups }}" )
114- console . log ( data )
115120 return data
116121 }
117122
118123 async function generate ( ) {
119124 const graph = await grabData ( ) ;
120- const nodes = graph . nodes . map ( d => ( { ...d } ) ) ;
125+ const nodes = graph . nodes . map ( d => ( { ...d , fd : [ ] , bk : [ ] } ) ) ;
121126 const links = graph . links . map ( d => ( { ...d } ) ) ;
122-
123- const color = d3 . scaleOrdinal ( d3 . schemeCategory10 ) ;
127+
128+ const nodeById = new Map ( nodes . map ( d => [ d . id , d ] ) ) ;
129+ links . forEach ( l => {
130+ nodeById . get ( l . source ) . fd . push ( l . target ) ;
131+ nodeById . get ( l . target ) . bk . push ( l . source ) ;
132+ } ) ;
133+
134+ const color = d3 . scaleOrdinal ( d3 . schemePaired ) ;
124135
125136 const simulation = d3 . forceSimulation ( nodes )
126- . force ( "link" , d3 . forceLink ( links ) . id ( d => d . id ) . strength ( 0.05 ) )
137+ . force ( "link" , d3 . forceLink ( links )
138+ . id ( d => d . id )
139+ . strength ( 0.05 )
140+ . distance ( ( d ) => ( d . title ? title_circle_radius * 10 : circle_radius * 10 ) ) )
127141 . force ( "charge" , d3 . forceManyBody ( ) . strength ( - 300 ) )
128- . force ( "overlap" , d3 . forceCollide ( ) )
142+ . force ( "overlap" , d3 . forceCollide ( ( d ) => ( d . title ? title_circle_radius : circle_radius ) * 1.25 ) . strength ( 1 ) )
129143 . force ( "x" , d3 . forceX ( ) )
130144 . force ( "y" , d3 . forceY ( ) )
131- . force ( "radial" , d3 . forceRadial ( d => d . level * 300 ) . strength ( 0.7 ) ) ;
132-
133- svg . append ( "defs" ) . append ( "marker" )
134- . attr ( "id" , "arrow" )
135- . attr ( "viewBox" , `0 0 ${ arrowhead_size } ${ arrowhead_size } ` ) // simplified
136- . attr ( "refX" , circle_radius + arrowhead_size * 0.6 ) // offset by radius + a bit
137- . attr ( "refY" , arrowhead_size / 2 )
138- . attr ( "markerWidth" , arrowhead_size )
139- . attr ( "markerHeight" , arrowhead_size )
140- . attr ( "orient" , "auto" )
141- . append ( "path" )
142- . attr ( "d" , `M0,0 L${ arrowhead_size } ,${ arrowhead_size / 2 } L0,${ arrowhead_size } Z` )
143- . attr ( "fill" , "#999" ) ;
145+ // .force("center", d3.forceCenter(centerX, centerY));
146+ . force ( "radial" , d3 . forceRadial ( d => d . level * 300 , centerX , centerY ) . strength ( 0 ) ) ;
144147
145148 // Creates a group that holds everything in the svg (ie nodes, link, etc)
146149 const container = svg . append ( "g" )
@@ -157,9 +160,6 @@ <h2 style="text-align: center;">
157160 . attr ( "fill" , "none" ) ;
158161
159162
160-
161-
162-
163163 // Defines a zoom interaction that occurs on whatever elements calls it
164164 const zoom = d3 . zoom ( )
165165 . scaleExtent ( [ 0.1 , 5 ] )
@@ -170,49 +170,117 @@ <h2 style="text-align: center;">
170170 // Binds the defined zoom interaction to the entire svg element
171171 svg . call ( zoom )
172172
173+ const defs = svg . insert ( "defs" , ":first-child" ) ;
174+ const arrowhead_size = link_stroke_width * 1.5 ;
175+ for ( let i = 1 ; i <= { { groups| length } } ; i ++ ) {
176+ defs . append ( "marker" )
177+ . attr ( "id" , `head-${ i } ` )
178+ . attr ( "viewBox" , `0 -${ arrowhead_size } ${ arrowhead_size * 2 } ${ arrowhead_size * 2 } ` )
179+ . attr ( "markerUnits" , "userSpaceOnUse" )
180+ . attr ( "refX" , circle_radius + arrowhead_size * 1.5 )
181+ . attr ( "refY" , 0 )
182+ . attr ( "markerWidth" , arrowhead_size * 2 )
183+ . attr ( "markerHeight" , arrowhead_size * 2 )
184+ . attr ( "orient" , "auto" )
185+ . append ( "path" )
186+ . attr ( "d" , `M 0 -${ arrowhead_size } L ${ arrowhead_size * 2 } 0 L 0 ${ arrowhead_size } ` )
187+ . attr ( "fill" , color ( i ) ) ;
188+ }
189+
173190 // Creates the links that appear
174191 const link = container . append ( "g" )
175- . attr ( "stroke" , "#aaa" )
176- . attr ( "stroke-opacity" , 0.6 )
192+ . attr ( "class" , "links" )
177193 . selectAll ( "line" )
178- . data ( links )
179- . enter ( )
180- . append ( "line" )
181- . attr ( "stroke-width" , 1.5 )
182- . attr ( "marker-end" , "url(#arrow)" ) ;
194+ . data ( links )
195+ . enter ( ) . append ( "path" )
196+ . attr ( "fill" , "none" )
197+ . attr ( "stroke-width" , link_stroke_width )
198+ . attr ( "stroke" , d => color (
199+ nodeById . get (
200+ typeof d . source === "object" ? d . source . id : d . source
201+ ) . group ) )
202+ . attr ( "marker-end" , d => {
203+ const g = nodeById . get (
204+ typeof d . source === "object" ? d . source . id : d . source
205+ ) . group ;
206+ return `url(#head-${ g } )` ;
207+ } ) ;
183208
184209 // Creates the nodes that appear
185210 const node = container . append ( "g" )
211+ . attr ( "class" , "nodes" )
186212 . selectAll ( "g" )
187- . data ( nodes )
188- . enter ( )
189- . append ( "g" )
213+ . data ( nodes )
214+ . enter ( ) . append ( "g" )
190215 . call ( d3 . drag ( )
191216 . on ( "start" , dragstarted )
192217 . on ( "drag" , dragged )
193218 . on ( "end" , dragended ) ) ;
194219
195- node . append ( "circle" )
196- . attr ( "r" , circle_radius )
197- . attr ( "fill" , d => color ( d . group ) ) ;
220+ circles = node . append ( "a" )
221+ . attr ( "xlink:href" , d => d . link )
222+ . attr ( "target" , "_blank" )
223+ . append ( "circle" )
224+ . attr ( "r" , d => d . title ? title_circle_radius : circle_radius )
225+ . attr ( "fill" , d => color ( d . group ) ) ;
198226
199- node . append ( "a" )
200- . attr ( "xlink:href" , d => d . link )
201- . attr ( "target" , "_blank" )
202- . append ( "text" )
203- . attr ( "text-anchor" , "middle" )
204- . attr ( "alignment-baseline" , "middle" )
227+ labels = node . append ( "a" )
228+ . attr ( "xlink:href" , d => d . link )
229+ . attr ( "target" , "_blank" )
230+ . append ( "text" )
231+ . attr ( "text-anchor" , "middle" )
232+ . attr ( "alignment-baseline" , "middle" )
205233 . text ( d => d . id )
206234
207235 node . append ( "title" )
208236 . text ( d => d . id ) ;
237+
238+ node . on ( "mouseover" , function ( d ) {
239+ d = d . srcElement . __data__ ;
240+ circles
241+ . attr ( "opacity" , 0.6 ) ;
242+
243+ let marked = new Set ( ) ,
244+ queue = [ ] ;
245+ queue . push ( d . id ) ;
246+ while ( queue . length ) {
247+ const id0 = queue . shift ( ) ;
248+ if ( marked . has ( id0 ) ) continue ;
249+ marked . add ( id0 ) ;
250+ queue = [ ...queue , ...nodeById . get ( id0 ) . bk ] ;
251+ }
252+ circles
253+ . filter ( d2 => marked . has ( d2 . id ) )
254+ . attr ( "opacity" , 1 ) ;
255+ circles
256+ . filter ( d2 => d2 . id === d . id )
257+ . attr ( "stroke" , "black" )
258+ . attr ( "stroke-width" , node_stroke_width * 3 ) ;
259+ link . filter ( l => {
260+ return ! marked . has ( l . source . id ) || ! marked . has ( l . target . id ) ;
261+ } )
262+ . attr ( "opacity" , 0.05 ) ;
263+ svg . selectAll ( "text" )
264+ . filter ( d2 => d2 . group !== d . group )
265+ . attr ( "opacity" , 0.5 ) ;
266+ svg . selectAll ( "text" )
267+ . filter ( d2 => d2 . id === d . id )
268+ . attr ( "font-weight" , "bold" ) ;
269+ } )
270+ . on ( "mouseout" , function ( ) {
271+ svg . selectAll ( "circle" )
272+ . attr ( "opacity" , 1 )
273+ . attr ( "stroke" , node_stroke )
274+ . attr ( "stroke-width" , node_stroke_width ) ;
275+ link . attr ( "opacity" , 1 ) ;
276+ svg . selectAll ( "text" )
277+ . attr ( "opacity" , 1 )
278+ . attr ( "font-weight" , "normal" ) ;
279+ } ) ;
209280
210281 simulation . on ( "tick" , ( ) => {
211282 link
212- . attr ( "x1" , d => d . source . x )
213- . attr ( "y1" , d => d . source . y )
214- . attr ( "x2" , d => d . target . x )
215- . attr ( "y2" , d => d . target . y ) ;
283+ . attr ( "d" , d => `M ${ d . source . x } ${ d . source . y } L ${ d . target . x } ${ d . target . y } ` ) ;
216284
217285 node
218286 . attr ( "transform" , d => `translate(${ d . x } ,${ d . y } )` )
@@ -230,9 +298,14 @@ <h2 style="text-align: center;">
230298 }
231299
232300 function dragended ( event , d ) {
233- if ( ! event . active ) simulation . alphaTarget ( 0 ) ;
234- d . fx = null ;
235- d . fy = null ;
301+ if ( isEditMode ) {
302+ d . fx = event . x ;
303+ d . fy = event . y ;
304+ } else {
305+ if ( ! event . active ) simulation . alphaTarget ( 0 ) ;
306+ d . fx = null ;
307+ d . fy = null ;
308+ }
236309 }
237310
238311
0 commit comments