Skip to content

Logarithmic colour scale

The ground-level concentration of a pollutant downwind of a 60 m stack, from the Gaussian plume model with the Briggs dispersion coefficients of a neutral atmosphere. The concentration spans four decades over the plume, so the scalar image maps it through a colour scale with a logarithmic norm between 0.1 and 1000 µg/m³, which gives each decade an equal share of the colormap. The scale draws nothing for a concentration below 0.1 µg/m³, so the air the plume has not reached is left blank rather than painted in the first colour, and the black isolines mark the decades 0.1, 1, 10 and 100 µg/m³. The colourbar beside the axes is ticked at the decades, which are equally spaced along it, and its empty key below the bar stands for the concentrations below 0.1 µg/m³ that are left blank.

Logarithmic colour scale

The figure above is live, drawn in the browser by the same engine that draws the viewer: drag it to pan, and click it to zoom with the wheel. Download the PDF for the vector figure that IronLAB exports, or read how to embed a figure of your own.

Source

The figure is built by the following code.

use ironlab::prelude::*;

pub const TITLE: &str = "Logarithmic colour scale";
pub const DESCRIPTION: &str = "The ground-level concentration of a pollutant downwind of a 60 m stack, from the \
     Gaussian plume model with the Briggs dispersion coefficients of a neutral atmosphere. The concentration spans \
     four decades over the plume, so the scalar image maps it through a colour scale with a logarithmic norm between \
     0.1 and 1000 µg/m³, which gives each decade an equal share of the colormap. The scale draws nothing for a \
     concentration below 0.1 µg/m³, so the air the plume has not reached is left blank rather than painted in the \
     first colour, and the black isolines mark the decades 0.1, 1, 10 and 100 µg/m³. The colourbar beside the axes \
     is ticked at the decades, which are equally spaced along it, and its empty key below the bar stands for the \
     concentrations below 0.1 µg/m³ that are left blank.";

/// The emission rate of the stack, in grams per second.
const EMISSION_G_PER_S: f64 = 100.0;

/// The wind speed at the height of the stack, in metres per second.
const WIND_M_PER_S: f64 = 5.0;

/// The effective height of the stack, in metres.
const STACK_HEIGHT_M: f64 = 60.0;

/// The number of samples downwind of the stack.
const DOWNWIND: usize = 251;

/// The number of samples across the wind.
const CROSSWIND: usize = 151;

/// Returns the ground-level concentration, in micrograms per cubic metre, at a distance `x` downwind of the stack and
/// `y` across the wind, both in metres, from the Gaussian plume model with reflection at the ground.
///
/// The lateral and vertical spreads are the Briggs coefficients for open country in a neutral atmosphere (Pasquill
/// class D), σy = 0.08 x (1 + 0.0001 x)^−½ and σz = 0.06 x (1 + 0.0015 x)^−½.
fn concentration(x: f64, y: f64) -> f64 {
    let sigma_y = 0.08 * x / (1.0 + 0.0001 * x).sqrt();
    let sigma_z = 0.06 * x / (1.0 + 0.0015 * x).sqrt();
    let grams_per_m3 = EMISSION_G_PER_S / (std::f64::consts::PI * WIND_M_PER_S * sigma_y * sigma_z)
        * (-y * y / (2.0 * sigma_y * sigma_y)).exp()
        * (-STACK_HEIGHT_M * STACK_HEIGHT_M / (2.0 * sigma_z * sigma_z)).exp();
    grams_per_m3 * 1e6
}

pub fn figure() -> Figure {
    let x_km = linspace(0.05, 5.0, DOWNWIND);
    let y_km = linspace(-1.5, 1.5, CROSSWIND);
    let c = Matrix::from_fn(CROSSWIND, DOWNWIND, |row, col| {
        concentration(x_km[col] * 1000.0, y_km[row] * 1000.0)
    });

    let mut fig = Figure::new()
        .size_mm(160.0, 90.0)
        .tiles(1, 2)
        .fit_col(1)
        .title("Ground-level concentration downwind of a 60 m stack")
        .tag("image")
        .tag("contour")
        .tag("color-scale")
        .tag("out-of-range")
        .tag("colorbar")
        .parameter("dimensionality", "2D")
        .parameter("artists", 2)
        .parameter("data_points", 38_303)
        .parameter("has_legend", false);
    let c = fig.data("concentration (µg/m³)", c).id();
    let decades = fig
        .color_scale()
        .colormap(Colormap::Inferno)
        .norm(ColorScaleNorm::Log)
        .limits(0.1, 1000.0)
        .below(OutOfRange::Transparent)
        .show_key(ColorScaleKey::Below)
        .title("Concentration (µg/m³)")
        .id();

    let mut ax = fig.axes(0, 0);
    ax.scalar_image(c)
        .pixel_columns(x_km[0], x_km[DOWNWIND - 1])
        .pixel_rows(y_km[0], y_km[CROSSWIND - 1])
        .color_scale(decades);
    ax.contour(&x_km, &y_km, c)
        .level_values([0.1, 1.0, 10.0, 100.0])
        .color(Color::BLACK)
        .line_width(0.5);
    ax.xlabel("Distance downwind (km)")
        .ylabel("Distance across the wind (km)");
    fig.panel(0, 1).colorbar(decades);
    fig
}

Back to the gallery