# -*- coding: utf-8 -*-
import colorsys
[docs]def pretty_si(number):
"""
Return a SI-postfixed string representation of a number (int or float).
"""
postfixes = ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
value = number
for postfix in postfixes:
if value / 1000.0 < 1:
break
value /= 1000.0
return "%.2f%s" % (value, postfix)
[docs]def pretty_bytes(bytecount):
"""
Return a human-readable representation given a size in bytes.
"""
units = ['Byte', 'kbyte', 'Mbyte', 'Gbyte', 'Tbyte']
value = bytecount
for unit in units:
if value / 1024.0 < 1:
break
value /= 1024.0
return "%.2f %s" % (value, unit)
[docs]def pretty_bits(bytecount):
"""
Return a human-readable representation in bits given a size in bytes.
"""
units = ['bit', 'kbit', 'Mbit', 'Gbit', 'Tbit']
value = bytecount * 8 # bytes to bits
for unit in units:
if value / 1024.0 < 1:
break
value /= 1024.0
return "%.2f %s" % (value, unit)
def pretty_time(seconds):
days = int(seconds / 86400)
seconds = seconds % 86400
hours = int(seconds / 3600)
seconds = seconds % 3600
minutes = int(seconds / 60)
seconds = seconds % 60
parts = []
if days > 0:
parts.append(f"{days}d")
if hours > 0:
parts.append(f"{hours}h")
if minutes > 0:
parts.append(f"{minutes}m")
parts.append("{seconds}s")
return " ".join(parts)
[docs]def ignore_none(*args):
"""
Return the first passed value that isn't ``None``.
"""
for arg in args:
if not arg is None:
return arg
def condense_addr_parts(items):
matching = []
max_match = min([len(x) for x in items])
for i in range(0, max_match):
s = set()
for item_idx in range(0, len(items)):
s.add(items[item_idx][i])
if len(s) > 1: # lazy hack, means not all items have the same part at index i
break
matching.append(items[item_idx][i])
return '.'.join(matching)
def alignment_offset(align, size):
x_align, y_align = align.split('_')
if x_align == 'left':
x_offset = 0
elif x_align == 'center':
x_offset = -size[0] / 2
elif x_align == 'right':
x_offset = -size[0]
else:
raise ValueError("unknown horizontal alignment: '%s', must be one of: left, center, right" % x_align)
if y_align == 'top':
y_offset = 0
elif y_align == 'center':
y_offset = -size[1] / 2
elif y_align == 'bottom':
y_offset = -size[1]
else:
raise ValueError("unknown horizontal alignment: '%s', must be one of: top, center, bottom" % y_align)
return (x_offset, y_offset)
[docs]class Color(object):
"""
Magic color class implementing and supplying on-the-fly manipulation of
RGB and HSV (and alpha) attributes.
"""
def __init__(self, red=None, green=None, blue=None, alpha=None, hue=None, saturation=None, value=None):
rgb_passed = bool(red)|bool(green)|bool(blue)
hsv_passed = bool(hue)|bool(saturation)|bool(value)
if not alpha:
alpha = 0.0
if rgb_passed and hsv_passed:
raise ValueError("Color can't be initialized with RGB and HSV at the same time.")
elif hsv_passed:
if not hue:
hue = 0.0
if not saturation:
saturation = 0.0
if not value:
value = 0.0
super(Color, self).__setattr__('hue', hue)
super(Color, self).__setattr__('saturation', saturation)
super(Color, self).__setattr__('value', value)
self._update_rgb()
else:
if not red:
red = 0
if not green:
green = 0
if not blue:
blue = 0
super(Color, self).__setattr__('red', red)
super(Color, self).__setattr__('green', green)
super(Color, self).__setattr__('blue', blue)
self._update_hsv()
super(Color, self).__setattr__('alpha', alpha)
def __setattr__(self, key, value):
if key in ('red', 'green', 'blue'):
if value > 1.0:
value = value % 1.0
super(Color, self).__setattr__(key, value)
self._update_hsv()
elif key in ('hue', 'saturation', 'value'):
if key == 'hue' and (value >= 360.0 or value < 0):
value = value % 360.0
elif key != 'hue' and value > 1.0:
value = 1.0
super(Color, self).__setattr__(key, value)
self._update_rgb()
else:
if key == 'alpha' and value > 1.0: # TODO: Might this be more fitting in another place?
value = 1.0
super(Color, self).__setattr__(key, value)
def __repr__(self):
return '<%s: red %f, green %f, blue %f, hue %f, saturation %f, value %f, alpha %f>' % (
self.__class__.__name__,
self.red,
self.green,
self.blue,
self.hue,
self.saturation,
self.value,
self.alpha
)
def clone(self):
return Color(red=self.red, green=self.green, blue=self.blue, alpha=self.alpha)
def blend(self, other, mode='normal'):
if self.alpha != 1.0: # no clue how to blend with a translucent bottom layer
self.red = self.red * self.alpha
self.green = self.green * self.alpha
self.blue = self.blue * self.alpha
self.alpha = 1.0
if mode == 'normal':
own_influence = 1.0 - other.alpha
self.red = (self.red * own_influence) + (other.red * other.alpha)
self.green = (self.green * own_influence) + (other.green * other.alpha)
self.blue = (self.blue * own_influence) + (other.blue * other.alpha)
def lighten(self, other):
if isinstance(other, int) or isinstance(other, float):
other = Color(red=other, green=other, blue=other, alpha=1.0)
if self.alpha != 1.0:
self.red = self.red * self.alpha
self.green = self.green * self.alpha
self.blue = self.blue * self.alpha
self.alpha = 1.0
red = self.red + (other.red * other.alpha)
green = self.green + (other.green * other.alpha)
blue = self.blue + (other.blue * other.alpha)
if red > 1.0:
red = 1.0
if green > 1.0:
green = 1.0
if blue > 1.0:
blue = 1.0
self.red = red
self.green = green
self.blue = blue
def darken(self, other):
if isinstance(other, int) or isinstance(other, float):
other = Color(red=other, green=other, blue=other, alpha=1.0)
red = self.red - other.red
green = self.green - other.green
blue = self.blue - other.blue
if red < 0:
red = 0
if green < 0:
green = 0
if blue < 0:
blue = 0
self.red = red
self.green = green
self.blue = blue
[docs] def tuple_rgb(self):
""" return color (without alpha) as tuple, channels being float 0.0-1.0 """
return (self.red, self.green, self.blue)
[docs] def tuple_rgba(self):
""" return color (*with* alpha) as tuple, channels being float 0.0-1.0 """
return (self.red, self.green, self.blue, self.alpha)
def _update_hsv(self):
hue, saturation, value = colorsys.rgb_to_hsv(self.red, self.green, self.blue)
super(Color, self).__setattr__('hue', hue * 360.0)
super(Color, self).__setattr__('saturation', saturation)
super(Color, self).__setattr__('value', value)
def _update_rgb(self):
red, green, blue = colorsys.hsv_to_rgb(self.hue / 360.0, self.saturation, self.value)
super(Color, self).__setattr__('red', red)
super(Color, self).__setattr__('green', green)
super(Color, self).__setattr__('blue', blue)
[docs]class DotDict(dict):
"""
A dictionary with its data being readable through faked attributes.
Used to avoid [[[][][][][]] in caption formatting.
"""
def __getattribute__(self, name):
#data = super(DotDict, self).__getattribute__('data')
keys = super(DotDict, self).keys()
if name in keys:
return self.get(name)
return super(DotDict, self).__getattribute__(name)
[docs]class Box(object):
"""
Can wrap multiple :ref:`visualizer`\s, used for layouting.
Orders added visualizers from left to right and top to bottom.
This is basically a smart helper for :func:`Gulik.add_visualizer`.
"""
def __init__(self, app, x, y, width, height):
self._last_right = 0
self._last_top = 0
self._last_bottom = 0
self._next_row_x = 0
self.app = app
self.x = x
self.y = y
self.width = width
self.height = height
[docs] def place(self, component, cls, **kwargs):
"""
place a new :ref:`visualizer`.
Parameters:
component (str): The :ref:`component` string identifying the data source.
cls (:class:`type`, child of :class:`visualizers.Visualizer`): The visualizer class to instantiate.
**kwargs: passed on to ``cls``\' constructor. width and height defined in here are honored.
"""
width = kwargs.get('width', None)
height = kwargs.get('height', None)
if width is None:
width = self.width - self._last_right
if height is None:
height = self._last_bottom - self._last_top # same height as previous visualizer
if height == 0: # should only happen on first visualizer
height = self.height
elif height is None:
height = self.height - self._last_bottom
if self._last_right + width > self.width: # move to next "row"
#print("next row", component, cls, kwargs)
x = self._next_row_x
y = self._last_bottom
self._next_row_x = 0 # this will probably break adding a third multi-stacked column, but works for now
else:
x = self._last_right
y = self._last_top
kwargs['x'] = self.x + x
kwargs['y'] = self.y + y
kwargs['width'] = width
kwargs['height'] = height
self.app.add_visualizer(component, cls, **kwargs)
if y + height < self._last_bottom:
self._next_row_x = self._last_right
self._last_right = x + width
self._last_top = y
self._last_bottom = y + height
assert self._last_bottom <= self.y + self.height, f"Box already full! Can't place {cls.__name__}"