From fa06b92a88bdd008d96681d80a972a9f23277609 Mon Sep 17 00:00:00 2001 From: nkorinek Date: Fri, 20 Mar 2020 14:26:49 -0600 Subject: [PATCH 01/10] Legend raster assertion fix! --- matplotcheck/raster.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/matplotcheck/raster.py b/matplotcheck/raster.py index 822daea2..653c976a 100644 --- a/matplotcheck/raster.py +++ b/matplotcheck/raster.py @@ -146,13 +146,17 @@ def assert_legend_accuracy_classified_image( # labels (im_data_labels) and the other with the expected labels # (im_expected_labels) im_class_dict = {} + + # Accounting for images that don't start at 0 + correction = np.min(im_data) + for val in np.unique(im_data): - im_class_dict[val] = legend_dict[im_cmap(im.norm(val))] + im_class_dict[val-correction] = legend_dict[im_cmap(im.norm(val))] im_data_labels = [ - [im_class_dict[val] for val in row] for row in im_data.data + [im_class_dict[val-correction] for val in row] for row in im_data.data ] im_expected_labels = [ - [all_label_options[val][0] for val in row] for row in im_expected + [all_label_options[val-correction][0] for val in row] for row in im_expected ] # Check that expected and actual labels match up From affd4465732acde04edd7b9e83a3010030a9bd72 Mon Sep 17 00:00:00 2001 From: nkorinek Date: Fri, 20 Mar 2020 14:30:23 -0600 Subject: [PATCH 02/10] Changelog and black --- CHANGELOG.md | 1 + matplotcheck/raster.py | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86f7f067..1a9a27e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +* Fixed bug with asserting raster legend labels (@nkorinek, #163) * Created functions to test point geometries in VectorTester (@nkorinek, #176) * made `assert_string_contains()` accept correct strings with spaces in them (@nkorinek, #182) * added contributors file and updated README to remove that information (@nkorinek, #121) diff --git a/matplotcheck/raster.py b/matplotcheck/raster.py index 653c976a..5303d78c 100644 --- a/matplotcheck/raster.py +++ b/matplotcheck/raster.py @@ -151,12 +151,16 @@ def assert_legend_accuracy_classified_image( correction = np.min(im_data) for val in np.unique(im_data): - im_class_dict[val-correction] = legend_dict[im_cmap(im.norm(val))] + im_class_dict[val - correction] = legend_dict[ + im_cmap(im.norm(val)) + ] im_data_labels = [ - [im_class_dict[val-correction] for val in row] for row in im_data.data + [im_class_dict[val - correction] for val in row] + for row in im_data.data ] im_expected_labels = [ - [all_label_options[val-correction][0] for val in row] for row in im_expected + [all_label_options[val - correction][0] for val in row] + for row in im_expected ] # Check that expected and actual labels match up From 1f99fb6522a9c27c4aa646a493e309a1c005d6a3 Mon Sep 17 00:00:00 2001 From: nkorinek Date: Mon, 6 Apr 2020 11:41:23 -0600 Subject: [PATCH 03/10] Cleared up errors thrown --- matplotcheck/raster.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/matplotcheck/raster.py b/matplotcheck/raster.py index 5303d78c..20ce6542 100644 --- a/matplotcheck/raster.py +++ b/matplotcheck/raster.py @@ -140,7 +140,10 @@ def assert_legend_accuracy_classified_image( # Check that each legend entry label is in one of all_label_options assert len([val for val in legend_dict.values() if val]) == len( all_label_options - ), "Incorrect legend labels" + ), ( + "Number of label options provided doesn't match the number of", + " labels found in the image.", + ) # Create two copies of image array, one filled with the plot data class # labels (im_data_labels) and the other with the expected labels @@ -166,7 +169,7 @@ def assert_legend_accuracy_classified_image( # Check that expected and actual labels match up assert np.array_equal( im_data_labels, im_expected_labels - ), "Incorrect legend to data relation" + ), "Provided legend labels don't match labels found." # IMAGE TESTS/HELPER FUNCTIONS From a972c9e8756ead9f2f2fa0be034d1cebb950b9fb Mon Sep 17 00:00:00 2001 From: nkorinek Date: Thu, 9 Apr 2020 12:10:34 -0600 Subject: [PATCH 04/10] Beginning to create more functions --- matplotcheck/cases.py | 2 +- matplotcheck/raster.py | 83 +++++++++++++++++++++---------- matplotcheck/tests/test_raster.py | 22 +++----- 3 files changed, 64 insertions(+), 43 deletions(-) diff --git a/matplotcheck/cases.py b/matplotcheck/cases.py index c2370987..9783a698 100644 --- a/matplotcheck/cases.py +++ b/matplotcheck/cases.py @@ -769,7 +769,7 @@ def test_image_mask(self): not im_classified, "Image not expected to be classified" ) def test_legend_accuracy(self): - self.rt.assert_legend_accuracy_classified_image( + self.rt.assert_legend_labels( im_expected=im_expected, all_label_options=legend_labels ) diff --git a/matplotcheck/raster.py b/matplotcheck/raster.py index 20ce6542..df69bf84 100644 --- a/matplotcheck/raster.py +++ b/matplotcheck/raster.py @@ -57,7 +57,7 @@ def assert_colorbar_range(self, crange): ), "Colorbar maximum is not expected value:{0}".format(crange[1]) def _which_label(self, label, all_label_options): - """Helper function for assert_legend_accuracy_classified_image + """Helper function for assert_legend_labels Returns string that represents a category label for label. Parameters @@ -80,7 +80,7 @@ def _which_label(self, label, all_label_options): return label_opts[0] return None - def assert_legend_accuracy_classified_image( + def assert_legend_labels( self, im_expected, all_label_options ): """Asserts legend correctly describes classified image on Axes ax, @@ -114,36 +114,18 @@ def assert_legend_accuracy_classified_image( Finally those two arrays of strings are compared. Passes if they match. """ # Retrieve image array - im_data = [] - if self.ax.get_images(): - im = self.ax.get_images()[0] - im_data, im_cmap = im.get_array(), im.get_cmap() + im_data, im_cmap = self.get_plot_image(return_cmap=True) assert list(im_data), "No Image Displayed" - # Retrieve legend - legends = self.get_legends() - assert legends, "No legend displayed" - # Retrieve legend entries and find which element of all_label_options # matches that entry - legend_dict = {} - for p in [ - p - for sublist in [leg.get_patches() for leg in legends] - for p in sublist - ]: - label = p.get_label().lower() - legend_dict[p.get_facecolor()] = self._which_label( - label, all_label_options - ) + legend_dict = self.get_legend_labels(all_label_options) # Check that each legend entry label is in one of all_label_options assert len([val for val in legend_dict.values() if val]) == len( all_label_options - ), ( - "Number of label options provided doesn't match the number of", - " labels found in the image.", - ) + ), "Number of label options provided doesn't match the number of"\ + " labels found in the image." # Create two copies of image array, one filled with the plot data class # labels (im_data_labels) and the other with the expected labels @@ -173,23 +155,70 @@ def assert_legend_accuracy_classified_image( # IMAGE TESTS/HELPER FUNCTIONS - def get_plot_image(self): + def get_legend_labels(self, all_label_options): + """Return labels from legend + + Returns + ------- + im_data: List + Numpy array of images stored on Axes object. + all_label_options: list of lists + Each internal list represents a class and said list is a list of + strings where at least one string is expected to be in the legend + label for this category. Internal lists must be in the same order + as bins in im_expected, e.g. first internal list has the expected + label options for class 0. + """ + + # Retrieve legend + legends = self.get_legends() + assert legends, "No legend displayed" + + legend_dict = {} + + for p in [ + p + for sublist in [leg.get_patches() for leg in legends] + for p in sublist + ]: + label = p.get_label().lower() + legend_dict[p.get_facecolor()] = self._which_label( + label, all_label_options + ) + + return(legend_dict) + + def get_plot_image(self, return_cmap=False): """Returns images stored on the Axes object as a list of numpy arrays. + Parameters + ---------- + return_cmap: boolean + If true, returns a cmap from the plot image alongside the images + data + Returns ------- im_data: List Numpy array of images stored on Axes object. + im_cmap: List + Numpy array of cmaps stored on Axes object. """ im_data = [] if self.ax.get_images(): - im_data = self.ax.get_images()[0].get_array() + im = self.ax.get_images()[0] + im_data = im.get_array() + if return_cmap: + im_cmap = im.get_cmap() assert list(im_data), "No Image Displayed" # If image array has 3 dims (e.g. rgb image), remove alpha channel if len(im_data.shape) == 3: im_data = im_data[:, :, :3] - return im_data + if return_cmap: + return im_data, im_cmap + else: + return im_data def assert_image( self, im_expected, im_classified=False, m="Incorrect Image Displayed" diff --git a/matplotcheck/tests/test_raster.py b/matplotcheck/tests/test_raster.py index 7c4d7fd8..4473c0f5 100644 --- a/matplotcheck/tests/test_raster.py +++ b/matplotcheck/tests/test_raster.py @@ -163,9 +163,7 @@ def test_raster_assert_legend_accuracy(raster_plt_class, np_ar_discrete): values = np.sort(np.unique(np_ar_discrete)) label_options = [[str(i)] for i in values] - raster_plt_class.assert_legend_accuracy_classified_image( - np_ar_discrete, label_options - ) + raster_plt_class.assert_legend_labels(np_ar_discrete, label_options) plt.close() @@ -177,8 +175,8 @@ def test_raster_assert_legend_accuracy_badlabel( # Should fail with bad label bad_label_options = [["foo"] * values.shape[0]] - with pytest.raises(AssertionError, match="Incorrect legend labels"): - raster_plt_class.assert_legend_accuracy_classified_image( + with pytest.raises(AssertionError, match="Number of label options provid"): + raster_plt_class.assert_legend_labels( np_ar_discrete, bad_label_options ) plt.close() @@ -198,11 +196,9 @@ def test_raster_assert_legend_accuracy_badvalues( # Should fail with bad image with pytest.raises( - AssertionError, match="Incorrect legend to data relation" + AssertionError, match="Provided legend labels don't match labels found" ): - raster_plt_class.assert_legend_accuracy_classified_image( - bad_image, label_options - ) + raster_plt_class.assert_legend_labels(bad_image, label_options) plt.close() @@ -213,9 +209,7 @@ def test_raster_assert_legend_accuracy_nolegend(raster_plt, np_ar_discrete): # Fails without legend with pytest.raises(AssertionError, match="No legend displayed"): - raster_plt.assert_legend_accuracy_classified_image( - np_ar_discrete, label_options - ) + raster_plt.assert_legend_labels(np_ar_discrete, label_options) plt.close() @@ -228,9 +222,7 @@ def test_raster_assert_legend_accuracy_noimage( # Fails when no image displayed with pytest.raises(AssertionError, match="No Image Displayed"): - raster_plt_blank.assert_legend_accuracy_classified_image( - np_ar_discrete, label_options - ) + raster_plt_blank.assert_legend_labels(np_ar_discrete, label_options) plt.close() From 19bd70fb5878c1b809d7096c6625e5c20d16876d Mon Sep 17 00:00:00 2001 From: nkorinek Date: Thu, 9 Apr 2020 15:11:03 -0600 Subject: [PATCH 05/10] Moving parts of assert_legends into different helper functions to make the function shorter --- matplotcheck/raster.py | 49 +++++++++++++++++------------------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/matplotcheck/raster.py b/matplotcheck/raster.py index df69bf84..f5f1187c 100644 --- a/matplotcheck/raster.py +++ b/matplotcheck/raster.py @@ -80,9 +80,7 @@ def _which_label(self, label, all_label_options): return label_opts[0] return None - def assert_legend_labels( - self, im_expected, all_label_options - ): + def assert_legend_labels(self, im_expected, all_label_options): """Asserts legend correctly describes classified image on Axes ax, checking the legend labels and the values @@ -114,19 +112,16 @@ def assert_legend_labels( Finally those two arrays of strings are compared. Passes if they match. """ # Retrieve image array - im_data, im_cmap = self.get_plot_image(return_cmap=True) + im_data = self.get_plot_image() + im = self.ax.get_images()[0] + im_cmap = im.get_cmap() + assert list(im_data), "No Image Displayed" # Retrieve legend entries and find which element of all_label_options # matches that entry legend_dict = self.get_legend_labels(all_label_options) - # Check that each legend entry label is in one of all_label_options - assert len([val for val in legend_dict.values() if val]) == len( - all_label_options - ), "Number of label options provided doesn't match the number of"\ - " labels found in the image." - # Create two copies of image array, one filled with the plot data class # labels (im_data_labels) and the other with the expected labels # (im_expected_labels) @@ -137,7 +132,7 @@ def assert_legend_labels( for val in np.unique(im_data): im_class_dict[val - correction] = legend_dict[ - im_cmap(im.norm(val)) + im_cmap(self.ax.get_images()[0].norm(val)) ] im_data_labels = [ [im_class_dict[val - correction] for val in row] @@ -186,39 +181,35 @@ def get_legend_labels(self, all_label_options): label, all_label_options ) - return(legend_dict) + # Check that each legend entry label is in one of all_label_options + assert len([val for val in legend_dict.values() if val]) == len( + all_label_options + ), ( + "Number of label options provided doesn't match the number of" + " labels found in the image." + ) - def get_plot_image(self, return_cmap=False): - """Returns images stored on the Axes object as a list of numpy arrays. + return legend_dict - Parameters - ---------- - return_cmap: boolean - If true, returns a cmap from the plot image alongside the images - data + def get_plot_image(self): + """Returns images stored on the Axes object as a list of numpy arrays. Returns ------- im_data: List Numpy array of images stored on Axes object. - im_cmap: List - Numpy array of cmaps stored on Axes object. """ im_data = [] if self.ax.get_images(): - im = self.ax.get_images()[0] - im_data = im.get_array() - if return_cmap: - im_cmap = im.get_cmap() + im_data = self.ax.get_images()[0].get_array() + assert list(im_data), "No Image Displayed" # If image array has 3 dims (e.g. rgb image), remove alpha channel if len(im_data.shape) == 3: im_data = im_data[:, :, :3] - if return_cmap: - return im_data, im_cmap - else: - return im_data + + return im_data def assert_image( self, im_expected, im_classified=False, m="Incorrect Image Displayed" From fc2b19e57ce7ad403f5b2ed4e0989db41e0ddba8 Mon Sep 17 00:00:00 2001 From: nkorinek Date: Mon, 27 Apr 2020 14:41:01 -0600 Subject: [PATCH 06/10] Added get_cmap function --- matplotcheck/raster.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/matplotcheck/raster.py b/matplotcheck/raster.py index f5f1187c..15c91aa6 100644 --- a/matplotcheck/raster.py +++ b/matplotcheck/raster.py @@ -80,7 +80,7 @@ def _which_label(self, label, all_label_options): return label_opts[0] return None - def assert_legend_labels(self, im_expected, all_label_options): + def assert_raster_legend_labels(self, im_expected, all_label_options): """Asserts legend correctly describes classified image on Axes ax, checking the legend labels and the values @@ -113,8 +113,7 @@ def assert_legend_labels(self, im_expected, all_label_options): """ # Retrieve image array im_data = self.get_plot_image() - im = self.ax.get_images()[0] - im_cmap = im.get_cmap() + im_cmap = self.get_image_cmap() assert list(im_data), "No Image Displayed" @@ -150,6 +149,10 @@ def assert_legend_labels(self, im_expected, all_label_options): # IMAGE TESTS/HELPER FUNCTIONS + def get_image_cmap(self): + """Return the cmap for the image in the matplotlib""" + return self.ax.get_images()[0].get_cmap() + def get_legend_labels(self, all_label_options): """Return labels from legend From 8d91906d5277ea1395ddb04c5a783499e40cd376 Mon Sep 17 00:00:00 2001 From: nkorinek Date: Wed, 29 Apr 2020 14:52:31 -0600 Subject: [PATCH 07/10] Complete overhaul of raster legend labels check and updates to the testing around it --- matplotcheck/raster.py | 116 ++++++++++-------------------- matplotcheck/tests/test_raster.py | 24 +++++-- 2 files changed, 57 insertions(+), 83 deletions(-) diff --git a/matplotcheck/raster.py b/matplotcheck/raster.py index 15c91aa6..4181dd72 100644 --- a/matplotcheck/raster.py +++ b/matplotcheck/raster.py @@ -64,7 +64,8 @@ def _which_label(self, label, all_label_options): ---------- label: string from legend to see if it contains an option in all_label_options - all_label_options: list of lists + all_label_options: list + List should be from an internal list from a list of lists. Each internal list represents a class and said list is a list of strings where at least one string is expected to be in the legend label for this category. @@ -74,10 +75,9 @@ def _which_label(self, label, all_label_options): string that is the first entry in the internal list which label is matched with. If no match is found, return value is None """ - for label_opts in all_label_options: - for s in label_opts: - if s in label: - return label_opts[0] + for label_option in all_label_options: + if label_option == label: + return label_option return None def assert_raster_legend_labels(self, im_expected, all_label_options): @@ -87,7 +87,6 @@ def assert_raster_legend_labels(self, im_expected, all_label_options): Parameters ---------- im_expected: array of arrays with expected classified image on ax. - Class values must start with 0, 1, 2, etc. all_label_options: list of lists Each internal list represents a class and said list is a list of strings where at least one string is expected to be in the legend @@ -98,101 +97,64 @@ def assert_raster_legend_labels(self, im_expected, all_label_options): Returns ---------- Nothing (if checks pass) or raises error - - - Notes - ---------- - First compares all_label_options against the legend labels to find - which element of all_label_options matches that entry. E.g. if the - first legend entry has a match in the first list in all_label_options, - then that legend entry corresponds to the first class (value 0). - Then the plot image array is copied and the values are set to the - legend label that match the values (i.e. the element in - all_label_options). The same is done for the expected image array. - Finally those two arrays of strings are compared. Passes if they match. """ # Retrieve image array im_data = self.get_plot_image() - im_cmap = self.get_image_cmap() assert list(im_data), "No Image Displayed" # Retrieve legend entries and find which element of all_label_options # matches that entry - legend_dict = self.get_legend_labels(all_label_options) - - # Create two copies of image array, one filled with the plot data class - # labels (im_data_labels) and the other with the expected labels - # (im_expected_labels) - im_class_dict = {} - - # Accounting for images that don't start at 0 - correction = np.min(im_data) - - for val in np.unique(im_data): - im_class_dict[val - correction] = legend_dict[ - im_cmap(self.ax.get_images()[0].norm(val)) - ] - im_data_labels = [ - [im_class_dict[val - correction] for val in row] - for row in im_data.data - ] - im_expected_labels = [ - [all_label_options[val - correction][0] for val in row] - for row in im_expected + + labels = self.get_legend_labels() + + assert len(labels) == len(all_label_options), ( + "Number of label options provided doesn't match the number of" + " labels found in the image." + ) + + labels_check = [ + self._which_label(label, all_label_options[i]) + for i, label in enumerate(labels) ] - # Check that expected and actual labels match up - assert np.array_equal( - im_data_labels, im_expected_labels + # Check that each legend entry label is in one of all_label_options + assert all( + labels_check ), "Provided legend labels don't match labels found." + # Check that expected and actual arrays data match up + assert np.array_equal( + im_data, im_expected + ), "Expected image data doesn't match data in image." + # IMAGE TESTS/HELPER FUNCTIONS - def get_image_cmap(self): - """Return the cmap for the image in the matplotlib""" - return self.ax.get_images()[0].get_cmap() - - def get_legend_labels(self, all_label_options): - """Return labels from legend + def get_legend_labels(self, return_facecolors=False): + """Return labels from legend in a list + + Parameters + ---------- + return_facecolors: boolean + Returns a list of facecolors alongside the labels themselves. Returns ------- - im_data: List - Numpy array of images stored on Axes object. - all_label_options: list of lists - Each internal list represents a class and said list is a list of - strings where at least one string is expected to be in the legend - label for this category. Internal lists must be in the same order - as bins in im_expected, e.g. first internal list has the expected - label options for class 0. + labels: List + List of labels found in the legend of a raster plot. """ # Retrieve legend legends = self.get_legends() assert legends, "No legend displayed" - legend_dict = {} + patches = [leg.get_patches() for leg in legends] - for p in [ - p - for sublist in [leg.get_patches() for leg in legends] - for p in sublist - ]: - label = p.get_label().lower() - legend_dict[p.get_facecolor()] = self._which_label( - label, all_label_options - ) - - # Check that each legend entry label is in one of all_label_options - assert len([val for val in legend_dict.values() if val]) == len( - all_label_options - ), ( - "Number of label options provided doesn't match the number of" - " labels found in the image." - ) - - return legend_dict + return [ + label.get_label().lower() + for sublist in patches + for label in sublist + ] def get_plot_image(self): """Returns images stored on the Axes object as a list of numpy arrays. diff --git a/matplotcheck/tests/test_raster.py b/matplotcheck/tests/test_raster.py index 4473c0f5..33fcd9cd 100644 --- a/matplotcheck/tests/test_raster.py +++ b/matplotcheck/tests/test_raster.py @@ -157,13 +157,23 @@ def test_raster_assert_colorbar_range_blank(raster_plt_blank, np_ar): """ LEGEND TESTS """ +def test_get_legend_labels_accuracy(raster_plt_class, np_ar_discrete): + """Checks that helper function get_legend_labels returns the right labels. + """ + values = np.sort(np.unique(np_ar_discrete)) + label_options = ["level " + str(i) for i in values] + + assert label_options == raster_plt_class.get_legend_labels() + plt.close() + + def test_raster_assert_legend_accuracy(raster_plt_class, np_ar_discrete): """Checks that legend matches image, checking both the labels and color patches""" values = np.sort(np.unique(np_ar_discrete)) - label_options = [[str(i)] for i in values] + label_options = [["level " + str(i)] for i in values] - raster_plt_class.assert_legend_labels(np_ar_discrete, label_options) + raster_plt_class.assert_raster_legend_labels(np_ar_discrete, label_options) plt.close() @@ -176,7 +186,7 @@ def test_raster_assert_legend_accuracy_badlabel( # Should fail with bad label bad_label_options = [["foo"] * values.shape[0]] with pytest.raises(AssertionError, match="Number of label options provid"): - raster_plt_class.assert_legend_labels( + raster_plt_class.assert_raster_legend_labels( np_ar_discrete, bad_label_options ) plt.close() @@ -198,7 +208,7 @@ def test_raster_assert_legend_accuracy_badvalues( with pytest.raises( AssertionError, match="Provided legend labels don't match labels found" ): - raster_plt_class.assert_legend_labels(bad_image, label_options) + raster_plt_class.assert_raster_legend_labels(bad_image, label_options) plt.close() @@ -209,7 +219,7 @@ def test_raster_assert_legend_accuracy_nolegend(raster_plt, np_ar_discrete): # Fails without legend with pytest.raises(AssertionError, match="No legend displayed"): - raster_plt.assert_legend_labels(np_ar_discrete, label_options) + raster_plt.assert_raster_legend_labels(np_ar_discrete, label_options) plt.close() @@ -222,7 +232,9 @@ def test_raster_assert_legend_accuracy_noimage( # Fails when no image displayed with pytest.raises(AssertionError, match="No Image Displayed"): - raster_plt_blank.assert_legend_labels(np_ar_discrete, label_options) + raster_plt_blank.assert_raster_legend_labels( + np_ar_discrete, label_options + ) plt.close() From 097e4cc96e592fa5f88d745052ecc5af2e21ab61 Mon Sep 17 00:00:00 2001 From: nkorinek Date: Wed, 29 Apr 2020 14:57:31 -0600 Subject: [PATCH 08/10] Minor wording updates --- matplotcheck/raster.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/matplotcheck/raster.py b/matplotcheck/raster.py index 4181dd72..aaf27f83 100644 --- a/matplotcheck/raster.py +++ b/matplotcheck/raster.py @@ -130,14 +130,9 @@ def assert_raster_legend_labels(self, im_expected, all_label_options): # IMAGE TESTS/HELPER FUNCTIONS - def get_legend_labels(self, return_facecolors=False): + def get_legend_labels(self): """Return labels from legend in a list - Parameters - ---------- - return_facecolors: boolean - Returns a list of facecolors alongside the labels themselves. - Returns ------- labels: List @@ -148,8 +143,10 @@ def get_legend_labels(self, return_facecolors=False): legends = self.get_legends() assert legends, "No legend displayed" + # Get each patch stored in the legends object patches = [leg.get_patches() for leg in legends] + # Go through each patch to retrieve the labels from the legend return [ label.get_label().lower() for sublist in patches From 12b386b88ec9e4729e0aa34eabb1fed0b6ab70a9 Mon Sep 17 00:00:00 2001 From: Leah Wasser Date: Sat, 2 May 2020 17:12:52 -0600 Subject: [PATCH 09/10] black --- matplotcheck/raster.py | 141 +++++++++++++++++++++++++++++++---------- 1 file changed, 106 insertions(+), 35 deletions(-) diff --git a/matplotcheck/raster.py b/matplotcheck/raster.py index aaf27f83..b7cf69d0 100644 --- a/matplotcheck/raster.py +++ b/matplotcheck/raster.py @@ -56,78 +56,149 @@ def assert_colorbar_range(self, crange): cb[0].vmax == crange[1] ), "Colorbar maximum is not expected value:{0}".format(crange[1]) - def _which_label(self, label, all_label_options): + def _check_label(self, labels, expected_labels): """Helper function for assert_legend_labels - Returns string that represents a category label for label. + Tests each label in the legend to see if the text in expected labels + matches the text found in the legend labels. Parameters ---------- - label: string from legend to see if it contains an option in - all_label_options - all_label_options: list - List should be from an internal list from a list of lists. - Each internal list represents a class and said list is a list of - strings where at least one string is expected to be in the legend - label for this category. + # TODO: update all parameters and associated parameter description + labels: string from legend to see if it contains an option in + expected_labels + expected_labels: list of lists + Each list within the main list should contain a list of strings + that are expected to be found in each label in the plot + legend that is being tested. + TODO: clarify if this is or or "and" - ie i think it's or - is + just makes sure that one of the words in the sublist of expected + labels is in the plot legend Returns ------ + Dictionary ... #TODO update this return statement string that is the first entry in the internal list which label is matched with. If no match is found, return value is None """ - for label_option in all_label_options: - if label_option == label: - return label_option - return None + # TODO: return boolean instead of a none value - true if it matches, + # false if it does not match - def assert_raster_legend_labels(self, im_expected, all_label_options): + # + # for label_option in expected_labels: + # if label_option == label: + # return label_option + + label_check = {} + + # Iteratively test each label found in the plot legend to see if it is + # in the list of expected labels + for i, label in enumerate(labels): + test_output = any( + label in expected_label + for expected_label in expected_labels[i] + ) + label_check[label] = test_output + + return label_check + + def assert_raster_legend_labels(self, im_expected, expected_labels): """Asserts legend correctly describes classified image on Axes ax, checking the legend labels and the values Parameters ---------- im_expected: array of arrays with expected classified image on ax. - all_label_options: list of lists - Each internal list represents a class and said list is a list of - strings where at least one string is expected to be in the legend - label for this category. Internal lists must be in the same order - as bins in im_expected, e.g. first internal list has the expected - label options for class 0. + expected_labels: list of lists + Each sublist within the expected_labels list contains the word + or word variations expected to be found in the legend labels of + the plot being tested. Example list: [["gain", "increase"]] + would be provided if you wanted to test that the word "gain" OR + "increase" were found in the first legend element. + TODO: i think it's or but let's just clarify it's not "and" + TODO: we should have tests that check what happens if someone + provides only 2 sublist but there are three legend labels. + Sublists must be in the same order as the legend elements are + in. EXAMPLE: the first sublist will map to the first labeled item + in a plot legend. Returns ---------- - Nothing (if checks pass) or raises error + Nothing (if checks pass) or raises assertion error """ + + # TODO add test for a plot with no image in it. get_plot_image should + # return an error + # Retrieve image array im_data = self.get_plot_image() - assert list(im_data), "No Image Displayed" - - # Retrieve legend entries and find which element of all_label_options - # matches that entry + # TODO: We shouldn't need these tests because they happen in + # get_plot_image already. But we should improve the output message in + # get_plot image to be something more expressive + # assert list(im_data), "No Image Displayed" labels = self.get_legend_labels() - assert len(labels) == len(all_label_options), ( + # TODO: i think this should be a try, catch / return value error + assert len(labels) == len(expected_labels), ( "Number of label options provided doesn't match the number of" " labels found in the image." ) - labels_check = [ - self._which_label(label, all_label_options[i]) - for i, label in enumerate(labels) - ] - - # Check that each legend entry label is in one of all_label_options - assert all( - labels_check - ), "Provided legend labels don't match labels found." + # TODO: this currently only returns a list of values. It would be + # better if it returns a dictionary with the key being each + # label being tested and the value being a boolean (True if there + # is a match, False if there is no match) + + labels_dict = self._check_label(labels, expected_labels) + + # labels_check = [ + # self._check_label(label, expected_labels[i]) + # for i, label in enumerate(labels) + # ] + + # TODO: this now becomes a bit more tricky but we need to 1) test that + # each label is true - if one is false, then return useful message + # stating which label is incorrect. + + # Pull out any labels that failed the above test for final printing + # below + bad_labels = { + key: labels_dict[key] + for key in labels_dict + if not labels_dict[key] + } + + # TODO: raise assertion error (value error?) and print out a list of + # labels that are wrong ONLY if some are wrong + if bad_labels: + # get just the labels that are + bad_keys = [str(a_key) for a_key in bad_labels.keys()] + raise ValueError( + "Oops. It looks like atleast one of your legend " + "labels is incorrect. Double check the " + "following label(s): {" + "}".format(bad_keys) + ) + + # Check that each legend entry label is in one of expected_labels + # assert all( + # labels_check + # ), "Provided legend labels don't match labels found." + + # TODO: once we get the above working, let's then add another layer + # where we grab the RGB values and also add that to the dictionary + # At that point we can test whether the colors in the plot array, map + # to the legend patch colors and in turn the expected image # Check that expected and actual arrays data match up assert np.array_equal( im_data, im_expected ), "Expected image data doesn't match data in image." + # TODO: warning -- proj_create: init=epsg:/init=IGNF: syntax not + # supported in non-PROJ4 emulation mode - where is this coming from? + # IMAGE TESTS/HELPER FUNCTIONS def get_legend_labels(self): From 1dddb686a6609c58a850688e06e92c78540f01d6 Mon Sep 17 00:00:00 2001 From: Leah Wasser Date: Sun, 3 May 2020 16:30:53 -0600 Subject: [PATCH 10/10] flake 8 --- matplotcheck/raster.py | 178 ++++++++++++++++++++++++++++------------- 1 file changed, 124 insertions(+), 54 deletions(-) diff --git a/matplotcheck/raster.py b/matplotcheck/raster.py index b7cf69d0..3748847a 100644 --- a/matplotcheck/raster.py +++ b/matplotcheck/raster.py @@ -56,6 +56,43 @@ def assert_colorbar_range(self, crange): cb[0].vmax == crange[1] ), "Colorbar maximum is not expected value:{0}".format(crange[1]) + def get_legend_labels(self): + """Return labels from legend in a list + + Returns + ------- + labels: List + List of labels found in the legend of a raster plot. + """ + + # Retrieve legend + legends = self.get_legends() + # TODO add better error message -- make this a try except as + # get + # legends should return an error if no legends exist + assert legends, "No legend displayed" + + # Get each patch stored in the legends object + patches = [leg.get_patches() for leg in legends] + + # Grab rgb, alpha color and associated label for each patch + # TODO: this is a nested list because patches is returned as a list + # above. could the patches object every have more than one sublist? + # TODO because we are making this power case here we need to ensure + # the expected labels list are also lower case. then we need tests + # for upper and lower case labels in expected labels and in the + # plot legend to ensure this works. current it fails if upper case + # expected labels are provided but lowercase is in the legend. + # to simplify will + label_dict = {} + + # Iterate through each patch (legend box) and grab label and facecolor + for a_patch in patches[0]: + label = a_patch.get_label().lower() + label_dict[label] = {"color": a_patch.get_facecolor()} + + return label_dict + def _check_label(self, labels, expected_labels): """Helper function for assert_legend_labels Tests each label in the legend to see if the text in expected labels @@ -64,6 +101,7 @@ def _check_label(self, labels, expected_labels): Parameters ---------- # TODO: update all parameters and associated parameter description + # input -- dictionary now for labels object labels: string from legend to see if it contains an option in expected_labels expected_labels: list of lists @@ -88,19 +126,61 @@ def _check_label(self, labels, expected_labels): # if label_option == label: # return label_option - label_check = {} + label_check = labels.copy() # Iteratively test each label found in the plot legend to see if it is # in the list of expected labels - for i, label in enumerate(labels): - test_output = any( - label in expected_label + # Implementing dictionaries here! + for i, a_label in enumerate(labels.keys()): + # print(a_label) + # for expected_label in expected_labels[i]: + # test = a_label in expected_label + # print(a_label, expected_label) + # print(test) + + label_check[a_label]["match"] = any( + a_label in expected_label for expected_label in expected_labels[i] ) - label_check[label] = test_output + + # test2 = [a_label in expected_label + # for expected_label in expected_labels[i]] + # any(test2) + # print(expected_label) + + # for i, label in enumerate(labels): + # test_output = any( + # label in expected_label + # for expected_label in expected_labels[i] + # ) + # label_check[label] = test_output return label_check + def get_plot_image(self): + """Returns images stored on the Axes object as a list of numpy arrays. + + Returns + ------- + im_data: List + Numpy array of images stored on Axes object. + """ + im_data = [] + if self.ax.get_images(): + im = self.ax.get_images()[0] + im_data = im.get_array() + im_cmap = im.get_cmap() + + # TODO make this a better test (Try / except??) / return more + # expressive error + assert list(im_data), "No Image Displayed" + + # If image array has 3 dims (e.g. rgb image), remove alpha channel + if len(im_data.shape) == 3: + im_data = im_data[:, :, :3] + + return (im_data, im_cmap) + def assert_raster_legend_labels(self, im_expected, expected_labels): """Asserts legend correctly describes classified image on Axes ax, checking the legend labels and the values @@ -130,7 +210,7 @@ def assert_raster_legend_labels(self, im_expected, expected_labels): # return an error # Retrieve image array - im_data = self.get_plot_image() + im_data, im_cmap = self.get_plot_image() # TODO: We shouldn't need these tests because they happen in # get_plot_image already. But we should improve the output message in @@ -140,6 +220,7 @@ def assert_raster_legend_labels(self, im_expected, expected_labels): labels = self.get_legend_labels() # TODO: i think this should be a try, catch / return value error + # this still works as a dictionary as there is 3 keys assert len(labels) == len(expected_labels), ( "Number of label options provided doesn't match the number of" " labels found in the image." @@ -149,7 +230,7 @@ def assert_raster_legend_labels(self, im_expected, expected_labels): # better if it returns a dictionary with the key being each # label being tested and the value being a boolean (True if there # is a match, False if there is no match) - + # TODO: UPDATE CKECK LABEL to take input dictionary rather than list labels_dict = self._check_label(labels, expected_labels) # labels_check = [ @@ -157,16 +238,15 @@ def assert_raster_legend_labels(self, im_expected, expected_labels): # for i, label in enumerate(labels) # ] - # TODO: this now becomes a bit more tricky but we need to 1) test that - # each label is true - if one is false, then return useful message - # stating which label is incorrect. + # TODO: fix the dict comprehension below to grab the correct key for + # true / false # Pull out any labels that failed the above test for final printing # below bad_labels = { key: labels_dict[key] for key in labels_dict - if not labels_dict[key] + if not labels_dict[key]["match"] } # TODO: raise assertion error (value error?) and print out a list of @@ -188,9 +268,39 @@ def assert_raster_legend_labels(self, im_expected, expected_labels): # TODO: once we get the above working, let's then add another layer # where we grab the RGB values and also add that to the dictionary + # in the above we have the color and the label. now we need another + # dictionary that has the array value and map that to color. + # At that point we can test whether the colors in the plot array, map # to the legend patch colors and in turn the expected image + # Get image -- this can be a helper... + # TODO: remember how cmaps map to data in a np array. i believe we + # can pull from the earthpy legend function to help with this. + # We will need to know whether the vmin and vmax are modified in the + # plot i think as well... this could get tricky... + + # BEGIN WIP + # Essential what this should do is grab the colors used in the plot + # for each unique array value. NOTE that we may have to consider both + # continuous and non continuous data here (so arrays with 123, + # 012 or 0,4,7 as examples) We will need tests for all cases. + + # im_class_dict = {} + for val in np.unique(im_data): + print(val) + # We may need to handle a list different from an existin gcmap + # cmap_type = im_cmap.name + # im_class_dict[val] = legend_dict[im_cmap(im.norm(val))] + + # im_data_labels = [ + # [im_class_dict[val] for val in row] for row in im_data.data + # ] + # im_expected_labels = [ + # [all_label_options[val][0] for val in row] for row in im_expected + # ] + # END WIP + # Check that expected and actual arrays data match up assert np.array_equal( im_data, im_expected @@ -201,49 +311,6 @@ def assert_raster_legend_labels(self, im_expected, expected_labels): # IMAGE TESTS/HELPER FUNCTIONS - def get_legend_labels(self): - """Return labels from legend in a list - - Returns - ------- - labels: List - List of labels found in the legend of a raster plot. - """ - - # Retrieve legend - legends = self.get_legends() - assert legends, "No legend displayed" - - # Get each patch stored in the legends object - patches = [leg.get_patches() for leg in legends] - - # Go through each patch to retrieve the labels from the legend - return [ - label.get_label().lower() - for sublist in patches - for label in sublist - ] - - def get_plot_image(self): - """Returns images stored on the Axes object as a list of numpy arrays. - - Returns - ------- - im_data: List - Numpy array of images stored on Axes object. - """ - im_data = [] - if self.ax.get_images(): - im_data = self.ax.get_images()[0].get_array() - - assert list(im_data), "No Image Displayed" - - # If image array has 3 dims (e.g. rgb image), remove alpha channel - if len(im_data.shape) == 3: - im_data = im_data[:, :, :3] - - return im_data - def assert_image( self, im_expected, im_classified=False, m="Incorrect Image Displayed" ): @@ -264,6 +331,9 @@ def assert_image( ---------- Nothing (if checks pass) or raises error """ + # TODO this should be able to call the get_image helper above rather + # than recreating get image. + im_data = [] if self.ax.get_images(): im_data = self.ax.get_images()[0].get_array()