From 007f2c0fa4563baa95d43da08b5e4edc99ddc9a0 Mon Sep 17 00:00:00 2001 From: gmungoc Date: Mon, 11 Dec 2017 15:16:05 +0000 Subject: [PATCH] JAL-2843 add colour by attribute, and no value colour, to features file --- src/jalview/api/FeatureColourI.java | 3 +- src/jalview/schemes/FeatureColour.java | 215 ++++++++++++++++++++++----- test/jalview/schemes/FeatureColourTest.java | 149 +++++++++++++++++-- 3 files changed, 310 insertions(+), 57 deletions(-) diff --git a/src/jalview/api/FeatureColourI.java b/src/jalview/api/FeatureColourI.java index 0780271..4dbb1bb 100644 --- a/src/jalview/api/FeatureColourI.java +++ b/src/jalview/api/FeatureColourI.java @@ -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 */ diff --git a/src/jalview/schemes/FeatureColour.java b/src/jalview/schemes/FeatureColour.java index aa0b640..7d14662 100644 --- a/src/jalview/schemes/FeatureColour.java +++ b/src/jalview/schemes/FeatureColour.java @@ -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: + *

+ * + * [label|score|[attribute|attributeName]|][mincolour|maxcolour| + * [absolute|]minvalue|maxvalue|[noValueOption|]thresholdtype|thresholdvalue] + *

+ * '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 noValueMin, noValueMax, noValueNone + * with default noValueMin. + *

+ * Examples: *

@@ -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 { diff --git a/test/jalview/schemes/FeatureColourTest.java b/test/jalview/schemes/FeatureColourTest.java index 72c29d3..2eb718b 100644 --- a/test/jalview/schemes/FeatureColourTest.java +++ b/test/jalview/schemes/FeatureColourTest.java @@ -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()); } -- 1.7.10.2