@@ -57,10 +57,10 @@ const _ROLE_COLOR = {
5757 'Funding Acquisition' : '#4455BB' , // darker indigo-blue
5858
5959 // ── Group 2: Data acquisition (green → teal-cyan) ───────────────────────
60- 'Methodology' : '#00BB55 ' , // vibrant green
60+ 'Methodology' : '#DB9500 ' , // ochre (CMYK 0/32/100/14)
6161 'Validation' : '#009950' , // medium-dark green
6262 'Investigation' : '#00AA88' , // teal-green
63- 'Resources' : '#0099BB ' , // teal-blue
63+ 'Resources' : '#c38a0e ' , // ochre (CMYK 0/32/100/14)
6464 'Data curation' : '#00BBCC' , // cyan-teal
6565
6666 // ── Group 3: Analysis & writing (pink → red-pink / magenta) ────────────
@@ -77,11 +77,29 @@ const _ROLE_ABBREV = (r) => r
7777 . replace ( 'Writing \u2013 ' , 'W: ' )
7878 . replace ( 'Formal analysis' , 'Formal anal.' ) ;
7979
80- const _PALETTE = [
81- '#4f46e5' , '#0d9488' , '#7c3aed' , '#d97706' ,
82- '#e11d48' , '#059669' , '#1e40af' , '#4338ca' ,
83- '#be185d' , '#0891b2' , '#65a30d' , '#9333ea' ,
84- ] ;
80+ // ── Author node color — semantic group derived from majority CRediT roles ──
81+ //
82+ // Group → hue band [center°, halfSpread°]
83+ const _NODE_HUE = {
84+ leadership : [ 252 , 32 ] , // blue-violet → violet-purple (~220–284°)
85+ methods : [ 41 , 22 ] , // ochre → amber (~19–63°) (Methodology, Resources)
86+ data : [ 165 , 28 ] , // green → teal (~137–193°)
87+ analysis : [ 340 , 22 ] , // pink → magenta (~318–362°)
88+ } ;
89+
90+ // Derived from _ROLE_COLOR comment groups above.
91+ const _NODE_ROLE_GROUP = ( ( ) => {
92+ const m = { } ;
93+ for ( const r of [ 'Conceptualization' , 'Supervision' , 'Project Administration' , 'Funding Acquisition' ] )
94+ m [ r . toLowerCase ( ) ] = 'leadership' ;
95+ for ( const r of [ 'Methodology' , 'Resources' ] )
96+ m [ r . toLowerCase ( ) ] = 'methods' ;
97+ for ( const r of [ 'Validation' , 'Investigation' , 'Data curation' ] )
98+ m [ r . toLowerCase ( ) ] = 'data' ;
99+ for ( const r of [ 'Formal analysis' , 'Software' , 'Writing \u2013 original draft' , 'Writing \u2013 review & editing' , 'Visualization' ] )
100+ m [ r . toLowerCase ( ) ] = 'analysis' ;
101+ return m ;
102+ } ) ( ) ;
85103
86104// ── Utilities ─────────────────────────────────────────────────────────────────
87105
@@ -91,7 +109,55 @@ function _hash(s) {
91109 return Math . abs ( h ) ;
92110}
93111
94- function _nodeColor ( name ) { return _PALETTE [ _hash ( name ) % _PALETTE . length ] ; }
112+ function _nodeColor ( author , allAuthors ) {
113+ const counts = { leadership : 0 , methods : 0 , data : 0 , analysis : 0 } ;
114+
115+ // Own contributions (weight 1.0 each)
116+ const ownRoles = new Set ( ) ;
117+ if ( author . credit_levels ) {
118+ for ( const cl of author . credit_levels ) {
119+ if ( ! cl . role ) continue ;
120+ const key = cl . role . toLowerCase ( ) . replace ( / \s + / g, ' ' ) . replace ( / \u2014 / g, '\u2013' ) . trim ( ) ;
121+ ownRoles . add ( key ) ;
122+ const grp = _NODE_ROLE_GROUP [ key ] ;
123+ if ( grp ) counts [ grp ] ++ ;
124+ }
125+ }
126+
127+ // Co-contributor influence: authors who share ≥1 role each add 0.1 per role
128+ if ( allAuthors && ownRoles . size > 0 ) {
129+ for ( const other of allAuthors ) {
130+ if ( other . name === author . name || ! other . credit_levels ) continue ;
131+ const shares = other . credit_levels . some ( cl => {
132+ const key = cl . role . toLowerCase ( ) . replace ( / \s + / g, ' ' ) . replace ( / \u2014 / g, '\u2013' ) . trim ( ) ;
133+ return ownRoles . has ( key ) ;
134+ } ) ;
135+ if ( ! shares ) continue ;
136+ for ( const cl of other . credit_levels ) {
137+ if ( ! cl . role ) continue ;
138+ const key = cl . role . toLowerCase ( ) . replace ( / \s + / g, ' ' ) . replace ( / \u2014 / g, '\u2013' ) . trim ( ) ;
139+ const grp = _NODE_ROLE_GROUP [ key ] ;
140+ if ( grp ) counts [ grp ] += 0.1 ;
141+ }
142+ }
143+ }
144+
145+ const best = Math . max ( counts . leadership , counts . methods , counts . data , counts . analysis ) ;
146+ let group ;
147+ if ( best === 0 ) {
148+ group = [ 'leadership' , 'methods' , 'data' , 'analysis' ] [ _hash ( author . name ) % 4 ] ;
149+ } else {
150+ const tied = Object . entries ( counts ) . filter ( ( [ , v ] ) => v === best ) . map ( ( [ k ] ) => k ) ;
151+ group = tied . length === 1 ? tied [ 0 ] : tied [ _hash ( author . name ) % tied . length ] ;
152+ }
153+ const h1 = _hash ( author . name ) ;
154+ const h2 = _hash ( author . name + '~' ) ;
155+ const [ hCenter , hHalf ] = _NODE_HUE [ group ] ;
156+ const hue = ( ( hCenter - hHalf + ( h1 % ( hHalf * 2 + 1 ) ) ) + 360 ) % 360 ;
157+ const sat = 62 + ( h2 % 18 ) ; // 62–79 %
158+ const lgt = 40 + ( ( h1 >> 6 ) % 14 ) ; // 40–53 %
159+ return `hsl(${ hue } ,${ sat } %,${ lgt } %)` ;
160+ }
95161
96162function _initials ( name ) {
97163 const p = name . trim ( ) . split ( / \s + / ) ;
@@ -333,6 +399,7 @@ function _injectCSS() {
333399
334400let _xpopEl = null ;
335401let _xpopTid = null ;
402+ let _xAllAuthors = [ ] ; // set by createExploreView for use in _showXpop
336403
337404function _showXpop ( anchorEl , author ) {
338405 _hideXpop ( ) ;
@@ -358,7 +425,7 @@ function _showXpop(anchorEl, author) {
358425 const header = document . createElement ( 'div' ) ;
359426 header . style . cssText = 'display:flex;align-items:center;gap:9px;margin-bottom:7px;' ;
360427 const avEl = document . createElement ( 'div' ) ;
361- const avColor = _nodeColor ( author . name ) ;
428+ const avColor = _nodeColor ( author , _xAllAuthors ) ;
362429 avEl . style . cssText = [
363430 `background:${ avColor } ` , 'border-radius:50%' ,
364431 'width:30px' , 'height:30px' , 'flex-shrink:0' ,
@@ -648,7 +715,7 @@ function _renderGraph(svgEl, nodes, edges, width, height) {
648715
649716 for ( const node of nodes ) {
650717 const { author, i } = node ;
651- const color = _nodeColor ( author . name ) ;
718+ const color = _nodeColor ( author , _xAllAuthors ) ;
652719
653720 const g = _svg ( 'g' , {
654721 class : 'ae-explore-node' ,
@@ -913,6 +980,7 @@ function _buildRightLegend(el, clusterMap, onHover, onLeave) {
913980 * @returns {Function } - Cleanup function; call when unmounting.
914981 */
915982export function createExploreView ( container , authors , zoomState ) {
983+ _xAllAuthors = authors || [ ] ;
916984 _injectCSS ( ) ;
917985 if ( ! authors || authors . length === 0 ) return ( ) => { } ;
918986
0 commit comments