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;
28 import jalview.util.MessageManager;
30 import java.awt.Color;
31 import java.util.StringTokenizer;
34 * A class that represents a colour scheme for a feature type. Options supported
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>
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
49 * <li>colour by (text) value of a named attribute</li> <li>graduated colour by
50 * (numeric) value of a named attribute</li> </ul>
52 public class FeatureColour implements FeatureColourI
54 private static final String I18N_LABEL = MessageManager
55 .getString("label.label");
57 private static final String I18N_SCORE = MessageManager
58 .getString("label.score");
60 private static final String ABSOLUTE = "abso";
62 private static final String ABOVE = "above";
64 private static final String BELOW = "below";
67 * constants used to read or write a Jalview Features file
69 private static final String LABEL = "label";
71 private static final String SCORE = "score";
73 private static final String ATTRIBUTE = "attribute";
75 private static final String NO_VALUE_MIN = "noValueMin";
77 private static final String NO_VALUE_MAX = "noValueMax";
79 private static final String NO_VALUE_NONE = "noValueNone";
81 static final Color DEFAULT_NO_COLOUR = null;
83 private static final String BAR = "|";
85 final private Color colour;
87 final private Color minColour;
89 final private Color maxColour;
92 * colour to use for colour by attribute when the
93 * attribute value is absent
95 final private Color noColour;
98 * if true, then colour has a gradient based on a numerical
99 * range (either feature score, or an attribute value)
101 private boolean graduatedColour;
104 * if true, colour values are generated from a text string,
105 * either feature description, or an attribute value
107 private boolean colourByLabel;
110 * if not null, the value of [attribute, [sub-attribute] ...]
111 * is used for colourByLabel or graduatedColour
113 private String[] attributeName;
115 private float threshold;
121 private boolean belowThreshold;
123 private boolean aboveThreshold;
125 private boolean isHighToLow;
127 private boolean autoScaled;
129 final private float minRed;
131 final private float minGreen;
133 final private float minBlue;
135 final private float deltaRed;
137 final private float deltaGreen;
139 final private float deltaBlue;
142 * Parses a Jalview features file format colour descriptor
145 * [label|score|[attribute|attributeName]|][mincolour|maxcolour|
146 * [absolute|]minvalue|maxvalue|[noValueOption|]thresholdtype|thresholdvalue]</code>
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.
157 * <li>25,125,213</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>
172 * @throws IllegalArgumentException
175 public static FeatureColourI parseJalviewFeatureColour(String descriptor)
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;
187 * first token should be 'label', or 'score', or an
188 * attribute name, or simple colour, or minimum colour
190 String nextToken = gcol.nextToken();
191 if (nextToken == BAR)
193 throw new IllegalArgumentException(
194 "Expected either 'label' or a colour specification in the line: "
197 if (nextToken.toLowerCase().startsWith(LABEL))
200 // get the token after the next delimiter:
201 mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
202 mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
204 else if (nextToken.toLowerCase().startsWith(SCORE))
206 mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
207 mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
209 else if (nextToken.toLowerCase().startsWith(ATTRIBUTE))
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);
223 * if only one token, it can validly be label, attributeName,
224 * or a plain colour value
226 if (!gcol.hasMoreTokens())
228 if (byLabel || byAttribute)
230 FeatureColourI fc = new FeatureColour();
231 fc.setColourByLabel(true);
235 FeatureMatcher.fromAttributeDisplayName(attName));
240 Color colour = ColorUtils.parseColourString(descriptor);
243 throw new IllegalArgumentException(
244 "Invalid colour descriptor: " + descriptor);
246 return new FeatureColour(colour);
250 * continue parsing for min/max/no colour (if graduated)
251 * and for threshold (colour by text or graduated)
255 * autoScaled == true: colours range over actual score range
256 * autoScaled == false ('abso'): colours range over min/max range
258 boolean autoScaled = true;
259 String tok = null, minval, maxval;
260 String noValueColour = NO_VALUE_MIN;
264 // at least four more tokens
265 if (mincol.equals(BAR))
271 gcol.nextToken(); // skip next '|'
273 maxcol = gcol.nextToken();
274 if (maxcol.equals(BAR))
280 gcol.nextToken(); // skip next '|'
282 tok = gcol.nextToken();
285 * check for specifier for colour for no attribute value
286 * (new in 2.11, defaults to minColour if not specified)
288 if (tok.equalsIgnoreCase(NO_VALUE_MIN))
290 tok = gcol.nextToken();
291 tok = gcol.nextToken();
293 else if (tok.equalsIgnoreCase(NO_VALUE_MAX))
295 noValueColour = NO_VALUE_MAX;
296 tok = gcol.nextToken();
297 tok = gcol.nextToken();
299 else if (tok.equalsIgnoreCase(NO_VALUE_NONE))
301 noValueColour = NO_VALUE_NONE;
302 tok = gcol.nextToken();
303 tok = gcol.nextToken();
306 gcol.nextToken(); // skip next '|'
307 if (tok.toLowerCase().startsWith(ABSOLUTE))
309 minval = gcol.nextToken();
310 gcol.nextToken(); // skip next '|'
317 maxval = gcol.nextToken();
318 if (gcol.hasMoreTokens())
320 gcol.nextToken(); // skip next '|'
324 if (minval.length() > 0)
326 min = new Float(minval).floatValue();
328 } catch (Exception e)
330 throw new IllegalArgumentException(
331 "Couldn't parse the minimum value for graduated colour ("
336 if (maxval.length() > 0)
338 max = new Float(maxval).floatValue();
340 } catch (Exception e)
342 throw new IllegalArgumentException(
343 "Couldn't parse the maximum value for graduated colour ("
350 * dummy min/max colours for colour by text
351 * (label or attribute value)
359 * construct the FeatureColour!
361 FeatureColour featureColour;
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,
370 featureColour.setColourByLabel(minColour == null);
371 featureColour.setAutoScaled(autoScaled);
374 featureColour.setAttributeName(
375 FeatureMatcher.fromAttributeDisplayName(attName));
377 // add in any additional parameters
378 String ttype = null, tval = null;
379 if (gcol.hasMoreTokens())
381 // threshold type and possibly a threshold value
382 ttype = gcol.nextToken();
383 if (ttype.toLowerCase().startsWith(BELOW))
385 featureColour.setBelowThreshold(true);
387 else if (ttype.toLowerCase().startsWith(ABOVE))
389 featureColour.setAboveThreshold(true);
393 if (!ttype.toLowerCase().startsWith("no"))
396 "Ignoring unrecognised threshold type : " + ttype);
400 if (featureColour.hasThreshold())
405 tval = gcol.nextToken();
406 featureColour.setThreshold(new Float(tval).floatValue());
407 } catch (Exception e)
409 System.err.println("Couldn't parse threshold value as a float: ("
413 if (gcol.hasMoreTokens())
416 "Ignoring additional tokens in parameters in graduated colour specification\n");
417 while (gcol.hasMoreTokens())
419 System.err.println(BAR + gcol.nextToken());
421 System.err.println("\n");
423 return featureColour;
424 } catch (Exception e)
426 throw new IllegalArgumentException(e.getMessage());
431 * Default constructor
433 public FeatureColour()
439 * Constructor given a simple colour
443 public FeatureColour(Color c)
445 minColour = Color.WHITE;
446 maxColour = Color.BLACK;
447 noColour = DEFAULT_NO_COLOUR;
458 * Constructor given a colour range and a score range, defaulting 'no value
459 * colour' to be the same as minimum colour
466 public FeatureColour(Color low, Color high, float min, float max)
468 this(low, high, low, min, max);
476 public FeatureColour(FeatureColour fc)
478 graduatedColour = fc.graduatedColour;
480 minColour = fc.minColour;
481 maxColour = fc.maxColour;
482 noColour = fc.noColour;
484 minGreen = fc.minGreen;
485 minBlue = fc.minBlue;
486 deltaRed = fc.deltaRed;
487 deltaGreen = fc.deltaGreen;
488 deltaBlue = fc.deltaBlue;
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());
501 * Copy constructor with new min/max ranges
507 public FeatureColour(FeatureColour fc, float min, float max)
510 updateBounds(min, max);
514 * Constructor for a graduated colour
518 * @param noValueColour
522 public FeatureColour(Color low, Color high, Color noValueColour,
523 float min, float max)
533 graduatedColour = true;
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;
559 public boolean isGraduatedColour()
561 return graduatedColour;
565 * Sets the 'graduated colour' flag. If true, also sets 'colour by label' to
568 void setGraduatedColour(boolean b)
573 setColourByLabel(false);
578 public Color getColour()
584 public Color getMinColour()
590 public Color getMaxColour()
596 public Color getNoColour()
602 public boolean isColourByLabel()
604 return colourByLabel;
608 * Sets the 'colour by label' flag. If true, also sets 'graduated colour' to
612 public void setColourByLabel(boolean b)
617 setGraduatedColour(false);
622 public boolean isBelowThreshold()
624 return belowThreshold;
628 public void setBelowThreshold(boolean b)
633 setAboveThreshold(false);
638 public boolean isAboveThreshold()
640 return aboveThreshold;
644 public void setAboveThreshold(boolean b)
649 setBelowThreshold(false);
654 public float getThreshold()
660 public void setThreshold(float f)
666 public boolean isAutoScaled()
672 public void setAutoScaled(boolean b)
681 public void updateBounds(float min, float max)
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()).
707 public Color getColor(SequenceFeature feature)
709 if (isColourByLabel())
711 String label = attributeName == null ? feature.getDescription()
712 : feature.getValueAsString(attributeName);
713 return label == null ? noColour : ColorUtils
714 .createColourFromName(label);
717 if (!isGraduatedColour())
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
727 float scr = feature.getScore();
728 if (attributeName != null)
732 String attVal = feature.getValueAsString(attributeName);
733 scr = Float.valueOf(attVal);
734 } catch (Throwable e)
739 if (Float.isNaN(scr))
744 if (isAboveThreshold() && scr <= threshold)
749 if (isBelowThreshold() && scr >= threshold)
755 return getMaxColour();
757 float scl = (scr - base) / range;
770 return new Color(minRed + scl * deltaRed, minGreen + scl * deltaGreen,
771 minBlue + scl * deltaBlue);
775 * Returns the maximum score of the graduated colour range
780 public float getMax()
782 // regenerate the original values passed in to the constructor
783 return (isHighToLow) ? base : (base + range);
787 * Returns the minimum score of the graduated colour range
792 public float getMin()
794 // regenerate the original value passed in to the constructor
795 return (isHighToLow) ? (base + range) : base;
799 public boolean isSimpleColour()
801 return (!isColourByLabel() && !isGraduatedColour());
805 public boolean hasThreshold()
807 return isAboveThreshold() || isBelowThreshold();
811 public String toJalviewFormat(String featureType)
813 String colourString = null;
814 if (isSimpleColour())
816 colourString = Format.getHexString(getColour());
820 StringBuilder sb = new StringBuilder(32);
821 if (isColourByAttribute())
823 sb.append(ATTRIBUTE).append(BAR);
825 FeatureMatcher.toAttributeDisplayName(getAttributeName()));
827 else if (isColourByLabel())
835 if (isGraduatedColour())
837 sb.append(BAR).append(Format.getHexString(getMinColour()))
839 sb.append(Format.getHexString(getMaxColour())).append(BAR);
840 String noValue = minColour.equals(noColour) ? NO_VALUE_MIN
841 : (maxColour.equals(noColour) ? NO_VALUE_MAX
843 sb.append(noValue).append(BAR);
846 sb.append(ABSOLUTE).append(BAR);
852 * colour by text with score threshold: empty fields for
853 * minColour and maxColour (not used)
857 sb.append(BAR).append(BAR).append(BAR);
860 if (hasThreshold() || isGraduatedColour())
862 sb.append(getMin()).append(BAR);
863 sb.append(getMax()).append(BAR);
864 if (isBelowThreshold())
866 sb.append(BELOW).append(BAR).append(getThreshold());
868 else if (isAboveThreshold())
870 sb.append(ABOVE).append(BAR).append(getThreshold());
877 colourString = sb.toString();
879 return String.format("%s\t%s", featureType, colourString);
883 public boolean isColourByAttribute()
885 return attributeName != null;
889 public String[] getAttributeName()
891 return attributeName;
895 public void setAttributeName(String... name)
897 attributeName = name;
901 public String getDescription()
903 if (isSimpleColour())
905 return "r=" + colour.getRed() + ",g=" + colour.getGreen() + ",b="
908 StringBuilder tt = new StringBuilder();
911 if (getAttributeName() != null)
913 by = FeatureMatcher.toAttributeDisplayName(getAttributeName());
915 else if (isColourByLabel())
923 tt.append(MessageManager.formatMessage("action.by_title_param", by));
926 * add threshold if any
928 if (isAboveThreshold() || isBelowThreshold())
931 if (isColourByLabel())
934 * Jalview features file supports the combination of
935 * colour by label or attribute text with score threshold
937 tt.append(I18N_SCORE).append(" ");
939 tt.append(isAboveThreshold() ? "> " : "< ");
940 tt.append(getThreshold()).append(")");
943 return tt.toString();