2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
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.
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.
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.
21 package jalview.schemes;
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;
29 import java.awt.Color;
30 import java.util.StringTokenizer;
33 * A class that represents a colour scheme for a feature type. Options supported
36 * <li>a simple colour e.g. Red</li>
37 * <li>colour by label - a colour is generated from the feature description</li>
38 * <li>graduated colour by feature score</li>
40 * <li>minimum and maximum score range must be provided</li>
41 * <li>minimum and maximum value colours should be specified</li>
42 * <li>a colour for 'no value' may optionally be provided</li>
43 * <li>colours for intermediate scores are interpolated RGB values</li>
44 * <li>there is an optional threshold above/below which to colour values</li>
45 * <li>the range may be the full value range, or may be limited by the threshold
48 * <li>colour by (text) value of a named attribute</li> <li>graduated colour by
49 * (numeric) value of a named attribute</li> </ul>
51 public class FeatureColour implements FeatureColourI
53 private static final String ABSOLUTE = "abso";
55 private static final String ABOVE = "above";
57 private static final String BELOW = "below";
60 * constants used to read or write a Jalview Features file
62 private static final String LABEL = "label";
64 private static final String SCORE = "score";
66 private static final String ATTRIBUTE = "attribute";
68 private static final String NO_VALUE_MIN = "noValueMin";
70 private static final String NO_VALUE_MAX = "noValueMax";
72 private static final String NO_VALUE_NONE = "noValueNone";
74 static final Color DEFAULT_NO_COLOUR = null;
76 private static final String BAR = "|";
78 final private Color colour;
80 final private Color minColour;
82 final private Color maxColour;
85 * colour to use for colour by attribute when the
86 * attribute value is absent
88 final private Color noColour;
91 * if true, then colour has a gradient based on a numerical
92 * range (either feature score, or an attribute value)
94 private boolean graduatedColour;
97 * if true, colour values are generated from a text string,
98 * either feature description, or an attribute value
100 private boolean colourByLabel;
103 * if not null, the value of [attribute, [sub-attribute] ...]
104 * is used for colourByLabel or graduatedColour
106 private String[] attributeName;
108 private float threshold;
114 private boolean belowThreshold;
116 private boolean aboveThreshold;
118 private boolean isHighToLow;
120 private boolean autoScaled;
122 final private float minRed;
124 final private float minGreen;
126 final private float minBlue;
128 final private float deltaRed;
130 final private float deltaGreen;
132 final private float deltaBlue;
135 * Parses a Jalview features file format colour descriptor
138 * [label|score|[attribute|attributeName]|][mincolour|maxcolour|
139 * [absolute|]minvalue|maxvalue|[noValueOption|]thresholdtype|thresholdvalue]</code>
141 * 'Score' is optional (default) for a graduated colour. An attribute with
142 * sub-attribute should be written as (for example) CSQ:Consequence.
143 * noValueOption is one of <code>noValueMin, noValueMax, noValueNone</code>
144 * with default noValueMin.
150 * <li>25,125,213</li>
152 * <li>attribute|CSQ:PolyPhen</li>
153 * <li>label|||0.0|0.0|above|12.5</li>
154 * <li>label|||0.0|0.0|below|12.5</li>
155 * <li>red|green|12.0|26.0|none</li>
156 * <li>score|red|green|12.0|26.0|none</li>
157 * <li>attribute|AF|red|green|12.0|26.0|none</li>
158 * <li>attribute|AF|red|green|noValueNone|12.0|26.0|none</li>
159 * <li>a28bbb|3eb555|12.0|26.0|above|12.5</li>
160 * <li>a28bbb|3eb555|abso|12.0|26.0|below|12.5</li>
165 * @throws IllegalArgumentException
168 public static FeatureColourI parseJalviewFeatureColour(String descriptor)
170 StringTokenizer gcol = new StringTokenizer(descriptor, BAR, true);
171 float min = Float.MIN_VALUE;
172 float max = Float.MAX_VALUE;
173 boolean byLabel = false;
174 boolean byAttribute = false;
175 String attName = null;
176 String mincol = null;
177 String maxcol = null;
180 * first token should be 'label', or 'score', or an
181 * attribute name, or simple colour, or minimum colour
183 String nextToken = gcol.nextToken();
184 if (nextToken == BAR)
186 throw new IllegalArgumentException(
187 "Expected either 'label' or a colour specification in the line: "
190 if (nextToken.toLowerCase().startsWith(LABEL))
193 // get the token after the next delimiter:
194 mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
195 mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
197 else if (nextToken.toLowerCase().startsWith(SCORE))
199 mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
200 mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
202 else if (nextToken.toLowerCase().startsWith(ATTRIBUTE))
205 attName = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
206 attName = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
207 mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
208 mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
216 * if only one token, it can validly be label, attributeName,
217 * or a plain colour value
219 if (!gcol.hasMoreTokens())
221 if (byLabel || byAttribute)
223 FeatureColourI fc = new FeatureColour();
224 fc.setColourByLabel(true);
228 FeatureMatcher.fromAttributeDisplayName(attName));
233 Color colour = ColorUtils.parseColourString(descriptor);
236 throw new IllegalArgumentException(
237 "Invalid colour descriptor: " + descriptor);
239 return new FeatureColour(colour);
243 * continue parsing for min/max/no colour (if graduated)
244 * and for threshold (colour by text or graduated)
248 * autoScaled == true: colours range over actual score range
249 * autoScaled == false ('abso'): colours range over min/max range
251 boolean autoScaled = true;
252 String tok = null, minval, maxval;
253 String noValueColour = NO_VALUE_MIN;
257 // at least four more tokens
258 if (mincol.equals(BAR))
264 gcol.nextToken(); // skip next '|'
266 maxcol = gcol.nextToken();
267 if (maxcol.equals(BAR))
273 gcol.nextToken(); // skip next '|'
275 tok = gcol.nextToken();
278 * check for specifier for colour for no attribute value
279 * (new in 2.11, defaults to minColour if not specified)
281 if (tok.equalsIgnoreCase(NO_VALUE_MIN))
283 tok = gcol.nextToken();
284 tok = gcol.nextToken();
286 else if (tok.equalsIgnoreCase(NO_VALUE_MAX))
288 noValueColour = NO_VALUE_MAX;
289 tok = gcol.nextToken();
290 tok = gcol.nextToken();
292 else if (tok.equalsIgnoreCase(NO_VALUE_NONE))
294 noValueColour = NO_VALUE_NONE;
295 tok = gcol.nextToken();
296 tok = gcol.nextToken();
299 gcol.nextToken(); // skip next '|'
300 if (tok.toLowerCase().startsWith(ABSOLUTE))
302 minval = gcol.nextToken();
303 gcol.nextToken(); // skip next '|'
310 maxval = gcol.nextToken();
311 if (gcol.hasMoreTokens())
313 gcol.nextToken(); // skip next '|'
317 if (minval.length() > 0)
319 min = new Float(minval).floatValue();
321 } catch (Exception e)
323 throw new IllegalArgumentException(
324 "Couldn't parse the minimum value for graduated colour ('"
329 if (maxval.length() > 0)
331 max = new Float(maxval).floatValue();
333 } catch (Exception e)
335 throw new IllegalArgumentException(
336 "Couldn't parse the maximum value for graduated colour ("
343 * dummy min/max colours for colour by text
344 * (label or attribute value)
352 * construct the FeatureColour!
354 FeatureColour featureColour;
357 Color minColour = ColorUtils.parseColourString(mincol);
358 Color maxColour = ColorUtils.parseColourString(maxcol);
359 Color noColour = noValueColour.equals(NO_VALUE_MAX) ? maxColour
360 : (noValueColour.equals(NO_VALUE_NONE) ? null : minColour);
361 featureColour = new FeatureColour(minColour, maxColour, noColour, min,
363 featureColour.setColourByLabel(minColour == null);
364 featureColour.setAutoScaled(autoScaled);
367 featureColour.setAttributeName(
368 FeatureMatcher.fromAttributeDisplayName(attName));
370 // add in any additional parameters
371 String ttype = null, tval = null;
372 if (gcol.hasMoreTokens())
374 // threshold type and possibly a threshold value
375 ttype = gcol.nextToken();
376 if (ttype.toLowerCase().startsWith(BELOW))
378 featureColour.setBelowThreshold(true);
380 else if (ttype.toLowerCase().startsWith(ABOVE))
382 featureColour.setAboveThreshold(true);
386 if (!ttype.toLowerCase().startsWith("no"))
389 "Ignoring unrecognised threshold type : " + ttype);
393 if (featureColour.hasThreshold())
398 tval = gcol.nextToken();
399 featureColour.setThreshold(new Float(tval).floatValue());
400 } catch (Exception e)
402 System.err.println("Couldn't parse threshold value as a float: ("
406 if (gcol.hasMoreTokens())
409 "Ignoring additional tokens in parameters in graduated colour specification\n");
410 while (gcol.hasMoreTokens())
412 System.err.println(BAR + gcol.nextToken());
414 System.err.println("\n");
416 return featureColour;
417 } catch (Exception e)
419 throw new IllegalArgumentException(e.getMessage());
424 * Default constructor
426 public FeatureColour()
432 * Constructor given a simple colour
436 public FeatureColour(Color c)
438 minColour = Color.WHITE;
439 maxColour = Color.BLACK;
440 noColour = DEFAULT_NO_COLOUR;
451 * Constructor given a colour range and a score range, defaulting 'no value
452 * colour' to be the same as minimum colour
459 public FeatureColour(Color low, Color high, float min, float max)
461 this(low, high, low, min, max);
469 public FeatureColour(FeatureColour fc)
471 graduatedColour = fc.graduatedColour;
473 minColour = fc.minColour;
474 maxColour = fc.maxColour;
475 noColour = fc.noColour;
477 minGreen = fc.minGreen;
478 minBlue = fc.minBlue;
479 deltaRed = fc.deltaRed;
480 deltaGreen = fc.deltaGreen;
481 deltaBlue = fc.deltaBlue;
484 isHighToLow = fc.isHighToLow;
485 attributeName = fc.attributeName;
486 setAboveThreshold(fc.isAboveThreshold());
487 setBelowThreshold(fc.isBelowThreshold());
488 setThreshold(fc.getThreshold());
489 setAutoScaled(fc.isAutoScaled());
490 setColourByLabel(fc.isColourByLabel());
494 * Copy constructor with new min/max ranges
500 public FeatureColour(FeatureColour fc, float min, float max)
503 updateBounds(min, max);
507 * Constructor for a graduated colour
511 * @param noValueColour
515 public FeatureColour(Color low, Color high, Color noValueColour,
516 float min, float max)
526 graduatedColour = true;
530 noColour = noValueColour;
531 threshold = Float.NaN;
532 isHighToLow = min >= max;
533 minRed = low.getRed() / 255f;
534 minGreen = low.getGreen() / 255f;
535 minBlue = low.getBlue() / 255f;
536 deltaRed = (high.getRed() / 255f) - minRed;
537 deltaGreen = (high.getGreen() / 255f) - minGreen;
538 deltaBlue = (high.getBlue() / 255f) - minBlue;
552 public boolean isGraduatedColour()
554 return graduatedColour;
558 * Sets the 'graduated colour' flag. If true, also sets 'colour by label' to
561 void setGraduatedColour(boolean b)
566 setColourByLabel(false);
571 public Color getColour()
577 public Color getMinColour()
583 public Color getMaxColour()
589 public Color getNoColour()
595 public boolean isColourByLabel()
597 return colourByLabel;
601 * Sets the 'colour by label' flag. If true, also sets 'graduated colour' to
605 public void setColourByLabel(boolean b)
610 setGraduatedColour(false);
615 public boolean isBelowThreshold()
617 return belowThreshold;
621 public void setBelowThreshold(boolean b)
626 setAboveThreshold(false);
631 public boolean isAboveThreshold()
633 return aboveThreshold;
637 public void setAboveThreshold(boolean b)
642 setBelowThreshold(false);
647 public float getThreshold()
653 public void setThreshold(float f)
659 public boolean isAutoScaled()
665 public void setAutoScaled(boolean b)
674 public void updateBounds(float min, float max)
691 * Returns the colour for the given instance of the feature. This may be a
692 * simple colour, a colour generated from the feature description (if
693 * isColourByLabel()), or a colour derived from the feature score (if
694 * isGraduatedColour()).
700 public Color getColor(SequenceFeature feature)
702 if (isColourByLabel())
704 String label = attributeName == null ? feature.getDescription()
705 : feature.getValueAsString(attributeName);
706 return label == null ? noColour : ColorUtils
707 .createColourFromName(label);
710 if (!isGraduatedColour())
716 * graduated colour case, optionally with threshold
717 * may be based on feature score on an attribute value
718 * Float.NaN, or no value, is assigned the 'no value' colour
720 float scr = feature.getScore();
721 if (attributeName != null)
725 String attVal = feature.getValueAsString(attributeName);
726 scr = Float.valueOf(attVal);
727 } catch (Throwable e)
732 if (Float.isNaN(scr))
737 if (isAboveThreshold() && scr <= threshold)
742 if (isBelowThreshold() && scr >= threshold)
748 return getMaxColour();
750 float scl = (scr - base) / range;
763 return new Color(minRed + scl * deltaRed, minGreen + scl * deltaGreen,
764 minBlue + scl * deltaBlue);
768 * Returns the maximum score of the graduated colour range
773 public float getMax()
775 // regenerate the original values passed in to the constructor
776 return (isHighToLow) ? base : (base + range);
780 * Returns the minimum score of the graduated colour range
785 public float getMin()
787 // regenerate the original value passed in to the constructor
788 return (isHighToLow) ? (base + range) : base;
792 public boolean isSimpleColour()
794 return (!isColourByLabel() && !isGraduatedColour());
798 public boolean hasThreshold()
800 return isAboveThreshold() || isBelowThreshold();
804 public String toJalviewFormat(String featureType)
806 String colourString = null;
807 if (isSimpleColour())
809 colourString = Format.getHexString(getColour());
813 StringBuilder sb = new StringBuilder(32);
814 if (isColourByAttribute())
816 sb.append(ATTRIBUTE).append(BAR);
818 FeatureMatcher.toAttributeDisplayName(getAttributeName()));
820 else if (isColourByLabel())
828 if (isGraduatedColour())
830 sb.append(BAR).append(Format.getHexString(getMinColour()))
832 sb.append(Format.getHexString(getMaxColour())).append(BAR);
833 String noValue = minColour.equals(noColour) ? NO_VALUE_MIN
834 : (maxColour.equals(noColour) ? NO_VALUE_MAX
836 sb.append(noValue).append(BAR);
839 sb.append(ABSOLUTE).append(BAR);
845 * colour by text with score threshold: empty fields for
846 * minColour and maxColour (not used)
850 sb.append(BAR).append(BAR).append(BAR);
853 if (hasThreshold() || isGraduatedColour())
855 sb.append(getMin()).append(BAR);
856 sb.append(getMax()).append(BAR);
857 if (isBelowThreshold())
859 sb.append(BELOW).append(BAR).append(getThreshold());
861 else if (isAboveThreshold())
863 sb.append(ABOVE).append(BAR).append(getThreshold());
870 colourString = sb.toString();
872 return String.format("%s\t%s", featureType, colourString);
876 public boolean isColourByAttribute()
878 return attributeName != null;
882 public String[] getAttributeName()
884 return attributeName;
888 public void setAttributeName(String... name)
890 attributeName = name;