1+ // docs/assets/js/cwe-sunburst.js
2+ async function loadData ( ) {
3+ const res = await fetch ( '../../assets/data/cwe_complete_hierarchy_dag.json' ) ;
4+ if ( ! res . ok ) throw new Error ( res . status ) ;
5+ createSunburst ( await res . json ( ) ) ;
6+ }
7+
8+ function createSunburst ( data ) {
9+ d3 . select ( "#chart" ) . selectAll ( "*" ) . remove ( ) ;
10+
11+ const container = document . getElementById ( "chart" ) ;
12+ const width = container . clientWidth ;
13+ const height = container . clientHeight || width ;
14+ const radius = Math . min ( width , height ) / 2 - 10 ;
15+ const innerRadius = radius * 0.12 ;
16+
17+ // Enhanced color scheme with better contrast
18+ const colorScheme = {
19+ 'Pillar' : '#e41a1c' ,
20+ 'Class' : '#377eb8' ,
21+ 'Base' : '#4daf4a' ,
22+ 'Variant' : '#984ea3' ,
23+ 'Compound' : '#ff7f00' ,
24+ 'Composite' : '#e377c2' ,
25+ 'Chain' : '#8c564b' ,
26+ 'Unknown' : '#7f7f7f'
27+ } ;
28+
29+ // Improved color function with opacity for better readability
30+ const color = d => {
31+ if ( d . depth === 0 ) return '#ddd' ;
32+ if ( d . data . abstraction && colorScheme [ d . data . abstraction ] ) {
33+ return colorScheme [ d . data . abstraction ] ;
34+ }
35+ return '#999' ;
36+ } ;
37+
38+ const svg = d3 . select ( "#chart" )
39+ . append ( "svg" )
40+ . attr ( "viewBox" , [ 0 , 0 , width , height ] )
41+ . style ( "width" , "100%" )
42+ . style ( "height" , "auto" ) ;
43+
44+ const g = svg . append ( "g" )
45+ . attr ( "transform" , `translate(${ width / 2 } ,${ height / 2 } )` ) ;
46+
47+ // Enhanced zoom functionality
48+ svg . call ( d3 . zoom ( )
49+ . scaleExtent ( [ 0.5 , 12 ] )
50+ . on ( "zoom" , ( { transform} ) => {
51+ g . attr ( "transform" ,
52+ `translate(${ transform . x + width / 2 } ,${ transform . y + height / 2 } ) scale(${ transform . k } )`
53+ ) ;
54+
55+ // Dynamically adjust text size based on zoom level
56+ d3 . selectAll ( ".slice-label" )
57+ . style ( "font-size" , d => {
58+ // Base size calculation with increased values
59+ const angle = d . x1 - d . x0 ;
60+ let baseSize ;
61+
62+ if ( d . depth < 2 ) {
63+ baseSize = Math . min ( 2.5 , Math . max ( 1.5 , angle * 35 ) ) ;
64+ } else if ( d . depth < 3 ) {
65+ baseSize = Math . min ( 2.0 , Math . max ( 1.2 , angle * 30 ) ) ;
66+ } else {
67+ baseSize = Math . min ( 1.7 , Math . max ( 1.0 , angle * 25 ) ) ;
68+ }
69+
70+ // Scale font size based on zoom level, with a minimum size
71+ return `${ Math . max ( 0.8 , baseSize / transform . k ) } px` ;
72+ } ) ;
73+ } )
74+ ) ;
75+
76+ // Optimize hierarchy calculation
77+ const root = d3 . partition ( )
78+ . size ( [ 2 * Math . PI , 1 ] ) (
79+ d3 . hierarchy ( data )
80+ . sum ( d => ( ! d . children || ! d . children . length ) ? 1 : 0 )
81+ . sort ( ( a , b ) => b . value - a . value )
82+ ) ;
83+
84+ // Improved arc generator with padding
85+ const arc = d3 . arc ( )
86+ . startAngle ( d => d . x0 )
87+ . endAngle ( d => d . x1 )
88+ . padAngle ( d => Math . min ( ( d . x1 - d . x0 ) / 2 , 0.003 ) )
89+ . padRadius ( radius / 2 )
90+ . innerRadius ( d => Math . max ( innerRadius , d . y0 * radius ) )
91+ . outerRadius ( d => Math . max ( d . y0 * radius , d . y1 * radius - 1 ) ) ;
92+
93+ // Draw slices with hover effects
94+ g . selectAll ( "path" )
95+ . data ( root . descendants ( ) . slice ( 1 ) )
96+ . join ( "path" )
97+ . attr ( "fill" , color )
98+ . attr ( "d" , arc )
99+ . attr ( "stroke" , "white" )
100+ . attr ( "stroke-width" , 0.5 )
101+ . style ( "cursor" , "pointer" )
102+ . style ( "opacity" , 0.9 )
103+ . on ( "mouseover" , ( event , d ) => {
104+ d3 . select ( event . currentTarget )
105+ . style ( "opacity" , 1 )
106+ . attr ( "stroke-width" , 1 ) ;
107+ showInfo ( event , d ) ;
108+ } )
109+ . on ( "mouseout" , ( event ) => {
110+ d3 . select ( event . currentTarget )
111+ . style ( "opacity" , 0.9 )
112+ . attr ( "stroke-width" , 0.5 ) ;
113+ } )
114+ . on ( "click" , ( event , d ) => {
115+ const idNum = d . data . id ?. split ( "CWE-" ) [ 1 ] ;
116+ if ( idNum ) {
117+ window . open (
118+ `https://cwe.mitre.org/data/definitions/${ idNum } .html` ,
119+ "_blank"
120+ ) ;
121+ }
122+ } ) ;
123+
124+ // Draw ALL labels with improved visibility - maximize text display
125+ g . append ( "g" )
126+ . attr ( "pointer-events" , "none" )
127+ . attr ( "text-anchor" , "middle" )
128+ . selectAll ( "text" )
129+ . data ( root . descendants ( ) . slice ( 1 ) )
130+ . join ( "text" )
131+ . attr ( "class" , "slice-label" )
132+ . attr ( "transform" , d => {
133+ const angle = ( d . x0 + d . x1 ) / 2 * 180 / Math . PI ;
134+ const radius_factor = 0.5 + ( d . y0 + d . y1 ) / 2 ;
135+ const y = radius_factor * radius * 0.92 ;
136+ return `rotate(${ angle - 90 } ) translate(${ y } ,0) rotate(${ angle < 180 ? 0 : 180 } )` ;
137+ } )
138+ . attr ( "dy" , "0.35em" )
139+ . attr ( "font-size" , d => {
140+ const angle = d . x1 - d . x0 ;
141+ // Maximize font size for all segments
142+ if ( d . depth < 2 ) {
143+ return Math . min ( 2.5 , Math . max ( 1.5 , angle * 35 ) ) + "px" ;
144+ } else if ( d . depth < 3 ) {
145+ return Math . min ( 2.0 , Math . max ( 1.2 , angle * 30 ) ) + "px" ;
146+ } else {
147+ return Math . min ( 1.7 , Math . max ( 1.0 , angle * 25 ) ) + "px" ;
148+ }
149+ } )
150+ . attr ( "fill" , d => {
151+ const bgColor = color ( d ) ;
152+ // Determine text color based on background brightness
153+ return isColorDark ( bgColor ) ? "white" : "black" ;
154+ } )
155+ . text ( d => {
156+ if ( ! d . data . id ) return "" ;
157+
158+ // Always show CWE ID for all segments
159+ return d . data . id . split ( ':' ) [ 0 ] ;
160+ } )
161+ . style ( "pointer-events" , "none" )
162+ // Extra-strong text shadow for maximum readability
163+ . style ( "text-shadow" , "0 0 4px rgba(255,255,255,0.9), 0 0 2px rgba(0,0,0,0.5)" )
164+ . style ( "font-weight" , d => d . depth < 3 ? "bold" : "normal" )
165+ . style ( "user-select" , "none" ) ;
166+
167+ // Helper function to determine if a color is dark
168+ function isColorDark ( color ) {
169+ if ( ! color ) return false ;
170+
171+ let r , g , b ;
172+ if ( color . startsWith ( '#' ) ) {
173+ const hex = color . substring ( 1 ) ;
174+ r = parseInt ( hex . substring ( 0 , 2 ) , 16 ) ;
175+ g = parseInt ( hex . substring ( 2 , 4 ) , 16 ) ;
176+ b = parseInt ( hex . substring ( 4 , 6 ) , 16 ) ;
177+ } else {
178+ const rgb = color . match ( / \d + / g) ;
179+ if ( rgb && rgb . length >= 3 ) {
180+ r = parseInt ( rgb [ 0 ] ) ;
181+ g = parseInt ( rgb [ 1 ] ) ;
182+ b = parseInt ( rgb [ 2 ] ) ;
183+ } else {
184+ return false ;
185+ }
186+ }
187+
188+ const luminance = ( 0.299 * r + 0.587 * g + 0.114 * b ) / 255 ;
189+ return luminance < 0.5 ;
190+ }
191+
192+ // Add tooltip for better information display
193+ d3 . select ( ".cwe-tooltip" ) . remove ( ) ;
194+
195+ const tooltip = d3 . select ( "body" )
196+ . append ( "div" )
197+ . attr ( "class" , "cwe-tooltip" )
198+ . style ( "position" , "absolute" )
199+ . style ( "visibility" , "hidden" )
200+ . style ( "background-color" , "white" )
201+ . style ( "border" , "1px solid #ddd" )
202+ . style ( "border-radius" , "4px" )
203+ . style ( "padding" , "5px 8px" )
204+ . style ( "box-shadow" , "0 0 5px rgba(0,0,0,0.2)" )
205+ . style ( "pointer-events" , "none" )
206+ . style ( "z-index" , "1000" )
207+ . style ( "font-size" , "12px" )
208+ . style ( "max-width" , "300px" ) ;
209+
210+ // Enhanced info-panel
211+ function showInfo ( event , d ) {
212+ // Show tooltip near mouse with more detailed info
213+ if ( event ) {
214+ tooltip
215+ . style ( "visibility" , "visible" )
216+ . style ( "left" , ( event . pageX + 10 ) + "px" )
217+ . style ( "top" , ( event . pageY - 10 ) + "px" )
218+ . html ( `<strong>${ d . data . id || "Root" } </strong>${ d . data . name ? '<br>' + d . data . name . substring ( 0 , 50 ) + ( d . data . name . length > 50 ? '...' : '' ) : '' } ` ) ;
219+ }
220+
221+ // Update info panel
222+ const bc = d . ancestors ( ) . reverse ( )
223+ . map ( a => a . data . id || a . data . name || "Root" ) . join ( " > " ) ;
224+
225+ let html = `<div class="breadcrumb">${ bc } </div>` ;
226+ if ( d . data . id ) html += `
227+ <div class="cwe-id">
228+ <a href="https://cwe.mitre.org/data/definitions/${ d . data . id . split ( '-' ) [ 1 ] } .html" target="_blank">
229+ ${ d . data . id }
230+ </a>
231+ </div>` ;
232+ if ( d . data . name ) html += `<div class="cwe-name">${ d . data . name } </div>` ;
233+ if ( d . data . abstraction )
234+ html += `<div class="cwe-abstraction">Abstraction: ${ d . data . abstraction } </div>` ;
235+
236+ // Add children information - always show all children without toggle
237+ const childCount = d . children ? d . children . length : 0 ;
238+ if ( childCount > 0 ) {
239+ html += `<div class="children-count">Children: ${ childCount } </div>` ;
240+
241+ // Always show all children directly
242+ html += `<div class="children-list"><ul>` ;
243+ d . children . forEach ( child => {
244+ html += `<li>${ child . data . id || "" } : ${ child . data . name || "" } </li>` ;
245+ } ) ;
246+ html += `</ul></div>` ;
247+ } else {
248+ html += `<div class="children-count">Leaf node (no children)</div>` ;
249+ }
250+
251+ // Add reference indicator if this is a reference node
252+ if ( d . data . isReference ) {
253+ html += `<div class="reference-note">This is a reference to another occurrence (to avoid cycles)</div>` ;
254+ }
255+
256+ d3 . select ( ".node-info" ) . html ( html ) ;
257+
258+ // Add toggle functionality for large child lists
259+ setTimeout ( ( ) => {
260+ document . getElementById ( "toggleChildren" ) ?. addEventListener ( "click" , function ( e ) {
261+ e . preventDefault ( ) ;
262+ const list = this . nextElementSibling ;
263+ list . style . display = list . style . display === "none" ? "block" : "none" ;
264+ } ) ;
265+ } , 0 ) ;
266+ }
267+
268+ // Enhanced legend
269+ const legend = d3 . select ( "#legend" ) ;
270+ legend . selectAll ( "*" ) . remove ( ) ; // Clear existing legend
271+
272+ legend . append ( "div" )
273+ . attr ( "class" , "legend-title" )
274+ . text ( "CWE Abstraction Levels" ) ;
275+
276+ Object . entries ( colorScheme ) . forEach ( ( [ lbl , col ] ) => {
277+ legend . append ( "div" )
278+ . attr ( "class" , "legend-item" )
279+ . html ( `
280+ <div class="legend-color" style="background:${ col } ; width:15px; height:15px; display:inline-block;"></div>
281+ <span style="margin-left:5px;">${ lbl } </span>
282+ ` ) ;
283+ } ) ;
284+
285+ // Initial info
286+ showInfo ( null , root ) ;
287+ }
288+
289+ loadData ( ) ;
0 commit comments