Source code for pi3d.util.Graph

#!/usr/bin/python
from __future__ import absolute_import, division, print_function, unicode_literals
''' 
'''
import pi3d
import numpy as np
import logging

LOGGER = logging.getLogger(__name__)

[docs]class Graph(object): '''Providing some basic functionality for a GPU accelerated x, y graph (i.e. for real-time display of instrumentation data etc)''' def __init__(self, x_values, y_values, width, height, font, title=None, line_width=2, axes_desc=None, legend=None, xpos=0, ypos=0, xmin=None, xmax=None, ymin=None, ymax=None, camera=None, shader=None): ''' Arguments: *x_values* 1D numpy array *y_values* 1 or 2D numpy array with size same as x_values in 2nd D draws a line graph or 3D numpy array with size same as x along axis=1 and last axis has 2 values. In this case the graph is drawn as vertical lines *width, height* as expected *font* pi3d.Font instance *title, line_width* as expected *axes_desc* tuple -> (x.axis.desc, y.axis.desc) *legend* tuple -> (y0.desc, y1.desc, y2.desc...) *xpos, ypos* offset relative to origin of display (centre) *xmin, xmax, ymin, ymax* override sampled values from init data *camera* if no other Shape to be drawn then a 2D Camera can be created here *shader* if no other Shape uses mat_flat shader then one will be created ''' if len(y_values.shape) < 2: y_values.shape = (1,) + y_values.shape if x_values.shape[0] != y_values.shape[1]: LOGGER.error('mismatched array lengths') return if camera is None: camera = pi3d.Camera(is_3d=False) if shader is None: shader = pi3d.Shader('mat_flat') # title ########## point_size = max(min(48, int(height * 0.1)), 24) self.text = pi3d.PointText(font, camera, max_chars=400, point_size=point_size) if title is not None: title = pi3d.TextBlock(x=xpos, y=ypos+height/2-point_size+5, z=0.1, rot=0.0, char_count=len(title) + 2, spacing='F', text_format=title, space=0.05, justify=0.5) self.text.add_text_block(title) # axes ########### axex, axey = width * 0.4, height * 0.4 # implies 10% margin all round self.axes = pi3d.Lines(vertices=[[axex, -axey-line_width, 0], [-axex-line_width, -axey-line_width, 0], [-axex-line_width, axey, 0]], x=xpos, y=ypos, z=5.0, line_width=line_width) self.axes.set_shader(shader) # lines to represent data n = x_values.shape[-1] if xmin is None: xmin = x_values.min() if xmax is None: xmax = x_values.max() x_factor = (2.0 * axex) / (xmax - xmin) x_offset = xmin if ymin is None: ymin = y_values.min() if ymax is None: ymax = y_values.max() y_factor = (2.0 * axey) / (ymax - ymin) y_offset = ymin self.lines = [] for i in range(y_values.shape[0]): data = np.zeros((n, 3)) data[:,0] = (x_values - x_offset) * x_factor - axex + xpos if len(y_values[i].shape) == 1: # i.e. normal line graph data[:,1] = (y_values[i] - y_offset) * y_factor - axey + ypos strip = True else: # has to be pairs of values for separate line segments xx_vals = np.stack([data[:,0], data[:,0]], axis=1).flatten() # make x into pairs data = np.zeros((n * 2, 3)) data[:,0] = xx_vals data[:,1] = (y_values[i].flatten() - y_offset) * y_factor - axey + ypos strip = False data[:,2] = 4.0 # z value line = pi3d.Lines(vertices=data, line_width=line_width, strip=strip) line.set_shader(shader) j = i + 1 rgb_val = (0.913 * j % 1.0, 0.132 * j % 1.0, 0.484 * j % 1.0) line.set_material(rgb_val) self.lines.append(line) # axis values point_size *= 0.3 # first add the max and min vals vals = [(-axex + xpos, -axey + ypos - point_size, '{:.3g}'.format(xmin), 0.0), (axex + xpos, -axey + ypos - point_size, '{:.3g}'.format(xmax), 0.0), (-axex + xpos - point_size, -axey + ypos, '{:.3g}'.format(ymin), 90.0), (-axex + xpos - point_size, axey + ypos, '{:.3g}'.format(ymax), 90.0)] data = [] # use this to hold vertex positions for grid lines (called ticks) for val in self.tick_pos(xmin, xmax): x = (val - x_offset) * x_factor - axex vals.append((x + xpos, -axey + ypos - point_size, '{:.3g}'.format(val), 0.0)) data.extend([[x, -axey, 0],[x, axey, 0]]) # NB points in pairs to use GL_LINES option with strip=False below. for val in self.tick_pos(ymin, ymax): y = (val - y_offset) * y_factor - axey vals.append((-axex + xpos - point_size, y + ypos, '{:.3g}'.format(val), 90.0)) data.extend([[-axex, y, 0],[axex, y, 0]]) for val in vals: self.text.add_text_block(pi3d.TextBlock(val[0], val[1], 4.0, val[3], 8, size=0.7, text_format=val[2], spacing='F', space=0.05, justify=0.5)) self.ticks = pi3d.Lines(vertices=data, x=xpos, y=ypos, z=5.0, line_width=line_width, material=(0.5, 0.5, 0.5), strip=False) self.ticks.set_shader(shader) # axes descritions if axes_desc is not None: self.text.add_text_block(pi3d.TextBlock(xpos, ypos - 1.15 * axey, 4.0, 0.0, len(axes_desc[0]) + 2, size=0.7, text_format=axes_desc[0], spacing='F', space=0.05, justify=0.5)) self.text.add_text_block(pi3d.TextBlock(xpos - 1.15 * axex, ypos, 4.0, 90.0, len(axes_desc[1]) + 2, size=0.7, text_format=axes_desc[1], spacing='F', space=0.05, justify=0.5)) # legend ########## if legend is not None: if not hasattr(legend, '__iter__'): # single string, make into list legend = [legend] for i, lgd in enumerate(legend): self.text.add_text_block(pi3d.TextBlock(axex + xpos, axey + ypos - (i + 1) * point_size * 2.0, 4.0, 0.0, len(lgd) + 2, size=0.8, text_format=lgd, spacing='F', space=0.05, justify=1.0, colour=tuple(self.lines[i].buf[0].unib[3:6]) + (1.0,))) # scale factors for use in update() so add to self self.y_offset = y_offset self.y_factor = y_factor self.axey = axey self.ypos = ypos
[docs] def draw(self): self.text.draw() self.axes.draw() self.ticks.draw() for line in self.lines: line.draw()
[docs] def update(self, y_values): ''' update all y_values ''' if len(y_values.shape) < 2: # in case single line y_values.shape = (1,) + y_values.shape for i in range(y_values.shape[0]): self.lines[i].buf[0].array_buffer[:,1] = (y_values[i].flatten() - self.y_offset) * self.y_factor - self.axey + self.ypos self.lines[i].re_init()
[docs] def tick_pos(self, minv, maxv, num=3): ''' work out nice looking grid line positions ''' steps = np.array([1.0, 2.0, 2.5, 5.0, 10.0]) span = maxv - minv d = span / (num + 1) d_exp = np.floor(np.log10(abs(d))) d_man = d / 10 ** d_exp diffs = np.abs((d_man - steps) / steps) st = steps[np.argmin(diffs)] * 10 ** d_exp val = np.floor((minv + st + span * 0.1) / st) * st vals = [] while val < (maxv - span * 0.1): vals.append(val) val += st return vals