#include <morph/ColourMap.h>

Introduction

Colour is one of the most important ways you can indicate value in a visualization. morph::ColourMap is the class that provides many mappings of values to graded colours.

The class ColourMap exists to provide the function of converting an input number (usually a float) in the range [0, 1] into an RGB colour triplet, returned as (usually) std::array<float, 3>. The examples below show several colour maps which will be familiar to those who have used colour maps in Python or MATLAB (the maps are shown as morph::ColourBarVisual objects).

A selection of colour maps available in morph::ColourMap

Morphologica now includes a wide variety of colour maps, most of which are perceptually uniform. Thanks to Fabio Crameri, William Lenthe and the teams at CET and Matplotlib for providing open source code and tables for these maps.

As well as the one dimensional colour maps shown above, morph::ColourMap can convert two (and three) dimensional numbers into colours. Here you can see i) the ‘HSV’ map which converts the ‘x’ and ‘y’ of a two-dimensional input into polar coordinates, then uses these as the hue (r) and saturation (phi) of an HSV colour specification and ii) two ‘Duochrome’ maps that encode two dimensions of data as complementary colours. Here, we’re encoding the 2D coordinate on a square grid into colour.

A selection of 2D colour maps available in morph::ColourMap

A ColourMap is often a member of a VisualModel-derived class. For example, the HexGridVisual class has a ColourMap which is used to select colours for each hex in the grid. These are coloured with the ‘plasma’ colour map:

A hex grid with the hexes coloured according to the Plasma colour map

Available maps

The full list of available maps is found in the enumerated class morph::ColourMapType, found in ColourMap.h:

enum class ColourMapType // in morph namespace
{
    Jet,
    Rainbow,
    RainbowZeroBlack, // As Rainbow, but if datum is 0, then colour is pure black.
    RainbowZeroWhite, // As Rainbow, but if datum is 0, then colour is pure white.
    Magma,      // Like matplotlib's magma
    Inferno,    // matplotlib's inferno
    Plasma,     // etc
    Viridis,
    Cividis,
    Twilight,
    Greyscale,    // Greyscale is any hue; saturation=0; *value* varies. High signal (datum ->1) gives dark greys to black
    GreyscaleInv, // Inverted Greyscale. High signal gives light greys to white
    Monochrome,   // Monochrome is 'monohue': fixed hue; vary the *saturation* with value fixed at 1.
    MonochromeRed,
    MonochromeBlue,
    MonochromeGreen,
    Monoval,      // Monoval varies the *value* of the colour
    MonovalRed,
    MonovalBlue,
    MonovalGreen,
    Duochrome,    // Two fixed hues, vary saturation of each with two input numbers.
    Trichrome,    // As for Duochrome, but with three inputs
    RGB,          // A kind of 'null' colour map that takes R, G and B values and returns as an RGB colour.
                  // Of course, you don't really need a morph::ColourMap to do this, but it can be useful where
                  // the ColourMap is embedded in the workflow, such as in a VisualDataModel.
    RGBMono,      // Takes RGB input and outputs a coloured monochrome version (datum varies value)
    RGBGrey,      // Takes RGB input and outputs a greyscale version
    HSV,          // A special map in which two input numbers are used to compute a hue and a saturation.
    HSV1D,        // A 1D version of HSV, traverses a line across the HSV circle for a set value of hue,
                  // which determines what colour the value 1 will return. Useful for positive/negative ranges.
                  // Map the negative portion of our input to the rnage 0->0.5 and the positive to 0.5->1
    Fixed         // Fixed colour. Should return same colour for any datum. User must set hue, sat, val.
};

All of these maps are one dimensional, converting a scalar (usually in the range [0, 1]) to a colour. Exceptions are Duochrome, Trichrome, RGB, RGBMono, RGBGrey and HSV which convert 2 or 3 dimensional vectors into a colour. Lastly, Fixed is a special case. It’s a colour map which always returns the same colour, but it will pretend to be a 1D map.

Client coding with ColourMap

Constructors and ColourMapType

ColourMap is a templated class whose template argument T is the type of the data values. T may be any floating point or integer type.

template <typename T>
class ColourMap
{ /* ... */ }

Construct with a ColourMapType as argument, with a string representation of the colour map type or with no argument:

// ColourMap::type will default to ColourMapType::Plasma
morph::ColourMap<float> colour_map1;

// Explicitly choose ColourMapType::Plasma:
morph::ColourMap<float> colour_map2(morph::ColourMapType::Plasma);

// Choose using the 'string name':
morph::ColourMap<float> colour_map3("plasma");

The string name is always a lower-case version of the ColourMapType name. Plasma -> “plasma”; GreyscaleInv -> “greyscaleinv” and so on. A ColourMapType can be obtained for a given string name with the static function ColourMap::strToColourMapType (and back again):

morph::ColourMapType cmtype = morph::ColourMap::strToColourMapType ("jet");
std::string cmtype_str = morph::ColourMap::colourMapTypeToStr (cmtype);

You can check the number of datums that the map requires with

int ndatums = morph::ColourMap::numDatums (morph::ColourMapType::HSV); // returns 2

or

morph::ColourMap<float> colour_map1 (morph::ColourMapType::Twilight);
int ndatums = colour_map1.numDatums(); // returns 1

You can set the type after construction and getType() at any time:

colour_map1.setType (morph::ColourMapType::Jet);
colour_map1.setType (std::string("jet"));
auto thetype = colour_map1.getType();
std::string string_type = colour_map1.getTypeStr();

Accessing colours with convert()

Access a colour from the map using the convert functions:

// 1D input maps:
morph::ColourMap<float> colour_map1 (morph::ColourMapType::Viridis);
std::array<float, 3> mycolour1 = colour_map1.convert (0.5f);

// 2D input maps:
morph::ColourMap<float> colour_map2 (morph::ColourMapType::Duochrome);
std::array<float, 3> mycolour2 = colour_map2.convert (0.5f, 0.3f);

// 3D input maps:
morph::ColourMap<float> colour_map3 (morph::ColourMapType::RGBGrey);
std::array<float, 3> mycolour3 = colour_map3.convert (0.5f, 0.3f, 0.2f);

If you choose an incompatible convert function (such as convert(float, float) for a 1D ColourMapType) then a runtime error will be thrown.

Choice of template type T

The examples above show instances of morph::ColourMap<T> with T=float which is most commonly used. double may also be used. When the ColourMap template type T is floating point, inputs should be given in the range [0, 1].

It is also possible to use integral types with ColourMap. In these cases the range of the input can be between 0 and the maximum value for the type or 255, whichever is smaller.

You can obtain the maximum value of the range with the T ColourMap::range_max variable.

morph::ColourMap<char> cm_char;
std::array<float, 3> max_colour = cm_char.convert (127);
std::array<float, 3> min_colour = cm_char.convert (0);
std::cout << cm_char.range_max << "\n"; // 127

morph::ColourMap<unsigned char> cm_unsigned_char;
max_colour = cm_unsigned_char.convert (255);
min_colour = cm_unsigned_char.convert (0);
std::cout << cm_unsigned_char.range_max << "\n"; // 255

// Anything with >8 bits uses range [0 255]. Pull requests to change this would be welcome.
morph::ColourMap<unsigned int> cm_unsigned_int;
max_colour = cm_unsigned_int.convert (255);
min_colour = cm_unsigned_int.convert (0);
std::cout << cm_unsigned_int.range_max << "\n"; // 255

// You can even ColourMap<bool>!
morph::ColourMap<bool> cm_bool;
max_colour = cm_bool.convert (true);
min_colour = cm_bool.convert (false);
std::cout << cm_bool.range_max << "\n"; // 1

Using 1D maps

One dimensional maps, which convert a scalar value into a colour are easy to use. In most cases, you construct a ColourMap<T> object, set its ColourMapType and then call ColourMap::convert(T).

Importantly, all the 1D maps use the same input range for the mapping. This is [0, 1] for floating point types. Your data may well fall into a different range. For this reason, a ColourMap is usually used along with a morph::Scale object. You set the Scale so that your input data is mapped to a range [0, 1] (or [0, 127] for T=char/[0, 255] for other integral types).

Variable hue in 1D maps

The Monochrome map has a gradually increasing saturation of one colour. The colour, which defaults to red, is held in the private member T ColourMap::hue and can be changed with setHue. Monochrome simply wires the input datum to the saturation and creates an HSV colour based on ( ColourMap::hue, input datum, ColourMap::value ). ColourMap::value is 1 by default, but it can be set with setVal().

morph::ColourMap<float> colour_map1 (morph::ColourMapType::Monochrome);
colour_map1.setHue (0.3f); // Range [0, 1]. See rainbow map for the hue mapping. 0.3 gives green.
std::array<float, 3> mycolour1 = colour_map1.convert (0.5f);

A selection of Hue variable 1D colour Monochrome maps morph::ColourMap

This image shows six Monochrome colour maps with varying hues

There are some convenience ColourMapTypes that set the hue when you set the type. These are MonochromeRed, MonochromeGreen and MonochromeBlue which set the hue to 0, 0.333 and 0.667, respectively.

The Monoval maps are similar to Monochrome, but instead of varying saturation, they vary the value of the HSV colour. They transition from black to the maximally bright colour. They look like this:

A selection of Hue variable 1D colour Monoval maps morph::ColourMap

HSV1D

HSV1D is a one dimensional hue-saturation-value map that is related to the two dimensional ColourMapType::HSV. The hue set for a ColourMap of type HSV1D defines the angle on the HSV wheel at which the linear map will be taken. Although these maps are useful for values which are both positive and negative, they still accept the same input range ([0, 1], [0 127 or [0 255]) as the other colour maps.

HSV colour maps morph::ColourMap

Using 2D ColourMaps

Duochrome, HSV.

Static colour conversion methods

ColourMap contains some static methods that could be used in isolation in your programs.

// Convert float ([0, 1]) into a colour from the Jet colour map
static std::array<float,3> jetcolour (float datum);

// The Hue-Saturation-Value to Red-Green-Blue conversion
static std::array<float, 3> hsv2rgb (const std::array<folat, 3>& hsv);
static std::array<float, 3> hsv2rgb (float h, float s, float v);
static morph::vec<float, 3> hsv2rgb_vec (const morph::vec<float, 3>& hsv);

// RGB to HSV
static std::array<float, 3> rgb2hsv (float r, float g, float b);