@@ -128,44 +128,34 @@ const AlignmentFilterButton = ({
128
128
currentFilter,
129
129
onClick,
130
130
icon : Icon ,
131
- label,
132
- iconOnly = false
131
+ label
133
132
} : {
134
133
filter : 'all' | 'forward' | 'reverse' ;
135
134
currentFilter : 'all' | 'forward' | 'reverse' ;
136
135
onClick : ( filter : 'all' | 'forward' | 'reverse' ) => void ;
137
136
icon : any ;
138
137
label : string ;
139
- iconOnly ?: boolean ;
140
138
} ) => {
141
139
return (
142
140
< button
143
141
onClick = { ( ) => onClick ( filter ) }
144
142
className = { cn (
145
- 'group relative inline-flex h-8 items-center justify-center overflow-hidden rounded-md' ,
143
+ 'group relative inline-flex h-7 items-center justify-center overflow-hidden rounded-md' ,
146
144
'bg-background border transition-all duration-200' ,
147
145
filter === currentFilter
148
- ? 'border-primary text-primary'
149
- : 'border-border hover:border-primary/50 text-muted-foreground' ,
150
- iconOnly
151
- ? 'w-8'
152
- : filter === currentFilter
153
- ? 'w-24'
154
- : 'w-8 hover:w-24'
146
+ ? 'border-primary text-primary w-24'
147
+ : 'border-border hover:border-primary/50 text-muted-foreground w-7 hover:w-24'
155
148
) }
156
149
>
157
- { ! iconOnly && (
158
- < div className = { cn (
159
- 'inline-flex whitespace-nowrap transition-all duration-200' ,
160
- filter === currentFilter ? '-translate-x-2 opacity-100' : 'opacity-0 group-hover:-translate-x-2 group-hover:opacity-100'
161
- ) } >
162
- { label }
163
- </ div >
164
- ) }
165
150
< div className = { cn (
166
- "transition-all duration-200" ,
167
- iconOnly ? "static" : "absolute right-2"
151
+ 'inline-flex whitespace-nowrap transition-all duration-200' ,
152
+ filter === currentFilter
153
+ ? '-translate-x-2 opacity-100'
154
+ : 'opacity-0 group-hover:-translate-x-2 group-hover:opacity-100'
168
155
) } >
156
+ { label }
157
+ </ div >
158
+ < div className = "absolute right-2" >
169
159
< Icon className = "h-4 w-4" />
170
160
</ div >
171
161
</ button >
@@ -257,6 +247,8 @@ const ChromosomeScrollbar = ({
257
247
} , [ containerRef , svgRef ] ) ;
258
248
259
249
const handleThumbMouseDown = useCallback ( ( e : React . MouseEvent ) => {
250
+ e . preventDefault ( ) ; // Prevent default selection
251
+ e . stopPropagation ( ) ; // Stop event propagation
260
252
setIsDragging ( true ) ;
261
253
setStartX ( e . clientX - scrollThumbRef . current ! . offsetLeft ) ;
262
254
} , [ ] ) ;
@@ -265,6 +257,7 @@ const ChromosomeScrollbar = ({
265
257
if ( ! isDragging || ! scrollTrackRef . current || ! scrollThumbRef . current || ! svgRef . current ) return ;
266
258
267
259
e . preventDefault ( ) ;
260
+ e . stopPropagation ( ) ;
268
261
const trackRect = scrollTrackRef . current . getBoundingClientRect ( ) ;
269
262
const thumbWidth = scrollThumbRef . current . clientWidth ;
270
263
const x = e . clientX - trackRect . left - startX ;
@@ -304,31 +297,47 @@ const ChromosomeScrollbar = ({
304
297
} , [ isDragging , handleMouseMove , handleMouseUp ] ) ;
305
298
306
299
return (
307
- < div className = "absolute bottom-4 left-4 right-4 h-6" >
300
+ < div
301
+ className = "absolute bottom-4 left-4 right-4 h-6 select-none"
302
+ onMouseDown = { ( e ) => e . stopPropagation ( ) } // Prevent selection events
303
+ >
308
304
< div
309
305
ref = { scrollTrackRef }
310
- className = "relative w-full h-2 bg-muted rounded-full"
306
+ className = "relative w-full h-2 bg-muted rounded-full select-none"
307
+ onMouseDown = { ( e ) => e . stopPropagation ( ) } // Prevent selection events
311
308
>
312
309
< div
313
310
ref = { scrollThumbRef }
314
311
style = { {
315
312
width : `${ getThumbWidth ( ) } px` ,
316
- left : scrollLeft
313
+ left : scrollLeft ,
314
+ userSelect : 'none' , // Prevent text selection
315
+ WebkitUserSelect : 'none' ,
316
+ MozUserSelect : 'none' ,
317
+ msUserSelect : 'none'
317
318
} }
318
319
className = { cn (
319
320
"absolute top-0 h-full rounded-full bg-primary/50" ,
320
321
"cursor-grab hover:bg-primary/70 transition-colors" ,
322
+ "select-none touch-none" , // Prevent selection and touch events
321
323
isDragging && "cursor-grabbing bg-primary"
322
324
) }
323
325
onMouseDown = { handleThumbMouseDown }
326
+ onDragStart = { ( e ) => e . preventDefault ( ) } // Prevent drag events
324
327
>
325
328
{ /* Thumbnail preview */ }
326
- < div className = "absolute -top-20 left-0 w-40 h-16 bg-background border rounded-lg overflow-hidden opacity-0 group-hover:opacity-100 transition-opacity" >
329
+ < div
330
+ className = { cn (
331
+ "absolute -top-20 left-0 w-40 h-16 bg-background border rounded-lg overflow-hidden" ,
332
+ "opacity-0 group-hover:opacity-100 transition-opacity select-none pointer-events-none"
333
+ ) }
334
+ >
327
335
< svg
328
336
width = "100%"
329
337
height = "100%"
330
338
viewBox = { `0 0 ${ width } ${ height } ` }
331
339
preserveAspectRatio = "xMidYMid meet"
340
+ className = "select-none pointer-events-none"
332
341
>
333
342
{ /* Miniature version of chromosomes */ }
334
343
</ svg >
@@ -1204,38 +1213,87 @@ export function ChromosomeSynteny({
1204
1213
if ( ! svgRef . current ) return ;
1205
1214
1206
1215
try {
1207
- // Get SVG content
1208
1216
const svgElement = svgRef . current ;
1209
- const svgData = new XMLSerializer ( ) . serializeToString ( svgElement ) ;
1217
+ const clone = svgElement . cloneNode ( true ) as SVGSVGElement ;
1218
+ const bbox = svgElement . getBBox ( ) ;
1219
+
1220
+ // Add padding (50px on each side)
1221
+ const padding = 50 ;
1222
+ const totalWidth = bbox . width + ( padding * 2 ) ;
1223
+ const totalHeight = bbox . height + ( padding * 2 ) + 30 ; // Extra 30px for credits
1224
+
1225
+ // Check dark mode once at the beginning
1226
+ const isDarkMode = document . documentElement . classList . contains ( 'dark' ) ;
1227
+
1228
+ // Update clone dimensions with padding
1229
+ clone . setAttribute ( 'width' , `${ totalWidth } ` ) ;
1230
+ clone . setAttribute ( 'height' , `${ totalHeight } ` ) ;
1231
+ clone . setAttribute ( 'viewBox' , `${ bbox . x - padding } ${ bbox . y - padding } ${ totalWidth } ${ totalHeight } ` ) ;
1232
+
1233
+ // Inline styles
1234
+ const styles = document . styleSheets ;
1235
+ let stylesText = '' ;
1236
+ for ( let i = 0 ; i < styles . length ; i ++ ) {
1237
+ try {
1238
+ const rules = styles [ i ] . cssRules || styles [ i ] . rules ;
1239
+ for ( let j = 0 ; j < rules . length ; j ++ ) {
1240
+ stylesText += rules [ j ] . cssText + '\n' ;
1241
+ }
1242
+ } catch ( e ) {
1243
+ console . warn ( 'Could not read styles' , e ) ;
1244
+ }
1245
+ }
1246
+
1247
+ // Add styles with dark mode consideration
1248
+ const styleElement = document . createElement ( 'style' ) ;
1249
+ styleElement . textContent = `
1250
+ ${ stylesText }
1251
+ ${ isDarkMode ? `
1252
+ text, .text-foreground {
1253
+ fill: #ffffff !important;
1254
+ color: #ffffff !important;
1255
+ }
1256
+ .text-muted-foreground {
1257
+ fill: #a1a1aa !important;
1258
+ color: #a1a1aa !important;
1259
+ }
1260
+ ` : '' }
1261
+ ` ;
1262
+ clone . insertBefore ( styleElement , clone . firstChild ) ;
1263
+
1264
+ const svgData = new XMLSerializer ( ) . serializeToString ( clone ) ;
1265
+ const svgBlob = new Blob ( [
1266
+ '<?xml version="1.0" standalone="no"?>\r\n' ,
1267
+ svgData
1268
+ ] , { type : 'image/svg+xml;charset=utf-8' } ) ;
1210
1269
1211
- // Create canvas
1212
1270
const canvas = document . createElement ( 'canvas' ) ;
1213
1271
const ctx = canvas . getContext ( '2d' ) ;
1214
1272
if ( ! ctx ) throw new Error ( 'Could not get canvas context' ) ;
1215
1273
1216
- // Set canvas size to match SVG
1217
- const svgSize = svgElement . getBoundingClientRect ( ) ;
1218
- canvas . width = svgSize . width * 2 ; // 2x for better quality
1219
- canvas . height = svgSize . height * 2 ;
1274
+ const scale2x = 2 ;
1275
+ canvas . width = totalWidth * scale2x ;
1276
+ canvas . height = totalHeight * scale2x ;
1220
1277
1221
- // Set background color based on theme
1222
- const isDarkMode = document . documentElement . classList . contains ( 'dark' ) ;
1278
+ // Use the same isDarkMode value for background
1223
1279
ctx . fillStyle = isDarkMode ? '#020817' : '#ffffff' ;
1224
1280
ctx . fillRect ( 0 , 0 , canvas . width , canvas . height ) ;
1225
1281
1226
- // Create image from SVG
1227
- const blob = new Blob ( [ svgData ] , { type : 'image/svg+xml;charset=utf-8' } ) ;
1228
- const url = URL . createObjectURL ( blob ) ;
1229
-
1230
- const img = new window . Image ( ) ;
1231
- img . src = url ;
1282
+ const url = URL . createObjectURL ( svgBlob ) ;
1283
+ const img = document . createElement ( 'img' ) as HTMLImageElement ;
1232
1284
1233
1285
await new Promise ( ( resolve , reject ) => {
1234
1286
img . onload = ( ) => {
1235
- ctx . scale ( 2 , 2 ) ; // Scale up for better quality
1236
- ctx . drawImage ( img , 0 , 0 ) ;
1287
+ ctx . scale ( scale2x , scale2x ) ;
1288
+ ctx . drawImage ( img , 0 , 0 , totalWidth , totalHeight ) ;
1289
+
1290
+ // Add credits with same isDarkMode value
1291
+ ctx . scale ( 0.5 , 0.5 ) ;
1292
+ ctx . fillStyle = isDarkMode ? '#a1a1aa' : '#94a3b8' ;
1293
+ ctx . font = '24px system-ui, sans-serif' ;
1294
+ ctx . textAlign = 'right' ;
1295
+ ctx . fillText ( '© 2024 CHITRA' , totalWidth * 2 - 20 , totalHeight * 2 - 20 ) ;
1237
1296
1238
- // Convert to desired format
1239
1297
const mimeType = format === 'png' ? 'image/png' : 'image/jpeg' ;
1240
1298
const quality = format === 'png' ? 1 : 0.95 ;
1241
1299
@@ -1245,23 +1303,20 @@ export function ChromosomeSynteny({
1245
1303
return ;
1246
1304
}
1247
1305
1248
- // Create download link
1249
1306
const downloadLink = document . createElement ( 'a' ) ;
1250
1307
downloadLink . href = URL . createObjectURL ( blob ) ;
1251
- downloadLink . download = `chromoviz-overview -${ new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] } .${ format } ` ;
1308
+ downloadLink . download = `chromoviz-full -${ new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] } .${ format } ` ;
1252
1309
document . body . appendChild ( downloadLink ) ;
1253
1310
downloadLink . click ( ) ;
1254
1311
document . body . removeChild ( downloadLink ) ;
1255
1312
1256
- // Cleanup
1257
1313
URL . revokeObjectURL ( downloadLink . href ) ;
1258
1314
URL . revokeObjectURL ( url ) ;
1259
1315
resolve ( true ) ;
1260
1316
} , mimeType , quality ) ;
1261
1317
} ;
1262
- img . onerror = ( ) => {
1263
- reject ( new Error ( 'Failed to load image' ) ) ;
1264
- } ;
1318
+ img . onerror = ( ) => reject ( new Error ( 'Failed to load image' ) ) ;
1319
+ img . src = url ;
1265
1320
} ) ;
1266
1321
} catch ( error ) {
1267
1322
console . error ( 'Error exporting image:' , error ) ;
@@ -1298,45 +1353,28 @@ export function ChromosomeSynteny({
1298
1353
< div className = "flex items-center gap-2" >
1299
1354
{ /* Show full controls on larger screens */ }
1300
1355
< div className = "hidden md:flex items-center gap-2" >
1301
- < div className = "flex items-center gap-1" >
1356
+ < div className = "flex items-center gap-1.5 " >
1302
1357
< AlignmentFilterButton
1303
1358
filter = "all"
1304
1359
currentFilter = { alignmentFilter }
1305
1360
onClick = { setAlignmentFilter }
1306
1361
icon = { ArrowLeftRight }
1307
1362
label = "All"
1308
- iconOnly = { true }
1309
1363
/>
1310
1364
< AlignmentFilterButton
1311
1365
filter = "forward"
1312
1366
currentFilter = { alignmentFilter }
1313
1367
onClick = { setAlignmentFilter }
1314
1368
icon = { ArrowRight }
1315
1369
label = "Forward"
1316
- iconOnly = { true }
1317
1370
/>
1318
1371
< AlignmentFilterButton
1319
1372
filter = "reverse"
1320
1373
currentFilter = { alignmentFilter }
1321
1374
onClick = { setAlignmentFilter }
1322
1375
icon = { ArrowLeft }
1323
1376
label = "Reverse"
1324
- iconOnly = { true }
1325
- />
1326
- </ div >
1327
-
1328
- < Separator orientation = "vertical" className = "h-6" />
1329
-
1330
- < div className = "flex items-center gap-1.5" >
1331
- < Switch
1332
- id = "annotations-mode"
1333
- checked = { showAnnotations }
1334
- onCheckedChange = { setShowAnnotations }
1335
- className = "scale-75"
1336
1377
/>
1337
- < Label htmlFor = "annotations-mode" className = "text-xs text-muted-foreground" >
1338
- Annotations
1339
- </ Label >
1340
1378
</ div >
1341
1379
</ div >
1342
1380
@@ -1419,7 +1457,7 @@ export function ChromosomeSynteny({
1419
1457
</ div >
1420
1458
1421
1459
{ /* Main Content Area */ }
1422
- < div className = "relative flex-1 h-[calc(100% )]" >
1460
+ < div className = "relative flex-1 h-[calc(100vh-6rem )]" >
1423
1461
< div className = "w-full h-full" >
1424
1462
< svg
1425
1463
ref = { svgRef }
@@ -1431,7 +1469,7 @@ export function ChromosomeSynteny({
1431
1469
preserveAspectRatio = "xMidYMid meet"
1432
1470
/>
1433
1471
1434
- < div className = "absolute bottom-20 right-4 z-20" >
1472
+ < div className = "absolute bottom-16 right-4 z-20 hidden md:block scale-90 " >
1435
1473
< MiniMap
1436
1474
mainSvgRef = { svgRef }
1437
1475
zoomBehaviorRef = { zoomBehaviorRef }
@@ -1455,7 +1493,7 @@ export function ChromosomeSynteny({
1455
1493
</ >
1456
1494
) }
1457
1495
1458
- < div className = "absolute left-4 bottom-20 z-20" >
1496
+ < div className = "absolute left-4 bottom-16 z-20 hidden md:block scale-90 " >
1459
1497
< div className = "inline-grid w-fit grid-cols-3 gap-1" >
1460
1498
< div > </ div >
1461
1499
< Button
@@ -1468,11 +1506,11 @@ export function ChromosomeSynteny({
1468
1506
onTouchStart = { ( ) => startContinuousPan ( 'up' ) }
1469
1507
onTouchEnd = { stopContinuousPan }
1470
1508
className = { cn (
1471
- "h-8 w-8 transition-colors" ,
1509
+ "h-7 w-7 transition-colors" ,
1472
1510
isPanningRef . current && "bg-blue-500/10 border-blue-500/50"
1473
1511
) }
1474
1512
>
1475
- < ChevronUp className = "h-4 w-4 " />
1513
+ < ChevronUp className = "h-3.5 w-3.5 " />
1476
1514
</ Button >
1477
1515
< div > </ div >
1478
1516
< Button
@@ -1485,11 +1523,11 @@ export function ChromosomeSynteny({
1485
1523
onTouchStart = { ( ) => startContinuousPan ( 'left' ) }
1486
1524
onTouchEnd = { stopContinuousPan }
1487
1525
className = { cn (
1488
- "h-8 w-8 transition-colors" ,
1526
+ "h-7 w-7 transition-colors" ,
1489
1527
isPanningRef . current && "bg-blue-500/10 border-blue-500/50"
1490
1528
) }
1491
1529
>
1492
- < ChevronLeft className = "h-4 w-4 " />
1530
+ < ChevronLeft className = "h-3.5 w-3.5 " />
1493
1531
</ Button >
1494
1532
< div className = "flex items-center justify-center" >
1495
1533
< Circle className = "h-4 w-4 opacity-60" />
@@ -1504,11 +1542,11 @@ export function ChromosomeSynteny({
1504
1542
onTouchStart = { ( ) => startContinuousPan ( 'right' ) }
1505
1543
onTouchEnd = { stopContinuousPan }
1506
1544
className = { cn (
1507
- "h-8 w-8 transition-colors" ,
1545
+ "h-7 w-7 transition-colors" ,
1508
1546
isPanningRef . current && "bg-blue-500/10 border-blue-500/50"
1509
1547
) }
1510
1548
>
1511
- < ChevronRight className = "h-4 w-4 " />
1549
+ < ChevronRight className = "h-3.5 w-3.5 " />
1512
1550
</ Button >
1513
1551
< div > </ div >
1514
1552
< Button
@@ -1521,11 +1559,11 @@ export function ChromosomeSynteny({
1521
1559
onTouchStart = { ( ) => startContinuousPan ( 'down' ) }
1522
1560
onTouchEnd = { stopContinuousPan }
1523
1561
className = { cn (
1524
- "h-8 w-8 transition-colors" ,
1562
+ "h-7 w-7 transition-colors" ,
1525
1563
isPanningRef . current && "bg-blue-500/10 border-blue-500/50"
1526
1564
) }
1527
1565
>
1528
- < ChevronDown className = "h-4 w-4 " />
1566
+ < ChevronDown className = "h-3.5 w-3.5 " />
1529
1567
</ Button >
1530
1568
< div > </ div >
1531
1569
</ div >
0 commit comments