# -*- coding: utf-8 -*-
# Land_cover_plotting.py
"""
Plotting and animating Digital Earth Australia Land Cover data.
License: The code in this notebook is licensed under the Apache License,
Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0). Digital Earth
Australia data is licensed under the Creative Commons by Attribution 4.0
license (https://creativecommons.org/licenses/by/4.0/).
Contact: If you need assistance, please post a question on the Open Data
Cube Discord chat (https://discord.com/invite/4hhBQVas5U) or on the GIS Stack
Exchange (https://gis.stackexchange.com/questions/ask?tags=open-data-cube)
using the `open-data-cube` tag (you can view previously asked questions
here: https://gis.stackexchange.com/questions/tagged/open-data-cube).
If you would like to report an issue with this script, you can file one
on GitHub (https://github.com/GeoscienceAustralia/dea-notebooks/issues/new).
Last modified: January 2022
"""
import numpy as np
import pandas as pd
import ast
import sys
from IPython.display import Image
import matplotlib.pyplot as plt
from matplotlib import colors as mcolours
from matplotlib import patheffects
from matplotlib.animation import FuncAnimation
# Define colour schemes for each land cover measurement
lc_colours = {
'level3': {0: (255, 255, 255, 255, "No Data"),
111: (172, 188, 45, 255, "Cultivated Terrestrial\n Vegetation"),
112: (14, 121, 18, 255, "Natural Terrestrial\n Vegetation"),
124: (30, 191, 121, 255, "Natural Aquatic\n Vegetation"),
215: (218, 92, 105, 255, "Artificial Surface"),
216: (243, 171, 105, 255, "Natural Bare\n Surface"),
220: (77, 159, 220, 255, "Water")},
'level3_change_colour_scheme': {0: (255, 255, 255, 255, "No Change"),
111112: (14, 121, 18, 255, "CTV -> NTV"),
111215: (218, 92, 105, 255, "CTV -> AS"),
111216: (243, 171, 105, 255, "CTV -> BS"),
111220: (77, 159, 220, 255, "CTV -> Water"),
112111: (172, 188, 45, 255, "NTV -> CTV"),
112215: (218, 92, 105, 255, "NTV -> AS"),
112216: (243, 171, 105, 255, "NTV -> BS"),
112220: (77, 159, 220, 255, "NTV -> Water"),
124220: (77, 159, 220, 255, "NAV -> Water"),
215111: (172, 188, 45, 255, "AS -> CTV"),
215112: (14, 121, 18, 255, "AS -> NTV"),
215216: (243, 171, 105, 255, "AS -> BS"),
215220: (77, 159, 220, 255, "AS -> Water"),
216111: (172, 188, 45, 255, "BS -> CTV"),
216112: (14, 121, 18, 255, "BS -> NTV"),
216215: (218, 92, 105, 255, "BS -> AS"),
216220: (77, 159, 220, 255, "BS -> Water"),
220112: (14, 121, 18, 255, "Water -> NTV"),
220216: (243, 171, 105, 255, "Water -> BS")},
'level3_change_colour_bar': {0: (255, 255, 255, 255, "No change"),
111: (172, 188, 45, 255, "Changed to Cultivated\n Terrestrial Vegetation"),
112: (14, 121, 18, 255, "Changed to Natural\n Terrestrial Vegetation"),
124: (30, 191, 121, 255, "Changed to Natural\n Aquatic Vegetation"),
215: (218, 92, 105, 255, "Changed to Artificial\n Surface"),
216: (243, 171, 105, 255, "Changed to Natural\n Bare Surface"),
220: (77, 159, 220, 255, "Changed to Water")},
'lifeform_veg_cat_l4a': {0: (255, 255, 255, 255, "No Data /\n Not vegetated"),
1: (14, 121, 18, 255, "Woody Vegetation"),
2: (172, 188, 45, 255, "Herbaceous\n Vegetation")},
'canopyco_veg_cat_l4d': {0: (255, 255, 255, 255, "No Data /\n Not vegetated"),
10: (14, 121, 18, 255, "> 65 % cover"),
12: (45, 141, 47, 255, "40 to 65 % cover"),
13: (80, 160, 82, 255, "15 to 40 % cover"),
15: (117, 180, 118, 255, "4 to 15 % cover"),
16: (154, 199, 156, 255, "1 to 4 % cover")},
'waterstt_wat_cat_l4a': {0: (255, 255, 255, 255, "No Data /\n Not water"),
1: (77, 159, 220, 255, "Water")},
'watersea_veg_cat_l4a_au': {0: (255, 255, 255, 255, "No data /\n Not aquatic vegetation"),
1: (25, 173, 109, 255, "> 3 months"),
2: (176, 218, 201, 255, "< 3 months")},
'inttidal_wat_cat_l4a': {0: (255, 255, 255, 255, "No data /\n Not intertidal"),
3: (77, 159, 220, 255, "Intertidal")},
'waterper_wat_cat_l4d_au': {0: (255, 255, 255, 255, "No data /\n Not water"),
1: (27, 85, 186, 255, "> 9 months"),
7: (52, 121, 201, 255, "7 to 9 months"),
8: (79, 157, 217, 255, "4 to 6 months"),
9: (113, 202, 253, 255, "1 to 3 months")},
'baregrad_phy_cat_l4d_au': {0: (255, 255, 255, 255, "No data /\n Not bare"),
10: (255, 230, 140, 255, "Sparsely vegetated\n (< 20% bare)"),
12: (250, 210, 110, 255, "Very sparsely\n vegetated (20 to 60% bare)"),
15: (243, 171, 105, 255, "Bare areas,\n unvegetated (> 60% bare)")},
'level4': {0: (255, 255, 255, 255, "No Data"),
1: (151, 187, 26, 255, 'Cultivated Terrestrial\n Vegetated:'),
2: (151, 187, 26, 255, 'Cultivated Terrestrial\n Vegetated: Woody'),
3: (209, 224, 51, 255, 'Cultivated Terrestrial\n Vegetated: Herbaceous'),
4: (197, 168, 71, 255, 'Cultivated Terrestrial\n Vegetated: Closed\n (> 65 %)'),
5: (205, 181, 75, 255, 'Cultivated Terrestrial\n Vegetated: Open\n (40 to 65 %)'),
6: (213, 193, 79, 255, 'Cultivated Terrestrial\n Vegetated: Open\n (15 to 40 %)'),
7: (228, 210, 108, 255, 'Cultivated Terrestrial\n Vegetated: Sparse\n (4 to 15 %)'),
8: (242, 227, 138, 255, 'Cultivated Terrestrial\n Vegetated: Scattered\n (1 to 4 %)'),
# 9: (197, 168, 71, 255, 'Cultivated Terrestrial\n Vegetated: Woody Closed\n (> 65 %)'),
# 10: (205, 181, 75, 255, 'Cultivated Terrestrial\n Vegetated: Woody Open\n (40 to 65 %)'),
# 11: (213, 193, 79, 255, 'Cultivated Terrestrial\n Vegetated: Woody Open\n (15 to 40 %)'),
# 12: (228, 210, 108, 255, 'Cultivated Terrestrial\n Vegetated: Woody Sparse\n (4 to 15 %)'),
# 13: (242, 227, 138, 255, 'Cultivated Terrestrial\n Vegetated: Woody Scattered\n (1 to 4 %)'),
14: (228, 224, 52, 255, 'Cultivated Terrestrial\n Vegetated: Herbaceous Closed\n (> 65 %)'),
15: (235, 232, 84, 255, 'Cultivated Terrestrial\n Vegetated: Herbaceous Open\n (40 to 65 %)'),
16: (242, 240, 127, 255, 'Cultivated Terrestrial\n Vegetated: Herbaceous Open\n (15 to 40 %)'),
17: (249, 247, 174, 255, 'Cultivated Terrestrial\n Vegetated: Herbaceous Sparse\n (4 to 15 %)'),
18: (255, 254, 222, 255, 'Cultivated Terrestrial\n Vegetated: Herbaceous Scattered\n (1 to 4 %)'),
19: (14, 121, 18, 255, 'Natural Terrestrial Vegetated:'),
20: (26, 177, 87, 255, 'Natural Terrestrial Vegetated: Woody'),
21: (94, 179, 31, 255, 'Natural Terrestrial Vegetated: Herbaceous'),
22: (14, 121, 18, 255, 'Natural Terrestrial Vegetated: Closed (> 65 %)'),
23: (45, 141, 47, 255, 'Natural Terrestrial Vegetated: Open (40 to 65 %)'),
24: (80, 160, 82, 255, 'Natural Terrestrial Vegetated: Open (15 to 40 %)'),
25: (117, 180, 118, 255, 'Natural Terrestrial Vegetated: Sparse (4 to 15 %)'),
26: (154, 199, 156, 255, 'Natural Terrestrial Vegetated: Scattered (1 to 4 %)'),
27: (14, 121, 18, 255, 'Natural Terrestrial Vegetated: Woody Closed (> 65 %)'),
28: (45, 141, 47, 255, 'Natural Terrestrial Vegetated: Woody Open (40 to 65 %)'),
29: (80, 160, 82, 255, 'Natural Terrestrial Vegetated: Woody Open (15 to 40 %)'),
30: (117, 180, 118, 255, 'Natural Terrestrial Vegetated: Woody Sparse (4 to 15 %)'),
31: (154, 199, 156, 255, 'Natural Terrestrial Vegetated: Woody Scattered (1 to 4 %)'),
32: (119, 167, 30, 255, 'Natural Terrestrial Vegetated: Herbaceous Closed (> 65 %)'),
33: (136, 182, 51, 255, 'Natural Terrestrial Vegetated: Herbaceous Open (40 to 65 %)'),
34: (153, 196, 80, 255, 'Natural Terrestrial Vegetated: Herbaceous Open (15 to 40 %)'),
35: (170, 212, 113, 255, 'Natural Terrestrial Vegetated: Herbaceous Sparse (4 to 15 %)'),
36: (186, 226, 146, 255, 'Natural Terrestrial Vegetated: Herbaceous Scattered (1 to 4 %)'),
# 37: (86, 236, 231, 255, 'Cultivated Aquatic Vegetated:'),
# 38: (61, 170, 140, 255, 'Cultivated Aquatic Vegetated: Woody'),
# 39: (82, 231, 172, 255, 'Cultivated Aquatic Vegetated: Herbaceous'),
# 40: (43, 210, 203, 255, 'Cultivated Aquatic Vegetated: Closed (> 65 %)'),
# 41: (73, 222, 216, 255, 'Cultivated Aquatic Vegetated: Open (40 to 65 %)'),
# 42: (110, 233, 228, 255, 'Cultivated Aquatic Vegetated: Open (15 to 40 %)'),
# 43: (149, 244, 240, 255, 'Cultivated Aquatic Vegetated: Sparse (4 to 15 %)'),
# 44: (187, 255, 252, 255, 'Cultivated Aquatic Vegetated: Scattered (1 to 4 %)'),
# 45: (43, 210, 203, 255, 'Cultivated Aquatic Vegetated: Woody Closed (> 65 %)'),
# 46: (73, 222, 216, 255, 'Cultivated Aquatic Vegetated: Woody Open (40 to 65 %)'),
# 47: (110, 233, 228, 255, 'Cultivated Aquatic Vegetated: Woody Open (15 to 40 %)'),
# 48: (149, 244, 240, 255, 'Cultivated Aquatic Vegetated: Woody Sparse (4 to 15 %)'),
# 49: (187, 255, 252, 255, 'Cultivated Aquatic Vegetated: Woody Scattered (1 to 4 %)'),
# 50: (82, 231, 196, 255, 'Cultivated Aquatic Vegetated: Herbaceous Closed (> 65 %)'),
# 51: (113, 237, 208, 255, 'Cultivated Aquatic Vegetated: Herbaceous Open (40 to 65 %)'),
# 52: (144, 243, 220, 255, 'Cultivated Aquatic Vegetated: Herbaceous Open (15 to 40 %)'),
# 53: (175, 249, 232, 255, 'Cultivated Aquatic Vegetated: Herbaceous Sparse (4 to 15 %)'),
# 54: (207, 255, 244, 255, 'Cultivated Aquatic Vegetated: Herbaceous Scattered (1 to 4 %)'),
55: (30, 191, 121, 255, 'Natural Aquatic Vegetated:'),
56: (18, 142, 148, 255, 'Natural Aquatic Vegetated: Woody'),
57: (112, 234, 134, 255, 'Natural Aquatic Vegetated: Herbaceous'),
58: (25, 173, 109, 255, 'Natural Aquatic Vegetated: Closed (> 65 %)'),
59: (53, 184, 132, 255, 'Natural Aquatic Vegetated: Open (40 to 65 %)'),
60: (93, 195, 155, 255, 'Natural Aquatic Vegetated: Open (15 to 40 %)'),
61: (135, 206, 178, 255, 'Natural Aquatic Vegetated: Sparse (4 to 15 %)'),
62: (176, 218, 201, 255, 'Natural Aquatic Vegetated: Scattered (1 to 4 %)'),
63: (25, 173, 109, 255, 'Natural Aquatic Vegetated: Woody Closed (> 65 %)'),
64: (25, 173, 109, 255, 'Natural Aquatic Vegetated: Woody Closed (> 65 %) Water > 3 months (semi-) permenant'),
65: (25, 173, 109, 255, 'Natural Aquatic Vegetated: Woody Closed (> 65 %) Water < 3 months (temporary or seasonal)'),
66: (53, 184, 132, 255, 'Natural Aquatic Vegetated: Woody Open (40 to 65 %)'),
67: (53, 184, 132, 255, 'Natural Aquatic Vegetated: Woody Open (40 to 65 %) Water > 3 months (semi-) permenant'),
68: (53, 184, 132, 255, 'Natural Aquatic Vegetated: Woody Open (40 to 65 %) Water < 3 months (temporary or seasonal)'),
69: (93, 195, 155, 255, 'Natural Aquatic Vegetated: Woody Open (15 to 40 %)'),
70: (93, 195, 155, 255, 'Natural Aquatic Vegetated: Woody Open (15 to 40 %) Water > 3 months (semi-) permenant'),
71: (93, 195, 155, 255, 'Natural Aquatic Vegetated: Woody Open (15 to 40 %) Water < 3 months (temporary or seasonal)'),
72: (135, 206, 178, 255, 'Natural Aquatic Vegetated: Woody Sparse (4 to 15 %)'),
73: (135, 206, 178, 255, 'Natural Aquatic Vegetated: Woody Sparse (4 to 15 %) Water > 3 months (semi-) permenant'),
74: (135, 206, 178, 255, 'Natural Aquatic Vegetated: Woody Sparse (4 to 15 %) Water < 3 months (temporary or seasonal)'),
75: (176, 218, 201, 255, 'Natural Aquatic Vegetated: Woody Scattered (1 to 4 %)'),
76: (176, 218, 201, 255, 'Natural Aquatic Vegetated: Woody Scattered (1 to 4 %) Water > 3 months (semi-) permenant'),
77: (176, 218, 201, 255, 'Natural Aquatic Vegetated: Woody Scattered (1 to 4 %) Water < 3 months (temporary or seasonal)'),
78: (39, 204, 139, 255, 'Natural Aquatic Vegetated: Herbaceous Closed (> 65 %)'),
79: (39, 204, 139, 255, 'Natural Aquatic Vegetated: Herbaceous Closed (> 65 %) Water > 3 months (semi-) permenant'),
80: (39, 204, 139, 255, 'Natural Aquatic Vegetated: Herbaceous Closed (> 65 %) Water < 3 months (temporary or seasonal)'),
81: (66, 216, 159, 255, 'Natural Aquatic Vegetated: Herbaceous Open (40 to 65 %)'),
82: (66, 216, 159, 255, 'Natural Aquatic Vegetated: Herbaceous Open (40 to 65 %) Water > 3 months (semi-) permenant'),
83: (66, 216, 159, 255, 'Natural Aquatic Vegetated: Herbaceous Open (40 to 65 %) Water < 3 months (temporary or seasonal)'),
84: (99, 227, 180, 255, 'Natural Aquatic Vegetated: Herbaceous Open (15 to 40 %)'),
85: (99, 227, 180, 255, 'Natural Aquatic Vegetated: Herbaceous Open (15 to 40 %) Water > 3 months (semi-) permenant'),
86: (99, 227, 180, 255, 'Natural Aquatic Vegetated: Herbaceous Open (15 to 40 %) Water < 3 months (temporary or seasonal)'),
87: (135, 239, 201, 255, 'Natural Aquatic Vegetated: Herbaceous Sparse (4 to 15 %)'),
88: (135, 239, 201, 255, 'Natural Aquatic Vegetated: Herbaceous Sparse (4 to 15 %) Water > 3 months (semi-) permenant'),
89: (135, 239, 201, 255, 'Natural Aquatic Vegetated: Herbaceous Sparse (4 to 15 %) Water < 3 months (temporary or seasonal)'),
90: (171, 250, 221, 255, 'Natural Aquatic Vegetated: Herbaceous Scattered (1 to 4 %)'),
91: (171, 250, 221, 255, 'Natural Aquatic Vegetated: Herbaceous Scattered (1 to 4 %) Water > 3 months (semi-) permenant'),
92: (171, 250, 221, 255, 'Natural Aquatic Vegetated: Herbaceous Scattered (1 to 4 %) Water < 3 months (temporary or seasonal)'),
93: (218, 92, 105, 255, 'Artificial Surface:'),
94: (243, 171, 105, 255, 'Natural Surface:'),
95: (255, 230, 140, 255, 'Natural Surface: Sparsely vegetated'),
96: (250, 210, 110, 255, 'Natural Surface: Very sparsely vegetated'),
97: (243, 171, 105, 255, 'Natural Surface: Bare areas, unvegetated'),
98: (77, 159, 220, 255, 'Water:'),
99: (77, 159, 220, 255, 'Water: (Water)'),
100: (187, 220, 233, 255, 'Water: (Water) Tidal area'),
101: (27, 85, 186, 255, 'Water: (Water) Perennial (> 9 months)'),
102: (52, 121, 201, 255, 'Water: (Water) Non-perennial (7 to 9 months)'),
103: (79, 157, 217, 255, 'Water: (Water) Non-perennial (4 to 6 months)'),
104: (133, 202, 253, 255, 'Water: (Water) Non-perennial (1 to 3 months)'),
# 105: (250, 250, 250, 255, 'Water: (Snow)')
},
'level4_colourbar_labels': {0: (255, 255, 255, 255, "No Data"),
14: (228, 224, 52, 255, 'Cultivated Terrestrial Vegetated: Herbaceous Closed (> 65 %)'),
15: (235, 232, 84, 255, 'Cultivated Terrestrial Vegetated: Herbaceous Open (40 to 65 %)'),
16: (242, 240, 127, 255, 'Cultivated Terrestrial Vegetated: Herbaceous Open (15 to 40 %)'),
17: (249, 247, 174, 255, 'Cultivated Terrestrial Vegetated: Herbaceous Sparse (4 to 15 %)'),
18: (255, 254, 222, 255, 'Cultivated Terrestrial Vegetated: Herbaceous Scattered (1 to 4 %)'),
27: (14, 121, 18, 255, 'Natural Terrestrial Vegetated: Woody Closed (> 65 %)'),
28: (45, 141, 47, 255, 'Natural Terrestrial Vegetated: Woody Open (40 to 65 %)'),
29: (80, 160, 82, 255, 'Natural Terrestrial Vegetated: Woody Open (15 to 40 %)'),
30: (117, 180, 118, 255, 'Natural Terrestrial Vegetated: Woody Sparse (4 to 15 %)'),
31: (154, 199, 156, 255, 'Natural Terrestrial Vegetated: Woody Scattered (1 to 4 %)'),
32: (119, 167, 30, 255, 'Natural Terrestrial Vegetated: Herbaceous Closed (> 65 %)'),
33: (136, 182, 51, 255, 'Natural Terrestrial Vegetated: Herbaceous Open (40 to 65 %)'),
34: (153, 196, 80, 255, 'Natural Terrestrial Vegetated: Herbaceous Open (15 to 40 %)'),
35: (170, 212, 113, 255, 'Natural Terrestrial Vegetated: Herbaceous Sparse (4 to 15 %)'),
36: (186, 226, 146, 255, 'Natural Terrestrial Vegetated: Herbaceous Scattered (1 to 4 %)'),
65: (25, 173, 109, 255, 'Natural Aquatic Vegetated: Woody Closed (> 65 %)'),
68: (53, 184, 132, 255, 'Natural Aquatic Vegetated: Woody Open (40 to 65 %)'),
71: (93, 195, 155, 255, 'Natural Aquatic Vegetated: Woody Open (15 to 40 %)'),
74: (135, 206, 178, 255, 'Natural Aquatic Vegetated: Woody Sparse (4 to 15 %)'),
77: (176, 218, 201, 255, 'Natural Aquatic Vegetated: Woody Scattered (1 to 4 %)'),
80: (39, 204, 139, 255, 'Natural Aquatic Vegetated: Herbaceous Closed (> 65 %)'),
83: (66, 216, 159, 255, 'Natural Aquatic Vegetated: Herbaceous Open (40 to 65 %)'),
86: (99, 227, 180, 255, 'Natural Aquatic Vegetated: Herbaceous Open (15 to 40 %)'),
89: (135, 239, 201, 255, 'Natural Aquatic Vegetated: Herbaceous Sparse (4 to 15 %)'),
92: (171, 250, 221, 255, 'Natural Aquatic Vegetated: Herbaceous Scattered (1 to 4 %)'),
93: (218, 92, 105, 255, 'Artificial Surface'),
95: (255, 230, 140, 255, 'Natural Surface: Sparsely vegetated'),
96: (250, 210, 110, 255, 'Natural Surface: Very sparsely vegetated'),
97: (243, 171, 105, 255, 'Natural Surface: Bare areas, unvegetated'),
100: (187, 220, 233, 255, 'Water: (Water) Tidal area'),
101: (27, 85, 186, 255, 'Water: (Water) Perennial (> 9 months)'),
102: (52, 121, 201, 255, 'Water: (Water) Non-perennial (7 to 9 months)'),
103: (79, 157, 217, 255, 'Water: (Water) Non-perennial (4 to 6 months)'),
104: (133, 202, 253, 255, 'Water: (Water) Non-perennial (1 to 3 months)')},
}
def get_layer_name(measurement, da):
aliases = {
'lifeform': 'lifeform_veg_cat_l4a',
'vegetation_cover': 'canopyco_veg_cat_l4d',
'water_seasonality': 'watersea_veg_cat_l4a_au',
'water_state': 'waterstt_wat_cat_l4a',
'intertidal': 'inttidal_wat_cat_l4a',
'water_persistence': 'waterper_wat_cat_l4d_au',
'bare_gradation': 'baregrad_phy_cat_l4d_au',
'full_classification': 'level4',
'level_4': 'level4'
}
# Use provided measurement if able
measurement = measurement.lower() if measurement else da.name
measurement = aliases[measurement] if measurement in aliases.keys(
) else measurement
return measurement
[docs]
def make_colorbar(fig, ax, measurement, horizontal=False, animation=False):
"""
Adds a new colorbar with appropriate land cover colours and labels.
For DEA Land Cover Level 4 data, this function must be used with a double plot.
The 'ax' should be on the left side of the figure, and the colour bar will added
on the right hand side.
Parameters
----------
fig : matplotlib figure
Figure to add colourbar to
ax : matplotlib ax
Matplotlib figure ax to add colorbar to.
measurement : str
Land cover measurement to use for colour map and labels.
"""
# Create new axis object for colorbar
# parameters for add_axes are [left, bottom, width, height], in
# fractions of total plot
if measurement == 'level4' and animation == True:
# special spacing settings for level 4
cax = fig.add_axes([0.62, 0.10, 0.02, 0.80])
orient = 'vertical'
# get level 4 colour bar colour map ect
cb_cmap, cb_norm, cb_labels, cb_ticks = lc_colourmap('level4_colourbar_labels',
colour_bar=True)
elif measurement == 'level4' and animation == False:
# get level 4 colour bar colour map ect
cb_cmap, cb_norm, cb_labels, cb_ticks = lc_colourmap('level4_colourbar_labels',
colour_bar=True)
#move plot over to make room for colourbar
fig.subplots_adjust(right=0.825)
# Settings for axis positions
cax = fig.add_axes([0.84, 0.15, 0.02, 0.70])
orient = 'vertical'
else:
#for all other measurements
#move plot over to make room for colourbar
fig.subplots_adjust(right=0.825)
# Settings for different axis positions
if horizontal:
cax = fig.add_axes([0.02, 0.05, 0.90, 0.03])
orient = 'horizontal'
else:
cax = fig.add_axes([0.84, 0.15, 0.02, 0.70])
orient = 'vertical'
# get measurement colour bar colour map ect
cb_cmap, cb_norm, cb_labels, cb_ticks = lc_colourmap(measurement,
colour_bar=True)
img = ax.imshow([cb_ticks], cmap=cb_cmap, norm=cb_norm)
cb = fig.colorbar(img, cax=cax, orientation=orient)
cb.ax.tick_params(labelsize=12)
cb.set_ticks(cb_ticks + np.diff(cb_ticks, append=cb_ticks[-1]+1) / 2)
cb.set_ticklabels(cb_labels)
[docs]
def lc_colourmap(colour_scheme, colour_bar=False):
"""
Returns colour map and normalisation for the provided DEA Land Cover
measurement, for use in plotting with Matplotlib library
Parameters
----------
colour_scheme : string
Name of land cover colour scheme to use
Valid options: 'level3', 'level4', 'lifeform_veg_cat_l4a',
'canopyco_veg_cat_l4d', 'watersea_veg_cat_l4a_au',
'waterstt_wat_cat_l4a', 'inttidal_wat_cat_l4a',
'waterper_wat_cat_l4d_au', 'baregrad_phy_cat_l4d_au'.
colour_bar : bool, optional
Controls if colour bar labels are returned as a list for
plotting a colour bar. Default: False.
Returns
---------
cmap : matplotlib colormap
Matplotlib colormap containing the colour scheme for the
specified DEA Land Cover measurement.
norm : matplotlib colormap index
Matplotlib colormap index based on the discrete intervals of the
classes in the specified DEA Land Cover measurement. Ensures the
colormap maps the colours to the class numbers correctly.
cblables : array
A two dimentional array containing the numerical class values
(first dim) and string labels (second dim) of the classes found
in the chosen DEA Land Cover measurement.
"""
colour_scheme = colour_scheme.lower()
# Ensure a valid colour scheme was requested
# try:
assert (colour_scheme in lc_colours.keys(
)), f'colour scheme must be one of [{lc_colours.keys()}] (got "{colour_scheme}")'
# ('The dataset provided does not have a valid '
# 'name. Please specify which DEA Landcover measurement is being plotted '
# 'by providing the name using the "measurement" variable. For example (measurement = "full_classification")')
# Get colour definitions
lc_colour_scheme = lc_colours[colour_scheme]
# Create colour map
colour_arr = []
for key, value in lc_colour_scheme.items():
colour_arr.append(np.array(value[:-2]) / 255)
cmap = mcolours.ListedColormap(colour_arr)
bounds = list(lc_colour_scheme)
if colour_bar == True:
if colour_scheme == 'level4':
# Set colour labels to shortened level 4 list
lc_colour_scheme = lc_colours['level4_colourbar_labels']
cb_ticks = list(lc_colour_scheme)
cb_labels = []
for x in cb_ticks:
cb_labels.append(lc_colour_scheme[x][4])
bounds.append(bounds[-1]+1)
norm = mcolours.BoundaryNorm(np.array(bounds), cmap.N)
if colour_bar == False:
return (cmap, norm)
else:
return (cmap, norm, cb_labels, cb_ticks)
[docs]
def plot_land_cover(data, year=None, measurement=None, out_width=15, cols=4,):
"""
Plot a single land cover measurement with appropriate colour scheme.
Parameters
----------
data : xarray.DataArray
A dataArray containing a DEA Land Cover classification.
year : int, optional
Can be used to select to plot a specific year. If not provided,
all time slices are plotted.
measurement : string, optional
Name of the DEA land cover classification to be plotted. Passed to
lc_colourmap to specify which colour scheme will be used. If non
provided, reads data array name from `da` to determine.
"""
# get measurement name
measurement = get_layer_name(measurement, data)
# get colour map, normalisation
try:
cmap, norm = lc_colourmap(measurement)
except AssertionError:
raise KeyError('Could not automatically determine colour scheme from'
f'DataArray name {measurement}. Please specify which '
'DEA Landcover measurement is being plotted by providing'
'the name using the "measurement" variable For example'
'(measurement = "full_classification")')
height, width = data.geobox.shape
scale = out_width / width
if year:
#plotting protocall if 'year' variable is passed
year_string = f"{year}-01-01"
data = data.sel(time=year_string, method="nearest")
fig, ax = plt.subplots()
fig.set_size_inches(width * scale, height * scale)
make_colorbar(fig, ax, measurement)
im = ax.imshow(data, cmap=cmap, norm=norm, interpolation="nearest")
elif len(data.time) == 1:
#plotting protocall if only one timestep is passed and not a year variable
fig, ax = plt.subplots()
fig.set_size_inches(width * scale, height * scale)
make_colorbar(fig, ax, measurement)
im = ax.imshow(data.isel(time=0), cmap=cmap, norm=norm, interpolation="nearest")
else:
#plotting protocall if multible time steps are passed to plot
if cols > len(data.time):
cols = len(data.time)
rows = int((len(data.time) + cols-1)/cols)
fig, ax = plt.subplots(nrows=rows, ncols=cols)
fig.set_size_inches(
width * scale, (height * scale / cols) * (len(data.time) / cols))
make_colorbar(fig, ax.flat[0], measurement)
for a, b in enumerate(ax.flat):
if a < data.shape[0]:
im = b.imshow(data[a], cmap=cmap, norm=norm,
interpolation="nearest")
return im
[docs]
def lc_animation(
da,
file_name="default_animation",
measurement=None,
stacked_plot=False,
colour_bar=False,
animation_interval=500,
width_pixels=10,
dpi=150,
font_size=15,
label_ax=True):
"""
Creates an animation of DEA Landcover though time beside
corresponding stacked plots of the landcover classes. Saves the
animation to a file and displays the animation in notebook.
Parameters
----------
da : xarray.DataArray
An xarray.DataArray containing a multi-date stack of
observations of a single landcover level.
file_name: string, optional.
string used to create filename for saved animation file.
Default: "default_animation" code adds .gif suffix.
measurement : string, optional
Name of the DEA land cover classification to be plotted. Passed to
lc_colourmap to specify which colour scheme will ve used. If non
provided, reads data array name from `da` to determine.
stacked_plot: boolean, optional
Determines if a stacked plot showing the percentage of area
taken up by each class in each time slice is added to the
animation. Default: False.
colour_bar : boolean, Optional
Determines if a colour bar is generated for the stand alone
animation. This is NOT recommended for use with level 4 data.
Does not work with stacked plot. Default: False.
animation_interval : int , optional
How quickly the frames of the animations should be re-drawn.
Default: 500.
width_pixels : int, optional
How wide in pixles the animation plot should be. Default: 10.
dpi : int, optional
Stands for 'Dots Per Inch'. Passed to the fuction that saves the
animation and determines the resolution. A higher number will
produce a higher resolution image but a larger file size and
slower processing. Default: 150.
font_size : int, optional.
Controls the size of the text which indicates the year
displayed. Default: 15.
label_ax : boolean, optional
Determines if animation plot should have tick marks and numbers
on axes. Also removes white space around plot. default: True
Returns
-------
A GIF (.gif) animation file.
"""
def calc_class_ratio(da):
"""
Creates a table listing year by year what percentage of the
total area is taken up by each class.
Parameters
----------
da : xarray.DataArray with time dimension
Returns
-------
Pandas Dataframe : containing class percentages per year
"""
# list all class codes in dataset
list_classes = (np.unique(da, return_counts=False)).tolist()
# create empty dataframe & dictionary
ratio_table = pd.DataFrame(data=None, columns=list_classes)
date_line = {}
# count all pixels, should be consistent
total_pix = int(np.sum(da.isel(time=1)))
# iterate through each year in dataset
for i in range(0, len(da.time)):
date = str(da.time[i].data)[0:10]
# for each year iterate though each present class number
# and count pixels
for n in list_classes:
number_of_pixles = int(np.sum(da.isel(time=i) == n))
percentage = number_of_pixles / total_pix * 100
date_line[n] = percentage
# add each year's counts to dataframe
ratio_table.loc[date] = date_line
return ratio_table
def rgb_to_hex(r, g, b):
hex = "#%x%x%x" % (r, g, b)
if len(hex) < 7:
hex = "#0" + hex[1:]
return hex
measurement = get_layer_name(measurement, da)
# Add gif to end of filename
file_name = file_name + ".gif"
# Create colour map and normalisation for specified lc measurement
try:
layer_cmap, layer_norm, cb_labels, cb_ticks = lc_colourmap(
measurement, colour_bar=True)
except AssertionError:
raise KeyError(f'Could not automatically determine colour scheme from '
f'DataArray name {measurement}. Please specify which '
'DEA Landcover measurement is being plotted by providing '
'the name using the "measurement" variable For example '
'(measurement = "full_classification")')
# Prepare variables needed
# Get info on dataset dimensions
height, width = da.geobox.shape
scale = width_pixels / width
left, bottom, right, top = da.geobox.extent.boundingbox
extent = [left, right, bottom, top]
outline = [patheffects.withStroke(linewidth=2.5, foreground="black")]
annotation_defaults = {
"xy": (1, 1),
"xycoords": "axes fraction",
"xytext": (-5, -5),
"textcoords": "offset points",
"horizontalalignment": "right",
"verticalalignment": "top",
"fontsize": font_size,
"color": "white",
"path_effects": outline,
}
# Get information needed to display the year in the top corner
times_list = da.time.dt.strftime("%Y").values
text_list = [False] * len(times_list)
annotation_list = ["\n".join([str(i) for i in (a, b) if i])
for a, b in zip(times_list, text_list)]
if stacked_plot == True:
# Create table for stacked plot
stacked_plot_table = calc_class_ratio(da)
# Build colour list of hex vals for stacked plot
hex_colour_list = []
colour_def = lc_colours[measurement]
# Custom error message to help if user puts incorrect measurement name
for val in list(stacked_plot_table):
try:
r, g, b = colour_def[val][0:3]
except KeyError:
raise KeyError(
"class number not found in colour definition. "
"Ensure measurement name provided matches the dataset being used")
hex_val = rgb_to_hex(r, g, b)
hex_colour_list.append(hex_val)
# Define & set up figure
fig, (ax1, ax2) = plt.subplots(1, 2, dpi=dpi, constrained_layout=True)
fig.set_size_inches(width * scale * 2, height * scale, forward=True)
fig.set_constrained_layout_pads(
w_pad=0.2, h_pad=0.2, hspace=0, wspace=0)
# This function is called at regular intervals with changing i
# values for each frame
def _update_frames(i, ax1, ax2, extent, annotation_text,
annotation_defaults, cmap, norm):
# Clear previous frame to optimise render speed and plot imagery
ax1.clear()
ax2.clear()
ax1.imshow(da[i, ...], cmap=cmap, norm=norm,
extent=extent, interpolation="nearest")
if(not label_ax):
ax1.set_axis_off()
clipped_table = stacked_plot_table.iloc[: int(i + 1)]
data = clipped_table.to_dict(orient="list")
date = clipped_table.index
ax2.stackplot(date, data.values(), colors=hex_colour_list)
ax2.tick_params(axis="x", labelrotation=-45)
ax2.margins(x=0, y=0)
# Add annotation text
ax1.annotate(annotation_text[i], **annotation_defaults)
ax2.annotate(annotation_text[i], **annotation_defaults)
# anim_fargs contains all the values we send to our
# _update_frames function.
# Note the layer_cmap and layer_norm which were calculated
# earlier being passed through
anim_fargs = (
ax1,
ax2, # axis to plot into
[left, right, bottom, top], # imshow extent
annotation_list,
annotation_defaults,
layer_cmap,
layer_norm,
)
else: # stacked_plot = False
# if plotting level 4 with colourbar
if measurement == 'level4' and colour_bar == True:
# specific setting to fit level 4 colour bar beside the plot
# we will plot the animation in the left hand plot
# and put the colour bar on the right hand side
# Define & set up figure, two subplots so colour bar fits :)
fig, (ax1, ax2) = plt.subplots(1, 2, dpi=dpi,
constrained_layout=True, gridspec_kw={'width_ratios': [3, 1]})
fig.set_size_inches(width * scale * 2,
height * scale, forward=True)
fig.set_constrained_layout_pads(
w_pad=0.2, h_pad=0.2, hspace=0, wspace=0)
# make colour bar
# provide left hand canvas to colour bar fuction which is where the image will go
# colourbar will plot on right side beside it
make_colorbar(fig, ax1, measurement, animation=True)
# turn off lines for second plot so it's not ontop of colourbar
ax2.set_axis_off()
# plotting any other measurement with or with-out colour bar or level 4 without
else:
# Define & set up figure
fig, ax1 = plt.subplots(1, 1, dpi=dpi)
fig.set_size_inches(width * scale, height * scale, forward=True)
if(not label_ax):
fig.subplots_adjust(left=0, bottom=0, right=1,
top=1, wspace=None, hspace=None)
# Add colourbar here
if colour_bar:
make_colorbar(fig, ax1, measurement)
# This function is called at regular intervals with changing i
# values for each frame
def _update_frames(i, ax1, extent, annotation_text,
annotation_defaults, cmap, norm):
# Clear previous frame to optimise render speed and plot imagery
ax1.clear()
ax1.imshow(da[i, ...], cmap=cmap, norm=norm,
extent=extent, interpolation="nearest")
if(not label_ax):
ax1.set_axis_off()
# Add annotation text
ax1.annotate(annotation_text[i], **annotation_defaults)
# anim_fargs contains all the values we send to our
# _update_frames function.
# Note the layer_cmap and layer_norm which were calculated
# earlier being passed through
anim_fargs = (
ax1,
[left, right, bottom, top], # imshow extent
annotation_list,
annotation_defaults,
layer_cmap,
layer_norm,
)
# Animate
anim = FuncAnimation(
fig=fig,
func=_update_frames,
fargs=anim_fargs,
frames=len(da.time),
interval=animation_interval,
repeat=False,
)
anim.save(file_name, writer="pillow", dpi=dpi)
plt.close()
return Image(filename=file_name)