Skip to content

Commit

Permalink
v.boxplot and r.boxplot: plot/layout options added / changed (#1271)
Browse files Browse the repository at this point in the history
* v.boxplot and r.boxplot: plot/layout options added / changed

Following suggestions in #1071
- Adding plot and layout options to v.boxplot
- Changes in parameter names of v.boxplot and r.boxplot

* v.boxplot: add layout/plot options

- Add option
  - to set limits of value axis
  - add grid lines
- Reorganize tabs
- Lazy loading matplotlib
- Fix: selected colors checked and changed to matplotlib format

* v.boxplot and r.boxplot: solve issue with running addons in VS Code

Running the addons in VS Code results in an error related with `matplotlib.use("WXAgg")`. A possible solution is using another backend like `WebAgg`. However, running VS Code probably means one wants to print the resulting graph to file. As in that case, the backend is not needed, we can just avoid loading it when the option to save the graph to file is selected.

* correction code
  • Loading branch information
ecodiv authored Dec 24, 2024
1 parent fecefcc commit 02f9ac3
Show file tree
Hide file tree
Showing 4 changed files with 405 additions and 144 deletions.
143 changes: 77 additions & 66 deletions src/raster/r.boxplot/r.boxplot.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ <h2>DESCRIPTION</h2>
values of the input raster that fall within that zone.

<p>
If there is a zonal map, the user can add a line and band to
represent the median and interquartile range (IQR) of the input layer. Note
that all values of the input raster (within the region's extent)
are used to compute the median and IQR. If the zones of the zonal map
cover only part of the region, the user can mask out the non-covered
parts of the input map first by means of <em>r.mask</em>. That will result in
an IQR and median representing the values that fall within the zones of
the zonal map only. Otherwise, the computational region can be changed to
If there is a zonal map, the user can add a line and band to represent
the median and interquartile range (IQR) of the input layer. Note that
all values of the input raster (within the region's extent) are used to
compute the median and IQR. If the zones of the zonal map cover only
part of the region, the user can mask out the non-covered parts of the
input map first by means of <em>r.mask</em>. That will result in an IQR
and median representing the values that fall within the zones of the
zonal map only. Otherwise, the computational region can be changed to
fit the extent of the zonal map with <em>g.region</em>

<p>
Expand All @@ -24,16 +24,16 @@ <h2>DESCRIPTION</h2>
output = outputfile.png, the plot will be saved as a PNG file.

<p>
The whiskers extend to the most extreme data point, which is no
more than <b>range</b> &#10005; the IQR from the box.
By default, a <b>range</b> of 1.5 is used, but the user can change
this. Note that range values need to be larger than 0.
The whiskers extend to the most extreme data point, which is no more
than <b>range</b> &#10005; the IQR from the box. By default, a
<b>range</b> of <tt>1.5</tt> is used, but the user can change this.
Note that range values need to be larger than <tt>0</tt>.

<p>
By default, outliers are not included in the plot. Set the <b>o</b> flag
to include them in the plot. To also create a point vector map with the
locations of the outliers, the user needs to provide the name of the
output map using <b>map_outliers</b>.
By default, outliers are not included in the plot. Set the <b>-o</b>
flag to include them in the plot. To also create a point vector map
with the locations of the outliers, the user needs to provide the name
of the output map using <b>map_outliers</b>.

<p>
There are a few layout options, including the option to rotate the
Expand All @@ -47,31 +47,32 @@ <h2>DESCRIPTION</h2>

<h2>NOTE</h2>

The <em>r.boxplot</em> module operates on the raster array defined by the
current region settings, not the original extent and resolution of the
input map. See <a
href="https://grass.osgeo.org/grass-stable/manuals/r.univar.html">g.region</a>
The <em>r.boxplot</em> module operates on the raster array defined by
the current region settings, not the original extent and resolution of
the input map. See <a
href="https://grass.osgeo.org/grass-stable/manuals/r.univar.html">g.region</a>
to understand the impact of the region settings on the calculations.

To include outliers, the function converts the raster
cell with outlier values to a point vector layer. This may take some
time if there are a lot of outliers. So, if users are working with very
large raster layers, they should be cautious to not set the <b>range</b> value
too low as that may result in a huge number of outliers.
To include outliers, the function converts the raster cell with outlier
values to a point vector layer. This may take some time if there are a
lot of outliers. So, if users are working with very large raster
layers, they should be cautious to not set the <b>range</b> value too
low as that may result in a huge number of outliers.

<p>
The zonal map needs to be an integer map. If it is not, the function will exit
with the error message, 'The zonal raster must be of type CELL (integer)'.

<p>
If the <b>c</b> flag is used, the <b>bxp_color</b> and <b>median_color</b>
are ignored, even if set by the user. The option to color boxploxs using the colors
of the zonal raster categories (<b>c</b> flag) only works if the zonal
map contains a color table. If it does not, the function exits with the
error message that 'The zonal map does not have a color table'. If the user
thinks there is a color table, run <em>r.colors.out</em> and check if the
categories are integers. If not, that is the problem. If they are all
integers, you probably have caught a bug.
If the <b>-c</b> flag is used, the <b>bxp_color</b> and
<b>median_color</b> are ignored, even if set by the user. The option to
color boxploxs using the colors of the zonal raster categories
(<b>c</b> flag) only works if the zonal map contains a color table. If
it does not, the function exits with the error message that 'The zonal
map does not have a color table'. If the user thinks there is a color
table, run <em>r.colors.out</em> and check if the categories are
integers. If not, that is the problem. If they are all integers, you
probably have caught a bug.

<p>
The module respects the mask (if set), and the region settings. This
Expand All @@ -81,9 +82,9 @@ <h2>NOTE</h2>
<h2>EXAMPLE</h2>

<h3>Example 1</h3>
Draw a boxplot of the values of the <i>elevation</i> layer from the
Draw a boxplot of the values of the <tt>elevation</tt> layer from the
<a href="https://grass.osgeo.org/download/data/">NC sample
dataset</a>. Set the <b>h</b> flag to print the boxplot horizontally.
dataset</a>. Set the <b>-h</b> flag to print the boxplot horizontally.
Set the plot dimensions to 7 inch wide, 1 inch high.

<div class="code"><pre>
Expand All @@ -95,11 +96,10 @@ <h3>Example 1</h3>
<p>
<img src="r_boxplot_01.png"><br>

<h3>Example 2</h3>
Draw boxplots of the values of the <i>elevation</i> layer per category from
the <i>landclass96</i> layer from the same
<a href="https://grass.osgeo.org/download/data/">NC sample
dataset</a>. Use the <b>r</b> flag to rotate the x-asis labels.
<h3>Example 2</h3> Draw boxplots of the values of the
<tt>elevation</tt> layer per category from the <tt>landclass96</tt>
layer from the same <a href="https://grass.osgeo.org/download/data/">NC
sample dataset</a>. Use the <b>-r</b> flag to rotate the x-asis labels.

<div class="code"><pre>
r.boxplot -r input=elevation zone=landclass96 output="r_boxplot_02.png"
Expand All @@ -110,12 +110,14 @@ <h3>Example 2</h3>
<img src="r_boxplot_02.png"><br>

<h3>Example 3</h3>
Draw boxplots of the values of the <i>elevation</i> layer per category from
the <i>landclass96</i> layer from the same
<a href="https://grass.osgeo.org/download/data/">NC sample
dataset</a>. Set the <b>o</b> flag to include outliers. Use <b>bx_sort=ascending</b>
to order the boxplots from low to high median. Provide a name for the outlier
map to save the outlier locations as a point vector map.

Draw boxplots of the values of the <tt>elevation</tt> layer per
category from the <tt>landclass96</tt> layer from the same <a
href="https://grass.osgeo.org/download/data/">NC sample dataset</a>.
Set the <b>-o</b> flag to include outliers. Use
<b>bx_sort=ascending</b> to order the boxplots from low to high median.
Provide a name for the outlier map to save the outlier locations as a
point vector map.

<div class="code"><pre>
r.boxplot -o bx_sort=ascending input=elevation zones=landclass96 output="r_boxplot_03.png" map_outliers="outliers"
Expand All @@ -126,48 +128,54 @@ <h3>Example 3</h3>
<img src="r_boxplot_03.png"><br>

<p>
Below, part of the <i>landclass96</i> raster map is shown, with the vector point
layer with location of outliers on top. Curiously, for some lakes, only part of the
raster cells are outliers.</p>
Below, part of the <tt>landclass96</tt> raster map is shown, with the
vector point layer with location of outliers on top. Curiously, for
some lakes, only part of the raster cells are outliers.

<p>
<img src="r_boxplot_map_03.png"><br>

<h3>Example 4</h3>
Draw boxplots of the values of the <i>elevation</i> layer per category from
the <i>landclass96</i> layer from the same
<a href="https://grass.osgeo.org/download/data/">NC sample
dataset</a>. Set the <b>c</b> flag to color the boxplots, use <b>bx_sort=ascending</b>
to order the boxplots from low to high median, and set the font size to 11.

Draw boxplots of the values of the <tt>elevation</tt> layer per
category from the <tt>landclass96</tt> layer from the same <a
href="https://grass.osgeo.org/download/data/">NC sample dataset</a>.
Set the <b>-c</b> flag to color the boxplots, use
<b>bx_sort=ascending</b> to order the boxplots from low to high median,
and set the font size to 11.

<div class="code"><pre>
r.boxplot -c bx_sort=ascending fontsize=11 input=elevation zones=landclass96 output="r_boxplot_04.png"
</pre>
</div><br>

<h3>Example 5</h3>

To make it easier to compare the elevation distribution across the different
land classes, you can plot a line and band representing the
median and interquartile range of the whole raster layer.
land classes, you can plot a line and band representing the median and
interquartile range of the whole raster layer.

<div class="code"><pre>
r.boxplot -c input=elevation zones=landclass96 raster_statistics=median,IQR
</pre>
</div><br>

<p>
Note, if the zones of your zonal map do not cover the entire area, you may want
to use <em>r.mask</em> to mask out the non-covered parts of the input map, or
alternatively, create a new input raster with only values within the zones
of the zonal layer.
Note, if the zones of your zonal map do not cover the entire area, you
may want to use <em>r.mask</em> to mask out the non-covered parts of
the input map, or alternatively, create a new input raster with only
values within the zones of the zonal layer.

<p>
<img src="r_boxplot_05.png"><br>

<h2>Acknowledgements</h2>
This work was carried in the framework of the <a href="https://savethetiger.nl/" target="_blank">Save the tiger, save the grassland, save the water</a>
project by the
<a href="https://www.has.nl/en/has-research/research-groups/innovative-bio-monitoring-research-group" target="_blank">Innovative Bio-Monitoring research group</a>.

This work was carried in the framework of the <a
href="https://savethetiger.nl/" target="_blank">Save the tiger, save
the grassland, save the water</a> project by the <a
href="https://www.has.nl/en/has-research/research-groups/innovative-bio-monitoring-research-group"
target="_blank">Innovative Bio-Monitoring research group</a>.

<h2>SEE ALSO</h2>

Expand All @@ -179,6 +187,9 @@ <h2>SEE ALSO</h2>

<h2>AUTHOR</h2>

Paulo van Breugel<br>
Applied Geo-information Sciences<br>
<a href="https://www.hasuniversity.nl/">HAS University of Applied Sciences</a><br>
<a href="https://ecodiv.earth">Paulo van Breugel</a>, <a
href="https://has.nl">HAS green academy</a>, <a
href="https://www.has.nl/en/research/professorships/innovative-bio-monitoring-professorship/">Innovative
Biomonitoring research group</a>, <a
href="https://www.has.nl/en/research/professorships/climate-robust-landscapes-professorship/">Climate-robust
Landscapes research group</a>
35 changes: 17 additions & 18 deletions src/raster/r.boxplot/r.boxplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,8 @@
# %end

# %option G_OPT_R_MAP
# % key: input
# % description: input raster
# % required: yes
# % label: Input raster
# % guisection: Input
# % required: yes
# %end

# %option G_OPT_R_MAP
Expand Down Expand Up @@ -122,7 +119,7 @@
# %end

# %option
# % key: bx_sort
# % key: order
# % type: string
# % label: Sort boxplots
# % description: Sort boxplots based on their median values
Expand Down Expand Up @@ -291,7 +288,7 @@
clean_maps = []


def lazy_import_py_modules():
def lazy_import_py_modules(backend):
"""Lazy import Py modules"""
global matplotlib
global plt
Expand All @@ -300,7 +297,8 @@ def lazy_import_py_modules():
try:
import matplotlib

matplotlib.use("WXAgg")
if backend is None:
matplotlib.use("WXAgg")
from matplotlib import pyplot as plt
except ModuleNotFoundError:
gs.fatal(_("Matplotlib is not installed. Please, install it."))
Expand Down Expand Up @@ -538,7 +536,7 @@ def get_zonalcolors(zones, labelsids):
return zones_rgb, txt_rgb


def bx_zonal_stats(zones, name, bx_sort):
def bx_zonal_stats(zones, name, order):
"""Compute the zonal stats to construct the boxplot (and order boxplots)
:param str zones: name of the zonal map
Expand Down Expand Up @@ -567,9 +565,9 @@ def bx_zonal_stats(zones, name, bx_sort):
for zone_id, value in enumerate(quantstats):
ids.append(zone_id)
medians.append(float(value[3]))
if bx_sort == "descending":
if order == "descending":
ordered_list = [i for _, i in sorted(zip(medians, ids), reverse=True)]
elif bx_sort == "ascending":
elif order == "ascending":
ordered_list = [i for _, i in sorted(zip(medians, ids), reverse=False)]
else:
ordered_list = list(range(0, len(quantstats)))
Expand Down Expand Up @@ -905,7 +903,7 @@ def bxp_zones(opt):

# Compute statistics
quantstats, ordered_list = bx_zonal_stats(
opt["zones_raster"], opt["value_raster"], opt["bx_sort"]
opt["zones_raster"], opt["value_raster"], opt["order"]
)

# Change the order of the colors of the boxplots and median to match the
Expand Down Expand Up @@ -1129,7 +1127,8 @@ def main(options, flags):
"""

# lazy import matplotlib
lazy_import_py_modules()
output = options["output"] if options["output"] else None
lazy_import_py_modules(output)

# Check if zonal map is an integer map
if options["zones"]:
Expand Down Expand Up @@ -1159,16 +1158,16 @@ def main(options, flags):
# extent and resolution do not match that of the current region
mask_present = checkmask()
if bool(options["zones"]):
valueraster_region = check_regionraster_match(options["input"])
valueraster_region = check_regionraster_match(options["map"])
if mask_present or not valueraster_region:
value_raster = create_temporary_name("tmpinput")
Module(
"r.mapcalc", expression="{} = {}".format(value_raster, options["input"])
"r.mapcalc", expression="{} = {}".format(value_raster, options["map"])
)
else:
value_raster = options["input"]
value_raster = options["map"]
else:
value_raster = options["input"]
value_raster = options["map"]

# Create temporary zonal rasters if there is a mask or the zonal raster
# extent and resolution do not match that of the current region
Expand All @@ -1189,7 +1188,7 @@ def main(options, flags):

# Collect options
base_options = {
"value_name": options["input"],
"value_name": options["map"],
"value_raster": value_raster,
"output": options["output"],
"outliers": flags["o"],
Expand Down Expand Up @@ -1219,7 +1218,7 @@ def main(options, flags):
"zones_raster": zonal_raster,
"show_catnumbers": flags["s"],
"bx_zonalcolors": flags["c"],
"bx_sort": options["bx_sort"],
"order": options["order"],
"plot_rast_stats": options["raster_statistics"],
"raster_stat_color": raster_stat_color,
"raster_stat_alpha": float(options["raster_stat_alpha"]),
Expand Down
Loading

0 comments on commit 02f9ac3

Please sign in to comment.