JAL-2843 add colour by attribute, and no value colour, to features file
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Mon, 11 Dec 2017 15:16:05 +0000 (15:16 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Mon, 11 Dec 2017 15:16:05 +0000 (15:16 +0000)
src/jalview/api/FeatureColourI.java
src/jalview/schemes/FeatureColour.java
test/jalview/schemes/FeatureColourTest.java

index 0780271..4dbb1bb 100644 (file)
@@ -72,7 +72,8 @@ public interface FeatureColourI
   boolean isSimpleColour();
 
   /**
-   * Answers true if the feature is coloured by label (description)
+   * Answers true if the feature is coloured by label (description) or by text
+   * value of an attribute
    * 
    * @return
    */
index aa0b640..7d14662 100644 (file)
@@ -22,6 +22,7 @@ package jalview.schemes;
 
 import jalview.api.FeatureColourI;
 import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.features.FeatureMatcher;
 import jalview.util.ColorUtils;
 import jalview.util.Format;
 
@@ -49,6 +50,27 @@ import java.util.StringTokenizer;
  */
 public class FeatureColour implements FeatureColourI
 {
+  private static final String ABSOLUTE = "abso";
+
+  private static final String ABOVE = "above";
+
+  private static final String BELOW = "below";
+
+  /*
+   * constants used to read or write a Jalview Features file
+   */
+  private static final String LABEL = "label";
+
+  private static final String SCORE = "score";
+
+  private static final String ATTRIBUTE = "attribute";
+
+  private static final String NO_VALUE_MIN = "noValueMin";
+
+  private static final String NO_VALUE_MAX = "noValueMax";
+
+  private static final String NO_VALUE_NONE = "noValueNone";
+
   static final Color DEFAULT_NO_COLOUR = null;
 
   private static final String BAR = "|";
@@ -111,16 +133,29 @@ public class FeatureColour implements FeatureColourI
 
   /**
    * Parses a Jalview features file format colour descriptor
-   * [label|][mincolour|maxcolour
-   * |[absolute|]minvalue|maxvalue|thresholdtype|thresholdvalue] Examples:
+   * <p>
+   * <code>
+   * [label|score|[attribute|attributeName]|][mincolour|maxcolour|
+   * [absolute|]minvalue|maxvalue|[noValueOption|]thresholdtype|thresholdvalue]</code>
+   * <p>
+   * 'Score' is optional (default) for a graduated colour. An attribute with
+   * sub-attribute should be written as (for example) CSQ:Consequence.
+   * noValueOption is one of <code>noValueMin, noValueMax, noValueNone</code>
+   * with default noValueMin.
+   * <p>
+   * Examples:
    * <ul>
    * <li>red</li>
    * <li>a28bbb</li>
    * <li>25,125,213</li>
    * <li>label</li>
+   * <li>attribute|CSQ:PolyPhen</li>
    * <li>label|||0.0|0.0|above|12.5</li>
    * <li>label|||0.0|0.0|below|12.5</li>
    * <li>red|green|12.0|26.0|none</li>
+   * <li>score|red|green|12.0|26.0|none</li>
+   * <li>attribute|AF|red|green|12.0|26.0|none</li>
+   * <li>attribute|AF|red|green|noValueNone|12.0|26.0|none</li>
    * <li>a28bbb|3eb555|12.0|26.0|above|12.5</li>
    * <li>a28bbb|3eb555|abso|12.0|26.0|below|12.5</li>
    * </ul>
@@ -130,34 +165,71 @@ public class FeatureColour implements FeatureColourI
    * @throws IllegalArgumentException
    *           if not parseable
    */
-  public static FeatureColour parseJalviewFeatureColour(String descriptor)
+  public static FeatureColourI parseJalviewFeatureColour(String descriptor)
   {
-    StringTokenizer gcol = new StringTokenizer(descriptor, "|", true);
+    StringTokenizer gcol = new StringTokenizer(descriptor, BAR, true);
     float min = Float.MIN_VALUE;
     float max = Float.MAX_VALUE;
-    boolean labelColour = false;
+    boolean byLabel = false;
+    boolean byAttribute = false;
+    String attName = null;
+    String mincol = null;
+    String maxcol = null;
 
-    String mincol = gcol.nextToken();
-    if (mincol == "|")
+    /*
+     * first token should be 'label', or 'score', or an
+     * attribute name, or simple colour, or minimum colour
+     */
+    String nextToken = gcol.nextToken();
+    if (nextToken == BAR)
     {
       throw new IllegalArgumentException(
               "Expected either 'label' or a colour specification in the line: "
                       + descriptor);
     }
-    String maxcol = null;
-    if (mincol.toLowerCase().indexOf("label") == 0)
+    if (nextToken.toLowerCase().startsWith(LABEL))
     {
-      labelColour = true;
+      byLabel = true;
+      // get the token after the next delimiter:
       mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
-      // skip '|'
       mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
     }
+    else if (nextToken.toLowerCase().startsWith(SCORE))
+    {
+      mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
+      mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
+    }
+    else if (nextToken.toLowerCase().startsWith(ATTRIBUTE))
+    {
+      byAttribute = true;
+      attName = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
+      attName = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
+      mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
+      mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
+    }
+    else
+    {
+      mincol = nextToken;
+    }
 
-    if (!labelColour && !gcol.hasMoreTokens())
+    /*
+     * if only one token, it can validly be label, attributeName,
+     * or a plain colour value
+     */
+    if (!gcol.hasMoreTokens())
     {
-      /*
-       * only a simple colour specification - parse it
-       */
+      if (byLabel || byAttribute)
+      {
+        FeatureColourI fc = new FeatureColour();
+        fc.setColourByLabel(true);
+        if (byAttribute)
+        {
+          fc.setAttributeName(
+                  FeatureMatcher.fromAttributeDisplayName(attName));
+        }
+        return fc;
+      }
+
       Color colour = ColorUtils.parseColourString(descriptor);
       if (colour == null)
       {
@@ -168,34 +240,64 @@ public class FeatureColour implements FeatureColourI
     }
 
     /*
+     * continue parsing for min/max/no colour (if graduated)
+     * and for threshold (colour by text or graduated)
+     */
+
+    /*
      * autoScaled == true: colours range over actual score range
      * autoScaled == false ('abso'): colours range over min/max range
      */
     boolean autoScaled = true;
     String tok = null, minval, maxval;
+    String noValueColour = NO_VALUE_MIN;
+
     if (mincol != null)
     {
       // at least four more tokens
-      if (mincol.equals("|"))
+      if (mincol.equals(BAR))
       {
-        mincol = "";
+        mincol = null;
       }
       else
       {
         gcol.nextToken(); // skip next '|'
       }
       maxcol = gcol.nextToken();
-      if (maxcol.equals("|"))
+      if (maxcol.equals(BAR))
       {
-        maxcol = "";
+        maxcol = null;
       }
       else
       {
         gcol.nextToken(); // skip next '|'
       }
       tok = gcol.nextToken();
+
+      /*
+       * check for specifier for colour for no attribute value
+       * (new in 2.11, defaults to minColour if not specified)
+       */
+      if (tok.equalsIgnoreCase(NO_VALUE_MIN))
+      {
+        tok = gcol.nextToken();
+        tok = gcol.nextToken();
+      }
+      else if (tok.equalsIgnoreCase(NO_VALUE_MAX))
+      {
+        noValueColour = NO_VALUE_MAX;
+        tok = gcol.nextToken();
+        tok = gcol.nextToken();
+      }
+      else if (tok.equalsIgnoreCase(NO_VALUE_NONE))
+      {
+        noValueColour = NO_VALUE_NONE;
+        tok = gcol.nextToken();
+        tok = gcol.nextToken();
+      }
+
       gcol.nextToken(); // skip next '|'
-      if (tok.toLowerCase().startsWith("abso"))
+      if (tok.toLowerCase().startsWith(ABSOLUTE))
       {
         minval = gcol.nextToken();
         gcol.nextToken(); // skip next '|'
@@ -237,34 +339,45 @@ public class FeatureColour implements FeatureColourI
     }
     else
     {
-      // add in some dummy min/max colours for the label-only
-      // colourscheme.
-      mincol = "FFFFFF";
-      maxcol = "000000";
+      /*
+       * dummy min/max colours for colour by text
+       * (label or attribute value)
+       */
+      mincol = "white";
+      maxcol = "black";
+      byLabel = true;
     }
 
     /*
-     * construct the FeatureColour
+     * construct the FeatureColour!
      */
     FeatureColour featureColour;
     try
     {
       Color minColour = ColorUtils.parseColourString(mincol);
       Color maxColour = ColorUtils.parseColourString(maxcol);
-      featureColour = new FeatureColour(minColour, maxColour, min, max);
-      featureColour.setColourByLabel(labelColour);
+      Color noColour = noValueColour.equals(NO_VALUE_MAX) ? maxColour
+              : (noValueColour.equals(NO_VALUE_NONE) ? null : minColour);
+      featureColour = new FeatureColour(minColour, maxColour, noColour, min,
+              max);
+      featureColour.setColourByLabel(minColour == null);
       featureColour.setAutoScaled(autoScaled);
+      if (byAttribute)
+      {
+        featureColour.setAttributeName(
+                FeatureMatcher.fromAttributeDisplayName(attName));
+      }
       // add in any additional parameters
       String ttype = null, tval = null;
       if (gcol.hasMoreTokens())
       {
         // threshold type and possibly a threshold value
         ttype = gcol.nextToken();
-        if (ttype.toLowerCase().startsWith("below"))
+        if (ttype.toLowerCase().startsWith(BELOW))
         {
           featureColour.setBelowThreshold(true);
         }
-        else if (ttype.toLowerCase().startsWith("above"))
+        else if (ttype.toLowerCase().startsWith(ABOVE))
         {
           featureColour.setAboveThreshold(true);
         }
@@ -296,7 +409,7 @@ public class FeatureColour implements FeatureColourI
                 "Ignoring additional tokens in parameters in graduated colour specification\n");
         while (gcol.hasMoreTokens())
         {
-          System.err.println("|" + gcol.nextToken());
+          System.err.println(BAR + gcol.nextToken());
         }
         System.err.println("\n");
       }
@@ -698,21 +811,43 @@ public class FeatureColour implements FeatureColourI
     else
     {
       StringBuilder sb = new StringBuilder(32);
-      if (isColourByLabel())
+      if (isColourByAttribute())
       {
-        sb.append("label");
-        if (hasThreshold())
-        {
-          sb.append(BAR).append(BAR).append(BAR);
-        }
+        sb.append(ATTRIBUTE).append(BAR);
+        sb.append(
+                FeatureMatcher.toAttributeDisplayName(getAttributeName()));
+      }
+      else if (isColourByLabel())
+      {
+        sb.append(LABEL);
+      }
+      else
+      {
+        sb.append(SCORE);
       }
       if (isGraduatedColour())
       {
-        sb.append(Format.getHexString(getMinColour())).append(BAR);
+        sb.append(BAR).append(Format.getHexString(getMinColour()))
+                .append(BAR);
         sb.append(Format.getHexString(getMaxColour())).append(BAR);
+        String noValue = minColour.equals(noColour) ? NO_VALUE_MIN
+                : (maxColour.equals(noColour) ? NO_VALUE_MAX
+                        : NO_VALUE_NONE);
+        sb.append(noValue).append(BAR);
         if (!isAutoScaled())
         {
-          sb.append("abso").append(BAR);
+          sb.append(ABSOLUTE).append(BAR);
+        }
+      }
+      else
+      {
+        /*
+         * colour by text with score threshold: empty fields for
+         * minColour and maxColour (not used)
+         */
+        if (hasThreshold())
+        {
+          sb.append(BAR).append(BAR).append(BAR);
         }
       }
       if (hasThreshold() || isGraduatedColour())
@@ -721,11 +856,11 @@ public class FeatureColour implements FeatureColourI
         sb.append(getMax()).append(BAR);
         if (isBelowThreshold())
         {
-          sb.append("below").append(BAR).append(getThreshold());
+          sb.append(BELOW).append(BAR).append(getThreshold());
         }
         else if (isAboveThreshold())
         {
-          sb.append("above").append(BAR).append(getThreshold());
+          sb.append(ABOVE).append(BAR).append(getThreshold());
         }
         else
         {
index 72c29d3..2eb718b 100644 (file)
@@ -27,6 +27,7 @@ import static org.testng.AssertJUnit.assertTrue;
 import static org.testng.AssertJUnit.fail;
 import static org.testng.internal.junit.ArrayAsserts.assertArrayEquals;
 
+import jalview.api.FeatureColourI;
 import jalview.datamodel.SequenceFeature;
 import jalview.gui.JvOptionPane;
 import jalview.util.ColorUtils;
@@ -325,20 +326,29 @@ public class FeatureColourTest
     assertEquals("domain\tlabel", fc.toJalviewFormat("domain"));
 
     /*
+     * colour by attribute text (no threshold)
+     */
+    fc = new FeatureColour();
+    fc.setColourByLabel(true);
+    fc.setAttributeName("CLIN_SIG");
+    assertEquals("domain\tattribute|CLIN_SIG", fc.toJalviewFormat("domain"));
+    
+    /*
      * colour by label (autoscaled) (an odd state you can reach by selecting
      * 'above threshold', then deselecting 'threshold is min/max' then 'colour
      * by label')
      */
+    fc.setAttributeName((String[]) null);
     fc.setAutoScaled(true);
     assertEquals("domain\tlabel", fc.toJalviewFormat("domain"));
 
     /*
-     * colour by label (above threshold) (min/max values are output though not
-     * used by this scheme)
+     * colour by label (above threshold) 
      */
     fc.setAutoScaled(false);
     fc.setThreshold(12.5f);
     fc.setAboveThreshold(true);
+    // min/max values are output though not used by this scheme
     assertEquals("domain\tlabel|||0.0|0.0|above|12.5",
             fc.toJalviewFormat("domain"));
 
@@ -350,38 +360,80 @@ public class FeatureColourTest
             fc.toJalviewFormat("domain"));
 
     /*
-     * graduated colour, no threshold
+     * colour by attributes text (below threshold)
+     */
+    fc.setBelowThreshold(true);
+    fc.setAttributeName("CSQ", "Consequence");
+    assertEquals("domain\tattribute|CSQ:Consequence|||0.0|0.0|below|12.5",
+            fc.toJalviewFormat("domain"));
+
+    /*
+     * graduated colour by score, no threshold
+     * - default constructor sets noValueColor = minColor
      */
     fc = new FeatureColour(Color.GREEN, Color.RED, 12f, 25f);
     String greenHex = Format.getHexString(Color.GREEN);
-    String expected = String.format("domain\t%s|%s|abso|12.0|25.0|none",
-            greenHex, redHex);
+    String expected = String.format(
+            "domain\tscore|%s|%s|noValueMin|abso|12.0|25.0|none", greenHex,
+            redHex);
     assertEquals(expected, fc.toJalviewFormat("domain"));
 
     /*
+     * graduated colour by score, no threshold, no value gets min colour
+     */
+    fc = new FeatureColour(Color.GREEN, Color.RED, Color.GREEN, 12f, 25f);
+    expected = String.format(
+            "domain\tscore|%s|%s|noValueMin|abso|12.0|25.0|none", greenHex,
+            redHex);
+    assertEquals(expected, fc.toJalviewFormat("domain"));
+
+    /*
+     * graduated colour by score, no threshold, no value gets max colour
+     */
+    fc = new FeatureColour(Color.GREEN, Color.RED, Color.RED, 12f, 25f);
+    expected = String.format(
+            "domain\tscore|%s|%s|noValueMax|abso|12.0|25.0|none", greenHex,
+            redHex);
+    assertEquals(expected, fc.toJalviewFormat("domain"));
+    
+    /*
      * colour ranges over the actual score ranges (not min/max)
      */
     fc.setAutoScaled(true);
-    expected = String.format("domain\t%s|%s|12.0|25.0|none", greenHex,
+    expected = String.format(
+            "domain\tscore|%s|%s|noValueMax|12.0|25.0|none", greenHex,
             redHex);
     assertEquals(expected, fc.toJalviewFormat("domain"));
 
     /*
-     * graduated colour below threshold
+     * graduated colour by score, below threshold
      */
     fc.setThreshold(12.5f);
     fc.setBelowThreshold(true);
-    expected = String.format("domain\t%s|%s|12.0|25.0|below|12.5",
+    expected = String.format(
+            "domain\tscore|%s|%s|noValueMax|12.0|25.0|below|12.5",
             greenHex, redHex);
     assertEquals(expected, fc.toJalviewFormat("domain"));
 
     /*
-     * graduated colour above threshold
+     * graduated colour by score, above threshold
      */
     fc.setThreshold(12.5f);
     fc.setAboveThreshold(true);
     fc.setAutoScaled(false);
-    expected = String.format("domain\t%s|%s|abso|12.0|25.0|above|12.5",
+    expected = String.format(
+            "domain\tscore|%s|%s|noValueMax|abso|12.0|25.0|above|12.5",
+            greenHex, redHex);
+    assertEquals(expected, fc.toJalviewFormat("domain"));
+
+    /*
+     * graduated colour by attribute, above threshold
+     */
+    fc.setAttributeName("CSQ", "AF");
+    fc.setAboveThreshold(true);
+    fc.setAutoScaled(false);
+    expected = String.format(
+            "domain\tattribute|CSQ:AF|%s|%s|noValueMax|abso|12.0|25.0|above|12.5",
             greenHex, redHex);
     assertEquals(expected, fc.toJalviewFormat("domain"));
   }
@@ -395,7 +447,7 @@ public class FeatureColourTest
     /*
      * simple colour by name
      */
-    FeatureColour fc = FeatureColour.parseJalviewFeatureColour("red");
+    FeatureColourI fc = FeatureColour.parseJalviewFeatureColour("red");
     assertTrue(fc.isSimpleColour());
     assertEquals(Color.RED, fc.getColour());
 
@@ -443,7 +495,28 @@ public class FeatureColourTest
     assertEquals(12.0f, fc.getThreshold());
 
     /*
-     * graduated colour (by name) (no threshold)
+     * colour by attribute text (no threshold)
+     */
+    fc = FeatureColour.parseJalviewFeatureColour("attribute|CLIN_SIG");
+    assertTrue(fc.isColourByAttribute());
+    assertTrue(fc.isColourByLabel());
+    assertFalse(fc.hasThreshold());
+    assertArrayEquals(new String[] { "CLIN_SIG" }, fc.getAttributeName());
+
+    /*
+     * colour by attributes text (with score threshold)
+     */
+    fc = FeatureColour.parseJalviewFeatureColour(
+            "attribute|CSQ:Consequence|||0.0|0.0|above|12.0");
+    assertTrue(fc.isColourByLabel());
+    assertTrue(fc.isColourByAttribute());
+    assertArrayEquals(new String[] { "CSQ", "Consequence" },
+            fc.getAttributeName());
+    assertTrue(fc.isAboveThreshold());
+    assertEquals(12.0f, fc.getThreshold());
+
+    /*
+     * graduated colour by score (with colour names) (no threshold)
      */
     fc = FeatureColour.parseJalviewFeatureColour("red|green|10.0|20.0");
     assertTrue(fc.isGraduatedColour());
@@ -455,7 +528,35 @@ public class FeatureColourTest
     assertTrue(fc.isAutoScaled());
 
     /*
-     * graduated colour (by hex code) (above threshold)
+     * graduated colour (explicitly by 'score') (no threshold)
+     */
+    fc = FeatureColour
+            .parseJalviewFeatureColour("Score|red|green|10.0|20.0");
+    assertTrue(fc.isGraduatedColour());
+    assertFalse(fc.hasThreshold());
+    assertEquals(Color.RED, fc.getMinColour());
+    assertEquals(Color.GREEN, fc.getMaxColour());
+    assertEquals(10f, fc.getMin());
+    assertEquals(20f, fc.getMax());
+    assertTrue(fc.isAutoScaled());
+
+    /*
+     * graduated colour by attribute (no threshold)
+     */
+    fc = FeatureColour
+            .parseJalviewFeatureColour("attribute|AF|red|green|10.0|20.0");
+    assertTrue(fc.isGraduatedColour());
+    assertTrue(fc.isColourByAttribute());
+    assertArrayEquals(new String[] { "AF" }, fc.getAttributeName());
+    assertFalse(fc.hasThreshold());
+    assertEquals(Color.RED, fc.getMinColour());
+    assertEquals(Color.GREEN, fc.getMaxColour());
+    assertEquals(10f, fc.getMin());
+    assertEquals(20f, fc.getMax());
+    assertTrue(fc.isAutoScaled());
+
+    /*
+     * graduated colour by score (colours by hex code) (above threshold)
      */
     String descriptor = String.format("%s|%s|10.0|20.0|above|15",
             Format.getHexString(Color.RED),
@@ -472,9 +573,26 @@ public class FeatureColourTest
     assertTrue(fc.isAutoScaled());
 
     /*
+     * graduated colour by attributes (below threshold)
+     */
+    fc = FeatureColour.parseJalviewFeatureColour(
+            "attribute|CSQ:AF|red|green|10.0|20.0|below|13");
+    assertTrue(fc.isGraduatedColour());
+    assertTrue(fc.isColourByAttribute());
+    assertArrayEquals(new String[] { "CSQ", "AF" }, fc.getAttributeName());
+    assertTrue(fc.hasThreshold());
+    assertTrue(fc.isBelowThreshold());
+    assertEquals(13f, fc.getThreshold());
+    assertEquals(Color.RED, fc.getMinColour());
+    assertEquals(Color.GREEN, fc.getMaxColour());
+    assertEquals(10f, fc.getMin());
+    assertEquals(20f, fc.getMax());
+    assertTrue(fc.isAutoScaled());
+
+    /*
      * graduated colour (by RGB triplet) (below threshold), absolute scale
      */
-    descriptor = String.format("255,0,0|0,255,0|abso|10.0|20.0|below|15");
+    descriptor = "255,0,0|0,255,0|abso|10.0|20.0|below|15";
     fc = FeatureColour.parseJalviewFeatureColour(descriptor);
     assertTrue(fc.isGraduatedColour());
     assertFalse(fc.isAutoScaled());
@@ -486,8 +604,7 @@ public class FeatureColourTest
     assertEquals(10f, fc.getMin());
     assertEquals(20f, fc.getMax());
 
-    descriptor = String
-            .format("blue|255,0,255|absolute|20.0|95.0|below|66.0");
+    descriptor = "blue|255,0,255|absolute|20.0|95.0|below|66.0";
     fc = FeatureColour.parseJalviewFeatureColour(descriptor);
     assertTrue(fc.isGraduatedColour());
   }