From 9bde0814fff97f50e3ac6165a9fb83787c37d39a Mon Sep 17 00:00:00 2001 From: gmungoc Date: Tue, 7 Nov 2017 11:45:49 +0000 Subject: [PATCH] JAL-2069 add attributeName and noValueColour to FeatureColour --- src/jalview/api/FeatureColourI.java | 35 ++++- src/jalview/schemes/FeatureColour.java | 166 ++++++++++++++++++------ test/jalview/schemes/FeatureColourTest.java | 184 ++++++++++++++++++++++++++- 3 files changed, 343 insertions(+), 42 deletions(-) diff --git a/src/jalview/api/FeatureColourI.java b/src/jalview/api/FeatureColourI.java index 0ded079..3eebf6c 100644 --- a/src/jalview/api/FeatureColourI.java +++ b/src/jalview/api/FeatureColourI.java @@ -56,6 +56,14 @@ public interface FeatureColourI Color getMaxColour(); /** + * Returns the 'no value' colour (used when a feature lacks score, or the + * attribute, being used for colouring) + * + * @return + */ + Color getNoColour(); + + /** * Answers true if the feature has a single colour, i.e. if isColourByLabel() * and isGraduatedColour() both answer false * @@ -156,7 +164,10 @@ public interface FeatureColourI Color getColor(SequenceFeature feature); /** - * Update the min-max range for a graduated colour scheme + * Update the min-max range for a graduated colour scheme. Note that the + * colour scheme may be configured to colour by feature score, or a + * (numeric-valued) attribute - the caller should ensure that the correct + * range is being set. * * @param min * @param max @@ -169,4 +180,26 @@ public interface FeatureColourI * @return */ String toJalviewFormat(String featureType); + + /** + * Answers true if colour is by attribute text or numerical value + * + * @return + */ + boolean isColourByAttribute(); + + /** + * Answers the name of the attribute used for colouring if any, or null + * + * @return + */ + String getAttributeName(); + + /** + * Sets the name of the attribute used for colouring if any, or null to remove + * this property + * + * @return + */ + void setAttributeName(String name); } diff --git a/src/jalview/schemes/FeatureColour.java b/src/jalview/schemes/FeatureColour.java index 54d1c6c..df4ea39 100644 --- a/src/jalview/schemes/FeatureColour.java +++ b/src/jalview/schemes/FeatureColour.java @@ -29,10 +29,28 @@ import java.awt.Color; import java.util.StringTokenizer; /** - * A class that wraps either a simple colour or a graduated colour + * A class that represents a colour scheme for a feature type. Options supported + * are currently + * */ public class FeatureColour implements FeatureColourI { + static final Color DEFAULT_NO_COLOUR = Color.LIGHT_GRAY; + private static final String BAR = "|"; final private Color colour; @@ -41,10 +59,30 @@ public class FeatureColour implements FeatureColourI final private Color maxColour; + /* + * colour to use for colour by attribute when the + * attribute value is absent + */ + final private Color noColour; + + /* + * if true, then colour has a gradient based on a numerical + * range (either feature score, or an attribute value) + */ private boolean graduatedColour; + /* + * if true, colour values are generated from a text string, + * either feature description, or an attribute value + */ private boolean colourByLabel; + /* + * if not null, the value of this named attribute is used for + * colourByLabel or graduatedColour + */ + private String byAttributeName; + private float threshold; private float base; @@ -288,6 +326,7 @@ public class FeatureColour implements FeatureColourI { minColour = Color.WHITE; maxColour = Color.BLACK; + noColour = DEFAULT_NO_COLOUR; minRed = 0f; minGreen = 0f; minBlue = 0f; @@ -298,7 +337,8 @@ public class FeatureColour implements FeatureColourI } /** - * Constructor given a colour range and a score range + * Constructor given a colour range and a score range, defaulting 'no value + * colour' to be the same as minimum colour * * @param low * @param high @@ -307,36 +347,7 @@ public class FeatureColour implements FeatureColourI */ public FeatureColour(Color low, Color high, float min, float max) { - if (low == null) - { - low = Color.white; - } - if (high == null) - { - high = Color.black; - } - graduatedColour = true; - colour = null; - minColour = low; - maxColour = high; - threshold = Float.NaN; - isHighToLow = min >= max; - minRed = low.getRed() / 255f; - minGreen = low.getGreen() / 255f; - minBlue = low.getBlue() / 255f; - deltaRed = (high.getRed() / 255f) - minRed; - deltaGreen = (high.getGreen() / 255f) - minGreen; - deltaBlue = (high.getBlue() / 255f) - minBlue; - if (isHighToLow) - { - base = max; - range = min - max; - } - else - { - base = min; - range = max - min; - } + this(low, high, low, min, max); } /** @@ -350,6 +361,7 @@ public class FeatureColour implements FeatureColourI colour = fc.colour; minColour = fc.minColour; maxColour = fc.maxColour; + noColour = fc.noColour; minRed = fc.minRed; minGreen = fc.minGreen; minBlue = fc.minBlue; @@ -359,6 +371,7 @@ public class FeatureColour implements FeatureColourI base = fc.base; range = fc.range; isHighToLow = fc.isHighToLow; + byAttributeName = fc.byAttributeName; setAboveThreshold(fc.isAboveThreshold()); setBelowThreshold(fc.isBelowThreshold()); setThreshold(fc.getThreshold()); @@ -376,10 +389,46 @@ public class FeatureColour implements FeatureColourI public FeatureColour(FeatureColour fc, float min, float max) { this(fc); - graduatedColour = true; + setGraduatedColour(true); updateBounds(min, max); } + public FeatureColour(Color low, Color high, Color noValueColour, + float min, float max) + { + if (low == null) + { + low = Color.white; + } + if (high == null) + { + high = Color.black; + } + graduatedColour = true; + colour = null; + minColour = low; + maxColour = high; + noColour = noValueColour; + threshold = Float.NaN; + isHighToLow = min >= max; + minRed = low.getRed() / 255f; + minGreen = low.getGreen() / 255f; + minBlue = low.getBlue() / 255f; + deltaRed = (high.getRed() / 255f) - minRed; + deltaGreen = (high.getGreen() / 255f) - minGreen; + deltaBlue = (high.getBlue() / 255f) - minBlue; + if (isHighToLow) + { + base = max; + range = min - max; + } + else + { + base = min; + range = max - min; + } + } + @Override public boolean isGraduatedColour() { @@ -418,6 +467,12 @@ public class FeatureColour implements FeatureColourI } @Override + public Color getNoColour() + { + return noColour; + } + + @Override public boolean isColourByLabel() { return colourByLabel; @@ -506,10 +561,7 @@ public class FeatureColour implements FeatureColourI } /** - * Updates the base and range appropriately for the given minmax range - * - * @param min - * @param max + * {@inheritDoc} */ @Override public void updateBounds(float min, float max) @@ -542,7 +594,10 @@ public class FeatureColour implements FeatureColourI { if (isColourByLabel()) { - return ColorUtils.createColourFromName(feature.getDescription()); + String label = byAttributeName == null ? feature.getDescription() + : feature.getValueAsString(byAttributeName); + return label == null ? noColour : ColorUtils + .createColourFromName(label); } if (!isGraduatedColour()) @@ -552,17 +607,32 @@ public class FeatureColour implements FeatureColourI /* * graduated colour case, optionally with threshold + * may be based on feature score on an attribute value * Float.NaN is assigned minimum visible score colour + * no such attribute is assigned the 'no value' colour */ float scr = feature.getScore(); + if (byAttributeName != null) + { + try + { + String attVal = feature.getValueAsString(byAttributeName); + scr = Float.valueOf(attVal); + } catch (Throwable e) + { + scr = Float.NaN; + } + } if (Float.isNaN(scr)) { - return getMinColour(); + return noColour; } + if (isAboveThreshold() && scr <= threshold) { return null; } + if (isBelowThreshold() && scr >= threshold) { return null; @@ -674,4 +744,22 @@ public class FeatureColour implements FeatureColourI return String.format("%s\t%s", featureType, colourString); } + @Override + public boolean isColourByAttribute() + { + return byAttributeName != null; + } + + @Override + public String getAttributeName() + { + return byAttributeName; + } + + @Override + public void setAttributeName(String name) + { + byAttributeName = name; + } + } diff --git a/test/jalview/schemes/FeatureColourTest.java b/test/jalview/schemes/FeatureColourTest.java index 7a72c15..3f60152 100644 --- a/test/jalview/schemes/FeatureColourTest.java +++ b/test/jalview/schemes/FeatureColourTest.java @@ -33,6 +33,8 @@ import jalview.util.Format; import java.awt.Color; +import junit.extensions.PA; + import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; @@ -57,6 +59,8 @@ public class FeatureColourTest assertTrue(fc1.getColour().equals(Color.RED)); assertFalse(fc1.isGraduatedColour()); assertFalse(fc1.isColourByLabel()); + assertFalse(fc1.isColourByAttribute()); + assertNull(fc1.getAttributeName()); /* * min-max colour @@ -68,9 +72,31 @@ public class FeatureColourTest assertTrue(fc1.isGraduatedColour()); assertFalse(fc1.isColourByLabel()); assertTrue(fc1.isAboveThreshold()); + assertFalse(fc1.isColourByAttribute()); + assertNull(fc1.getAttributeName()); assertEquals(12f, fc1.getThreshold()); assertEquals(Color.gray, fc1.getMinColour()); assertEquals(Color.black, fc1.getMaxColour()); + assertEquals(Color.gray, fc1.getNoColour()); + assertEquals(10f, fc1.getMin()); + assertEquals(20f, fc1.getMax()); + + /* + * min-max-noValue colour + */ + fc = new FeatureColour(Color.gray, Color.black, Color.green, 10f, 20f); + fc.setAboveThreshold(true); + fc.setThreshold(12f); + fc1 = new FeatureColour(fc); + assertTrue(fc1.isGraduatedColour()); + assertFalse(fc1.isColourByLabel()); + assertFalse(fc1.isColourByAttribute()); + assertNull(fc1.getAttributeName()); + assertTrue(fc1.isAboveThreshold()); + assertEquals(12f, fc1.getThreshold()); + assertEquals(Color.gray, fc1.getMinColour()); + assertEquals(Color.black, fc1.getMaxColour()); + assertEquals(Color.green, fc1.getNoColour()); assertEquals(10f, fc1.getMin()); assertEquals(20f, fc1.getMax()); @@ -82,6 +108,102 @@ public class FeatureColourTest fc1 = new FeatureColour(fc); assertTrue(fc1.isColourByLabel()); assertFalse(fc1.isGraduatedColour()); + assertFalse(fc1.isColourByAttribute()); + assertNull(fc1.getAttributeName()); + + /* + * colour by attribute (label) + */ + fc = new FeatureColour(); + fc.setColourByLabel(true); + fc.setAttributeName("AF"); + fc1 = new FeatureColour(fc); + assertTrue(fc1.isColourByLabel()); + assertFalse(fc1.isGraduatedColour()); + assertTrue(fc1.isColourByAttribute()); + assertEquals("AF", fc1.getAttributeName()); + + /* + * colour by attribute (value) + */ + fc = new FeatureColour(Color.gray, Color.black, Color.green, 10f, 20f); + fc.setAboveThreshold(true); + fc.setThreshold(12f); + fc.setAttributeName("AF"); + fc1 = new FeatureColour(fc); + assertTrue(fc1.isGraduatedColour()); + assertFalse(fc1.isColourByLabel()); + assertTrue(fc1.isColourByAttribute()); + assertEquals("AF", fc1.getAttributeName()); + assertTrue(fc1.isAboveThreshold()); + assertEquals(12f, fc1.getThreshold()); + assertEquals(Color.gray, fc1.getMinColour()); + assertEquals(Color.black, fc1.getMaxColour()); + assertEquals(Color.green, fc1.getNoColour()); + assertEquals(10f, fc1.getMin()); + assertEquals(20f, fc1.getMax()); + } + + @Test(groups = { "Functional" }) + public void testCopyConstructor_minMax() + { + /* + * graduated colour + */ + FeatureColour fc = new FeatureColour(Color.BLUE, Color.RED, 1f, 5f); + assertTrue(fc.isGraduatedColour()); + assertFalse(fc.isColourByLabel()); + assertFalse(fc.isColourByAttribute()); + assertNull(fc.getAttributeName()); + assertEquals(1f, fc.getMin()); + assertEquals(5f, fc.getMax()); + + /* + * update min-max bounds + */ + FeatureColour fc1 = new FeatureColour(fc, 2f, 6f); + assertTrue(fc1.isGraduatedColour()); + assertFalse(fc1.isColourByLabel()); + assertFalse(fc1.isColourByAttribute()); + assertNull(fc1.getAttributeName()); + assertEquals(2f, fc1.getMin()); + assertEquals(6f, fc1.getMax()); + assertFalse((boolean) PA.getValue(fc1, "isHighToLow")); + + /* + * update min-max bounds - high to low + */ + fc1 = new FeatureColour(fc, 23f, 16f); + assertTrue(fc1.isGraduatedColour()); + assertFalse(fc1.isColourByLabel()); + assertFalse(fc1.isColourByAttribute()); + assertNull(fc1.getAttributeName()); + assertEquals(23f, fc1.getMin()); + assertEquals(16f, fc1.getMax()); + assertTrue((boolean) PA.getValue(fc1, "isHighToLow")); + + /* + * colour by label + */ + fc = new FeatureColour(Color.BLUE, Color.RED, 1f, 5f); + fc.setColourByLabel(true); + assertFalse(fc.isGraduatedColour()); + assertTrue(fc.isColourByLabel()); + assertFalse(fc.isColourByAttribute()); + assertNull(fc.getAttributeName()); + assertEquals(1f, fc.getMin()); + assertEquals(5f, fc.getMax()); + + /* + * update min-max bounds - converts to graduated colour + */ + fc1 = new FeatureColour(fc, 2f, 6f); + assertTrue(fc1.isGraduatedColour()); + assertFalse(fc1.isColourByLabel()); + assertFalse(fc1.isColourByAttribute()); + assertNull(fc1.getAttributeName()); + assertEquals(2f, fc1.getMin()); + assertEquals(6f, fc1.getMax()); } @Test(groups = { "Functional" }) @@ -106,8 +228,11 @@ public class FeatureColourTest @Test(groups = { "Functional" }) public void testGetColor_Graduated() { - // graduated colour from score 0 to 100, gray(128, 128, 128) to red(255, 0, - // 0) + /* + * graduated colour from + * score 0 to 100 + * gray(128, 128, 128) to red(255, 0, 0) + */ FeatureColour fc = new FeatureColour(Color.GRAY, Color.RED, 0f, 100f); // feature score is 75 which is 3/4 of the way from GRAY to RED SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 75f, @@ -340,4 +465,59 @@ public class FeatureColourTest fc = FeatureColour.parseJalviewFeatureColour(descriptor); assertTrue(fc.isGraduatedColour()); } + + @Test(groups = { "Functional" }) + public void testGetColor_colourByAttributeText() + { + FeatureColour fc = new FeatureColour(); + fc.setColourByLabel(true); + fc.setAttributeName("consequence"); + SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 1f, + null); + + /* + * if feature has no such attribute, use 'no value' colour + */ + assertEquals(FeatureColour.DEFAULT_NO_COLOUR, fc.getColor(sf)); + + /* + * if feature has attribute, generate colour from value + */ + sf.setValue("consequence", "benign"); + Color expected = ColorUtils.createColourFromName("benign"); + assertEquals(expected, fc.getColor(sf)); + } + + @Test(groups = { "Functional" }) + public void testGetColor_GraduatedByAttributeValue() + { + /* + * graduated colour based on attribute value for AF + * given a min-max range of 0-100 + */ + FeatureColour fc = new FeatureColour(new Color(50, 100, 150), + new Color(150, 200, 250), Color.yellow, 0f, 100f); + String attName = "AF"; + fc.setAttributeName(attName); + + /* + * first case: feature lacks the attribute - use 'no value' colour + */ + SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 75f, + null); + assertEquals(Color.yellow, fc.getColor(sf)); + + /* + * second case: attribute present but not numeric - treat as if absent + */ + sf.setValue(attName, "twelve"); + assertEquals(Color.yellow, fc.getColor(sf)); + + /* + * third case: valid attribute value + */ + sf.setValue(attName, "20.0"); + Color expected = new Color(70, 120, 170); + assertEquals(expected, fc.getColor(sf)); + } } -- 1.7.10.2