6ec3c94465c621547de374cef3c52db43b9f7bee braney Sun Sep 21 11:01:16 2025 -0700 playing around with new display mode diff --git src/hg/js/parallelSegments.js src/hg/js/parallelSegments.js new file mode 100644 index 00000000000..1b5ed020d10 --- /dev/null +++ src/hg/js/parallelSegments.js @@ -0,0 +1,446 @@ + // Encapsulated namespace for the parallel segments visualization + const ParallelSegments = (function() { + const canvas = document.getElementById('canvas'); + const ctx = canvas.getContext('2d'); + + // Color palette for segments + const colors = [ + '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', + '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F', + '#BB8FCE', '#85C1E9', '#F8C471', '#82E0AA' + ]; + + function getColorForSegment(name, index) { + // Generate consistent color based on segment name + let hash = 0; + for (let i = 0; i < name.length; i++) { + hash = name.charCodeAt(i) + ((hash << 5) - hash); + } + return colors[Math.abs(hash) % colors.length]; + } + + // Helper function to convert coordinate to pixel position + function coordToPixel(coord, range, lineStartX, lineWidth) { + const rangeSize = range.end - range.start; + const ratio = (coord - range.start) / rangeSize; + return lineStartX + (ratio * lineWidth); + } + + // Main drawing function - the only public interface + function drawCoordinateBasedSegments(line1Data, line2Data, line1Y = 120, line2Y = 280, startX = 100, totalWidth = 700) { + // Clear canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + + const { range: line1Range, segments: line1Segments, label: line1Label } = line1Data; + const { range: line2Range, segments: line2Segments, label: line2Label } = line2Data; + + // Collect all unique segment names from both lines + const allSegmentNames = new Set([ + ...line1Segments.map(s => s.name), + ...line2Segments.map(s => s.name) + ]); + + // Draw polygons connecting corresponding segments by name + allSegmentNames.forEach(segmentName => { + const segment1 = line1Segments.find(seg => seg.name === segmentName); + const segment2 = line2Segments.find(seg => seg.name === segmentName); + + // Skip if segment doesn't exist on both lines + if (!segment1 || !segment2) return; + + const color = getColorForSegment(segmentName, 0); + + // Calculate pixel positions for line 1 segment + const x1_start = coordToPixel(segment1.start, line1Range, startX, totalWidth); + const x1_end = coordToPixel(segment1.end, line1Range, startX, totalWidth); + + // Calculate pixel positions for line 2 segment + const x2_start = coordToPixel(segment2.start, line2Range, startX, totalWidth); + const x2_end = coordToPixel(segment2.end, line2Range, startX, totalWidth); + + // Check if segment is reversed (start > end on either line) + const line1Reversed = segment1.start > segment1.end; + const line2Reversed = segment2.start > segment2.end; + const isReversed = line1Reversed !== line2Reversed; + + // Draw polygon between the segments + ctx.fillStyle = color; + ctx.globalAlpha = 0.6; + ctx.beginPath(); + + if (isReversed) { + // Draw smooth hourglass-shaped polygon for reversed segments with more control points + const midY = (line1Y + line2Y) / 2; + const waistWidth = Math.abs(x1_end - x1_start) * 0.2; // Narrower waist for more dramatic effect + const waistCenterX = (x1_start + x1_end + x2_start + x2_end) / 4; + const waistLeft = waistCenterX - waistWidth / 2; + const waistRight = waistCenterX + waistWidth / 2; + + // Start at top-left + ctx.moveTo(x1_start, line1Y); + + // Top edge (straight line) + ctx.lineTo(x1_end, line1Y); + + // Right side curve from top to waist (multiple segments for smoothness) + // First quarter curve + const q1Y = line1Y + (midY - line1Y) * 0.25; + const q1X = x1_end + (waistRight - x1_end) * 0.1; + ctx.quadraticCurveTo(x1_end + (waistRight - x1_end) * 0.05, line1Y + (midY - line1Y) * 0.1, q1X, q1Y); + + // Second quarter curve (more aggressive inward curve) + const q2Y = line1Y + (midY - line1Y) * 0.5; + const q2X = x1_end + (waistRight - x1_end) * 0.4; + ctx.quadraticCurveTo(x1_end + (waistRight - x1_end) * 0.2, line1Y + (midY - line1Y) * 0.3, q2X, q2Y); + + // Third quarter curve (approaching waist) + const q3Y = line1Y + (midY - line1Y) * 0.75; + const q3X = x1_end + (waistRight - x1_end) * 0.7; + ctx.quadraticCurveTo(x1_end + (waistRight - x1_end) * 0.55, line1Y + (midY - line1Y) * 0.6, q3X, q3Y); + + // Final curve to waist + ctx.quadraticCurveTo(x1_end + (waistRight - x1_end) * 0.85, line1Y + (midY - line1Y) * 0.9, waistRight, midY); + + // Right side curve from waist to bottom (mirror of top) + // First quarter from waist + const b1Y = midY + (line2Y - midY) * 0.25; + const b1X = waistRight + (x2_end - waistRight) * 0.3; + ctx.quadraticCurveTo(waistRight + (x2_end - waistRight) * 0.15, midY + (line2Y - midY) * 0.1, b1X, b1Y); + + // Second quarter + const b2Y = midY + (line2Y - midY) * 0.5; + const b2X = waistRight + (x2_end - waistRight) * 0.6; + ctx.quadraticCurveTo(waistRight + (x2_end - waistRight) * 0.45, midY + (line2Y - midY) * 0.35, b2X, b2Y); + + // Third quarter + const b3Y = midY + (line2Y - midY) * 0.75; + const b3X = waistRight + (x2_end - waistRight) * 0.8; + ctx.quadraticCurveTo(waistRight + (x2_end - waistRight) * 0.7, midY + (line2Y - midY) * 0.6, b3X, b3Y); + + // Final curve to bottom-right + ctx.quadraticCurveTo(waistRight + (x2_end - waistRight) * 0.9, midY + (line2Y - midY) * 0.9, x2_end, line2Y); + + // Bottom edge (straight line) + ctx.lineTo(x2_start, line2Y); + + // Left side curve from bottom to waist (mirror of right side) + // First quarter from bottom + const bl1Y = line2Y - (line2Y - midY) * 0.25; + const bl1X = x2_start - (x2_start - waistLeft) * 0.3; + ctx.quadraticCurveTo(x2_start - (x2_start - waistLeft) * 0.15, line2Y - (line2Y - midY) * 0.1, bl1X, bl1Y); + + // Second quarter + const bl2Y = line2Y - (line2Y - midY) * 0.5; + const bl2X = x2_start - (x2_start - waistLeft) * 0.6; + ctx.quadraticCurveTo(x2_start - (x2_start - waistLeft) * 0.45, line2Y - (line2Y - midY) * 0.35, bl2X, bl2Y); + + // Third quarter + const bl3Y = line2Y - (line2Y - midY) * 0.75; + const bl3X = x2_start - (x2_start - waistLeft) * 0.8; + ctx.quadraticCurveTo(x2_start - (x2_start - waistLeft) * 0.7, line2Y - (line2Y - midY) * 0.6, bl3X, bl3Y); + + // Final curve to waist + ctx.quadraticCurveTo(x2_start - (x2_start - waistLeft) * 0.9, line2Y - (line2Y - midY) * 0.9, waistLeft, midY); + + // Left side curve from waist to top (mirror of bottom to waist) + // First quarter from waist + const tl1Y = midY - (midY - line1Y) * 0.25; + const tl1X = waistLeft - (waistLeft - x1_start) * 0.3; + ctx.quadraticCurveTo(waistLeft - (waistLeft - x1_start) * 0.15, midY - (midY - line1Y) * 0.1, tl1X, tl1Y); + + // Second quarter + const tl2Y = midY - (midY - line1Y) * 0.5; + const tl2X = waistLeft - (waistLeft - x1_start) * 0.6; + ctx.quadraticCurveTo(waistLeft - (waistLeft - x1_start) * 0.45, midY - (midY - line1Y) * 0.35, tl2X, tl2Y); + + // Third quarter + const tl3Y = midY - (midY - line1Y) * 0.75; + const tl3X = waistLeft - (waistLeft - x1_start) * 0.8; + ctx.quadraticCurveTo(waistLeft - (waistLeft - x1_start) * 0.7, midY - (midY - line1Y) * 0.6, tl3X, tl3Y); + + // Final curve to top-left + ctx.quadraticCurveTo(waistLeft - (waistLeft - x1_start) * 0.9, midY - (midY - line1Y) * 0.9, x1_start, line1Y); + } else { + // Draw normal polygon with straight lines + ctx.moveTo(x1_start, line1Y); + ctx.lineTo(x1_end, line1Y); + ctx.lineTo(x2_end, line2Y); + ctx.lineTo(x2_start, line2Y); + } + + ctx.closePath(); + ctx.fill(); + + // Draw polygon border with different style for reversed segments + ctx.globalAlpha = 1; + ctx.strokeStyle = isReversed ? '#FF0000' : color; + ctx.lineWidth = isReversed ? 2 : 1; + ctx.setLineDash(isReversed ? [5, 5] : []); + + // Redraw the border path (EXACT same path as fill) + ctx.beginPath(); + if (isReversed) { + // Redraw exact hourglass border with all control points + const midY = (line1Y + line2Y) / 2; + const waistWidth = Math.abs(x1_end - x1_start) * 0.2; + const waistCenterX = (x1_start + x1_end + x2_start + x2_end) / 4; + const waistLeft = waistCenterX - waistWidth / 2; + const waistRight = waistCenterX + waistWidth / 2; + + // Start at top-left + ctx.moveTo(x1_start, line1Y); + + // Top edge (straight line) + ctx.lineTo(x1_end, line1Y); + + // Right side curve from top to waist (exact same as fill) + const q1Y = line1Y + (midY - line1Y) * 0.25; + const q1X = x1_end + (waistRight - x1_end) * 0.1; + ctx.quadraticCurveTo(x1_end + (waistRight - x1_end) * 0.05, line1Y + (midY - line1Y) * 0.1, q1X, q1Y); + + const q2Y = line1Y + (midY - line1Y) * 0.5; + const q2X = x1_end + (waistRight - x1_end) * 0.4; + ctx.quadraticCurveTo(x1_end + (waistRight - x1_end) * 0.2, line1Y + (midY - line1Y) * 0.3, q2X, q2Y); + + const q3Y = line1Y + (midY - line1Y) * 0.75; + const q3X = x1_end + (waistRight - x1_end) * 0.7; + ctx.quadraticCurveTo(x1_end + (waistRight - x1_end) * 0.55, line1Y + (midY - line1Y) * 0.6, q3X, q3Y); + + ctx.quadraticCurveTo(x1_end + (waistRight - x1_end) * 0.85, line1Y + (midY - line1Y) * 0.9, waistRight, midY); + + // Right side curve from waist to bottom + const b1Y = midY + (line2Y - midY) * 0.25; + const b1X = waistRight + (x2_end - waistRight) * 0.3; + ctx.quadraticCurveTo(waistRight + (x2_end - waistRight) * 0.15, midY + (line2Y - midY) * 0.1, b1X, b1Y); + + const b2Y = midY + (line2Y - midY) * 0.5; + const b2X = waistRight + (x2_end - waistRight) * 0.6; + ctx.quadraticCurveTo(waistRight + (x2_end - waistRight) * 0.45, midY + (line2Y - midY) * 0.35, b2X, b2Y); + + const b3Y = midY + (line2Y - midY) * 0.75; + const b3X = waistRight + (x2_end - waistRight) * 0.8; + ctx.quadraticCurveTo(waistRight + (x2_end - waistRight) * 0.7, midY + (line2Y - midY) * 0.6, b3X, b3Y); + + ctx.quadraticCurveTo(waistRight + (x2_end - waistRight) * 0.9, midY + (line2Y - midY) * 0.9, x2_end, line2Y); + + // Bottom edge (straight line) + ctx.lineTo(x2_start, line2Y); + + // Left side curve from bottom to waist + const bl1Y = line2Y - (line2Y - midY) * 0.25; + const bl1X = x2_start - (x2_start - waistLeft) * 0.3; + ctx.quadraticCurveTo(x2_start - (x2_start - waistLeft) * 0.15, line2Y - (line2Y - midY) * 0.1, bl1X, bl1Y); + + const bl2Y = line2Y - (line2Y - midY) * 0.5; + const bl2X = x2_start - (x2_start - waistLeft) * 0.6; + ctx.quadraticCurveTo(x2_start - (x2_start - waistLeft) * 0.45, line2Y - (line2Y - midY) * 0.35, bl2X, bl2Y); + + const bl3Y = line2Y - (line2Y - midY) * 0.75; + const bl3X = x2_start - (x2_start - waistLeft) * 0.8; + ctx.quadraticCurveTo(x2_start - (x2_start - waistLeft) * 0.7, line2Y - (line2Y - midY) * 0.6, bl3X, bl3Y); + + ctx.quadraticCurveTo(x2_start - (x2_start - waistLeft) * 0.9, line2Y - (line2Y - midY) * 0.9, waistLeft, midY); + + // Left side curve from waist to top + const tl1Y = midY - (midY - line1Y) * 0.25; + const tl1X = waistLeft - (waistLeft - x1_start) * 0.3; + ctx.quadraticCurveTo(waistLeft - (waistLeft - x1_start) * 0.15, midY - (midY - line1Y) * 0.1, tl1X, tl1Y); + + const tl2Y = midY - (midY - line1Y) * 0.5; + const tl2X = waistLeft - (waistLeft - x1_start) * 0.6; + ctx.quadraticCurveTo(waistLeft - (waistLeft - x1_start) * 0.45, midY - (midY - line1Y) * 0.35, tl2X, tl2Y); + + const tl3Y = midY - (midY - line1Y) * 0.75; + const tl3X = waistLeft - (waistLeft - x1_start) * 0.8; + ctx.quadraticCurveTo(waistLeft - (waistLeft - x1_start) * 0.7, midY - (midY - line1Y) * 0.6, tl3X, tl3Y); + + ctx.quadraticCurveTo(waistLeft - (waistLeft - x1_start) * 0.9, midY - (midY - line1Y) * 0.9, x1_start, line1Y); + } else { + // Redraw normal polygon border + ctx.moveTo(x1_start, line1Y); + ctx.lineTo(x1_end, line1Y); + ctx.lineTo(x2_end, line2Y); + ctx.lineTo(x2_start, line2Y); + } + ctx.closePath(); + ctx.stroke(); + ctx.setLineDash([]); // Reset dash + + // Add small indicator for reversed segments + if (isReversed) { + ctx.fillStyle = '#FF0000'; + ctx.font = '10px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('↻', (x1_start + x1_end) / 2, (line1Y + line2Y) / 2); + } + }); + + // Draw the parallel lines + ctx.strokeStyle = '#333'; + ctx.lineWidth = 3; + + // Top line + ctx.beginPath(); + ctx.moveTo(startX, line1Y); + ctx.lineTo(startX + totalWidth, line1Y); + ctx.stroke(); + + // Bottom line + ctx.beginPath(); + ctx.moveTo(startX, line2Y); + ctx.lineTo(startX + totalWidth, line2Y); + ctx.stroke(); + + // Draw regularly spaced tick marks and labels + ctx.lineWidth = 1; + ctx.font = '10px Arial'; + ctx.fillStyle = '#666'; + + // Helper function to format numbers with commas + function formatWithCommas(num) { + return num.toLocaleString(); + } + + // Line 1 regular ticks (integers only) + const line1Range_span = line1Range.end - line1Range.start; + const line1TickCount = Math.min(8, Math.floor(line1Range_span)); + const line1Step = Math.max(1, Math.floor(line1Range_span / line1TickCount)); + + for (let coord = Math.ceil(line1Range.start); coord <= line1Range.end; coord += line1Step) { + if (coord > line1Range.end) break; + const x = coordToPixel(coord, line1Range, startX, totalWidth); + + // Draw tick mark + ctx.beginPath(); + ctx.moveTo(x, line1Y - 6); + ctx.lineTo(x, line1Y + 6); + ctx.stroke(); + + // Draw coordinate label above the line with commas + ctx.textAlign = 'center'; + ctx.fillText(formatWithCommas(coord), x, line1Y - 12); + } + + // Line 2 regular ticks (integers only) + const line2Range_span = line2Range.end - line2Range.start; + const line2TickCount = Math.min(8, Math.floor(line2Range_span)); + const line2Step = Math.max(1, Math.floor(line2Range_span / line2TickCount)); + + for (let coord = Math.ceil(line2Range.start); coord <= line2Range.end; coord += line2Step) { + if (coord > line2Range.end) break; + const x = coordToPixel(coord, line2Range, startX, totalWidth); + + // Draw tick mark + ctx.beginPath(); + ctx.moveTo(x, line2Y - 6); + ctx.lineTo(x, line2Y + 6); + ctx.stroke(); + + // Draw coordinate label below the line with commas + ctx.textAlign = 'center'; + ctx.fillText(formatWithCommas(coord), x, line2Y + 18); + } + + // Add custom line labels + ctx.fillStyle = '#333'; + ctx.font = '14px Arial'; + ctx.textAlign = 'left'; + + // Format line labels as "chr1 start-end (extent bp)" + const line1Start = line1Range.start.toLocaleString(); + const line1End = line1Range.end.toLocaleString(); + const line1Extent = (line1Range.end - line1Range.start).toLocaleString(); + + const line2Start = line2Range.start.toLocaleString(); + const line2End = line2Range.end.toLocaleString(); + const line2Extent = (line2Range.end - line2Range.start).toLocaleString(); + + ctx.fillText(`chr1: ${line1Start}-${line1End} (${line1Extent} bp)`, startX - 90, line1Y - 60); + ctx.fillText(`chr1: ${line2Start}-${line2End} (${line2Extent} bp)`, startX - 90, line2Y + 85); + } + + // Return only the public interface + return { + drawCoordinateBasedSegments: drawCoordinateBasedSegments + }; + })(); + + // Example functions that use the public interface (outside the namespace) + function drawExample1() { + const line1Data = { + range: { start: 0, end: 24 }, // 24-hour format + label: "Monday Schedule", + segments: [ + { name: 'Meeting', start: 2, end: 4 }, // 2 hours each + { name: 'Work', start: 6, end: 8 }, // 2 hours each + { name: 'Break', start: 12, end: 14 }, // 2 hours each + { name: 'Review', start: 18, end: 20 } // 2 hours each + ] + }; + + const line2Data = { + range: { start: 0, end: 24 }, // Same scale + label: "Tuesday Schedule", + segments: [ + { name: 'Meeting', start: 10, end: 8 }, // 2 hours, REVERSED! + { name: 'Work', start: 14, end: 16 }, // 2 hours each + { name: 'Break', start: 6, end: 4 }, // 2 hours, REVERSED! + { name: 'Review', start: 20, end: 22 } // 2 hours each + ] + }; + + ParallelSegments.drawCoordinateBasedSegments(line1Data, line2Data); + } + + function drawExample2() { + const line1Data = { + range: { start: 0, end: 100 }, // Percentage scale (0-100%) + label: "Forward Process", + segments: [ + { name: 'Init', start: 10, end: 25 }, // 15% each + { name: 'Process', start: 30, end: 45 }, // 15% each + { name: 'Validate', start: 55, end: 70 }, // 15% each + { name: 'Complete', start: 80, end: 95 } // 15% each + ] + }; + + const line2Data = { + range: { start: 0, end: 200 }, // Different scale + label: "Reverse Process", + segments: [ + { name: 'Init', start: 70, end: 40 }, // 30 units, REVERSED! + { name: 'Process', start: 130, end: 100 }, // 30 units, REVERSED! + { name: 'Validate', start: 40, end: 10 }, // 30 units, REVERSED! + { name: 'Complete', start: 190, end: 160 } // 30 units, REVERSED! + ] + }; + + ParallelSegments.drawCoordinateBasedSegments(line2Data, line1Data); // Swapped to show different pattern + } + + function drawCustom() { + const line1Data = { + range: { start: 100000, end: 150000 }, // 5-digit range extent (50,000) + label: "Production Timeline", + segments: [ + { name: 'InitPhase', start: 105000, end: 115000 }, // 10k units each, normal + { name: 'BuildPhase', start: 120000, end: 130000 }, // 10k units each, normal + { name: 'TestPhase', start: 135000, end: 145000 }, // 10k units each, normal + { name: 'LaunchPhase', start: 146000, end: 148000 } // 2k units, normal + ] + }; + + const line2Data = { + range: { start: 200000, end: 275000 }, // 5-digit range extent (75,000) + label: "Resource Allocation", + segments: [ + { name: 'InitPhase', start: 220000, end: 205000 }, // 15k units, REVERSED! + { name: 'BuildPhase', start: 235000, end: 250000 }, // 15k units, normal + { name: 'TestPhase', start: 270000, end: 255000 }, // 15k units, REVERSED! + { name: 'LaunchPhase', start: 272000, end: 274000 } // 2k units, normal + ] + }; + + ParallelSegments.drawCoordinateBasedSegments(line1Data, line2Data); + } +