OWS Styling HOW-TO Guide: Legends

Legends

Many visualisation styles require a legend to be properly understood. Datacube-OWS provides support for auto-generation of legends that cover many common use cases. For specialised use cases not covered by OWS automatic legends, you can provide OWS with manually (or at least externally) created legend image.

Auto-legend-generation for Colour Ramp Styles

The first type of style that supports automatic legend-generation is colour ramp styles.

For these examples, we will use a simple colour ramp style using a Matplotlib named colour ramp:

# Create a style definition with a legend section:

style_with_default_legend = {
    "index_function": {
        "function": "datacube_ows.band_utils.norm_diff",
        "kwargs": {"band1": "nir", "band2": "red"},
    },
    "mpl_ramp": "RdYlGn",
    "range": [-1.0, 1.0],
    "legend": {
        # The discussion in this chapter will focus on what goes here.
    }
}

# Generate and save a legend image:

from datacube_ows.styles.api import generate_ows_legend_style_cfg

# Note this command will also display the legend in a JupyterHub/notebook-type environment)
image = generate_ows_legend_style_cfg(cfg)

# Write out as PNG:
with open("filename.png", "wb") as fp:
    image.save(fp)

Default colour-ramp legend

Let’s start with default legend, with the “legend” section left empty or missing all together:

"legend": {}
https://user-images.githubusercontent.com/4548530/135552385-8a719936-299e-4c1b-b875-be6a3e421397.png

By default, the legend strip runs across the full value range of the ramp, and only has “ticks” for minimum and maximum values with none in between.

For custom ramps, the legend strip runs from first non-transparent value to the last non-transparent value. For example, an NDVI custom ramp that was transparent for negative values would run from 0.0 to 1.0 by default.

Legend title

The “Stand-Alone Style” title comes from this being a standalone style within the API. Real OWS styles must have a unique title that would normally go here. E.g.

style_with_default_legend_and_title = {
    "title": "My Autolegend Style",
    "index_function": {
        "function": "datacube_ows.band_utils.norm_diff",
        "kwargs": {"band1": "nir", "band2": "red"},
    },
    "mpl_ramp": "RdYlGn",
    "range": [-1.0, 1.0],
    "legend": {
    }
}
https://user-images.githubusercontent.com/4548530/135553917-f0add568-bc1d-4379-a661-aff62d226218.png

You can also set a title for the legend separate to the style title. E.g.

"legend": {
    "title": "Bi-directional NDVI",
}
https://user-images.githubusercontent.com/4548530/135553920-c30b61b8-7061-4411-9fdb-b7a3fa6006f3.png

You can also specify units, which are added in parentheses after the title:

"legend": {
    "title": "Geomagnetic Anomaly",
    "units": "uT",
}
https://user-images.githubusercontent.com/4548530/135938028-c293beb0-0b9a-4a76-af12-5176ac490265.png

Setting legend range

The start and end of the legend strip can be over-ridden with the “begin” and “end” elements. These elements can be integers, floats or numeric strings - however floats are strongly discouraged as they can result in unexpected behaviour. For more information refer to `Floating Point Arithmetic: Issues and Limitations<https://docs.python.org/3/tutorial/floatingpoint.html>`_ in the official Python tutorial.

E.g.

"legend": {
    "title": "Custom begin and end",
    "begin": "0.0",
    "end": "1.0",
}
https://user-images.githubusercontent.com/4548530/135554887-92610cd2-6f7d-429a-9450-d6804809ee35.png

Legend Ticks

Additional ticks can be added in three ways:

  1. Regularly spaced ticks by spacing (ticks_every)

  2. Regularly spaced ticks by count (tick_count)

  3. Explicit ticks (ticks)

Regularly spaced ticks by spacing (ticks_every)

This example has ticks every 0.5, from -1.0 to 1.0. (Again, avoid using raw floats)

"legend": {
    "title": "Regular Ticks By Spacing",
    "begin": "-1.0",
    "end": "1.0",
    "ticks_every": "0.5",
}
https://user-images.githubusercontent.com/4548530/135556160-fdba434f-f72e-42fb-b62d-ba624fc4613f.png
Regularly spaced ticks by count (tick_count)

This example has 5 regular spaced ticks, from 0.0 to 1.0.

"legend": {
    "title": "Regular Ticks By Count",
    "begin": "0.0",    # avoid floats, but
    "end": "1.0",
    "ticks_count": 5,  # integers are ok
}
https://user-images.githubusercontent.com/4548530/135556158-c3208cf6-a881-499d-9d90-c5cd2548807e.png

Note that first tick is not counted in the tick count: setting tick_count to zero results in a single tick at the start of the legend bar and setting tick_count to 1 is the same as the default.

https://user-images.githubusercontent.com/4548530/135556926-ca5f7bba-584d-4554-9d52-c733db9d65e8.png https://user-images.githubusercontent.com/4548530/135556929-944add9d-b5e4-49d5-a9e4-00c1bafdcc14.png
Explicit ticks

Tick locations can also be specified explicitly by setting ticks to a list of values. Again, please use numeric strings rather than floats.

The following examples cover some interesting cases that are not possible using either tick_count or ticks_every:

"legend": {
    "title": "No Ticks At All",
    "begin": "-1.0",
    "end": "1.0",
    "ticks": []
}
https://user-images.githubusercontent.com/4548530/135563403-49d1a3b9-aa4c-4cc6-ba75-8f0f9b6a6381.png
"legend": {
        "title": "No Ticks at the Ends",
        "begin": "-1.0",
        "end": "1.0",
        "ticks": ["-0.5", "0.0", "0.5"],
    }
https://user-images.githubusercontent.com/4548530/135563316-3cc625a2-7687-4430-888f-fb5122fb6125.png
"legend": {
    "title": "Irregular Ticks",
    "begin": "-1.0",
    "end": "1.0",
    "ticks": ["-1.0", "0.0", "0.5", "0.7", "0.9", "1.0"],
}
https://user-images.githubusercontent.com/4548530/135563314-ed63594a-19b5-4cf4-8b5a-8a66f493b14e.png

You can cannot combine ticks, ticks_every, or tick_count. Not using any is equivalent to:

"tick_count": 1,

For full control over legend range and tick positions, use explicit “ticks” - and set begin and end manually.

"legend": {
    "title": "Custom Scale and Ticks",
    "begin": "-0.1",
    "end": "1.0",
    "ticks": ["0.0", "0.3", "0.7", "0.9", "1.0"],
}
https://user-images.githubusercontent.com/4548530/135572795-fb7386cd-0169-4245-aa44-30947f27ccd0.png
Tick labels
decimal_places

By default, tick labels are displayed to one decimal place:

"legend": {
    "title": "One Decimal Place",
    "begin": "-1",
    "end": "0",
    "tick_count": 4,
}
https://user-images.githubusercontent.com/4548530/135572793-4ee31078-042d-4334-9970-0884a779884f.png

In this case, we really wanted two decimal places:

"legend": {
    "title": "Two Decimal Places",
    "begin": "-1",
    "end": "0",
    "decimal_places": 2,
    "tick_count": 4,
}
https://user-images.githubusercontent.com/4548530/135572796-878f44a8-e953-49d0-b887-0bda36c5b10a.png
Prefixes and Suffixes and Custom Labels

Or you can override the label for each tick individually using a tick_labels dictionary. Note that the keys must exactly match the non-overridden labels as they were calculated. If you experience problems, an explicit ticks list will minimise confusion.

"legend": {
    "title": "Custom Tick Lables",
    "begin": "-1",
    "end": "1",
    "ticks": ["-0.75", "0.0", "0.75"],
    "tick_labels": {
        "-0.75": {"label": "low"},
        "0.0": {"label": "middling"},
        "0.75": {"label": "high"},
    }
}
https://user-images.githubusercontent.com/4548530/135935943-698c979d-e388-4310-95e1-95d3a2bd335e.png

You can also specify prefixes and suffixes for tick labels, either individually or as defaults for all labels.

"legend": {
    "title": "0 to 1, displayed as percentage",
    "begin": "0.0",
    "end": "1.0",
    "ticks": ["0.0", "0.2", "0.4", "0.6", "0.8", "1.0"],
    "tick_labels": {
        "default": {
            "suffix": "%",
        },
        "0.0": {"label": "0"},
        "0.2": {"label": "20"},
        "0.4": {"label": "40"},
        "0.6": {"label": "60"},
        "0.8": {"label": "80"},
        "1.0": {"label": "100"},
    }
}
https://user-images.githubusercontent.com/4548530/135936408-6aff7d3f-37b2-4775-bf07-c728cd370991.png
"legend": {
    "begin": "0.0",
    "end": "1.0",
    "ticks": ["0.0", "0.2", "0.4", "0.6", "0.8", "1.0"],
    "tick_labels": {
        # Surround every tick label in square brackets (unless over-ridden)
        "default": {
            "prefix": "[",
            "suffix": "]",
        },
        # There is no "0.0" entry, so the 0.0 tick will be labelled "[0.0]"
        # (according to the default rules)
        # ---------------------------------
        # The 0.2 tick will be labelled "(0.2)"
        "0.2": {
            "prefix": "(",
            "suffix": ")",
        },
        # ---------------------------------
        # The 0.4 tick will be labelled "[foo]"
        # (Note the default prefix and suffix are still applied)
        "0.4": {
            "label": "foo",
        },
        # ---------------------------------
        # The 0.6 tick will be labelled "bar" with no prefix or suffix
        "0.6": {
            "prefix": "",
            "label": "bar",
            "suffix": "",
        },
        # ---------------------------------
        # The 0.8 tick will be labelled ":-)"
        "0.8": {
            "prefix": ":",
            "label": "-",
            "suffix": ")",
        },
        # ---------------------------------
        # The 1.0 tick, will be labeled "+1.0]"
        # (prefix over-ridden, default suffix still applies)
        "1.0": {
            "prefix": "+",
        },
    }
}
https://user-images.githubusercontent.com/4548530/135937334-9664be22-3278-40bb-9fb1-b92d72871f86.png

Customising legend layout

Image Size

The width and height values are passed to matplotlib to specify the size of the generated image.

The image size defaults to 4 inches wide by 1.25 inches tall. The default dpi for MatPlotLib is 100, so this corresponds to 400x125 pixels (unless you have over-ridden the default dpi).

E.g.:

# A legend in need of more room
"legend": {
    "title": "Over Crowded Legend",
    "begin": "-1.0",
    "end": "1.0",
    "ticks_every": "0.1",
    "tick_labels": {
        "0.0": "This\nis\na\nvery\ntall\nlabel"
    }
}
https://user-images.githubusercontent.com/4548530/135939082-3fbf736e-3cf8-4049-b59d-d9f8c563ce57.png

This legend image is too small to fit all the needed data legibly. This can be fixed by enlarging the image:

# Bigger legend image
"legend": {
    "title": "Over Crowded Legend",
    "begin": "-1.0",
    "end": "1.0",
    "ticks_every": "0.1",
    "tick_labels": {
        "0.0": "This\nis\na\nvery\ntall\nlabel"
    },
    "width": 6,    # 600 pixels at default dpi
    "height": 2.5, # 250 pixels at default dpi
}
https://user-images.githubusercontent.com/4548530/135939074-2f0284a8-5cca-434a-8f0c-8ee3e71a2bbe.png
Ramp Strip Location

The location of the coloured ramp strip within the legend image can be customised with the strip_location element. This should be a tuple of four floats which is passed directly to the MatPlotLib Figure.add_axes function.

The four floats are expressed as fractions of the width or heigth (i.e. are numbers between 0.0 and 1.0). The values are interpreted as follows: [left, bottom, width, height].

The default value is [0.05, 0.5, 0.9, 0.15]

For example to move the bar towards the top of the image, raise the bottom:

"legend": {
    "title": "Ramp bar to top of image",
    "begin": "-1.0",
    "end": "1.0",
    "ticks_every": "0.5",
    "strip_location": [0.05, 0.85, 0.9, 0.15],
}
https://user-images.githubusercontent.com/4548530/135939645-85e9c4f9-e5ac-49c0-a678-e3118a883fa5.png

Auto-legend-generation for Colour Map Styles

Auto-generated legends are also available for `colour map styles<https://datacube-ows.readthedocs.io/en/latest/style_howto_color_map.html>`_.

For these examples, we will be extending the following style example:

transparency_map_cfg = {
    "value_map": {
        "water": [
            {
                # Make noncontiguous and invalid data transparent
                "title": "",
                "flags": {
                    "or": {
                        "noncontiguous": True,
                        "nodata": True,
                    },
                },
                "alpha": 0.0,
                "color": "#ffffff",
            },
            {
                "title": "Cloudy Steep Terrain",
                "flags": {
                    "and": {
                        "high_slope": True,
                        "cloud": True
                    }
                },
                "color": "#f2dcb4",
            },
            {
                "title": "Cloudy Water",
                "flags": {
                    "and": {
                        "water_observed": True,
                        "cloud": True
                    }
                },
                "color": "#bad4f2",
            },
            {
                "title": "Shaded Water",
                "flags": {
                    "and": {
                        "water_observed": True,
                        "cloud_shadow": True
                    }
                },
                "color": "#335277",
            },
            {
                "title": "Cloud",
                "flags": {"cloud": True},
                "color": "#c2c1c0",
            },
            {
                "title": "Cloud Shadow",
                "flags": {"cloud_shadow": True},
                "color": "#4b4b37",
            },
            {
                "title": "Terrain Shadow or Low Sun Angle",
                "flags": {
                    "or": {
                        "terrain_shadow": True,
                        "low_solar_angle": True
                    },
                },
                "color": "#2f2922",
            },
            {
                "title": "Steep Terrain",
                "abstract": "",
                "flags": {"high_slope": True},
                "color": "#776857",
            },
            {
                "title": "Water",
                "abstract": "",
                "flags": {"water_observed": True},
                "color": "#4f81bd",
            },
            {
                "title": "Dry",
                "abstract": "",
                "flags": {"water_observed": False},
                "color": "#96966e",
            },
        ]
    },
    "legend": {
        # legend config goes here
    }
}

With the default legend settings, the resulting legend looks like this:

https://user-images.githubusercontent.com/4548530/135941097-29eddd2e-5c01-4402-af5b-66ec240dde59.png

The first thing to note is that the resulting image is not big enough to contain all the possible values. This can be addressed with the width and height entries, which work the same for color ramp legends, as described above. The default width and height for colour map legends are 3 inches and 1.25 inches respectively - which is slightly different to the default values for colour-ramp legends.

"legend": {
    "width": 3.0,
    "height": 2.0,
},
https://user-images.githubusercontent.com/4548530/135942120-b27befce-9fe7-4a94-a083-d36326ff31c4.png

Note that a legend-patch is generated for all non-transparent value_map rules, using the title and/or abstract defined in the value_map.