Source code for gulik.visualizers

# -*- 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)