@@ -63,14 +63,6 @@ public class BioPolymerCoverageMapViewModel : BaseViewModel
6363 new SolidColorBrush ( Colors . MediumAquamarine )
6464 } ) ;
6565
66- private static readonly SolidColorBrush [ ] ViridisColors = InitViridisPalette ( ) ;
67-
68- private static SolidColorBrush [ ] InitViridisPalette ( )
69- {
70- var hex = new [ ] { "440154" , "481467" , "482576" , "453781" , "3f4d8a" , "39568c" , "2e6f8e" , "238a8d" , "20a386" , "34b679" , "60c85d" , "93d443" , "c7df2b" , "eddf2b" , "f8e51d" , "fce61b" , "fbdf0e" , "f6d300" , "edc600" , "fde725" } ;
71- return hex . Select ( h => new SolidColorBrush ( ( Color ) ColorConverter . ConvertFromString ( "#" + h ) ) ) . ToArray ( ) ;
72- }
73-
7466 private static readonly Dictionary < string , SolidColorBrush > IdentifierToColor = [ ] ;
7567
7668 public ColorResultsBy [ ] AllColorByTypes { get ; } = Enum . GetValues < ColorResultsBy > ( ) ;
@@ -92,18 +84,47 @@ public ColorResultsBy ColorBy
9284 }
9385 }
9486
95- private SolidColorBrush GetColorBrush ( BioPolymerCoverageResultModel result )
87+ private BioPolymerCoverageColorMapper CreateColorMapper ( )
88+ {
89+ return BioPolymerCoverageColorMapperFactory . Create (
90+ _colorBy ,
91+ identifierBrushResolver : GetColorByIdentifier ) ;
92+ }
93+
94+ private BioPolymerCoverageColorScale ? CreateNumericScale (
95+ BioPolymerCoverageColorMapper mapper ,
96+ List < BioPolymerCoverageResultModel > filteredResults )
9697 {
97- switch ( _colorBy )
98+ var gradient = ColorGradientFactory . Create ( ColorGradientType . Viridis ) ;
99+
100+ if ( mapper is not NumericBioPolymerCoverageColorMapper numericMapper )
101+ return null ;
102+
103+ var values = filteredResults
104+ . Select ( numericMapper . GetNumericValue )
105+ . Where ( v => v . HasValue )
106+ . Select ( v => v . Value )
107+ . ToList ( ) ;
108+
109+ if ( values . Count == 0 && numericMapper is PrecursorIntensityColorMapper )
98110 {
99- case ColorResultsBy . CoverageType :
100- return MetaDrawSettings . BioPolymerCoverageColors [ result . CoverageType ] ;
101- case ColorResultsBy . FileOrigin :
102- // Use FileName as identifier since FullFilePath is not available
103- return GetColorByIdentifier ( result . Match . FileName ) ;
104- default :
105- return new SolidColorBrush ( Colors . Gray ) ;
111+ numericMapper = new ScoreColorMapper ( ) ;
112+ values = filteredResults
113+ . Select ( numericMapper . GetNumericValue )
114+ . Where ( v => v . HasValue )
115+ . Select ( v => v . Value )
116+ . ToList ( ) ;
106117 }
118+
119+ if ( values . Count == 0 )
120+ return new BioPolymerCoverageColorScale ( 0 , 0 , gradient ) ;
121+
122+ double minVal = values . Min ( ) ;
123+ double maxVal = values . Max ( ) ;
124+ double range = maxVal - minVal ;
125+ if ( range < double . Epsilon ) range = 1 ;
126+
127+ return new BioPolymerCoverageColorScale ( minVal , maxVal , gradient ) ;
107128 }
108129
109130 // Helper method for FileOrigin coloring
@@ -120,12 +141,6 @@ private SolidColorBrush GetColorByIdentifier(string identifier)
120141 return brush ;
121142 }
122143
123- private SolidColorBrush GetIntensityBrush ( double normalizedValue )
124- {
125- int bin = ( int ) Math . Clamp ( normalizedValue * 19 , 0 , 19 ) ;
126- return ViridisColors [ bin ] ;
127- }
128-
129144 #endregion
130145
131146 private DrawingImage _coverageDrawing ;
@@ -194,6 +209,9 @@ private void Redraw()
194209 var rowRectangles = new Dictionary < int , List < ( int startCol , int endCol ) > > ( ) ;
195210 var rowOccupancy = new Dictionary < int , int [ ] > ( ) ; // row -> per-column track usage
196211
212+ var mapper = CreateColorMapper ( ) ;
213+ var scale = CreateNumericScale ( mapper , filteredResults ) ;
214+
197215 var dv = new DrawingVisual ( ) ;
198216 using ( var dc = dv . RenderOpen ( ) )
199217 {
@@ -255,12 +273,12 @@ private void Redraw()
255273 var sepWidth = sep . Width ;
256274
257275 double legendY = metricsY + metricsFt . Height + legendSpacing ;
258- var legendItems = CreateLegendItems ( filteredResults , fontSize , dpi , ColorBy ) ;
276+ var legendItems = CreateLegendItems ( filteredResults , fontSize , dpi , _colorBy ) ;
259277
260278 var legendLines = WrapLegendItems ( legendItems , fontSize , usableWidth , sepWidth ) ;
261279
262280 double legendLineHeight = legendItems . Count == 0 ? 0 : Math . Max ( fontSize * 0.8 , legendItems . Max ( li => li . Text . Height ) ) ;
263-
281+
264282
265283 foreach ( var line in legendLines )
266284 {
@@ -287,42 +305,9 @@ private void Redraw()
287305 legendY += legendLineHeight + legendSpacing * 0.2 ;
288306 }
289307
290- bool isIntensityMode = _colorBy is ColorResultsBy . PrecursorIntensity or ColorResultsBy . Score ;
291-
292- double minVal = 0 , maxVal = 0 , range = 0 , gradientBlockH = 0 ;
293- if ( isIntensityMode )
294- {
295- var vals = filteredResults
296- . Select ( r => _colorBy == ColorResultsBy . PrecursorIntensity
297- ? r . Match . PrecursorIntensity ?? double . NaN
298- : r . Match . Score )
299- . Where ( v => ! double . IsNaN ( v ) ) . ToList ( ) ;
300- if ( vals . Count == 0 && _colorBy == ColorResultsBy . PrecursorIntensity )
301- vals = filteredResults . Select ( r => r . Match . Score ) . ToList ( ) ;
302- if ( vals . Count > 0 ) { minVal = vals . Min ( ) ; maxVal = vals . Max ( ) ; }
303- range = maxVal - minVal ;
304- if ( range < double . Epsilon ) range = 1 ;
305-
306- double barWidth = Math . Min ( 250 , usableWidth * 0.6 ) ;
307- double barHeight = fontSize * 0.8 ;
308- double barX = plotMargin + ( usableWidth - barWidth ) / 2 ;
309- double barY = legendY + legendSpacing ;
310- double bandW = barWidth / 20 ;
311- for ( int i = 0 ; i < 20 ; i ++ )
312- dc . DrawRectangle ( ViridisColors [ i ] , null , new Rect ( barX + i * bandW , barY , bandW + 0.5 , barHeight ) ) ;
313-
314- string metricName = _colorBy == ColorResultsBy . PrecursorIntensity ? "Precursor Intensity" : "Score" ;
315- var metricTxt = new FormattedText ( metricName , System . Globalization . CultureInfo . CurrentCulture , FlowDirection . LeftToRight , new Typeface ( "Segoe UI" ) , fontSize * 0.7 , Brushes . DimGray , dpi ) ;
316- dc . DrawText ( metricTxt , new Point ( barX + ( barWidth - metricTxt . Width ) / 2 , barY - metricTxt . Height - 2 ) ) ;
317-
318- var minTxt = new FormattedText ( $ "{ minVal : G3} ", System . Globalization . CultureInfo . CurrentCulture , FlowDirection . LeftToRight , new Typeface ( "Segoe UI" ) , fontSize * 0.7 , Brushes . Black , dpi ) ;
319- var maxTxt = new FormattedText ( $ "{ maxVal : G3} ", System . Globalization . CultureInfo . CurrentCulture , FlowDirection . LeftToRight , new Typeface ( "Segoe UI" ) , fontSize * 0.7 , Brushes . Black , dpi ) ;
320- dc . DrawText ( minTxt , new Point ( barX , barY + barHeight + 2 ) ) ;
321- dc . DrawText ( maxTxt , new Point ( barX + barWidth - maxTxt . Width , barY + barHeight + 2 ) ) ;
322-
323- gradientBlockH = barHeight + Math . Max ( minTxt . Height , maxTxt . Height ) + legendSpacing * 2 ;
324- legendY += gradientBlockH ;
325- }
308+ double gradientBlockH = 0 ;
309+ if ( mapper . IsNumeric && scale is not null )
310+ gradientBlockH = DrawNumericLegend ( dc , legendY , plotMargin , usableWidth , legendSpacing , fontSize , dpi , mapper , scale , out legendY ) ;
326311
327312 // --- Offset plot below header block (metrics + legend) ---
328313 double headerBlockHeight = bioPolymerNameText . Height + metricsFt . Height + legendSpacing
@@ -407,11 +392,7 @@ private void Redraw()
407392 roundRight = true ;
408393
409394 // Use a slightly opaque brush for fill
410- var baseBrush = isIntensityMode
411- ? GetIntensityBrush ( ( ( _colorBy == ColorResultsBy . PrecursorIntensity
412- ? res . Match . PrecursorIntensity . GetValueOrDefault ( ( minVal + maxVal ) / 2 )
413- : res . Match . Score ) - minVal ) / range )
414- : GetColorBrush ( res ) ;
395+ var baseBrush = mapper . GetBrush ( res , scale ) ;
415396 var color = ( baseBrush as SolidColorBrush ) ? . Color ?? Colors . Gray ;
416397 var brush = new SolidColorBrush ( color ) { Opacity = 0.75 } ;
417398 brush . Freeze ( ) ;
@@ -564,6 +545,43 @@ private void Redraw()
564545 CoverageDrawing = new DrawingImage ( dv . Drawing ) ;
565546 }
566547
548+ private double DrawNumericLegend (
549+ DrawingContext dc ,
550+ double legendY ,
551+ double plotMargin ,
552+ double usableWidth ,
553+ double legendSpacing ,
554+ double fontSize ,
555+ double dpi ,
556+ BioPolymerCoverageColorMapper mapper ,
557+ BioPolymerCoverageColorScale scale ,
558+ out double newLegendY )
559+ {
560+ double barWidth = Math . Min ( 250 , usableWidth * 0.6 ) ;
561+ double barHeight = fontSize * 0.8 ;
562+ double barX = plotMargin + ( usableWidth - barWidth ) / 2 ;
563+ double barY = legendY + legendSpacing ;
564+ var brushes = scale . Gradient . GetBrushes ( ) ;
565+ double bandW = barWidth / brushes . Count ;
566+ for ( int i = 0 ; i < brushes . Count ; i ++ )
567+ dc . DrawRectangle ( brushes [ i ] , null , new Rect ( barX + i * bandW , barY , bandW + 0.5 , barHeight ) ) ;
568+
569+ string metricName = mapper . ColorBy == ColorResultsBy . PrecursorIntensity
570+ ? "Precursor Intensity"
571+ : mapper . ColorBy . ToString ( ) ;
572+ var metricTxt = new FormattedText ( metricName , System . Globalization . CultureInfo . CurrentCulture , FlowDirection . LeftToRight , new Typeface ( "Segoe UI" ) , fontSize * 0.7 , Brushes . DimGray , dpi ) ;
573+ dc . DrawText ( metricTxt , new Point ( barX + ( barWidth - metricTxt . Width ) / 2 , barY - metricTxt . Height - 2 ) ) ;
574+
575+ var minTxt = new FormattedText ( $ "{ scale . MinValue : G3} ", System . Globalization . CultureInfo . CurrentCulture , FlowDirection . LeftToRight , new Typeface ( "Segoe UI" ) , fontSize * 0.7 , Brushes . Black , dpi ) ;
576+ var maxTxt = new FormattedText ( $ "{ scale . MaxValue : G3} ", System . Globalization . CultureInfo . CurrentCulture , FlowDirection . LeftToRight , new Typeface ( "Segoe UI" ) , fontSize * 0.7 , Brushes . Black , dpi ) ;
577+ dc . DrawText ( minTxt , new Point ( barX , barY + barHeight + 2 ) ) ;
578+ dc . DrawText ( maxTxt , new Point ( barX + barWidth - maxTxt . Width , barY + barHeight + 2 ) ) ;
579+
580+ double blockH = barHeight + Math . Max ( minTxt . Height , maxTxt . Height ) + legendSpacing * 2 ;
581+ newLegendY = legendY + blockH ;
582+ return blockH ;
583+ }
584+
567585 // Preferred legend order
568586 private static readonly BioPolymerCoverageType [ ] LegendOrder =
569587 {
@@ -664,4 +682,4 @@ private void Redraw()
664682
665683 return lines ;
666684 }
667- }
685+ }
0 commit comments