# coding=utf-8
"""Ladybug color, colorsets and colorrange."""
from __future__ import division
try:
from collections.abc import Iterable # python < 3.7
except ImportError:
from collections import Iterable # python >= 3.8
[docs]class Color(object):
"""Ladybug RGBA color.
Args:
r: red value 0-255, default: 0
g: green value 0-255, default: 0
b: blue red value 0-255, default: 0
a: alpha value 0-255. Alpha defines the opacity as a number between 0 (fully
transparent) and 255 (fully opaque). Default 255.
Properties:
* r
* g
* b
* a
"""
__slots__ = ("_r", "_g", "_b", "_a")
def __init__(self, r=0, g=0, b=0, a=255):
"""Generate RGB Color.
"""
self.r = r
self.g = g
self.b = b
self.a = a
[docs] @classmethod
def from_dict(cls, data):
"""Create a color from a dictionary.
Args:
data: A python dictionary in the following format
.. code-block:: python
{
"r": 255,
"g": 0,
"b": 150,
"a": 255
}
"""
a = data['a'] if 'a' in data else 255
return cls(data['r'], data['g'], data['b'], a)
@property
def r(self):
"""Return R value."""
return self._r
@r.setter
def r(self, value):
assert 0 <= int(value) <= 255, "%d is out of range. " % value + \
"R value should be between 0-255"
self._r = int(value)
@property
def g(self):
"""Return G value."""
return self._g
@g.setter
def g(self, value):
assert 0 <= int(value) <= 255, "%d is out of range. " % value + \
"G value should be between 0-255"
self._g = int(value)
@property
def b(self):
"""Return B value."""
return self._b
@b.setter
def b(self, value):
assert 0 <= int(value) <= 255, "%d is out of range. " % value + \
"B value should be between 0-255"
self._b = int(value)
@property
def a(self):
"""Return A value."""
return self._a
@a.setter
def a(self, value):
assert 0 <= int(value) <= 255, "%d is out of range. " % value + \
"B value should be between 0-255"
self._a = int(value)
[docs] def duplicate(self):
"""Return a copy of the current color."""
return self.__copy__()
[docs] def to_dict(self):
"""Get color as a dictionary."""
return {
'r': self.r,
'g': self.g,
'b': self.b,
'a': self.a,
'type': 'Color'
}
def __copy__(self):
return self.__class__(self.r, self.g, self.b, self.a)
def __eq__(self, other):
if isinstance(other, Color):
return self.r == other.r and self.g == other.g and self.b == other.b and \
self.a == other.a
else:
return False
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return hash((self.r, self.g, self.b, self.a))
def __len__(self):
return 4
def __getitem__(self, key):
return (self.r, self.g, self.b, self.a)[key]
def __iter__(self):
return iter((self.r, self.g, self.b, self.a))
[docs] def ToString(self):
"""Overwrite .NET ToString."""
return self.__repr__()
def __repr__(self):
"""Return RGB values."""
return "(R:%d, G:%d, B:%d, A:%d)" % (self._r, self._g, self._b, self._a)
# TODO: Add support for CMYK
[docs]class Colorset(object):
"""Ladybug Color-range repository.
Note that the colorblind friendly schemes have prioritized readability for red-green
colorblindness (deuteranomaly, protanomaly, protanopia, and deuteranopia),
which is by far more common than blue-yellow colorblindness. However, they
are not necessarily ideal for all types of color blindness, though they are
monotonic and perceptually uniform to all forms of color vision. This means
that they should be readable as a dark-to-light scale by anyone.
A list of default Ladybug colorsets for color range:
* 0 - Original Ladybug
* 1 - Nuanced Ladybug
* 2 - Multi-colored Ladybug
* 3 - Ecotect
* 4 - View Study
* 5 - Shadow Study
* 6 - Glare Study
* 7 - Annual Comfort
* 8 - Thermal Comfort
* 9 - Peak Load Balance
* 10 - Heat Sensation
* 11 - Cold Sensation
* 12 - Benefit/Harm
* 13 - Harm
* 14 - Benefit
* 15 - Shade Benefit/Harm
* 16 - Shade Harm
* 17 - Shade Benefit
* 18 - Energy Balance
* 19 - Energy Balance w/ Storage
* 20 - THERM
* 21 - Cloud Cover
* 22 - Black to White
* 23 - Blue, Green, Red
* 24 - Multicolored 2
* 25 - Multicolored 3
* 26 - OpenStudio Palette
* 27 - Cividis (colorblind friendly)
* 28 - Viridis (colorblind friendly)
* 29 - Parula (colorblind friendly)
Usage:
.. code-block:: python
# initialize colorsets
cs = Colorset()
print(cs[0])
>> [<R:75, G:107, B:169>, <R:115, G:147, B:202>, <R:170, G:200, B:247>,
<R:193, G:213, B:208>, <R:245, G:239, B:103>, <R:252, G:230, B:74>,
<R:239, G:156, B:21>, <R:234, G:123, B:0>, <R:234, G:74, B:0>,
<R:234, G:38, B:0>]
"""
# base color sets for which there are several variations
_multicolored = [(4, 25, 145), (7, 48, 224), (7, 88, 255), (1, 232, 255),
(97, 246, 156), (166, 249, 86), (254, 244, 1), (255, 121, 0),
(239, 39, 0), (138, 17, 0)]
_thermalcomfort = [(0, 136, 255), (200, 225, 255), (255, 255, 255),
(255, 230, 230), (255, 0, 0)]
_benefitharm = [(0, 191, 48), (255, 238, 184), (255, 0, 0)]
_shadebenefitharm = [(5, 48, 97), (33, 102, 172), (67, 147, 195), (146, 197, 222),
(209, 229, 240), (255, 255, 255), (253, 219, 199),
(244, 165, 130), (214, 96, 77), (178, 24, 43), (103, 0, 31)]
# dictionary of all color sets together
_colors = {
0: [(75, 107, 169), (115, 147, 202), (170, 200, 247), (193, 213, 208),
(245, 239, 103), (252, 230, 74), (239, 156, 21), (234, 123, 0),
(234, 74, 0), (234, 38, 0)],
1: [(49, 54, 149), (69, 117, 180), (116, 173, 209), (171, 217, 233),
(224, 243, 248), (255, 255, 191), (254, 224, 144), (253, 174, 97),
(244, 109, 67), (215, 48, 39), (165, 0, 38)],
2: _multicolored,
3: [(0, 0, 255), (53, 0, 202), (107, 0, 148), (160, 0, 95), (214, 0, 41),
(255, 12, 0), (255, 66, 0), (255, 119, 0), (255, 173, 0), (255, 226, 0),
(255, 255, 0)],
4: [(255, 20, 147), (240, 47, 145), (203, 117, 139), (160, 196, 133),
(132, 248, 129), (124, 253, 132), (96, 239, 160), (53, 217, 203),
(15, 198, 240), (0, 191, 255)],
5: [(55, 55, 55), (235, 235, 235)],
6: [(156, 217, 255), (255, 243, 77), (255, 115, 0), (255, 0, 0), (0, 0, 0)],
7: [(0, 0, 0), (110, 0, 153), (255, 0, 0), (255, 255, 102), (255, 255, 255)],
8: _thermalcomfort,
9: [(255, 251, 0), (255, 0, 0), (148, 24, 24), (135, 178, 224),
(255, 175, 46), (255, 242, 140), (204, 204, 204)],
10: _thermalcomfort[2:],
11: list(reversed(_thermalcomfort[:3])),
12: _benefitharm,
13: _benefitharm[1:],
14: list(reversed(_benefitharm[:2])),
15: _shadebenefitharm,
16: _shadebenefitharm[5:],
17: list(reversed(_shadebenefitharm[:6])),
18: list(reversed(_multicolored)),
19: list(reversed(_multicolored)) + [(128, 102, 64)],
20: [(0, 0, 0), (137, 0, 139), (218, 0, 218), (196, 0, 255), (0, 92, 255),
(0, 198, 252), (0, 244, 215), (0, 220, 101), (7, 193, 0), (115, 220, 0),
(249, 251, 0), (254, 178, 0), (253, 77, 0), (255, 15, 15),
(255, 135, 135), (255, 255, 255)],
21: [(0, 251, 255), (255, 255, 255), (217, 217, 217), (83, 114, 115)],
22: [(0, 0, 0), (255, 255, 255)],
23: [(0, 0, 255), (0, 255, 100), (255, 0, 0)],
24: [(0, 16, 120), (38, 70, 160), (5, 180, 222), (16, 180, 109),
(59, 183, 35), (143, 209, 19), (228, 215, 29), (246, 147, 17),
(243, 74, 0), (255, 0, 0)],
25: [(69, 92, 166), (66, 128, 167), (62, 176, 168), (78, 181, 137),
(120, 188, 59), (139, 184, 46), (197, 157, 54), (220, 144, 57),
(228, 100, 59), (233, 68, 60)],
26: [(230, 180, 60), (230, 215, 150), (165, 82, 0),
(128, 20, 20), (255, 128, 128), (64, 128, 128),
(128, 128, 128), (255, 128, 128), (128, 64, 0),
(64, 180, 255), (160, 150, 100), (120, 75, 190), (255, 255, 200),
(0, 128, 0)],
27: [(0, 32, 81), (60, 77, 110), (127, 124, 117), (187, 175, 113),
(253, 234, 69)],
28: [(68, 1, 84), (59, 82, 139), (33, 145, 140), (94, 201, 98),
(253, 231, 37)],
29: [(52, 62, 175), (2, 99, 225), (7, 155, 207), (36, 180, 170),
(107, 190, 130), (232, 185, 78), (252, 203, 47), (248, 250, 13)]
}
def __init__(self):
"""Initialize Color-sets."""
pass
[docs] @classmethod
def original(cls):
"""Original Ladybug colors."""
return tuple(Color(*color) for color in cls._colors[0])
[docs] @classmethod
def nuanced(cls):
"""Nuanced Ladybug colors."""
return tuple(Color(*color) for color in cls._colors[1])
[docs] @classmethod
def multi_colored(cls):
"""Multi-colored legend."""
return tuple(Color(*color) for color in cls._colors[2])
[docs] @classmethod
def ecotect(cls):
"""Ecotect colors, also known to some as Plasma."""
return tuple(Color(*color) for color in cls._colors[3])
[docs] @classmethod
def view_study(cls):
"""View analysis colors."""
return tuple(Color(*color) for color in cls._colors[4])
[docs] @classmethod
def shadow_study(cls):
"""Shadow study colors (dark to light grey)."""
return tuple(Color(*color) for color in cls._colors[5])
[docs] @classmethod
def glare_study(cls):
"""Useful for depicting spatial glare (light blue to yellow, red, black)."""
return tuple(Color(*color) for color in cls._colors[6])
[docs] @classmethod
def annual_comfort(cls):
"""Good for annual metrics like UDI and thermal comfort percent."""
return tuple(Color(*color) for color in cls._colors[7])
[docs] @classmethod
def thermal_comfort(cls):
"""Thermal comfort colors (blue to white to red)."""
return tuple(Color(*color) for color in cls._colors[8])
[docs] @classmethod
def peak_load_balance(cls):
"""Colors for the typical terms of a peak load balance."""
return tuple(Color(*color) for color in cls._colors[9])
[docs] @classmethod
def heat_sensation(cls):
"""Red colors for heat sensation."""
return tuple(Color(*color) for color in cls._colors[10])
[docs] @classmethod
def cold_sensation(cls):
"""Blue colors for cold sensation."""
return tuple(Color(*color) for color in cls._colors[11])
[docs] @classmethod
def benefit_harm(cls):
"""Benefit / harm study colors (red to light yellow to green)."""
return tuple(Color(*color) for color in cls._colors[12])
[docs] @classmethod
def harm(cls):
"""Harm colors (light yellow to red)."""
return tuple(Color(*color) for color in cls._colors[13])
[docs] @classmethod
def benefit(cls):
"""Benefit colors (light yellow to green)."""
return tuple(Color(*color) for color in cls._colors[14])
[docs] @classmethod
def shade_benefit_harm(cls):
"""Shade benefit / harm colors (dark red to white to dark blue)."""
return tuple(Color(*color) for color in cls._colors[15])
[docs] @classmethod
def shade_harm(cls):
"""Shade harm colors (white to dark red)."""
return tuple(Color(*color) for color in cls._colors[16])
[docs] @classmethod
def shade_benefit(cls):
"""Shade benefit colors (white to dark blue)."""
return tuple(Color(*color) for color in cls._colors[17])
[docs] @classmethod
def energy_balance(cls):
"""Energy Balance colors."""
return tuple(Color(*color) for color in cls._colors[18])
[docs] @classmethod
def energy_balance_storage(cls):
"""Energy Balance colors with a brown color for storage term."""
return tuple(Color(*color) for color in cls._colors[19])
[docs] @classmethod
def therm(cls):
"""THERM colors."""
return tuple(Color(*color) for color in cls._colors[20])
[docs] @classmethod
def cloud_cover(cls):
"""Cloud cover colors."""
return tuple(Color(*color) for color in cls._colors[21])
[docs] @classmethod
def black_to_white(cls):
"""Black to white colors."""
return tuple(Color(*color) for color in cls._colors[22])
[docs] @classmethod
def blue_green_red(cls):
"""Blue to Green to Red colors."""
return tuple(Color(*color) for color in cls._colors[23])
[docs] @classmethod
def multicolored_2(cls):
"""Multi-colored colors with less saturation."""
return tuple(Color(*color) for color in cls._colors[24])
[docs] @classmethod
def multicolored_3(cls):
"""Multi-colored colors with the least saturation."""
return tuple(Color(*color) for color in cls._colors[25])
[docs] @classmethod
def openstudio_palette(cls):
"""Standard color set for the OpenStudio surface types. Ordered as follows.
Exterior Wall, Interior Wall, Underground Wall,
Roof, Ceiling, Underground Roof,
Exposed Floor, Interior Floor, Ground Floor,
Window, Door, Shade, Air
"""
return tuple(Color(*color) for color in cls._colors[26])
[docs] @classmethod
def cividis(cls):
"""Cividis colors, which were designed to be colorblind friendly."""
return tuple(Color(*color) for color in cls._colors[27])
[docs] @classmethod
def viridis(cls):
"""Viridis colors, which were designed to be colorblind friendly."""
return tuple(Color(*color) for color in cls._colors[28])
[docs] @classmethod
def parula(cls):
"""Parula colors - the default of Matlab.
Parula was designed to be colorblind friendly
."""
return tuple(Color(*color) for color in cls._colors[29])
def __len__(self):
"""Return length of currently installed color sets."""
return len(self._colors)
def __getitem__(self, key):
"""Return one of the color sets."""
return tuple(Color(*color) for color in self._colors[key])
[docs] def ToString(self):
"""Overwrite .NET ToString."""
return self.__repr__()
def __repr__(self):
"""Colorset representation."""
return "{} currently installed Colorsets".format(len(self))
[docs]class ColorRange(object):
"""Ladybug Color Range. Used to generate colors from numerical values.
Args:
colors: A list of colors. Colors should be input as objects with
R, G, B values. Default is Ladybug's original colorset.
domain: A list of at least two numbers to set the lower and upper
boundary of the color range. This can also be a list of more than
two values, which can be used to approximate logarithmic or other types
of color scales. However, the number of values in the domain must
always be less than or equal to the number of colors.
Default: [0, 1].
continuous_colors: Boolean. If True, the colors generated from the
color range will be in a continuous gradient. If False,
they will be categorized in incremental groups according to the
number_of_segments. Default: True for continuous colors.
Properties:
* colors
* continuous_colors
* domain
Usage:
.. code-block:: python
1.
color_range = ColorRange(continuous_colors=False)
color_range.domain = [100, 2000]
color_range.colors = [Color(75, 107, 169), Color(245, 239, 103),
Color(234, 38, 0)]
print(color_range.color(99))
print(color_range.color(100))
print(color_range.color(2000))
print(color_range.color(2001))
>> (R:75, G:107, B:169)
>> (R:245, G:239, B:103)
>> (R:245, G:239, B:103)
>> (R:234, G:38, B:0)
2.
color_range = ColorRange(continuous_colors=False)
color_range.domain = [100, 2000]
color_range.colors = [Color(75, 107, 169), Color(245, 239, 103),
Color(234, 38, 0)]
color_range.color(300)
>> (R:245, G:239, B:103)
"""
def __init__(self, colors=None, domain=None, continuous_colors=True):
"""Initiate Ladybug color range.
"""
self._continuous_colors = True if continuous_colors is None \
else continuous_colors
assert isinstance(self._continuous_colors, bool), \
"continuous_colors should be a Boolean.\nGot {}.".format(
type(continuous_colors))
self._is_domain_set = False
self.colors = colors
self.domain = domain
[docs] @classmethod
def from_dict(cls, data):
"""Create a color range from a dictionary.
Args:
data: A python dictionary in the following format
.. code-block:: python
{
"colors": [{'r': 0, 'g': 0, 'b': 0}, {'r': 255, 'g': 255, 'b': 255}],
"domain": [0, 100],
"continuous_colors": True
}
"""
optional_keys = ('colors', 'domain', 'continuous_colors')
for key in optional_keys:
if key not in data:
data[key] = None
colors = None
if data['colors'] is not None:
colors = [Color.from_dict(col) for col in data['colors']]
return cls(colors, data['domain'], data['continuous_colors'])
@property
def colors(self):
"""Get or set the colors defining the color range."""
return self._colors
@colors.setter
def colors(self, cols):
if not cols:
self._colors = Colorset.original()
else:
assert isinstance(cols, Iterable) \
and not isinstance(cols, (str, dict, bytes, bytearray)), \
'Colors should be a list or tuple. Got {}'.format(type(cols))
try:
cols = tuple(col if isinstance(col, Color) else Color(
col.R, col.G, col.B) for col in cols)
except AttributeError:
try:
cols = tuple(Color(col.Red, col.Green, col.Blue) for col in cols)
except AttributeError:
raise ValueError("{} is not a valid list of colors".format(cols))
if self._is_domain_set:
self.domain = self.domain # re-check the domain against new colors
self._colors = cols
@property
def domain(self):
"""Get or set the domain defining the color range."""
return self._domain
@domain.setter
def domain(self, dom):
# check and prepare domain
if not dom:
dom = (0, 1)
else:
assert isinstance(dom, Iterable) \
and not isinstance(dom, (str, dict, bytes, bytearray)), \
'Domain should be a list or tuple. Got {}'.format(type(dom))
for val in dom:
assert isinstance(val, (float, int)), 'Values of a domain must be ' \
'numbers. Got {}.'.format(type(val))
dom = sorted(map(float, dom))
if self._continuous_colors: # continuous
# if type is continuous domain can only be 2 values
# or at least 1 value less than number of colors
if len(dom) == 2:
# remap domain based on colors
_step = float(dom[1] - dom[0]) / (len(self._colors) - 1)
_n = dom[0]
dom = tuple(_n + c * _step for c in range(len(self._colors)))
else:
assert len(self._colors) >= len(dom), \
"For a continuous color range, the length of the domain should " \
"be 2 or greater than the number of colors."
else: # segmented
# Number of colors should be at least one more than number of domain values
assert len(self._colors) > len(dom), \
"For a segmented color range, the length of colors " + \
"should be more than the number of domain values ."
self._is_domain_set = True
self._domain = tuple(dom)
@property
def continuous_colors(self):
"""Boolean noting whether colors generated are continuous or discrete."""
return self._continuous_colors
[docs] def color(self, value):
"""Calculate a color along the range for an input value."""
if value < self._domain[0]:
return self._colors[0]
if value > self._domain[-1]:
return self._colors[-1]
# find the index of the value in domain
for count, d in enumerate(self._domain):
if d <= value <= self._domain[count + 1]:
if self._continuous_colors:
return self._cal_color(value, count)
else:
return self._colors[count + 1]
[docs] def duplicate(self):
"""Return a copy of the current color range."""
return self.__copy__()
[docs] def to_dict(self):
"""Get color range as a dictionary."""
return {
'colors': [col.to_dict() for col in self.colors],
'domain': self.domain,
'continuous_colors': self.continuous_colors,
'type': 'ColorRange'
}
def _cal_color(self, value, color_index):
"""Blend between two colors based on input value."""
range_min_p = self._domain[color_index]
range_p = self._domain[color_index + 1] - range_min_p
try:
factor = (value - range_min_p) / range_p
except ZeroDivisionError:
factor = 0
min_color = self.colors[color_index]
max_color = self.colors[color_index + 1]
red = round(factor * (max_color.r - min_color.r) + min_color.r)
green = round(factor * (max_color.g - min_color.g) + min_color.g)
blue = round(factor * (max_color.b - min_color.b) + min_color.b)
return Color(red, green, blue)
def __copy__(self):
return self.__class__(self.colors, self.domain, self.continuous_colors)
def __len__(self):
"""Return length of colors."""
return len(self._colors)
def __getitem__(self, key):
"""Return key item from the color list."""
return self._colors[key]
def __iter__(self):
"""Use colors to iterate."""
return iter(self._colors)
[docs] def ToString(self):
"""Overwrite .NET ToString."""
return self.__repr__()
def __repr__(self):
"""Color Range representation."""
return "Color Range ({} colors) (domain {})".format(len(self), self.domain)