diff --git a/src/raster/r.boxplot/r.boxplot.html b/src/raster/r.boxplot/r.boxplot.html index e7fe2ee316..768f0f84fc 100644 --- a/src/raster/r.boxplot/r.boxplot.html +++ b/src/raster/r.boxplot/r.boxplot.html @@ -7,14 +7,14 @@
-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 r.mask. 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 r.mask. 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 g.region
@@ -24,16 +24,16 @@
-The whiskers extend to the most extreme data point, which is no -more than range ✕ the IQR from the box. -By default, a range 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 range ✕ the IQR from the box. By default, a +range of 1.5 is used, but the user can change this. +Note that range values need to be larger than 0.
-By default, outliers are not included in the plot. Set the o 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 map_outliers. +By default, outliers are not included in the plot. Set the -o +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 map_outliers.
There are a few layout options, including the option to rotate the @@ -47,31 +47,32 @@
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)'.
-If the c flag is used, the bxp_color and median_color -are ignored, even if set by the user. The option to color boxploxs using the colors -of the zonal raster categories (c 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 r.colors.out 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 -c flag is used, the bxp_color and +median_color are ignored, even if set by the user. The option to +color boxploxs using the colors of the zonal raster categories +(c 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 r.colors.out and check if the categories are +integers. If not, that is the problem. If they are all integers, you +probably have caught a bug.
The module respects the mask (if set), and the region settings. This @@ -81,9 +82,9 @@
@@ -95,11 +96,10 @@Example 1
-Example 2
-Draw boxplots of the values of the elevation layer per category from -the landclass96 layer from the same -NC sample -dataset. Use the r flag to rotate the x-asis labels. +Example 2
Draw boxplots of the values of the +elevation layer per category from the landclass96 +layer from the same NC +sample dataset. Use the -r flag to rotate the x-asis labels.r.boxplot -r input=elevation zone=landclass96 output="r_boxplot_02.png" @@ -110,12 +110,14 @@Example 2
Example 3
-Draw boxplots of the values of the elevation layer per category from -the landclass96 layer from the same -NC sample -dataset. Set the o flag to include outliers. Use bx_sort=ascending -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 elevation layer per +category from the landclass96 layer from the same NC sample dataset. +Set the -o flag to include outliers. Use +bx_sort=ascending 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.r.boxplot -o bx_sort=ascending input=elevation zones=landclass96 output="r_boxplot_03.png" map_outliers="outliers" @@ -126,19 +128,21 @@Example 3
-Below, part of the landclass96 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.
+Below, part of the landclass96 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.
Example 4
-Draw boxplots of the values of the elevation layer per category from -the landclass96 layer from the same -NC sample -dataset. Set the c flag to color the boxplots, use bx_sort=ascending -to order the boxplots from low to high median, and set the font size to 11. + +Draw boxplots of the values of the elevation layer per +category from the landclass96 layer from the same NC sample dataset. +Set the -c flag to color the boxplots, use +bx_sort=ascending to order the boxplots from low to high median, +and set the font size to 11.r.boxplot -c bx_sort=ascending fontsize=11 input=elevation zones=landclass96 output="r_boxplot_04.png" @@ -146,9 +150,10 @@Example 4
Example 5
+ 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.r.boxplot -c input=elevation zones=landclass96 raster_statistics=median,IQR @@ -156,18 +161,21 @@Example 5
-Note, if the zones of your zonal map do not cover the entire area, you may want -to use r.mask 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 r.mask 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.
Acknowledgements
-This work was carried in the framework of the Save the tiger, save the grassland, save the water -project by the -Innovative Bio-Monitoring research group. + +This work was carried in the framework of the Save the tiger, save +the grassland, save the water project by the Innovative Bio-Monitoring research group.SEE ALSO
@@ -179,6 +187,9 @@SEE ALSO
AUTHOR
-Paulo van Breugel
-Applied Geo-information Sciences
-HAS University of Applied Sciences
+Paulo van Breugel, HAS green academy, Innovative +Biomonitoring research group, Climate-robust +Landscapes research group diff --git a/src/raster/r.boxplot/r.boxplot.py b/src/raster/r.boxplot/r.boxplot.py index c87d56f2fe..c3ad69b007 100755 --- a/src/raster/r.boxplot/r.boxplot.py +++ b/src/raster/r.boxplot/r.boxplot.py @@ -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 @@ -122,7 +119,7 @@ # %end # %option -# % key: bx_sort +# % key: order # % type: string # % label: Sort boxplots # % description: Sort boxplots based on their median values @@ -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 @@ -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.")) @@ -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 @@ -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))) @@ -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 @@ -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"]: @@ -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 @@ -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"], @@ -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"]), diff --git a/src/vector/v.boxplot/v.boxplot.html b/src/vector/v.boxplot/v.boxplot.html index 59e091a6d6..83aa24f1af 100644 --- a/src/vector/v.boxplot/v.boxplot.html +++ b/src/vector/v.boxplot/v.boxplot.html @@ -1,36 +1,35 @@DESCRIPTION
-v.boxplot draws a boxplot of the values in a vector -map attribute column. The user can use the -where option to only select a subset of the attribute table. -There is also the option to group the values of the column according to -the categories in a second column (group_by and create one plot -with for each group a separate boxplot. +The v.boxplot module draws a boxplot of the values in a vector +map attribute column. Users can use the +where option to select a subset of the attribute table. Values +in the column can also be grouped according to the categories in +another column (group_by), creating separate boxplots for each +group.
-By default, the resulting plot is displayed on screen (default). -However, the user can also save the plot to file using the -plot_output option. The format is determined by the extension -given by the user. So, if plot_output = outputfile.pngtt>, the -plot will be saved as a png file. - -
-There are a few additonal layout options, including the option to rotate -the plot and the x-axis labels. Furthermore, the user can optinally plot -the boxplot(s) with notches and without outliers. +Options to customize the appearance of the plot include rotating the +plot and x-axis labels, adding notches, removing outliers, and defining +the colors of various boxplot components. By default, the resulting +plot is displayed on the screen. However, users can save the plot to a +file by specifying the desired width, height, and resolution. The +format of the saved file is determined by the provided file extension. +For example, if plot_output = outputfile.png, the plot will be +saved as a PNG file.
EXAMPLE
Example 1
-Use the vector layer schools_wake from the + +Use the vector layer schools_wake from the NC sample -dataset to create boxplots of the core capacity of the schools in -Wake County, North Carolina. Use the Where clause to exclude -all records with no data. +dataset to create boxplots of the core capacity of schools in +Wake County, North Carolina. Use the Where clause to exclude +all records with no data. Use the -o flag to draw outliers.@@ -41,16 +40,18 @@-v.boxplot -n map=schools_wake column=CORECAPACI where="CORECAPACI >0" +v.boxplot -n -o map=schools_wake column=CORECAPACI where="CORECAPACI >0"Example 1
core capacity of schools in Wake County.Example 2
-Use the vector layer schools_wake from the -NC sample -dataset to create boxplots of the core capacity of the schools in -Wake County, North Carolina, grouped by city. Use the Where -clause to exclude all records with no data. + +Use the vector layer schools_wake from the NC sample dataset to +create boxplots of the core capacity of the schools in Wake County, +North Carolina, grouped by city. Use the Where clause to +exclude all records with missing data. Use the -o flag to draw +outliers.-v.boxplot -h --overwrite map=schools_wake column=CORECAPACI where="CORECAPACI >0" group_by=ADDRCITY order=ascending -+v.boxplot -h -o map=schools_wake column=CORECAPACI where="CORECAPACI >0" group_by=ADDRCITY order=ascending +@@ -74,5 +75,9 @@
SEE ALSO
AUTHOR
-Paulo van Breugel
-Based on the d.vect.colhist addon by Moritz Lennert +Paulo van Breugel, HAS green academy, Innovative +Biomonitoring research group, Climate-robust +Landscapes research group diff --git a/src/vector/v.boxplot/v.boxplot.py b/src/vector/v.boxplot/v.boxplot.py index 26853b85d8..ee1df728ce 100755 --- a/src/vector/v.boxplot/v.boxplot.py +++ b/src/vector/v.boxplot/v.boxplot.py @@ -5,7 +5,7 @@ # AUTHOR: Paulo van Breugel # PURPOSE: Draws the boxplot(s) of values in a vector attribute column # -# COPYRIGHT: (c) 2019-2023 Paulo van Breugel, and the GRASS Development Team +# COPYRIGHT: (c) 2019-2024 Paulo van Breugel, and the GRASS Development Team # This program is free software under the GNU General Public # License (>=v2). Read the file COPYING that comes with GRASS # for details. @@ -13,7 +13,7 @@ ############################################################################# # %module -# % description: Draws the boxplot of values in a vector attribute column +# % description: Draws a boxplot of values from a specified attribute column in a vector dataset, with an optional grouping based on categories in another column. # % keyword: display # % keyword: vector # % keyword: plot @@ -22,34 +22,64 @@ # %end # %option G_OPT_V_MAP -# % guisection: General +# % guisection: Input # %end # %option G_OPT_V_FIELD +# % guisection: Input # %end # %option G_OPT_DB_COLUMN # % key: column # % description: Attribute column value to be plotted # % required: yes -# % guisection: General +# % guisection: Input +# %end + +# %option G_OPT_DB_COLUMN +# % key: group_by +# % description: Attribute column with categories to group the data by +# % required: no +# % guisection: Input # %end # %option G_OPT_DB_WHERE -# %guisection: General +# %guisection: Input # %end # %option G_OPT_F_OUTPUT -# % key: plot_output # % required: no -# % guisection: General +# % label: Name of output image file +# % guisection: Output # %end -# %option G_OPT_DB_COLUMN -# % key: group_by -# % description: Attribute column with categories to group the data by +# %option +# % key: plot_dimensions +# % type: string +# % label: Plot dimensions (width,height) +# % description: Dimensions (width,height) of the figure in inches +# % required: no +# % guisection: Output +# %end + +# %option +# % key: dpi +# % type: integer +# % label: DPI +# % description: resolution of plot +# % required: no +# % answer: 100 +# % guisection: Output +# %end + +# %option +# % key: fontsize +# % type: integer +# % label: Font size +# % answer: 10 +# % description: Default font size +# % guisection: Output # % required: no -# % guisection: Plot options # %end # %option @@ -59,54 +89,223 @@ # % description: Sort boxplots based on their median values # % required: no # % options: descending,ascending -# % guisection: Plot options +# % guisection: Plot format # %end # %flag # % key: h # % label: horizontal boxplot(s) # % description: Draw the boxplot horizontal -# % guisection: Plot options +# % guisection: Plot format # %end # %flag # % key: o -# % label: hide outliers -# % description: Draw boxplot(s) without outliers -# % guisection: Plot options +# % label: Include outliers +# % description: Draw boxplot(s) with outliers +# % guisection: Plot format # %end # %flag # % key: n # % label: notch # % description: Draw boxplot(s) with notch -# % guisection: Plot options +# % guisection: Plot format # %end # %flag # % key: r # % label: Rotate labels # % description: rotate x-axis labels -# % guisection: Plot options +# % guisection: Plot format +# %end + +# %flag +# % key: g +# % label: Add grid lines +# % description: Add grid lines +# % guisection: Plot format +# %end + +# %option +# % key: axis_limits +# % type: string +# % label: Limit value axis [min,max] +# % description: min and max value of y-axis, or x-axis if -h flag is set) +# % guisection: Plot format +# % required: no +# %end + +# %option G_OPT_CN +# % key: bx_color +# % label: Color of the boxplots +# % description: Color of boxplots +# % required: no +# % answer: white +# % guisection: Boxplot format +# %end + +# %option G_OPT_CN +# % key: bx_blcolor +# % label: Color of the borders of the boxplots +# % description: Color of the borderlines of the boxplots +# % required: no +# % answer: black +# % guisection: Boxplot format +# %end + +# %option +# % key: bx_width +# % type: double +# % label: Boxplot width +# % description: The width of the boxplots (0,1]) +# % required: no +# % guisection: Boxplot format +# % answer: 0.75 +# % options: 0.1-1 +# %end + +# %option +# % key: bx_lw +# % type: double +# % label: boxplot linewidth +# % description: The boxplots border, whisker and cap line width +# % required: no +# % guisection: Boxplot format +# % answer: 1 +# %end + +# %option +# % key: median_lw +# % type: double +# % description: width of the boxplot median line +# % required: no +# % guisection: Boxplot format +# % answer: 1.1 +# %end + +# %option G_OPT_C +# % key: median_color +# % label: Color of the boxlot median line +# % description: Color of median +# % required: no +# % answer: orange +# % guisection: Boxplot format +# %end + +# %option +# % key: flier_marker +# % type: string +# % label: Flier marker +# % description: Set flier marker (see https://matplotlib.org/stable/api/markers_api.html for options) +# % required: no +# % answer: o +# % guisection: Boxplot format +# %end + +# %option +# % key: flier_size +# % type: string +# % label: Flier size +# % description: Set the flier size +# % required: no +# % answer: 2 +# % guisection: Boxplot format +# %end + +# %option G_OPT_C +# % key: flier_color +# % label: Flier color +# % description: Set the flier color +# % required: no +# % answer: black +# % guisection: Boxplot format # %end import sys -import grass.script as gscript +import grass.script as gs import operator import numpy as np +def lazy_import_py_modules(backend): + """Lazy import Py modules""" + global matplotlib + global plt + + # lazy import matplotlib + try: + import matplotlib + + if backend is None: + matplotlib.use("WXAgg") + from matplotlib import pyplot as plt + except ModuleNotFoundError: + gs.fatal(_("Matplotlib is not installed. Please, install it.")) + + +def get_valid_color(color): + """Get valid Matplotlib color + + :param str color: input color + + :return str|list: color e.g. blue|[0.0, 0.0, 1.0] + """ + if ":" in color: + color = [int(x) / 255 for x in color.split(":")] + if not matplotlib.colors.is_color_like(color): + gs.fatal(_("{} is not a valid color.".format(color))) + return color + + def main(): - import matplotlib # required by windows - matplotlib.use("wxAGG") # required by windows - import matplotlib.pyplot as plt + # lazy import matplotlib + output = options["output"] if options["output"] else None + lazy_import_py_modules(output) # input vector = options["map"] column = options["column"] + dpi = float(options["dpi"]) + grid = flags["g"] + if options["plot_dimensions"]: + dimensions = [float(x) for x in options["plot_dimensions"].split(",")] + else: + if flags["h"]: + dimensions = [6, 4] + else: + dimensions = [4, 6] + blcolor = get_valid_color(options["bx_blcolor"]) + bxcolor = get_valid_color(options["bx_color"]) + boxprops = { + "color": blcolor, + "facecolor": bxcolor, + "linewidth": float(options["bx_lw"]), + } + median_color = get_valid_color(options["median_color"]) + medianprops = { + "color": median_color, + "linewidth": float(options["median_lw"]), + } + whiskerprops = { + "linewidth": float(options["bx_lw"]), + "color": blcolor, + } + capprops = { + "linewidth": float(options["bx_lw"]), + "color": blcolor, + } + flier_color = get_valid_color(options["flier_color"]) + flierprops = { + "marker": options["flier_marker"], + "markersize": float(options["flier_size"]), + "markerfacecolor": flier_color, + "markeredgecolor": flier_color, + "markeredgewidth": float(options["bx_lw"]), + } + bxp_width = float(options["bx_width"]) group_by = options["group_by"] if options["group_by"] else None - output = options["plot_output"] if options["plot_output"] else None where = ( options["where"] + " AND " + column + " IS NOT NULL" if options["where"] @@ -121,7 +320,7 @@ def main(): reverse = None cols = filter(None, [group_by, column]) flag_h = not flags["h"] - flag_o = not flags["o"] + flag_o = flags["o"] flag_n = flags["n"] flag_r = flags["r"] @@ -129,7 +328,7 @@ def main(): if where: df = [ x - for x in gscript.read_command( + for x in gs.read_command( "v.db.select", map_=vector, column=cols, where=where, flags="c" ).splitlines() ] @@ -137,10 +336,24 @@ def main(): else: df = [ x - for x in gscript.read_command( + for x in gs.read_command( "v.db.select", map_=vector, column=cols, flags="c" ).splitlines() ] + + # Set plot dimensions and fontsize + if bool(options["fontsize"]): + plt.rcParams["font.size"] = int(options["fontsize"]) + + # Closing message + if not options["output"]: + gs.message( + _("\n> Note, you need to close the figure to finish the script \n\n") + ) + + # Set plot dimensions and DPI + fig, ax = plt.subplots(figsize=dimensions, dpi=dpi) + # for grouped boxplot if group_by: # Split columns and create list with data and with labels @@ -163,21 +376,54 @@ def main(): sfo = [(ii[e]) for i, e in enumerate(uid) if e in ii] # Draw boxplot - plt.boxplot( + ax.boxplot( data, notch=flag_n, - sym="gD", labels=uid, vert=flag_h, showfliers=flag_o, positions=sfo, + boxprops=boxprops, + medianprops=medianprops, + whiskerprops=whiskerprops, + capprops=capprops, + flierprops=flierprops, + patch_artist=True, + widths=bxp_width, ) else: data = [float(x) for x in df] - plt.boxplot(data, notch=flag_n, sym="gD", vert=flag_h, showfliers=flag_o) + ax.boxplot( + data, + notch=flag_n, + vert=flag_h, + showfliers=flag_o, + boxprops=boxprops, + medianprops=medianprops, + whiskerprops=whiskerprops, + capprops=capprops, + flierprops=flierprops, + patch_artist=True, + widths=bxp_width, + ) if flag_r: plt.xticks(rotation=90) plt.tight_layout() + + # Set limits value axis + if bool(options["axis_limits"]): + minlim, maxlim = map(float, options["axis_limits"].split(",")) + if bool(flag_h): + plt.ylim([minlim, maxlim]) + else: + plt.xlim([minlim, maxlim]) + + # Set grid (optional) + if flag_h: + ax.yaxis.grid(bool(grid)) + else: + ax.xaxis.grid(bool(grid)) + if output: plt.savefig(output) else: @@ -185,5 +431,5 @@ def main(): if __name__ == "__main__": - options, flags = gscript.parser() + options, flags = gs.parser() main()