JAL-3054 table tooltip follow mouse in a sensible manner
[jalview.git] / src / jalview / schemes / FeatureColour.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.schemes;
22
23 import jalview.api.FeatureColourI;
24 import jalview.datamodel.SequenceFeature;
25 import jalview.datamodel.features.FeatureMatcher;
26 import jalview.util.ColorUtils;
27 import jalview.util.Format;
28 import jalview.util.MessageManager;
29
30 import java.awt.Color;
31 import java.util.StringTokenizer;
32
33 /**
34  * A class that represents a colour scheme for a feature type. Options supported
35  * are currently
36  * <ul>
37  * <li>a simple colour e.g. Red</li>
38  * <li>colour by label - a colour is generated from the feature description</li>
39  * <li>graduated colour by feature score</li>
40  * <ul>
41  * <li>minimum and maximum score range must be provided</li>
42  * <li>minimum and maximum value colours should be specified</li>
43  * <li>a colour for 'no value' may optionally be provided</li>
44  * <li>colours for intermediate scores are interpolated RGB values</li>
45  * <li>there is an optional threshold above/below which to colour values</li>
46  * <li>the range may be the full value range, or may be limited by the threshold
47  * value</li>
48  * </ul>
49  * <li>colour by (text) value of a named attribute</li> <li>graduated colour by
50  * (numeric) value of a named attribute</li> </ul>
51  */
52 public class FeatureColour implements FeatureColourI
53 {
54   private static final String I18N_LABEL = MessageManager
55           .getString("label.label");
56
57   private static final String I18N_SCORE = MessageManager
58           .getString("label.score");
59
60   private static final String ABSOLUTE = "abso";
61
62   private static final String ABOVE = "above";
63
64   private static final String BELOW = "below";
65
66   /*
67    * constants used to read or write a Jalview Features file
68    */
69   private static final String LABEL = "label";
70
71   private static final String SCORE = "score";
72
73   private static final String ATTRIBUTE = "attribute";
74
75   private static final String NO_VALUE_MIN = "noValueMin";
76
77   private static final String NO_VALUE_MAX = "noValueMax";
78
79   private static final String NO_VALUE_NONE = "noValueNone";
80
81   static final Color DEFAULT_NO_COLOUR = null;
82
83   private static final String BAR = "|";
84
85   final private Color colour;
86
87   final private Color minColour;
88
89   final private Color maxColour;
90
91   /*
92    * colour to use for colour by attribute when the 
93    * attribute value is absent
94    */
95   final private Color noColour;
96
97   /*
98    * if true, then colour has a gradient based on a numerical 
99    * range (either feature score, or an attribute value)
100    */
101   private boolean graduatedColour;
102
103   /*
104    * if true, colour values are generated from a text string,
105    * either feature description, or an attribute value
106    */
107   private boolean colourByLabel;
108
109   /*
110    * if not null, the value of [attribute, [sub-attribute] ...]
111    *  is used for colourByLabel or graduatedColour
112    */
113   private String[] attributeName;
114
115   private float threshold;
116
117   private float base;
118
119   private float range;
120
121   private boolean belowThreshold;
122
123   private boolean aboveThreshold;
124
125   private boolean isHighToLow;
126
127   private boolean autoScaled;
128
129   final private float minRed;
130
131   final private float minGreen;
132
133   final private float minBlue;
134
135   final private float deltaRed;
136
137   final private float deltaGreen;
138
139   final private float deltaBlue;
140
141   /**
142    * Parses a Jalview features file format colour descriptor
143    * <p>
144    * <code>
145    * [label|score|[attribute|attributeName]|][mincolour|maxcolour|
146    * [absolute|]minvalue|maxvalue|[noValueOption|]thresholdtype|thresholdvalue]</code>
147    * <p>
148    * 'Score' is optional (default) for a graduated colour. An attribute with
149    * sub-attribute should be written as (for example) CSQ:Consequence.
150    * noValueOption is one of <code>noValueMin, noValueMax, noValueNone</code>
151    * with default noValueMin.
152    * <p>
153    * Examples:
154    * <ul>
155    * <li>red</li>
156    * <li>a28bbb</li>
157    * <li>25,125,213</li>
158    * <li>label</li>
159    * <li>attribute|CSQ:PolyPhen</li>
160    * <li>label|||0.0|0.0|above|12.5</li>
161    * <li>label|||0.0|0.0|below|12.5</li>
162    * <li>red|green|12.0|26.0|none</li>
163    * <li>score|red|green|12.0|26.0|none</li>
164    * <li>attribute|AF|red|green|12.0|26.0|none</li>
165    * <li>attribute|AF|red|green|noValueNone|12.0|26.0|none</li>
166    * <li>a28bbb|3eb555|12.0|26.0|above|12.5</li>
167    * <li>a28bbb|3eb555|abso|12.0|26.0|below|12.5</li>
168    * </ul>
169    * 
170    * @param descriptor
171    * @return
172    * @throws IllegalArgumentException
173    *           if not parseable
174    */
175   public static FeatureColourI parseJalviewFeatureColour(String descriptor)
176   {
177     StringTokenizer gcol = new StringTokenizer(descriptor, BAR, true);
178     float min = Float.MIN_VALUE;
179     float max = Float.MAX_VALUE;
180     boolean byLabel = false;
181     boolean byAttribute = false;
182     String attName = null;
183     String mincol = null;
184     String maxcol = null;
185
186     /*
187      * first token should be 'label', or 'score', or an
188      * attribute name, or simple colour, or minimum colour
189      */
190     String nextToken = gcol.nextToken();
191     if (nextToken == BAR)
192     {
193       throw new IllegalArgumentException(
194               "Expected either 'label' or a colour specification in the line: "
195                       + descriptor);
196     }
197     if (nextToken.toLowerCase().startsWith(LABEL))
198     {
199       byLabel = true;
200       // get the token after the next delimiter:
201       mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
202       mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
203     }
204     else if (nextToken.toLowerCase().startsWith(SCORE))
205     {
206       mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
207       mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
208     }
209     else if (nextToken.toLowerCase().startsWith(ATTRIBUTE))
210     {
211       byAttribute = true;
212       attName = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
213       attName = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
214       mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
215       mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
216     }
217     else
218     {
219       mincol = nextToken;
220     }
221
222     /*
223      * if only one token, it can validly be label, attributeName,
224      * or a plain colour value
225      */
226     if (!gcol.hasMoreTokens())
227     {
228       if (byLabel || byAttribute)
229       {
230         FeatureColourI fc = new FeatureColour();
231         fc.setColourByLabel(true);
232         if (byAttribute)
233         {
234           fc.setAttributeName(
235                   FeatureMatcher.fromAttributeDisplayName(attName));
236         }
237         return fc;
238       }
239
240       Color colour = ColorUtils.parseColourString(descriptor);
241       if (colour == null)
242       {
243         throw new IllegalArgumentException(
244                 "Invalid colour descriptor: " + descriptor);
245       }
246       return new FeatureColour(colour);
247     }
248
249     /*
250      * continue parsing for min/max/no colour (if graduated)
251      * and for threshold (colour by text or graduated)
252      */
253
254     /*
255      * autoScaled == true: colours range over actual score range
256      * autoScaled == false ('abso'): colours range over min/max range
257      */
258     boolean autoScaled = true;
259     String tok = null, minval, maxval;
260     String noValueColour = NO_VALUE_MIN;
261
262     if (mincol != null)
263     {
264       // at least four more tokens
265       if (mincol.equals(BAR))
266       {
267         mincol = null;
268       }
269       else
270       {
271         gcol.nextToken(); // skip next '|'
272       }
273       maxcol = gcol.nextToken();
274       if (maxcol.equals(BAR))
275       {
276         maxcol = null;
277       }
278       else
279       {
280         gcol.nextToken(); // skip next '|'
281       }
282       tok = gcol.nextToken();
283
284       /*
285        * check for specifier for colour for no attribute value
286        * (new in 2.11, defaults to minColour if not specified)
287        */
288       if (tok.equalsIgnoreCase(NO_VALUE_MIN))
289       {
290         tok = gcol.nextToken();
291         tok = gcol.nextToken();
292       }
293       else if (tok.equalsIgnoreCase(NO_VALUE_MAX))
294       {
295         noValueColour = NO_VALUE_MAX;
296         tok = gcol.nextToken();
297         tok = gcol.nextToken();
298       }
299       else if (tok.equalsIgnoreCase(NO_VALUE_NONE))
300       {
301         noValueColour = NO_VALUE_NONE;
302         tok = gcol.nextToken();
303         tok = gcol.nextToken();
304       }
305
306       gcol.nextToken(); // skip next '|'
307       if (tok.toLowerCase().startsWith(ABSOLUTE))
308       {
309         minval = gcol.nextToken();
310         gcol.nextToken(); // skip next '|'
311         autoScaled = false;
312       }
313       else
314       {
315         minval = tok;
316       }
317       maxval = gcol.nextToken();
318       if (gcol.hasMoreTokens())
319       {
320         gcol.nextToken(); // skip next '|'
321       }
322       try
323       {
324         if (minval.length() > 0)
325         {
326           min = new Float(minval).floatValue();
327         }
328       } catch (Exception e)
329       {
330         throw new IllegalArgumentException(
331                 "Couldn't parse the minimum value for graduated colour ("
332                         + descriptor + ")");
333       }
334       try
335       {
336         if (maxval.length() > 0)
337         {
338           max = new Float(maxval).floatValue();
339         }
340       } catch (Exception e)
341       {
342         throw new IllegalArgumentException(
343                 "Couldn't parse the maximum value for graduated colour ("
344                         + descriptor + ")");
345       }
346     }
347     else
348     {
349       /*
350        * dummy min/max colours for colour by text
351        * (label or attribute value)
352        */
353       mincol = "white";
354       maxcol = "black";
355       byLabel = true;
356     }
357
358     /*
359      * construct the FeatureColour!
360      */
361     FeatureColour featureColour;
362     try
363     {
364       Color minColour = ColorUtils.parseColourString(mincol);
365       Color maxColour = ColorUtils.parseColourString(maxcol);
366       Color noColour = noValueColour.equals(NO_VALUE_MAX) ? maxColour
367               : (noValueColour.equals(NO_VALUE_NONE) ? null : minColour);
368       featureColour = new FeatureColour(minColour, maxColour, noColour, min,
369               max);
370       featureColour.setColourByLabel(minColour == null);
371       featureColour.setAutoScaled(autoScaled);
372       if (byAttribute)
373       {
374         featureColour.setAttributeName(
375                 FeatureMatcher.fromAttributeDisplayName(attName));
376       }
377       // add in any additional parameters
378       String ttype = null, tval = null;
379       if (gcol.hasMoreTokens())
380       {
381         // threshold type and possibly a threshold value
382         ttype = gcol.nextToken();
383         if (ttype.toLowerCase().startsWith(BELOW))
384         {
385           featureColour.setBelowThreshold(true);
386         }
387         else if (ttype.toLowerCase().startsWith(ABOVE))
388         {
389           featureColour.setAboveThreshold(true);
390         }
391         else
392         {
393           if (!ttype.toLowerCase().startsWith("no"))
394           {
395             System.err.println(
396                     "Ignoring unrecognised threshold type : " + ttype);
397           }
398         }
399       }
400       if (featureColour.hasThreshold())
401       {
402         try
403         {
404           gcol.nextToken();
405           tval = gcol.nextToken();
406           featureColour.setThreshold(new Float(tval).floatValue());
407         } catch (Exception e)
408         {
409           System.err.println("Couldn't parse threshold value as a float: ("
410                   + tval + ")");
411         }
412       }
413       if (gcol.hasMoreTokens())
414       {
415         System.err.println(
416                 "Ignoring additional tokens in parameters in graduated colour specification\n");
417         while (gcol.hasMoreTokens())
418         {
419           System.err.println(BAR + gcol.nextToken());
420         }
421         System.err.println("\n");
422       }
423       return featureColour;
424     } catch (Exception e)
425     {
426       throw new IllegalArgumentException(e.getMessage());
427     }
428   }
429
430   /**
431    * Default constructor
432    */
433   public FeatureColour()
434   {
435     this((Color) null);
436   }
437
438   /**
439    * Constructor given a simple colour
440    * 
441    * @param c
442    */
443   public FeatureColour(Color c)
444   {
445     minColour = Color.WHITE;
446     maxColour = Color.BLACK;
447     noColour = DEFAULT_NO_COLOUR;
448     minRed = 0f;
449     minGreen = 0f;
450     minBlue = 0f;
451     deltaRed = 0f;
452     deltaGreen = 0f;
453     deltaBlue = 0f;
454     colour = c;
455   }
456
457   /**
458    * Constructor given a colour range and a score range, defaulting 'no value
459    * colour' to be the same as minimum colour
460    * 
461    * @param low
462    * @param high
463    * @param min
464    * @param max
465    */
466   public FeatureColour(Color low, Color high, float min, float max)
467   {
468     this(low, high, low, min, max);
469   }
470
471   /**
472    * Copy constructor
473    * 
474    * @param fc
475    */
476   public FeatureColour(FeatureColour fc)
477   {
478     graduatedColour = fc.graduatedColour;
479     colour = fc.colour;
480     minColour = fc.minColour;
481     maxColour = fc.maxColour;
482     noColour = fc.noColour;
483     minRed = fc.minRed;
484     minGreen = fc.minGreen;
485     minBlue = fc.minBlue;
486     deltaRed = fc.deltaRed;
487     deltaGreen = fc.deltaGreen;
488     deltaBlue = fc.deltaBlue;
489     base = fc.base;
490     range = fc.range;
491     isHighToLow = fc.isHighToLow;
492     attributeName = fc.attributeName;
493     setAboveThreshold(fc.isAboveThreshold());
494     setBelowThreshold(fc.isBelowThreshold());
495     setThreshold(fc.getThreshold());
496     setAutoScaled(fc.isAutoScaled());
497     setColourByLabel(fc.isColourByLabel());
498   }
499
500   /**
501    * Copy constructor with new min/max ranges
502    * 
503    * @param fc
504    * @param min
505    * @param max
506    */
507   public FeatureColour(FeatureColour fc, float min, float max)
508   {
509     this(fc);
510     updateBounds(min, max);
511   }
512
513   /**
514    * Constructor for a graduated colour
515    * 
516    * @param low
517    * @param high
518    * @param noValueColour
519    * @param min
520    * @param max
521    */
522   public FeatureColour(Color low, Color high, Color noValueColour,
523           float min, float max)
524   {
525     if (low == null)
526     {
527       low = Color.white;
528     }
529     if (high == null)
530     {
531       high = Color.black;
532     }
533     graduatedColour = true;
534     colour = null;
535     minColour = low;
536     maxColour = high;
537     noColour = noValueColour;
538     threshold = Float.NaN;
539     isHighToLow = min >= max;
540     minRed = low.getRed() / 255f;
541     minGreen = low.getGreen() / 255f;
542     minBlue = low.getBlue() / 255f;
543     deltaRed = (high.getRed() / 255f) - minRed;
544     deltaGreen = (high.getGreen() / 255f) - minGreen;
545     deltaBlue = (high.getBlue() / 255f) - minBlue;
546     if (isHighToLow)
547     {
548       base = max;
549       range = min - max;
550     }
551     else
552     {
553       base = min;
554       range = max - min;
555     }
556   }
557
558   @Override
559   public boolean isGraduatedColour()
560   {
561     return graduatedColour;
562   }
563
564   /**
565    * Sets the 'graduated colour' flag. If true, also sets 'colour by label' to
566    * false.
567    */
568   void setGraduatedColour(boolean b)
569   {
570     graduatedColour = b;
571     if (b)
572     {
573       setColourByLabel(false);
574     }
575   }
576
577   @Override
578   public Color getColour()
579   {
580     return colour;
581   }
582
583   @Override
584   public Color getMinColour()
585   {
586     return minColour;
587   }
588
589   @Override
590   public Color getMaxColour()
591   {
592     return maxColour;
593   }
594
595   @Override
596   public Color getNoColour()
597   {
598     return noColour;
599   }
600
601   @Override
602   public boolean isColourByLabel()
603   {
604     return colourByLabel;
605   }
606
607   /**
608    * Sets the 'colour by label' flag. If true, also sets 'graduated colour' to
609    * false.
610    */
611   @Override
612   public void setColourByLabel(boolean b)
613   {
614     colourByLabel = b;
615     if (b)
616     {
617       setGraduatedColour(false);
618     }
619   }
620
621   @Override
622   public boolean isBelowThreshold()
623   {
624     return belowThreshold;
625   }
626
627   @Override
628   public void setBelowThreshold(boolean b)
629   {
630     belowThreshold = b;
631     if (b)
632     {
633       setAboveThreshold(false);
634     }
635   }
636
637   @Override
638   public boolean isAboveThreshold()
639   {
640     return aboveThreshold;
641   }
642
643   @Override
644   public void setAboveThreshold(boolean b)
645   {
646     aboveThreshold = b;
647     if (b)
648     {
649       setBelowThreshold(false);
650     }
651   }
652
653   @Override
654   public float getThreshold()
655   {
656     return threshold;
657   }
658
659   @Override
660   public void setThreshold(float f)
661   {
662     threshold = f;
663   }
664
665   @Override
666   public boolean isAutoScaled()
667   {
668     return autoScaled;
669   }
670
671   @Override
672   public void setAutoScaled(boolean b)
673   {
674     this.autoScaled = b;
675   }
676
677   /**
678    * {@inheritDoc}
679    */
680   @Override
681   public void updateBounds(float min, float max)
682   {
683     if (max < min)
684     {
685       base = max;
686       range = min - max;
687       isHighToLow = true;
688     }
689     else
690     {
691       base = min;
692       range = max - min;
693       isHighToLow = false;
694     }
695   }
696
697   /**
698    * Returns the colour for the given instance of the feature. This may be a
699    * simple colour, a colour generated from the feature description (if
700    * isColourByLabel()), or a colour derived from the feature score (if
701    * isGraduatedColour()).
702    * 
703    * @param feature
704    * @return
705    */
706   @Override
707   public Color getColor(SequenceFeature feature)
708   {
709     if (isColourByLabel())
710     {
711       String label = attributeName == null ? feature.getDescription()
712               : feature.getValueAsString(attributeName);
713       return label == null ? noColour : ColorUtils
714               .createColourFromName(label);
715     }
716
717     if (!isGraduatedColour())
718     {
719       return getColour();
720     }
721
722     /*
723      * graduated colour case, optionally with threshold
724      * may be based on feature score on an attribute value
725      * Float.NaN, or no value, is assigned the 'no value' colour
726      */
727     float scr = feature.getScore();
728     if (attributeName != null)
729     {
730       try
731       {
732         String attVal = feature.getValueAsString(attributeName);
733         scr = Float.valueOf(attVal);
734       } catch (Throwable e)
735       {
736         scr = Float.NaN;
737       }
738     }
739     if (Float.isNaN(scr))
740     {
741       return noColour;
742     }
743
744     if (isAboveThreshold() && scr <= threshold)
745     {
746       return null;
747     }
748
749     if (isBelowThreshold() && scr >= threshold)
750     {
751       return null;
752     }
753     if (range == 0.0)
754     {
755       return getMaxColour();
756     }
757     float scl = (scr - base) / range;
758     if (isHighToLow)
759     {
760       scl = -scl;
761     }
762     if (scl < 0f)
763     {
764       scl = 0f;
765     }
766     if (scl > 1f)
767     {
768       scl = 1f;
769     }
770     return new Color(minRed + scl * deltaRed, minGreen + scl * deltaGreen,
771             minBlue + scl * deltaBlue);
772   }
773
774   /**
775    * Returns the maximum score of the graduated colour range
776    * 
777    * @return
778    */
779   @Override
780   public float getMax()
781   {
782     // regenerate the original values passed in to the constructor
783     return (isHighToLow) ? base : (base + range);
784   }
785
786   /**
787    * Returns the minimum score of the graduated colour range
788    * 
789    * @return
790    */
791   @Override
792   public float getMin()
793   {
794     // regenerate the original value passed in to the constructor
795     return (isHighToLow) ? (base + range) : base;
796   }
797
798   @Override
799   public boolean isSimpleColour()
800   {
801     return (!isColourByLabel() && !isGraduatedColour());
802   }
803
804   @Override
805   public boolean hasThreshold()
806   {
807     return isAboveThreshold() || isBelowThreshold();
808   }
809
810   @Override
811   public String toJalviewFormat(String featureType)
812   {
813     String colourString = null;
814     if (isSimpleColour())
815     {
816       colourString = Format.getHexString(getColour());
817     }
818     else
819     {
820       StringBuilder sb = new StringBuilder(32);
821       if (isColourByAttribute())
822       {
823         sb.append(ATTRIBUTE).append(BAR);
824         sb.append(
825                 FeatureMatcher.toAttributeDisplayName(getAttributeName()));
826       }
827       else if (isColourByLabel())
828       {
829         sb.append(LABEL);
830       }
831       else
832       {
833         sb.append(SCORE);
834       }
835       if (isGraduatedColour())
836       {
837         sb.append(BAR).append(Format.getHexString(getMinColour()))
838                 .append(BAR);
839         sb.append(Format.getHexString(getMaxColour())).append(BAR);
840         String noValue = minColour.equals(noColour) ? NO_VALUE_MIN
841                 : (maxColour.equals(noColour) ? NO_VALUE_MAX
842                         : NO_VALUE_NONE);
843         sb.append(noValue).append(BAR);
844         if (!isAutoScaled())
845         {
846           sb.append(ABSOLUTE).append(BAR);
847         }
848       }
849       else
850       {
851         /*
852          * colour by text with score threshold: empty fields for
853          * minColour and maxColour (not used)
854          */
855         if (hasThreshold())
856         {
857           sb.append(BAR).append(BAR).append(BAR);
858         }
859       }
860       if (hasThreshold() || isGraduatedColour())
861       {
862         sb.append(getMin()).append(BAR);
863         sb.append(getMax()).append(BAR);
864         if (isBelowThreshold())
865         {
866           sb.append(BELOW).append(BAR).append(getThreshold());
867         }
868         else if (isAboveThreshold())
869         {
870           sb.append(ABOVE).append(BAR).append(getThreshold());
871         }
872         else
873         {
874           sb.append("none");
875         }
876       }
877       colourString = sb.toString();
878     }
879     return String.format("%s\t%s", featureType, colourString);
880   }
881
882   @Override
883   public boolean isColourByAttribute()
884   {
885     return attributeName != null;
886   }
887
888   @Override
889   public String[] getAttributeName()
890   {
891     return attributeName;
892   }
893
894   @Override
895   public void setAttributeName(String... name)
896   {
897     attributeName = name;
898   }
899
900   @Override
901   public String getDescription()
902   {
903     if (isSimpleColour())
904     {
905       return "r=" + colour.getRed() + ",g=" + colour.getGreen() + ",b="
906               + colour.getBlue();
907     }
908     StringBuilder tt = new StringBuilder();
909     String by = null;
910
911     if (getAttributeName() != null)
912     {
913       by = FeatureMatcher.toAttributeDisplayName(getAttributeName());
914     }
915     else if (isColourByLabel())
916     {
917       by = I18N_LABEL;
918     }
919     else
920     {
921       by = I18N_SCORE;
922     }
923     tt.append(MessageManager.formatMessage("action.by_title_param", by));
924
925     /*
926      * add threshold if any
927      */
928     if (isAboveThreshold() || isBelowThreshold())
929     {
930       tt.append(" (");
931       if (isColourByLabel())
932       {
933         /*
934          * Jalview features file supports the combination of 
935          * colour by label or attribute text with score threshold
936          */
937         tt.append(I18N_SCORE).append(" ");
938       }
939       tt.append(isAboveThreshold() ? "> " : "< ");
940       tt.append(getThreshold()).append(")");
941     }
942
943     return tt.toString();
944   }
945
946 }