1- import * as d3 from 'd3' ;
2- import { MindMapNode , NodeQConfig , Theme } from './types' ;
3- import { JsonSchemaAdapter } from './json-adapter' ;
4-
5- export class NodeQMindMap {
6- private config : Required < NodeQConfig > ;
7- private svg : d3 . Selection < SVGSVGElement , unknown , null , undefined > | null = null ;
8- private g : d3 . Selection < SVGGElement , unknown , null , undefined > | null = null ;
9- private data : MindMapNode ;
10- private container : HTMLElement ;
11-
12- constructor ( config : NodeQConfig ) {
13- this . config = {
14- container : config . container ,
15- data : config . data ,
16- width : config . width || 800 ,
17- height : config . height || 600 ,
18- theme : {
19- nodeColor : '#4299e1' ,
20- textColor : '#2d3748' ,
21- linkColor : '#a0aec0' ,
22- backgroundColor : '#ffffff' ,
23- fontSize : 14 ,
24- fontFamily : 'Arial, sans-serif' ,
25- ...config . theme
26- } ,
27- interactive : config . interactive !== undefined ? config . interactive : true ,
28- zoomable : config . zoomable !== undefined ? config . zoomable : true ,
29- collapsible : config . collapsible !== undefined ? config . collapsible : true ,
30- nodeSpacing : config . nodeSpacing || 200 ,
31- levelSpacing : config . levelSpacing || 300 ,
32- onNodeClick : config . onNodeClick || ( ( ) => { } ) ,
33- onNodeHover : config . onNodeHover || ( ( ) => { } )
34- } ;
35-
36- // Get container element
37- if ( typeof this . config . container === 'string' ) {
38- const element = document . querySelector ( this . config . container ) ;
39- if ( ! element ) {
40- throw new Error ( `Container element not found: ${ this . config . container } ` ) ;
41- }
42- this . container = element as HTMLElement ;
43- } else {
44- this . container = this . config . container ;
1+ import { MindMapNode } from './types' ;
2+
3+ export class JsonSchemaAdapter {
4+ static convertToStandard ( data : any ) : MindMapNode {
5+ if ( ! data || typeof data !== 'object' ) {
6+ return {
7+ topic : 'Invalid Data' ,
8+ summary : 'Unable to process the provided data' ,
9+ skills : [ 'error' ]
10+ } ;
4511 }
4612
47- // Convert data to standard format
48- this . data = JsonSchemaAdapter . convertToStandard ( this . config . data ) ;
49- }
50-
51- render ( ) : void {
52- this . createSVG ( ) ;
53- this . renderMindMap ( ) ;
54- }
55-
56- updateData ( data : any ) : void {
57- this . config . data = data ;
58- this . data = JsonSchemaAdapter . convertToStandard ( data ) ;
59- this . renderMindMap ( ) ;
60- }
13+ // Extract basic properties
14+ const topic = data . topic || data . title || data . name || data . label || 'Unnamed Node' ;
15+ const summary = data . summary || data . description || data . details || undefined ;
16+ const skills = this . extractSkills ( data ) ;
17+ const children = this . extractChildren ( data ) ;
18+
19+ const result : MindMapNode = {
20+ topic,
21+ summary,
22+ skills,
23+ children : children ? children . map ( child => this . convertToStandard ( child ) ) : undefined
24+ } ;
6125
62- updateTheme ( theme : Partial < Theme > ) : void {
63- this . config . theme = { ...this . config . theme , ...theme } ;
64- this . renderMindMap ( ) ;
65- }
26+ // Add any additional properties
27+ const excludedKeys = [ 'topic' , 'title' , 'name' , 'label' , 'summary' , 'description' , 'details' , 'skills' , 'tags' , 'categories' , 'keywords' , 'attributes' , 'children' , 'items' , 'nodes' , 'subitems' , 'elements' , 'branches' ] ;
6628
67- exportSVG ( ) : string {
68- if ( ! this . svg ) return '' ;
69- return new XMLSerializer ( ) . serializeToString ( this . svg . node ( ) ! ) ;
70- }
29+ Object . keys ( data ) . forEach ( key => {
30+ if ( ! excludedKeys . includes ( key ) ) {
31+ result [ key ] = data [ key ] ;
32+ }
33+ } ) ;
7134
72- destroy ( ) : void {
73- if ( this . svg ) {
74- this . svg . remove ( ) ;
75- }
35+ return result ;
7636 }
7737
78- private createSVG ( ) : void {
79- // Clear existing content
80- d3 . select ( this . container ) . selectAll ( '*' ) . remove ( ) ;
81-
82- // Create SVG
83- this . svg = d3 . select ( this . container )
84- . append ( 'svg' )
85- . attr ( 'width' , this . config . width )
86- . attr ( 'height' , this . config . height )
87- . style ( 'background-color' , this . config . theme . backgroundColor || '#ffffff' ) ;
88-
89- // Create main group for zooming/panning
90- this . g = this . svg . append ( 'g' ) ;
91-
92- // Add zoom behavior if enabled
93- if ( this . config . zoomable ) {
94- const zoom = d3 . zoom < SVGSVGElement , unknown > ( )
95- . scaleExtent ( [ 0.1 , 3 ] )
96- . on ( 'zoom' , ( event ) => {
97- this . g ! . attr ( 'transform' , event . transform ) ;
98- } ) ;
99-
100- this . svg . call ( zoom ) ;
38+ private static extractSkills ( obj : any ) : string [ ] | undefined {
39+ const skillFields = [ 'skills' , 'tags' , 'categories' , 'keywords' , 'attributes' ] ;
40+ for ( const field of skillFields ) {
41+ if ( Array . isArray ( obj [ field ] ) ) {
42+ return obj [ field ] . filter ( ( item : any ) => typeof item === 'string' ) ;
43+ }
10144 }
45+ return undefined ;
10246 }
10347
104- private renderMindMap ( ) : void {
105- if ( ! this . g ) return ;
106-
107- // Clear existing content
108- this . g . selectAll ( '*' ) . remove ( ) ;
109-
110- // Create hierarchy
111- const root = d3 . hierarchy ( this . data ) ;
112-
113- // Create tree layout
114- const treeLayout = d3 . tree < MindMapNode > ( )
115- . size ( [ this . config . height - 100 , this . config . width - 200 ] )
116- . separation ( ( a , b ) => a . parent === b . parent ? 1 : 2 ) ;
117-
118- // Generate tree
119- const treeData = treeLayout ( root ) ;
120-
121- // Create links
122- this . g . selectAll ( '.link' )
123- . data ( treeData . links ( ) )
124- . enter ( )
125- . append ( 'path' )
126- . attr ( 'class' , 'link' )
127- . attr ( 'd' , d3 . linkHorizontal < any , any > ( )
128- . x ( d => d . y + 100 )
129- . y ( d => d . x + 50 )
130- )
131- . style ( 'fill' , 'none' )
132- . style ( 'stroke' , this . config . theme . linkColor || '#a0aec0' )
133- . style ( 'stroke-width' , 2 ) ;
134-
135- // Create nodes
136- const nodes = this . g . selectAll ( '.node' )
137- . data ( treeData . descendants ( ) )
138- . enter ( )
139- . append ( 'g' )
140- . attr ( 'class' , 'node' )
141- . attr ( 'transform' , d => `translate(${ d . y + 100 } ,${ d . x + 50 } )` ) ;
142-
143- // Add node circles
144- nodes . append ( 'circle' )
145- . attr ( 'r' , 8 )
146- . style ( 'fill' , this . config . theme . nodeColor || '#4299e1' )
147- . style ( 'stroke' , '#fff' )
148- . style ( 'stroke-width' , 2 ) ;
149-
150- // Add node labels
151- nodes . append ( 'text' )
152- . attr ( 'dy' , '.35em' )
153- . attr ( 'x' , d => d . children ? - 13 : 13 )
154- . style ( 'text-anchor' , d => d . children ? 'end' : 'start' )
155- . style ( 'font-family' , this . config . theme . fontFamily || 'Arial, sans-serif' )
156- . style ( 'font-size' , `${ this . config . theme . fontSize || 14 } px` )
157- . style ( 'fill' , this . config . theme . textColor || '#2d3748' )
158- . text ( d => d . data . topic ) ;
159-
160- // Add interactivity
161- if ( this . config . interactive ) {
162- nodes
163- . style ( 'cursor' , 'pointer' )
164- . on ( 'click' , ( event , d ) => {
165- this . config . onNodeClick ( d . data ) ;
166- } )
167- . on ( 'mouseover' , ( event , d ) => {
168- this . config . onNodeHover ( d . data ) ;
169- } ) ;
48+ private static extractChildren ( obj : any ) : any [ ] | undefined {
49+ const childFields = [ 'children' , 'items' , 'nodes' , 'subitems' , 'elements' , 'branches' ] ;
50+ for ( const field of childFields ) {
51+ if ( Array . isArray ( obj [ field ] ) ) {
52+ return obj [ field ] ;
53+ }
17054 }
55+ return undefined ;
17156 }
17257}
0 commit comments