# -*- coding: utf-8 -*-
import math
import collections
import cairo
from gi.repository import Pango, PangoCairo
from . import helpers
from . import palettes
[docs]class Visualizer(object):
"""
The base class for all visualizers. Not called widget to avoid naming
confusion with gtk widgets (which aren't even used in gulik).
Usually you won't instantiate this by yourself but use :func:`gulik.Box.place`.
Parameters:
app (:class:`gulik.Gulik`): The app managing visualizers and monitors
monitor (:ref:`monitor`): The monitor managing data collection for this visualizer
x (int): leftmost coordinate on the x-axis
y (int): topmost coordinate on the y-axis
width (int): overall width (including **margin** and **padding**)
height (int): overall height (including **margin** and **padding**)
margin (int, optional): margin applied around all sides
margin_left (int, optional): margin applied to the left side
margin_right (int, optional): margin applied to the right side
margin_top (int, optional): margin applied to the top side
margin_bottom (int, optional): margin applied to the bottom side
padding (int, optional): padding applied around all sides
padding_left (int, optional): padding applied to the left side
padding_right (int, optional): padding applied to the right side
padding_top (int, optional): padding applied to the top side
padding_bottom (int, optional): padding applied to the bottom side
elements (list of str): A list of :ref:`element`\s to visualize
captions (list of dict, optional): A list of :ref:`caption descriptions<caption-description>`
legend (bool): Whether to try to automatically create a legend of elements
legend_order('normal' or 'reverse', optional): Whether to reverse the legend order
legend_format(str): A format string, can contain ``{element}`` to refer to the element legend items refer to.
legend_size (int, optional): height of a single legend item
legend_placement ('padding' or 'inner'): Where to place the legend
legend_margin (int, optional): margin applied around all sides of the legend
legend_margin_left (int, optional): margin applied to the left side of the legend
legend_margin_right (int, optional): margin applied to the right side of the legend
legend_margin_top (int, optional): margin applied to the top side of the legend
legend_margin_bottom (int, optional): margin applied to the bottom side of the legend
legend_padding (int, optional): padding applied around all sides of the legend
legend_padding_left (int, optional): padding applied to the left side of the legend
legend_padding_right (int, optional): padding applied to the right side of the legend
legend_padding_top (int, optional): padding applied to the top side of the legend
legend_padding_bottom (int, optional): padding applied to the bottom side of the legend
foreground (:class:`gulik,helpers.Color`, optional): The foreground color, base-color for generated palettes
background (:class:`gulik.helpers.Color`, optional): The background color
pattern (function, optional): An executable :ref:`pattern`
palette (function, optional): An executable :ref:`palette`
combination (str, optional): The :ref:`combination` mode used when displaying multiple :ref:`element`\s
"""
def __init__(
self,
app,
monitor,
x=0,
y=0,
width=None,
height=None,
margin=None,
margin_left=None,
margin_right=None,
margin_top=None,
margin_bottom=None,
padding=None,
padding_left=None,
padding_right=None,
padding_top=None,
padding_bottom=None,
elements=None,
captions=None,
caption_placement=None,
legend=None,
legend_order=None,
legend_format=None,
legend_size=None,
legend_placement=None,
legend_margin=None,
legend_margin_left=None,
legend_margin_right=None,
legend_margin_top=None,
legend_margin_bottom=None,
legend_padding=None,
legend_padding_left=None,
legend_padding_right=None,
legend_padding_top=None,
legend_padding_bottom=None,
foreground=None,
background=None,
pattern=None,
palette=None,
combination=None,
operator=None
):
self.app = app
self.monitor = monitor
self.x = x
self.y = y
self.elements = helpers.ignore_none(elements, [])
self.captions = helpers.ignore_none(captions, list())
self.caption_placement = helpers.ignore_none(caption_placement, self.get_style('caption_placement'))
self.legend = helpers.ignore_none(legend, self.get_style('legend'))
self.legend_order = helpers.ignore_none(legend_order, self.get_style('legend_order'))
self.legend_format = legend_format
self.legend_placement = helpers.ignore_none(legend_placement, self.get_style('legend_placement'))
self.legend_size= helpers.ignore_none(legend_size, self.get_style('legend_size'))
self.legend_margin_left = helpers.ignore_none(legend_margin_left, legend_margin, self.get_style('legend_margin', 'left'))
self.legend_margin_right = helpers.ignore_none(legend_margin_right, legend_margin, self.get_style('legend_margin', 'right'))
self.legend_margin_top = helpers.ignore_none(legend_margin_top, legend_margin, self.get_style('legend_margin', 'top'))
self.legend_margin_bottom = helpers.ignore_none(legend_margin_bottom, legend_margin, self.get_style('legend_margin', 'bottom'))
self.legend_padding_left = helpers.ignore_none(legend_padding_left, legend_padding, self.get_style('legend_padding', 'left'))
self.legend_padding_right = helpers.ignore_none(legend_padding_right, legend_padding, self.get_style('legend_padding', 'right'))
self.legend_padding_top = helpers.ignore_none(legend_padding_top, legend_padding, self.get_style('legend_padding', 'top'))
self.legend_padding_bottom = helpers.ignore_none(legend_padding_bottom, legend_padding, self.get_style('legend_padding', 'bottom'))
self.operator = helpers.ignore_none(operator, self.get_style('operator'))
self.width = helpers.ignore_none(width, self.get_style('width'))
self.height = helpers.ignore_none(height, self.get_style('height'))
self.margin_left = helpers.ignore_none(margin_left, margin, self.get_style('margin', 'left'))
self.margin_right = helpers.ignore_none(margin_right, margin, self.get_style('margin', 'right'))
self.margin_top = helpers.ignore_none(margin_top, margin, self.get_style('margin', 'top'))
self.margin_bottom = helpers.ignore_none(margin_bottom, margin, self.get_style('margin', 'bottom'))
self.padding_left = helpers.ignore_none(padding_left, padding, self.get_style('padding', 'left'))
self.padding_right = helpers.ignore_none(padding_right, padding, self.get_style('padding', 'right'))
self.padding_top = helpers.ignore_none(padding_top, padding, self.get_style('padding', 'top'))
self.padding_bottom = helpers.ignore_none(padding_bottom, padding, self.get_style('padding', 'bottom'))
self.colors = {}
self.colors['foreground'] = helpers.ignore_none(foreground, self.get_style('color', 'foreground'))
self.colors['background'] = helpers.ignore_none(background, self.get_style('color', 'background'))
self.pattern = helpers.ignore_none(pattern, self.get_style('pattern'))
self.palette = helpers.ignore_none(palette, self.get_style('palette')) # function to generate color palettes with
self.combination = helpers.ignore_none(combination, 'separate') # combination mode when handling multiple elements. 'separate', 'cumulative' or 'cumulative_force'. cumulative assumes all values add up to max 1.0, while separate assumes every value can reach 1.0 and divides all values by the number of elements handled
assert self.inner_width > 0, f"margin and padding too big, implying negative inner width for {self.__class__.__name__}/{self.monitor.component}/{self.elements}"
assert self.inner_height > 0, f"margin and padding too big, implying negative inner height for {self.__class__.__name__}/{self.monitor.component}/{self.elements}"
self.monitor.register_elements(self.elements)
if self.legend:
legend_x = self.x + self.margin_left + self.padding_left
legend_y = self.y + self.margin_top + self.padding_top
if self.legend_placement == 'inner':
legend_height = self.inner_height
else: # 'padding'
legend_y += self.inner_height
legend_height = self.padding_bottom
rownum = legend_height // self.legend_size
if rownum < 1:
print(f"Can't add autolegend to {self.__class__.__name__}/{self.monitor.component} because of insufficient space: {legend_height}px")
else:
legend_info = self.legend_info()
colnum = math.ceil(len(legend_info) // rownum) or 1
cell_width = self.inner_width // colnum
#colors = self.palette(self.colors['foreground'], len(self.elements))
box = self.app.box(legend_x, legend_y, self.inner_width, legend_height)
if self.legend_order == 'reverse':
iterator = reversed(legend_info)
else:
iterator = legend_info
#for element in legend_elements:
for element in iterator:
color, text = legend_info[element]
if self.legend_format:
text = self.legend_format.format(**{'element': element})
box.place(
self.monitor.component,
Text,
text=text,
align='center_center',
foreground=color,
background=helpers.Color(0,0,0, 0),
width=cell_width,
height=self.legend_size,
margin_left=self.legend_margin_left,
margin_right=self.legend_margin_right,
margin_top=self.legend_margin_top,
margin_bottom=self.legend_margin_bottom,
padding_left=self.legend_padding_left,
padding_right=self.legend_padding_right,
padding_top=self.legend_padding_top,
padding_bottom=self.legend_padding_bottom,
legend=False
)
[docs] def get_style(self, name, subname=None):
"""
load the most specific style setting available given a name and optional subname.
usage examples: self.get_style('margin', 'left'),
"""
keys = []
if subname:
keys.append('_'.join([name, self.__class__.__name__, subname]).upper())
keys.append('_'.join([name, self.__class__.__name__]).upper())
if subname:
keys.append('_'.join([name, subname]).upper())
keys.append(name.upper())
for key in keys:
if key in self.app.config:
return self.app.config[key]
[docs] def legend_info(self):
""" defines colors for legend elements """
data = collections.OrderedDict()
colors = self.palette(self.colors['foreground'], len(self.elements))
for idx, color in enumerate(colors):
element = self.elements[idx]
data[element] = (color, element)
return data
@property
def padded_width(self):
return self.width - self.margin_left - self.margin_right
@property
def padded_height(self):
return self.height - self.margin_top - self.margin_bottom
@property
def inner_width(self):
return self.padded_width - self.padding_left - self.padding_right
@property
def inner_height(self):
return self.padded_height - self.padding_top - self.padding_bottom
def set_brush(self, context, color): # possible TODO: better function name
if self.pattern:
context.set_source_surface(self.pattern(color))
context.get_source().set_extend(cairo.Extend.REPEAT)
else:
context.set_source_rgba(*color.tuple_rgba())
def draw_background(self, context):
context.set_source_rgba(*self.colors['background'].tuple_rgba())
context.rectangle(self.x + self.margin_left+ self.padding_left, self.y + self.margin_top + self.padding_top, self.inner_width, self.inner_height)
context.fill()
def draw_captions(self, context):
for caption in self.captions:
context.save()
context.set_operator(caption.get('operator', self.get_style('operator', 'caption')))
if 'position' in caption:
if isinstance(caption['position'], str):
# handle alignment-style strings like "center_bottom"
if self.caption_placement == 'inner':
offset = [-x for x in helpers.alignment_offset(caption['position'], (self.inner_width, self.inner_height))]
else:
offset = [-x for x in helpers.alignment_offset(caption['position'], (self.padded_width, self.padded_height))]
else:
offset = caption['position']
else:
offset = [0, 0]
if self.caption_placement == 'inner':
position = [offset[0] + self.x + self.margin_left + self.padding_left, offset[1] + self.y + self.margin_top + self.padding_top]
else:
position = [offset[0] + self.x + self.margin_left, offset[1] + self.y + self.margin_top]
caption_text = self.monitor.caption(caption['text'])
self.app.draw_text(
context,
caption_text,
position[0],
position[1],
align=caption.get('align', None),
color=caption.get('color', self.get_style('color', 'caption')),
font_size=caption.get('font_size', self.get_style('font_size', 'caption')),
font_weight=caption.get('font_weight', self.get_style('font_weight', 'caption'))
)
context.restore()
[docs] def update(self, context):
"""
Parameters:
context (:class:`cairo.Context`): cairo context of the window
"""
context.save()
context.set_operator(self.operator)
self.draw(context)
context.restore()
def draw(self, context):
raise NotImplementedError("%s.draw not implemented!" % self.__class__.__name__)
[docs]class Text(Visualizer):
"""
Scrollable text using monitors' ``caption`` function to give textual
representations of values, prettified where necessary.
"""
def __init__(self, app, monitor, text, speed=25, align=None, **kwargs):
super(Text, self).__init__(app, monitor, **kwargs)
self.text = text # the text to be rendered, a format string passed to monitor.caption
self.previous_text = '' # to be able to detect change
self.speed = speed
self.align = helpers.ignore_none(align, 'left_top')
surface = cairo.ImageSurface(cairo.Format.ARGB32, 10, 10)
context = cairo.Context(surface)
font = Pango.FontDescription('%s %s 10' % (self.get_style('font'), self.get_style('font_weight')))
layout = PangoCairo.create_layout(context)
layout.set_font_description(font)
layout.set_text('0', -1) # naively assuming 0 is the highest glyph
size = layout.get_pixel_size()
if size[1] > 10:
self.font_size = self.inner_height * 10/size[1]
else:
self.font_size = self.inner_height # probably not gonna happen
self.direction = 'left'
self.offset = 0.0
self.step = speed / self.app.config['FPS'] # i.e. speed in pixel/s
def draw_background(self, context):
context.set_source_rgba(*self.colors['background'].tuple_rgba())
context.rectangle(self.x + self.margin_left, self.y + self.margin_top, self.padded_width, self.padded_height)
context.fill()
def draw(self, context):
text = self.monitor.caption(self.text)
self.draw_background(context)
context.save()
context.rectangle(self.x + self.margin_left + self.padding_left, self.y + self.margin_top + self.padding_top, self.inner_width, self.inner_height)
context.clip()
context.set_source_rgba(*self.colors['foreground'].tuple_rgba())
font = Pango.FontDescription('%s %s %d' % (self.get_style('font'), self.get_style('font_weight'), self.font_size))
layout = PangoCairo.create_layout(context)
layout.set_font_description(font)
layout.set_text(text, -1)
size = layout.get_pixel_size()
align_offset = helpers.alignment_offset(self.align, size) # needed for the specified text alignment
#align_offset = [sum(x) for x in zip(helpers.alignment_offset(self.align, size), helpers.alignment_offset(self.align, [self.inner_width, self.inner_height]))] # needed for the specified text alignment
max_offset = size[0] - self.inner_width # biggest needed offset for marquee, can be negative if all text fits
if max_offset <= 0 or text != self.previous_text:
self.direction = 'left'
self.offset = 0
x = self.x + self.margin_left + self.padding_left - self.offset
if max_offset < 0: # only account for horizontal offset when space allows
x += align_offset[0]
if self.align.startswith('center'):
x += self.inner_width / 2
elif self.align.startswith('right'):
x += self.inner_width
y = self.y + self.margin_top + self.padding_top + align_offset[1] # NOTE: does stuff even show up with vertical align != center?
if self.align.endswith('center'):
y += self.inner_height / 2
elif self.align.endswith('bottom'):
y += self.inner_height
context.translate(x, y)
PangoCairo.update_layout(context, layout)
PangoCairo.show_layout(context, layout)
context.restore()
if self.direction == 'left':
self.offset += self.step
if self.offset > max_offset:
self.direction = 'right'
self.offset = max_offset
else:
self.offset -= self.step
if self.offset < 0:
self.direction = 'left'
self.offset = 0
self.previous_text = text
[docs]class Rect(Visualizer):
"""
.. image:: _static/rect.png
"""
def draw_rect(self, context, value, color, offset=0.0):
self.set_brush(context, color)
context.rectangle(
self.x + self.margin_left + self.padded_width * offset,
self.y + self.margin_top,
self.padded_width * value,
self.padded_height)
context.fill()
def draw_background(self, context):
self.draw_rect(context, 1, self.colors['background'])
def draw(self, context):
colors = self.palette(self.colors['foreground'], len(self.elements))
offset = 0.0
self.draw_background(context)
for idx, element in enumerate(self.elements):
value = self.monitor.normalize(element)
if self.combination != 'cumulative':
value /= len(self.elements)
color = colors[idx]
self.draw_rect(context, value, color, offset)
if self.combination.startswith('cumulative'):
offset += value
else:
offset += 1.0 / len(self.elements)
self.draw_captions(context)
[docs]class MirrorRect(Visualizer):
"""
Mirrored variant of :class:`Rect`.
"""
def __init__(self, app, monitor, **kwargs):
self.left = kwargs['elements'][0]
self.right = kwargs['elements'][1]
kwargs['elements'] = self.left + self.right
super(MirrorRect, self).__init__(app, monitor, **kwargs)
self.x_center = self.x + self.margin_left + (self.inner_width / 2)
self.draw_left = self.draw_rect_negative
self.draw_right = self.draw_rect
[docs] def legend_info(self):
""" defines colors for legend elements """
data = collections.OrderedDict()
colors = self.palette(self.colors['foreground'], max(len(self.left), len(self.right)))
parts = [[] for _ in range(0, len(colors))]
for elements in (self.left, self.right):
for idx, element in enumerate(elements):
parts[idx].append(element.split('.'))
condensed = []
for items in parts:
condensed.append(helpers.condense_addr_parts(items))
assert len(colors) == len(condensed)
for idx, condensed_name in enumerate(condensed):
data[condensed_name] = (colors[idx], condensed_name)
return data
def draw_rect(self, context, value, color, offset=0.0):
self.set_brush(context, color)
context.rectangle(self.x_center + self.inner_width / 2 * offset, self.y + self.margin_top + self.padding_top, self.inner_width / 2 * value, self.inner_height)
context.fill()
def draw_rect_negative(self, context, value, color, offset=0.0):
self.set_brush(context, color)
context.rectangle(self.x_center - self.inner_width / 2 * offset - self.inner_width / 2 * value, self.y + self.margin_top + self.padding_top, self.inner_width / 2 * value, self.inner_height)
context.fill()
def draw(self, context):
colors = self.palette(self.colors['foreground'], max(len(self.left), len(self.right)))
self.draw_background(context)
for elements, drawer in ((self.left, self.draw_left), (self.right, self.draw_right)):
offset = 0.0
for idx, element in enumerate(elements):
value = self.monitor.normalize(element)
if self.combination != 'cumulative':
value /= len(elements)
color = colors[idx]
drawer(context, value, color, offset)
if self.combination.startswith('cumulative'):
offset += value
else:
offset += 1.0 / len(elements)
self.draw_captions(context)
[docs]class Arc(Visualizer):
"""
.. image:: _static/arc.png
Parameters:
stroke_width (int): width of the arc in pixels.
"""
def __init__(self, app, monitor, stroke_width=5, **kwargs):
super(Arc, self).__init__(app, monitor, **kwargs)
self.stroke_width = stroke_width
#self.radius = (min(self.width, self.height) / 2) - (2 * self.padding) - (self.stroke_width / 2)
self.radius = (min(self.inner_width, self.inner_height) / 2) - self.stroke_width / 2
self.x_center = self.x + self.margin_left + self.padding_left + (self.inner_width / 2)
self.y_center = self.y + self.margin_top + self.padding_top + (self.inner_height / 2)
def draw_arc(self, context, value, color, offset=0.0):
context.set_line_width(self.stroke_width)
context.set_line_cap(cairo.LINE_CAP_BUTT)
self.set_brush(context, color)
context.arc(
self.x_center,
self.y_center,
self.radius,
math.pi / 2 + math.pi * 2 * offset,
math.pi / 2 + math.pi * 2 * (offset + value)
)
context.stroke()
def draw_background(self, context):
self.draw_arc(context, 1, self.colors['background'])
def draw(self, context):
self.draw_background(context)
colors = self.palette(self.colors['foreground'], len(self.elements))
offset = 0.0
for idx, element in enumerate(self.elements):
value = self.monitor.normalize(element)
if self.combination != 'cumulative':
value /= len(self.elements)
color = colors[idx]
self.draw_arc(context, value, color, offset=offset)
if self.combination == 'separate':
offset += 1 / len(self.elements)
else:
offset += value
self.draw_captions(context)
[docs]class MirrorArc(MirrorRect, Arc):
"""
Mirrored variant of :class:`Arc`.
"""
def __init__(self, app, monitor, **kwargs):
super(MirrorArc, self).__init__(app, monitor, **kwargs)
self.draw_left = self.draw_arc_negative
self.draw_right = self.draw_arc
def draw_arc(self, context, value, color, offset=0.0):
value /= 2
offset /= 2
super(MirrorArc, self).draw_arc(context, value, color, offset=offset)
def draw_arc_negative(self, context, value, color, offset=0.0):
context.set_line_width(self.stroke_width)
context.set_line_cap(cairo.LINE_CAP_BUTT)
self.set_brush(context, color)
context.arc_negative(
self.x_center,
self.y_center,
self.radius,
math.pi / 2 - math.pi * offset,
math.pi / 2 - math.pi * (offset + value)
)
context.stroke()
def draw_background(self, context):
self.draw_arc(context, 2, self.colors['background'])
[docs]class Plot(Visualizer):
"""
.. image:: _static/plot.png
Parameters:
num_points (int, optional): The number of datapoints to show.
autoscale (bool, optional): Whether to automatically "zoom" into the data.
markers (bool, optional): Whether tho render markers at data point coordinates.
line (bool, optional): Whether to draw a line.
grid (bool, optional): Whether to draw a grid. The grid automatically adapts if ``autoscale`` is ``True``.
**kwargs: passed up to :class:`Visualizer`\.
"""
def __init__(self, app, monitor, num_points=None, autoscale=None, markers=None, line=None, grid=None, **kwargs):
super(Plot, self).__init__(app, monitor, **kwargs)
if num_points:
self.num_points = num_points
self.step = self.inner_width / (num_points - 1)
assert int(self.step) >= 1, "num_points %d exceeds pixel density!" % num_points
else:
# closest fit to 8px step that fills self.inner_width perfectly
self.num_points = int(self.inner_width // 8)
self.step = self.inner_width / (self.num_points - 1)
self.prepare_points() # creates self.points with a deque for every plot
self.autoscale = helpers.ignore_none(autoscale, self.get_style('autoscale'))
self.markers = helpers.ignore_none(markers, self.get_style('markers'))
self.line = helpers.ignore_none(line, self.get_style('line'))
self.grid = helpers.ignore_none(grid, self.get_style('grid'))
self.grid_height = self.inner_height
self.colors['plot_line'] = self.colors['foreground'].clone()
self.colors['plot_line'].alpha *= 0.8
self.colors['plot_fill'] = self.colors['foreground'].clone()
if self.line:
self.colors['plot_fill'].alpha *= 0.25
self.colors['grid_major'] = self.colors['plot_line'].clone()
self.colors['grid_major'].alpha *= 0.5
self.colors['grid_minor'] = self.colors['background'].clone()
self.colors['grid_minor'].alpha *= 0.8
self.colors['grid_milli'] = self.colors['background'].clone()
self.colors['grid_milli'].alpha *= 0.4
self.colors['caption_scale'] = self.colors['foreground'].clone()
self.colors['caption_scale'].alpha *= 0.6
self.colors_plot_marker = self.palette(self.colors['foreground'], len(self.elements))
self.colors_plot_line = self.palette(self.colors['plot_line'], len(self.elements))
self.colors_plot_fill = self.palette(self.colors['plot_fill'], len(self.elements))
def prepare_points(self):
self.points = collections.OrderedDict()
for element in self.elements:
self.points[element] = collections.deque([], self.num_points)
def get_scale_factor(self, elements=None):
if elements is None:
elements = self.elements
if self.combination.startswith('cumulative'):
cumulative_points = []
for idx in range(0, self.num_points):
value = 0.0
for element in elements:
try:
value += self.points[element][idx]
except IndexError as e:
continue # means self.points deques aren't filled completely yet
if self.combination == 'cumulative_force':
cumulative_points.append(value / len(elements))
else:
cumulative_points.append(value)
p = max(cumulative_points)
else:
maxes = []
for element in elements:
if len(self.points[element]):
maxes.append(max(self.points[element]))
p = max(maxes)
if p > 0:
return 1.0 / p
return 0.0
def get_points_scaled(self, element, elements=None):
if elements is None:
elements = self.elements
scale_factor = self.get_scale_factor(elements)
if scale_factor == 0.0:
return [0.0 for _ in range(0, len(self.points[element]))]
r = []
for amplitude in self.points[element]:
r.append(amplitude * scale_factor)
return r
def draw_grid(self, context, elements=None):
if elements is None:
elements = self.elements
scale_factor = self.get_scale_factor(elements)
context.set_line_width(1)
context.set_source_rgba(*self.colors['grid_minor'].tuple_rgba())
#context.set_dash([1,1])
x_coords = [x * self.step for x in range(0, self.num_points - 1)]
x_coords = [self.x + self.margin_left + self.padding_left + x for x in x_coords]
for x in x_coords:
context.move_to(x, self.y + self.margin_top + self.padding_top)
context.line_to(x, self.y + self.margin_top + self.padding_top + self.grid_height)
context.stroke()
if not self.autoscale:
for i in range(0, 110, 10): # 0,10,20..100
value = i / 100.0
y = self.y + self.margin_top + self.padding_top + self.grid_height - self.grid_height * value
context.move_to(self.x + self.margin_left + self.padding_left, y)
context.line_to(self.x + self.margin_left + self.padding_left + self.inner_width, y)
context.stroke()
elif scale_factor > 0:
if scale_factor > 1000:
return # current maximum value under 1 permill, thus no guides are placed
elif scale_factor > 100:
# current maximum under 1 percent, place permill guides
context.set_source_rgba(*self.colors['grid_milli'].tuple_rgba())
for i in range(0, 10):
# place lines for 0-9 percent
value = i / 1000.0 * scale_factor
y = self.y + self.margin_top + self.padding_top + self.grid_height - self.grid_height * value
if y < self.y + self.margin_top + self.padding_top:
break # stop the loop if guides would be placed outside the visualizer
context.move_to(self.x + self.margin_left + self.padding_left, y)
context.line_to(self.x + self.margin_left + self.padding_left + self.inner_width, y)
context.stroke()
elif scale_factor > 10:
context.set_source_rgba(*self.colors['grid_minor'].tuple_rgba())
for i in range(0, 10):
# place lines for 0-9 percent
value = i / 100.0 * scale_factor
y = self.y + self.margin_top + self.padding_top + self.grid_height - self.grid_height * value
if y < self.y + self.margin_top + self.padding_top:
break # stop the loop if guides would be placed outside the visualizer
context.move_to(self.x + self.margin_left + self.padding_left, y)
context.line_to(self.x + self.margin_left + self.padding_left + self.inner_width, y)
context.stroke()
else: # major (10% step) guides
context.set_source_rgba(*self.colors['grid_major'].tuple_rgba())
for i in range(0, 110, 10): # 0,10,20..100
value = i / 100.0 * scale_factor
y = self.y + self.margin_top + self.padding_top + self.grid_height - self.grid_height * value
if y < self.y + self.margin_top + self.padding_top:
break # stop the loop if guides would be placed outside the visualizer
context.move_to(self.x + self.margin_left + self.padding_left, y)
context.line_to(self.x + self.margin_left + self.padding_left + self.inner_width, y)
context.stroke()
#context.set_dash([1,0]) # reset dash
def draw_plot(self, context, points, colors, offset=None):
coords = []
for idx, amplitude in enumerate(points):
if offset:
amplitude += offset[idx]
coords.append((
self.x + idx * self.step + self.margin_left + self.padding_left,
self.y + self.margin_top + self.padding_top + self.inner_height - (self.inner_height * amplitude)
))
if self.line:
# draw lines
context.set_source_rgba(*colors['plot_line'].tuple_rgba())
context.set_line_width(2)
#context.set_line_cap(cairo.LINE_CAP_BUTT)
for idx, (x, y) in enumerate(coords):
if idx == 0:
context.move_to(x, y)
else:
context.line_to(x, y)
context.stroke()
if self.pattern:
context.set_source_surface(self.pattern(colors['plot_fill']))
context.get_source().set_extend(cairo.Extend.REPEAT)
context.move_to(self.x + self.margin_left + self.padding_left, self.y + self.margin_top + self.padding_top + self.inner_height)
for idx, (x, y) in enumerate(coords):
context.line_to(x, y)
if offset: # "cut out" the offset at the bottom
previous_amplitude = None
for i, amplitude in enumerate(reversed(offset)):
if len(offset) - i > len(points):
continue # ignore x coordinates not reached yet by the graph
if (amplitude != previous_amplitude or i == len(offset) - 1):
offset_x = self.x + self.margin_left + self.padding_left + self.inner_width - i * self.step
offset_y = self.y + self.margin_top + self.padding_top + self.inner_height - self.inner_height * amplitude
context.line_to(offset_x, offset_y)
else:
context.line_to(x, self.y + self.margin_top + self.padding_top + self.inner_height)
context.close_path()
context.fill()
if self.markers:
# place points
for (x, y) in coords:
context.set_source_rgba(*colors['plot_marker'].tuple_rgba())
context.arc(
x,
y,
2,
0,
2 * math.pi
)
context.fill()
def update(self, context):
for element in set(self.elements): # without set() the same element multiple times leads to multiple same points added every time.
self.points[element].append(self.monitor.normalize(element))
super(Plot, self).update(context)
def draw(self, context):
self.draw_background(context)
if self.grid:
self.draw_grid(context)
if self.autoscale:
scale_factor = self.get_scale_factor()
if scale_factor == 0.0:
text = u"∞X"
else:
text = "%sX" % helpers.pretty_si(self.get_scale_factor())
self.app.draw_text(context, text, self.x + self.margin_left +self.padding_left + self.inner_width, self.y, align='right_top', color=self.colors['caption_scale'], font_size=self.get_style('font_size', 'scale'))
colors_plot_marker = self.palette(self.colors['foreground'], len(self.elements))
colors_plot_line = self.palette(self.colors['plot_line'], len(self.elements))
colors_plot_fill = self.palette(self.colors['plot_fill'], len(self.elements))
offset = [0.0 for _ in range(0, self.num_points)]
for idx, element in enumerate(self.elements):
colors = {
'plot_marker': self.colors_plot_marker[idx],
'plot_line': self.colors_plot_line[idx],
'plot_fill': self.colors_plot_fill[idx]
}
if self.autoscale:
points = self.get_points_scaled(element)
else:
points = list(self.points[element])
if self.combination == 'separate':
self.draw_plot(context, points, colors)
elif self.combination.startswith('cumulative'):
if self.combination == 'cumulative_force':
for idx in range(0, len(points)):
points[idx] /= len(self.elements)
self.draw_plot(context, points, colors, offset)
for idx, value in enumerate(points):
offset[idx] += value
self.draw_captions(context)
[docs]class MirrorPlot(Plot):
"""
Mirrored variant of :class:`Plot`.
"""
def __init__(self, app, monitor, scale_lock=True, **kwargs):
self.up = kwargs['elements'][0]
self.down = kwargs['elements'][1]
kwargs['elements'] = self.up + self.down
super(MirrorPlot, self).__init__(app, monitor, **kwargs)
self.y_center = self.y + self.margin_top + self.padding_top + (self.inner_height / 2)
self.scale_lock = scale_lock # bool, whether to use the same scale for up and down
self.grid_height /= 2
palettes.len = max((len(self.up), len(self.down)))
self.colors_plot_marker = self.palette(self.colors['foreground'], palettes.len)
self.colors_plot_line = self.palette(self.colors['plot_line'], palettes.len)
self.colors_plot_fill = self.palette(self.colors['plot_fill'], palettes.len)
def legend_info(self):
data = collections.OrderedDict()
colors = self.palette(self.colors['foreground'], max(len(self.up), len(self.down)))
parts = [[] for _ in range(0, len(colors))]
for elements in (self.up, self.down):
for idx, element in enumerate(elements):
parts[idx].append(element.split('.'))
condensed = []
for items in parts:
condensed.append(helpers.condense_addr_parts(items))
assert len(colors) == len(condensed)
for idx, condensed_name in enumerate(condensed):
data[condensed_name] = (colors[idx], condensed_name)
return data
def prepare_points(self):
self.points = collections.OrderedDict()
for element in self.elements:
self.points[element] = collections.deque([], self.num_points)
def get_scale_factor(self, elements=None):
if self.scale_lock:
scale_up = super(MirrorPlot, self).get_scale_factor(self.up)
scale_down = super(MirrorPlot, self).get_scale_factor(self.down)
if (scale_up > 0) and (scale_down > 0): # both values > 0
return min((scale_up, scale_down))
elif (scale_up == 0) != (scale_down == 0): # one value is 0
return max(scale_up, scale_down)
else: # both values are 0
return 0
return super(MirrorPlot, self).get_scale_factor(elements)
def draw_grid(self, context, elements=None):
context.set_line_width(1)
context.set_source_rgba(*self.colors['grid_milli'].tuple_rgba())
context.move_to(self.x + self.margin_left + self.padding_left, self.y_center)
context.line_to(self.x + self.margin_left + self.padding_left + self.inner_width, self.y_center)
context.stroke()
if elements == self.up:
context.translate(0, self.grid_height)
super(MirrorPlot, self).draw_grid(context, elements=elements)
context.translate(0, -self.grid_height)
else:
super(MirrorPlot, self).draw_grid(context, elements=elements)
def draw_plot(self, context, points, colors, offset=None):
points = [x/2 for x in points]
if not offset is None:
offset =[x/2 for x in offset]
context.translate(0, -self.inner_height / 2)
super(MirrorPlot, self).draw_plot(context, points, colors, offset=offset)
context.translate(0, self.inner_height / 2)
def draw_plot_negative(self, context, points, colors, offset=None):
points = [-x for x in points]
if not offset is None:
offset = [-x for x in offset]
self.draw_plot(context, points, colors, offset=offset)
def update(self, context):
for element in set(self.up + self.down):
self.points[element].append(self.monitor.normalize(element))
super(Plot, self).update(context) # TRAP: calls parent of parent, not direct parent!
def draw(self, context):
# TODO: This is mostly a copy-paste of Plot.update+draw, needs moar DRY.
self.draw_background(context)
for elements, drawer in ((self.up, self.draw_plot), (self.down, self.draw_plot_negative)):
if self.grid:
self.draw_grid(context, elements)
if self.autoscale:
scale_factor = self.get_scale_factor(elements)
if scale_factor == 0.0:
text = u"∞X"
else:
text = "%sX" % helpers.pretty_si(self.get_scale_factor(elements))
if elements == self.up:
self.app.draw_text(context, text, self.x + self.margin_left + self.padding_left + self.inner_width, self.y + self.margin_top + self.padding_top, align='right_bottom', color=self.colors['caption_scale'], font_size=10)
elif not self.scale_lock: # don't show 'down' scalefactor if it's locked
self.app.draw_text(context, text, self.x + self.margin_left + self.padding_left + self.inner_width, self.y + self.margin_top + self.padding_top + self.inner_height, align='right_top', color=self.colors['caption_scale'], font_size=10)
offset = [0.0 for _ in range(0, self.num_points)]
for idx, element in enumerate(elements):
colors = {
'plot_marker': self.colors_plot_marker[idx],
'plot_line': self.colors_plot_line[idx],
'plot_fill': self.colors_plot_fill[idx]
}
if self.autoscale:
points = self.get_points_scaled(element, elements)
else:
points = list(self.points[element])
if self.combination == 'separate':
drawer(context, points, colors)
elif self.combination.startswith('cumulative'):
if self.combination == 'cumulative_force':
for idx in range(0, len(points)):
points[idx] /= len(elements)
drawer(context, points, colors, offset)
for idx, value in enumerate(points):
offset[idx] += value
self.draw_captions(context)