@@ -32,6 +32,11 @@ export function createSpiderPlot(container, options) {
3232 const root = svg
3333 . append ( "g" )
3434 . attr ( "transform" , `translate(${ svgSize / 2 } ,${ svgSize / 2 } )` ) ;
35+ const tooltip = d3
36+ . select ( container )
37+ . append ( "div" )
38+ . attr ( "class" , "spider-tooltip" )
39+ . style ( "display" , "none" ) ;
3540
3641 const gridLayer = root . append ( "g" ) . attr ( "class" , "spider-grid" ) ;
3742 const axisLayer = root . append ( "g" ) . attr ( "class" , "spider-axes" ) ;
@@ -56,9 +61,13 @@ export function createSpiderPlot(container, options) {
5661 }
5762
5863 function update ( profiles ) {
59- const renderProfiles = profiles . map ( ( profile ) =>
60- buildRenderableProfile ( profile , axes , rScale , maxValue ) ,
61- ) ;
64+ const renderProfiles = profiles . map ( ( profile ) => {
65+ const profileWithBadge = {
66+ ...profile ,
67+ badge : options . getProfileBadge ? options . getProfileBadge ( profile ) : profile . label ,
68+ } ;
69+ return buildRenderableProfile ( profileWithBadge , axes , rScale , maxValue ) ;
70+ } ) ;
6271 renderLegend ( legendLayer , renderProfiles ) ;
6372
6473 const groups = dataLayer
@@ -74,6 +83,7 @@ export function createSpiderPlot(container, options) {
7483
7584 merged . each ( function renderProfile ( profile ) {
7685 const group = d3 . select ( this ) ;
86+ group . attr ( "data-profile-id" , profile . id ) ;
7787
7888 group
7989 . select ( "polygon" )
@@ -99,7 +109,18 @@ export function createSpiderPlot(container, options) {
99109 . attr ( "stroke-dasharray" , ( segment ) => ( segment . dotted ? "6,5" : null ) )
100110 . attr ( "class" , ( segment ) =>
101111 segment . dotted ? "spider-segment spider-segment--dotted" : "spider-segment" ,
102- ) ;
112+ )
113+ . on ( "pointerenter" , function handleEnter ( event , segment ) {
114+ setHoveredProfile ( profile . id ) ;
115+ showTooltip (
116+ event ,
117+ options . segmentTooltipFormatter
118+ ? options . segmentTooltipFormatter ( profile , segment )
119+ : defaultSegmentTooltip ( profile , segment ) ,
120+ ) ;
121+ } )
122+ . on ( "pointermove" , moveTooltip )
123+ . on ( "pointerleave" , clearHoverState ) ;
103124
104125 group
105126 . select ( "g.spider-dots" )
@@ -115,22 +136,76 @@ export function createSpiderPlot(container, options) {
115136 . attr ( "class" , ( point ) =>
116137 point . extrapolated ? "spider-dot spider-dot--extrapolated" : "spider-dot" ,
117138 )
118- . each ( function addTooltip ( point ) {
119- d3 . select ( this ) . selectAll ( "title" ) . remove ( ) ;
120- d3 . select ( this )
121- . append ( "title" )
122- . text (
123- options . pointTooltipFormatter
124- ? options . pointTooltipFormatter ( profile , point )
125- : `${ profile . label } ${ point . axis } : ${ point . value ?? "n/a" } ` ,
126- ) ;
127- } ) ;
139+ . on ( "pointerenter" , function handleEnter ( event , point ) {
140+ if ( point . value === null ) return ;
141+ setHoveredProfile ( profile . id ) ;
142+ showTooltip (
143+ event ,
144+ options . pointTooltipFormatter
145+ ? options . pointTooltipFormatter ( profile , point )
146+ : defaultPointTooltip ( profile , point ) ,
147+ ) ;
148+ } )
149+ . on ( "pointermove" , moveTooltip )
150+ . on ( "pointerleave" , clearHoverState ) ;
128151 } ) ;
129152
153+ legendLayer
154+ . selectAll ( "g" )
155+ . on ( "pointerenter" , ( _ , profile ) => setHoveredProfile ( profile . id ) )
156+ . on ( "pointerleave" , clearHoverState ) ;
157+
130158 groups . exit ( ) . remove ( ) ;
131159 }
132160
133161 return { update, setAxes } ;
162+
163+ function setHoveredProfile ( profileId ) {
164+ dataLayer
165+ . selectAll ( "g.spider-profile" )
166+ . classed ( "is-active" , ( profile ) => profile . id === profileId )
167+ . classed ( "is-muted" , ( profile ) => profile . id !== profileId ) ;
168+
169+ legendLayer
170+ . selectAll ( "g" )
171+ . classed ( "is-active" , ( profile ) => profile . id === profileId )
172+ . classed ( "is-muted" , ( profile ) => profile . id !== profileId ) ;
173+ }
174+
175+ function clearHoverState ( ) {
176+ dataLayer
177+ . selectAll ( "g.spider-profile" )
178+ . classed ( "is-active" , false )
179+ . classed ( "is-muted" , false ) ;
180+ legendLayer
181+ . selectAll ( "g" )
182+ . classed ( "is-active" , false )
183+ . classed ( "is-muted" , false ) ;
184+ tooltip . style ( "display" , "none" ) ;
185+ }
186+
187+ function showTooltip ( event , html ) {
188+ tooltip . style ( "display" , "block" ) . html ( html ) ;
189+ moveTooltip ( event ) ;
190+ }
191+
192+ function moveTooltip ( event ) {
193+ const bounds = container . getBoundingClientRect ( ) ;
194+ const tooltipNode = tooltip . node ( ) ;
195+ const tooltipWidth = tooltipNode ?. offsetWidth ?? 0 ;
196+ const tooltipHeight = tooltipNode ?. offsetHeight ?? 0 ;
197+ const left = Math . min (
198+ event . clientX - bounds . left + 16 ,
199+ bounds . width - tooltipWidth - 12 ,
200+ ) ;
201+ const top = Math . min (
202+ event . clientY - bounds . top + 16 ,
203+ bounds . height - tooltipHeight - 12 ,
204+ ) ;
205+ tooltip
206+ . style ( "left" , `${ Math . max ( 12 , left ) } px` )
207+ . style ( "top" , `${ Math . max ( 12 , top ) } px` ) ;
208+ }
134209}
135210
136211function normalizeAxes ( axes ) {
@@ -144,6 +219,7 @@ function renderLegend(layer, profiles) {
144219 . selectAll ( "g" )
145220 . data ( profiles , ( profile ) => profile . id )
146221 . join ( "g" )
222+ . attr ( "class" , "spider-profile-legend__item" )
147223 . attr ( "transform" , ( _ , index ) => {
148224 const column = index % 2 ;
149225 const row = Math . floor ( index / 2 ) ;
@@ -264,21 +340,37 @@ function buildRenderableProfile(profile, axes, rScale, maxValue) {
264340
265341 segments . push ( {
266342 id : `${ current . axis } ->${ next . axis } ` ,
343+ profileId : profile . id ,
267344 x1 : current . x ,
268345 y1 : current . y ,
269346 x2 : next . x ,
270347 y2 : next . y ,
271348 dotted : current . extrapolated || next . extrapolated ,
349+ fromAxis : current . axis ,
350+ fromLabel : current . label ,
351+ fromValue : current . value ,
352+ toAxis : next . axis ,
353+ toLabel : next . label ,
354+ toValue : next . value ,
272355 } ) ;
273356 }
274357
275358 return {
276359 id : profile . id ,
277360 label : profile . label ,
361+ badge : profile . badge ?? profile . label ,
278362 color : profile . color ,
279363 points,
280364 segments,
281365 isClosed : points . every ( ( point ) => point . value !== null ) ,
282366 polygonPoints : points . map ( ( point ) => [ point . x , point . y ] . join ( "," ) ) . join ( " " ) ,
283367 } ;
284368}
369+
370+ function defaultPointTooltip ( profile , point ) {
371+ return `${ profile . badge } ${ point . label } : ${ point . value ?? "n/a" } ` ;
372+ }
373+
374+ function defaultSegmentTooltip ( profile , segment ) {
375+ return `${ profile . badge } ${ segment . fromLabel } -> ${ segment . toLabel } ` ;
376+ }
0 commit comments