@@ -7,8 +7,9 @@ import { scaleLinear } from 'd3-scale';
7
7
import useHeatmapData from '../search-drawer/use-heatmap' ;
8
8
import { usePathname , useSearchParams } from 'next/navigation' ;
9
9
import { useUISearchParams } from '@/app/components/queue-control/ui-searchparams-provider' ;
10
+ import { Button , Select , Form , Space , Switch } from 'antd' ;
10
11
11
- const LEGEND_HEIGHT = 80 ;
12
+ const LEGEND_HEIGHT = 96 ; // Increased from 80
12
13
const BLUR_RADIUS = 10 ; // Increased blur radius
13
14
const HEAT_RADIUS_MULTIPLIER = 2 ; // Increased radius multiplier
14
15
@@ -36,18 +37,18 @@ interface BoardHeatmapProps {
36
37
boardDetails : BoardDetails ;
37
38
litUpHoldsMap ?: LitUpHoldsMap ;
38
39
onHoldClick ?: ( holdId : number ) => void ;
39
- colorMode ?: 'total' | 'starting' | 'hand' | 'foot' | 'finish' | 'difficulty' ;
40
40
}
41
41
42
42
const BoardHeatmap : React . FC < BoardHeatmapProps > = ( {
43
43
boardDetails,
44
44
litUpHoldsMap,
45
45
onHoldClick,
46
- colorMode = 'total'
47
46
} ) => {
48
47
const pathname = usePathname ( ) ;
49
48
const searchParams = useSearchParams ( ) ;
50
49
const { uiSearchParams } = useUISearchParams ( ) ;
50
+ const [ colorMode , setColorMode ] = useState < 'total' | 'starting' | 'hand' | 'foot' | 'finish' | 'difficulty' | 'ascents' > ( 'ascents' ) ;
51
+ const [ showNumbers , setShowNumbers ] = useState ( false ) ;
51
52
52
53
useEffect ( ( ) => {
53
54
const path = pathname . split ( '/' ) ;
@@ -83,6 +84,7 @@ const BoardHeatmap: React.FC<BoardHeatmapProps> = ({
83
84
case 'foot' : return data . footUses ;
84
85
case 'finish' : return data . finishUses ;
85
86
case 'difficulty' : return data . averageDifficulty || 0 ;
87
+ case 'ascents' : return data . totalAscents || 0 ;
86
88
default : return data . totalUses ;
87
89
}
88
90
} ;
@@ -157,10 +159,20 @@ const BoardHeatmap: React.FC<BoardHeatmapProps> = ({
157
159
158
160
const ColorLegend = ( ) => {
159
161
const gradientId = "heatmap-gradient" ;
160
- const legendWidth = 200 ;
161
- const legendHeight = 20 ;
162
+ const legendWidth = boardWidth * 0.8 ; // Make legend 80% of board width
163
+ const legendHeight = 36 ; // Increased from 30
162
164
const x = ( boardWidth - legendWidth ) / 2 ;
163
- const y = boardHeight + 20 ;
165
+ const y = boardHeight + 24 ; // Increased spacing
166
+
167
+ // Get the min, max, and middle values from the heatmap data
168
+ const values = heatmapData
169
+ . map ( data => getValue ( data ) )
170
+ . filter ( val => val > 0 )
171
+ . sort ( ( a , b ) => a - b ) ;
172
+
173
+ const minValue = values [ 0 ] || 0 ;
174
+ const maxValue = values [ values . length - 1 ] || 0 ;
175
+ const midValue = values [ Math . floor ( values . length / 2 ) ] || 0 ;
164
176
165
177
return (
166
178
< g transform = { `translate(${ x } , ${ y } )` } >
@@ -179,32 +191,36 @@ const BoardHeatmap: React.FC<BoardHeatmapProps> = ({
179
191
width = { legendWidth }
180
192
height = { legendHeight }
181
193
fill = { `url(#${ gradientId } )` }
182
- rx = { 4 }
194
+ rx = { 8 }
183
195
/>
184
- < text x = "0" y = "-5" fontSize = "12" textAnchor = "start" > Low Usage</ text >
185
- < text x = { legendWidth } y = "-5" fontSize = "12" textAnchor = "end" > High Usage</ text >
196
+ < text x = "0" y = "-10" fontSize = "28" textAnchor = "start" fontWeight = "500" > Low ({ minValue } )</ text >
197
+ < text x = { legendWidth / 2 } y = "-10" fontSize = "28" textAnchor = "middle" fontWeight = "500" > Mid ({ midValue } )</ text >
198
+ < text x = { legendWidth } y = "-10" fontSize = "28" textAnchor = "end" fontWeight = "500" > High ({ maxValue } )</ text >
186
199
</ g >
187
200
) ;
188
201
} ;
189
202
203
+ const [ showHeatmap , setShowHeatmap ] = useState ( false ) ;
204
+
205
+ const colorModeOptions = [
206
+ { value : 'ascents' , label : 'Ascents' } ,
207
+ { value : 'total' , label : 'Total Problems' } ,
208
+ { value : 'starting' , label : 'Starting Holds' } ,
209
+ { value : 'hand' , label : 'Hand Holds' } ,
210
+ { value : 'foot' , label : 'Foot Holds' } ,
211
+ { value : 'finish' , label : 'Finish Holds' } ,
212
+ { value : 'difficulty' , label : 'Difficulty' } ,
213
+ ] ;
214
+
215
+ const thresholdOptions = [
216
+ { value : 1 , label : 'Show All' } ,
217
+ { value : 2 , label : 'At Least 2 Uses' } ,
218
+ { value : 5 , label : 'At Least 5 Uses' } ,
219
+ { value : 10 , label : 'At Least 10 Uses' } ,
220
+ ] ;
221
+
190
222
return (
191
- < div className = "w-full" >
192
- < div className = "mb-4" >
193
- < label className = "block text-sm font-medium text-gray-700 mb-1" >
194
- Filter holds by minimum usage:
195
- </ label >
196
- < select
197
- value = { threshold }
198
- onChange = { ( e ) => setThreshold ( Number ( e . target . value ) ) }
199
- className = "block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
200
- >
201
- < option value = { 1 } > Show All</ option >
202
- < option value = { 2 } > At Least 2 Uses</ option >
203
- < option value = { 5 } > At Least 5 Uses</ option >
204
- < option value = { 10 } > At Least 10 Uses</ option >
205
- </ select >
206
- </ div >
207
-
223
+ < div className = "w-full" >
208
224
< svg
209
225
viewBox = { `0 0 ${ boardWidth } ${ boardHeight + LEGEND_HEIGHT } ` }
210
226
preserveAspectRatio = "xMidYMid meet"
@@ -228,64 +244,68 @@ const BoardHeatmap: React.FC<BoardHeatmapProps> = ({
228
244
) ) }
229
245
230
246
{ /* Heat overlay with blur effect */ }
231
- < g style = { { mixBlendMode : 'multiply' } } >
232
- { /* Blurred background layer */ }
233
- < g filter = "url(#blur)" >
247
+ { showHeatmap && (
248
+ < >
249
+ { /* Blurred background layer */ }
250
+ < g filter = "url(#blur)" >
251
+ { holdsData . map ( ( hold ) => {
252
+ const data = heatmapMap . get ( hold . id ) ;
253
+ const value = getValue ( data ) ;
254
+
255
+ if ( value === 0 || value < threshold ) return null ;
256
+
257
+ return (
258
+ < circle
259
+ key = { `heat-blur-${ hold . id } ` }
260
+ cx = { hold . cx }
261
+ cy = { hold . cy }
262
+ r = { hold . r * HEAT_RADIUS_MULTIPLIER }
263
+ fill = { colorScale ( value ) }
264
+ opacity = { opacityScale ( value ) * 0.5 }
265
+ />
266
+ ) ;
267
+ } ) }
268
+ </ g >
269
+ < filter id = "blurMe" >
270
+ < feGaussianBlur in = "SourceGraphic" stdDeviation = "20" />
271
+ </ filter >
272
+
273
+ { /* Sharp circles with numbers */ }
234
274
{ holdsData . map ( ( hold ) => {
235
275
const data = heatmapMap . get ( hold . id ) ;
236
276
const value = getValue ( data ) ;
237
277
238
- if ( value === 0 || value < threshold ) return null ;
278
+ if ( value < threshold ) return null ;
239
279
240
280
return (
241
- < circle
242
- key = { `heat-blur-${ hold . id } ` }
243
- cx = { hold . cx }
244
- cy = { hold . cy }
245
- r = { hold . r * HEAT_RADIUS_MULTIPLIER }
246
- fill = { colorScale ( value ) }
247
- opacity = { opacityScale ( value ) * 0.7 }
248
- />
281
+ < g key = { `heat-sharp-${ hold . id } ` } >
282
+ < circle
283
+ cx = { hold . cx }
284
+ cy = { hold . cy }
285
+ r = { hold . r }
286
+ fill = { colorScale ( value ) }
287
+ opacity = { opacityScale ( value ) }
288
+ filter = "url(#blurMe)"
289
+ />
290
+ { showNumbers && (
291
+ < text
292
+ x = { hold . cx }
293
+ y = { hold . cy }
294
+ textAnchor = "middle"
295
+ dominantBaseline = "middle"
296
+ fontSize = { Math . max ( 8 , hold . r * 0.6 ) }
297
+ fontWeight = "bold"
298
+ fill = { '#000' }
299
+ style = { { userSelect : 'none' } }
300
+ >
301
+ { value }
302
+ </ text >
303
+ ) }
304
+ </ g >
249
305
) ;
250
306
} ) }
251
- </ g >
252
- < filter id = "blurMe" >
253
- < feGaussianBlur in = "SourceGraphic" stdDeviation = "20" />
254
- </ filter >
255
-
256
- { /* Sharp circles with numbers */ }
257
- { holdsData . map ( ( hold ) => {
258
- const data = heatmapMap . get ( hold . id ) ;
259
- const value = getValue ( data ) ;
260
-
261
- if ( value < threshold ) return null ;
262
-
263
- return (
264
- < g key = { `heat-sharp-${ hold . id } ` } >
265
- < circle
266
- cx = { hold . cx }
267
- cy = { hold . cy }
268
- r = { hold . r }
269
- fill = { colorScale ( value ) }
270
- opacity = { opacityScale ( value ) }
271
- filter = "url(#blurMe)"
272
- />
273
- < text
274
- x = { hold . cx }
275
- y = { hold . cy }
276
- textAnchor = "middle"
277
- dominantBaseline = "middle"
278
- fontSize = { Math . max ( 8 , hold . r * 0.6 ) }
279
- fontWeight = "bold"
280
- fill = { '#000' }
281
- style = { { userSelect : 'none' } }
282
- >
283
- { value }
284
- </ text >
285
- </ g >
286
- ) ;
287
- } ) }
288
- </ g >
307
+ </ >
308
+ ) }
289
309
290
310
{ /* Interaction layer */ }
291
311
{ holdsData . map ( ( hold ) => (
@@ -318,9 +338,46 @@ const BoardHeatmap: React.FC<BoardHeatmapProps> = ({
318
338
) ;
319
339
} ) }
320
340
321
- < ColorLegend />
341
+ { showHeatmap && < ColorLegend /> }
322
342
</ g >
323
343
</ svg >
344
+ < Form layout = "inline" className = "mb-4" >
345
+ < Form . Item >
346
+ < Button
347
+ type = { showHeatmap ? "primary" : "default" }
348
+ onClick = { ( ) => setShowHeatmap ( ! showHeatmap ) }
349
+ >
350
+ { showHeatmap ? 'Hide Heatmap' : 'Show Heatmap' }
351
+ </ Button >
352
+ </ Form . Item >
353
+
354
+ { showHeatmap && (
355
+ < >
356
+ < Form . Item label = "View Mode" >
357
+ < Select
358
+ value = { colorMode }
359
+ onChange = { ( value ) => setColorMode ( value ) }
360
+ style = { { width : 200 } }
361
+ options = { colorModeOptions }
362
+ />
363
+ </ Form . Item >
364
+ < Form . Item label = "Minimum Usage" >
365
+ < Select
366
+ value = { threshold }
367
+ onChange = { ( value ) => setThreshold ( value ) }
368
+ style = { { width : 200 } }
369
+ options = { thresholdOptions }
370
+ />
371
+ </ Form . Item >
372
+ < Form . Item label = "Show Numbers" >
373
+ < Switch
374
+ checked = { showNumbers }
375
+ onChange = { setShowNumbers }
376
+ />
377
+ </ Form . Item >
378
+ </ >
379
+ ) }
380
+ </ Form >
324
381
</ div >
325
382
) ;
326
383
} ;
0 commit comments