ratio-color

Crates.ioratio-color
lib.rsratio-color
version0.7.1
created_at2023-04-03 13:13:27.131554+00
updated_at2025-12-02 15:35:50.455881+00
descriptionRatio's color palette management in Rust, built on 'palette' and 'enterpolation'.
homepagehttps://gitlab.com/ratio-case-os/rust/ratio-case/-/tree/main/ratio-color
repositoryhttps://gitlab.com/ratio-case-os/rust/ratio-case.git
max_upload_size
id829073
size99,647
Tiemen Schuijbroek (TiemenSch)

documentation

README

Ratio Color

A library to make palette management for plotting projects easier. It is built on the crate [palette] that allows for creating, mixing and generally working with colors and pixels.

A short categorical example utilizing the Bold preset:

use palette::Srgba;
use ratio_color::{Categorical, Container, Palette, key_to_order};
let keys = ["foo", "bar", "baz", "quux"];
let categorical: Palette<&str, Srgba<u8>> =
    Palette::with_preset(&Categorical::Bold, key_to_order(keys));
assert_eq!(
    categorical.get("foo").expect("a color"),
    &Srgba::new(57, 105, 172, 255)
);

A short numerical example utilizing several numerical presets:

use palette::Srgba;
use ratio_color::{
    ColorAtFraction, Container, LinearGradient, Numerical, Palette, key_to_order,
};
let keys = ["foo", "bar", "baz", "quux"];
let numerical: Palette<&str, LinearGradient> = Palette::with_presets(
    &[
        Numerical::SeqMagma,
        Numerical::SeqViridis,
        Numerical::CycEdge,
        Numerical::DivSpectral,
    ],
    key_to_order(keys),
);

let gradient: &LinearGradient = numerical.get("bar").expect("a gradient");
assert_eq!(gradient, &LinearGradient::from(Numerical::SeqMagma));
let color: Srgba<u8> = gradient.color_at(0.5);
assert_eq!(color, Srgba::<u8>::new(183, 56, 120, 255));

The key struct of this crate is the [Palette] for which the [Container] trait has been implemented. A container is a mix of a slice of values, usually a vector, and a [BTreeMap] of keys to access them in a number of ways using the [Selector] enum to obtain either a suitable index for the slice of values or an override value.

In terms of palettes this corresponds to a palette having a fixed number of colors or gradients to choose from (the values). When binding colors to keys, the default behavior would be to use the key's order in the mapping to determine the index of the palette's color to use using [Selector::KeyOrder]. However, you might want to use a specific color from the given palette for a given key using [Selector::Index] or even a complete overridden value not present in the usual palette [Selector::Value].

The [Palette] is a generic struct with an implementation of [Container] for any orderable key and any value.

Categorical palette

For our example palette, we will use red, green, and blue as our default colors, or values. Pink will be used as a special case later on. Next we create a set of keys for which we want to have colors available on demand. Just as an example, we will use "sun", "trees", "sky", "ground", "ditto", and "zzz". Note that we have more keys than default colors! For some, we will assign a fixed color by using an index. Others receive a color by their key's order with respect to the other keys. And finally, an exception will be made for ditto.

use std::collections::BTreeMap;
use ratio_color::{Container, Palette, Selector};
use palette::Srgba;

// Create some categorical palette colors.
let red: Srgba<u8> = Srgba::new(255, 0, 0, 100);
let green: Srgba<u8> = Srgba::new(0, 255, 0, 200);
let blue: Srgba<u8> = Srgba::new(0, 0, 255, 255);

// Bundle them together in a vec to supply to the categorical palette later on.
let values = vec![red.clone(), green.clone(), blue.clone()];

// Initialize a BTreeMap to map from key to a selector.
let mut selectors = BTreeMap::new();
selectors.insert("sun", Selector::Index(0)); // red
selectors.insert("trees", Selector::Index(1)); // green
selectors.insert("sky", Selector::Index(2)); // blue
selectors.insert("ground", Selector::KeyOrder); // what will this be?
selectors.insert("zzz", Selector::KeyOrder); // I'm probably last.

// Ditto's are always pink, so they get a special value.
let pink: Srgba<u8> = Srgba::new(255, 125, 200, 255);
selectors.insert("ditto", Selector::Value(pink.clone()));

// Create the palette.
let palette = Palette::new(values, selectors);

// Let's check the contents!
assert_eq!(
    palette.get("sun").expect("a color"),
    &red,
    "our sun is red"
);
assert_eq!(
    palette.get("ditto").expect("a color"),
    &pink,
    "a ditto is pink"
);
assert_eq!(
    palette.get("ground").expect("a color"),
    &green,
    "the ground is green, because it's key is the second one by order (ditto, *ground*, sky, sun, trees, zzz)"
);
assert_eq!(
    palette.get("zzz").expect("a color"),
    &blue,
    "even though I'm sixth, I'll be 2 (blue) instead (index=5, per modulo 3 makes 2)"
)

So now we can safely get our colors for our given keys. The [Palette]'s generic nature, means we can use any orderable key, and also any value. Adding or removing a key later on is done by mutating the [Palette::selectors] property or whatever property you decide to hook up to the [Container::selectors] trait function.

In order to not necessarily clog your error stack, we rely on the basic behavior of maps an vectors by returning values as an [Option] rather than throwing errors.

Numerical palette

Numerical palettes are completely similar to categorical palettes as far as storage and access go. The [palette] crate no longer provides us with continuous color scales directly. As a replacement, we introduce the [ColorAtFraction] trait and [LinearGradient] struct, that uses the [enterpolation] crate to interpolate between linearized colors instead.

use palette::Srgba;
use ratio_color::{ColorAtFraction, Container, LinearGradient, Palette, key_to_order};

// Create basic colors.
let transparent: Srgba<u8> = Srgba::new(0, 0, 0, 0);
let red: Srgba<u8> = Srgba::new(255, 0, 0, 255);
let green: Srgba<u8> = Srgba::new(0, 255, 0, 255);
let blue: Srgba<u8> = Srgba::new(0, 0, 255, 255);

// Create three gradients.
let to_red =
    LinearGradient::try_new([transparent.clone(), red.clone()], None).expect("a gradient");
let to_green =
    LinearGradient::try_new([transparent.clone(), green.clone()], None).expect("a gradient");
let to_blue =
    LinearGradient::try_new([transparent.clone(), blue.clone()], None).expect("a gradient");

// Get a color from a linear gradient and convert it back into a regular Srgba.
// Note that blueness interpolation in the linear color space does not equal regular "mean"
// taking, but the alpha channel does.
let halfway_blue = Srgba::<u8>::from_linear(to_blue.color_at(0.5));
assert_eq!(halfway_blue, Srgba::<u8>::new(0, 0, 188, 128));

// Let's assert the endpoints for completeness' sake.
let start: Srgba<u8> = to_blue.color_at(0.0);
let end: Srgba<u8> = to_blue.color_at(1.0);
assert_eq!(&start, &transparent);
assert_eq!(&end, &blue);

// Store these in an palette for these keys.
let keys = ["foo", "bar", "baz", "quux"];
// Let keys resolve to their order in the BTreeMap.
// Actual palette creation.
let palette = Palette::new(
    [to_red.clone(), to_green.clone(), to_blue.clone()],
    key_to_order(keys),
);

// Test accessing and using a gradient.
let full_red: Srgba<u8> = palette.get("quux").expect("a gradient").color_at(1.0);
assert_eq!(&full_red, &red);
assert_eq!(
    <LinearGradient as ColorAtFraction<Srgba<u8>>>::color_at(
        palette.get("quux").expect("a gradient"),
        0.66
    ),
    Srgba::<u8>::new(212, 0, 0, 168),
    "Access quux at %66 using the fully qualified path to access the method."
);

Documentation

Please refer to the crate's documentation on docs.rs for more information on it's usage.

Changelog

This repository keeps a CHANGELOG.md according to the recommendations by Keep a Changelog.

Contributions

Contributions are welcome! By submitting a contribution, you agree to license your work under the terms of the Mozilla Public License 2.0. Please ensure that your contributions adhere to the existing code style and include appropriate tests and documentation where applicable.

To get started:

  1. Fork the repo
  2. Create a new branch
  3. Make your changes
  4. Make sure you run just fix to adhere to the project's formatting
  5. Submit a merge request with a clear description of the changes

Licensing

This project is licensed under the Mozilla Public License 2.0. You are free to use, modify, and distribute this code, provided that any files you modify or create that are based on MPL-licensed files also remain under the MPL. You must include a copy of the license with the source and make the source code available when distributing binaries.

See the LICENSE file for the full license text.

Code examples both in the docstrings and rendered documentation thereof are free to use!

At Ratio, we are huge supporters of open-source code and the open-source community. In our Python projects we usually strive to use one of the (L)GPL flavors. These are difficult to pair with compiled codebases, however, which is where we see the MPL-2.0 as a great fit for our open-source Rust efforts. It's a weak copyleft license that just protects the source as it is written and encourages changes to the crate's source to be published accordingly. It's sort of "automagically" implied and done right when cargo would pull in the source files to build with, as (the mentioning of) the license is included in the header of each file, and any binaries you generate with them are not of our concern from a distribution perspective.

Enjoy the code!

Commit count: 176

cargo fmt