Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New MaskSet.get_cmap() implementation #1019

Merged
merged 8 commits into from
Jun 24, 2024
Merged

Conversation

forman
Copy link
Member

@forman forman commented Jun 19, 2024

Closes #1011

@konstntokas please also test with original LC data in last webinar notebook.

Checklist:

  • Add unit tests and/or doctests in docstrings
  • Add docstrings and API docs for any new/modified user-facing classes and functions
  • New/modified features documented in docs/source/*
  • Changes documented in CHANGES.md
  • GitHub CI passes
  • AppVeyor CI passes
  • Test coverage remains or increases (target 100%)

@forman forman requested a review from konstntokas June 19, 2024 09:23
@forman forman marked this pull request as ready for review June 19, 2024 09:39
Copy link
Contributor

@konstntokas konstntokas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still have the same problem that the user will need to add vmin and vmax manually. I find this, as a user, more confusing then getting vmin and vmax from the get_cmap method. See examples below:

land_cover_mask = MaskSet(land_cover_2019.lccs_class)
cmap = land_cover_mask.get_cmap()
land_cover_2019.lccs_class.plot(cmap=cmap)

landcover

land_cover_mask = MaskSet(land_cover_2019.lccs_class)
cmap = land_cover_mask.get_cmap()
land_cover_2019.lccs_class.plot(cmap=cmap, vmin=0, vmax=220)
plt.show()

landcover_vmin_vax

The colormap is not really nice, but this method at least buts the values on the correct position. I did something like:

def get_cmap(maskset):
    vals = maskset._flag_values
    extent = vals.max() - vals.min()
    vals_norm = (vals - vals.min()) / extent
    colors = [(0, 0, 0, 0)] + maskset._flag_colors
    color_mapping = [(v, c) for v, c in zip(vals_norm, colors)]
    for v, c in zip(vals, colors):
        print(v, c)
    return matplotlib.colors.LinearSegmentedColormap.from_list(
        str(maskset._flag_var.name), color_mapping, N=len(vals)
    )

land_cover_mask = MaskSet(land_cover_2019.lccs_class)
cmap = get_cmap(land_cover_mask)
land_cover_2019.lccs_class.plot(cmap=cmap, vmin=0, vmax=220)
plt.show()

landcover_oldcmap

This gives an aesthetically nicer colormap but distorts the actual values of course. I am indecisive whats better.

Still we should do something about vmin and vmax, I think. Otherwise a lot of users will run into problems. Would it be possible to add a plotting routine to MaskSet?

xcube/core/maskset.py Outdated Show resolved Hide resolved
def _sanitize_flag_values(
flag_var: xr.DataArray,
flag_values: np.ndarray,
) -> Optional[np.ndarray]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why Optional?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to remove.

@forman
Copy link
Member Author

forman commented Jun 20, 2024

We still have the same problem that the user will need to add vmin and vmax manually.

Then, xarray is not calling the color map in the right way. The new color map is a matplotlib.colors.ListedColormap and it's documentation (and ensured by our unit tests) says:

This may be most useful when indexing directly into a colormap

I find this, as a user, more confusing then getting vmin and vmax from the get_cmap method.

That's right!

@forman
Copy link
Member Author

forman commented Jun 20, 2024

The colormap is not really nice, but this method at least buts the values on the correct position.

This gives an aesthetically nicer colormap but distorts the actual values of course. I am indecisive whats better.

The Land Cover color mapping is not about aesthetics. The ultimate goal is to have an exact match between flag values and assigned color. It is really a pity that the provided ListedColormap is not used correctly by xarray. I'll need to update the implementation and also return a (cmap, norm) pair then, similar to your code.

Co-authored-by: Konstantin Ntokas <38956538+konstntokas@users.noreply.github.com>
@forman
Copy link
Member Author

forman commented Jun 20, 2024

Still we should do something about vmin and vmax, I think.

Yes, return a (cmap, norm) pair, see above.

Would it be possible to add a plotting routine to MaskSet?

Yes, but let's keep it simple for time being. I will change impl. to return the pair.

@konstntokas
Copy link
Contributor

konstntokas commented Jun 21, 2024

Including matplotlib.colors.BoundaryNorm the colormap looks like this. The numbers are of course confusing. We would need to create the plot and assign the ticks to the center of each discrete color and assign the tick label to either the flag value or even flag name.

Code snippet:

land_cover_mask = MaskSet(land_cover_2019.lccs_class)
cmap, norm = land_cover_mask.get_cmap()
land_cover_2019.lccs_class.plot.imshow(cmap=cmap, norm=norm)
plt.show()

landcover_bound_norm

Copy link
Member Author

@forman forman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See suggestions

Comment on lines 208 to 209
An suitable instance of ```matplotlib.colors.Colormap``` and
the corresponding ```matplotlib.colors.BoundaryNorm``` if applicable
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
An suitable instance of ```matplotlib.colors.Colormap``` and
the corresponding ```matplotlib.colors.BoundaryNorm``` if applicable
An suitable instance of ``matplotlib.colors.Colormap`` and
the corresponding ``matplotlib.colors.BoundaryNorm`` if applicable


if flag_value_min >= 0 and flag_value_max < _UINT16_MAX:
flag_colors = self._flag_colors
levels = np.append(flag_values - 0.5, flag_values[-1] + 0.5)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 0.5 shift?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shift is removed, see plot below.

@konstntokas
Copy link
Contributor

konstntokas commented Jun 24, 2024

The plot now looks like this without the shift of 0.5 as suggested by @forman:

Code snippet

land_cover_mask = MaskSet(land_cover_2019.lccs_class)
cmap, norm = land_cover_mask.get_cmap()
land_cover_2019.lccs_class.plot.imshow(cmap=cmap, norm=norm)
plt.show()

landcover_bound_norm

@forman
Copy link
Member Author

forman commented Jun 24, 2024

I approve & merge, broken CI has other reasons.

@forman forman requested a review from konstntokas June 24, 2024 09:08
@forman forman merged commit 243ccc4 into main Jun 24, 2024
1 of 3 checks passed
@konstntokas konstntokas deleted the forman-1011-cmap_in_mask_set_2 branch June 25, 2024 08:28
@konstntokas konstntokas mentioned this pull request Jul 18, 2024
7 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Provide color map for mask set with flag_values
2 participants