From 26f7c6c47dc50ff45b7e1738d4b9f958a6d66a0f Mon Sep 17 00:00:00 2001 From: yumoqing Date: Sun, 10 Jan 2021 12:37:20 +0800 Subject: [PATCH] bugfix --- kivyblocks/graph/.__init__.py.swp | Bin 0 -> 81920 bytes kivyblocks/graph/__init__.py | 1753 +++++++++++++++++++++++++ kivyblocks/graph/_version.py | 1 + kivyblocks/graph/tests/test_import.py | 6 + kivyblocks/newvideo.py | 15 + kivyblocks/register.py | 3 + 6 files changed, 1778 insertions(+) create mode 100644 kivyblocks/graph/.__init__.py.swp create mode 100644 kivyblocks/graph/__init__.py create mode 100644 kivyblocks/graph/_version.py create mode 100644 kivyblocks/graph/tests/test_import.py diff --git a/kivyblocks/graph/.__init__.py.swp b/kivyblocks/graph/.__init__.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..dab12606fbf4079b369b753c6ab6698380ad4e9d GIT binary patch literal 81920 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 literal 0 HcmV?d00001 diff --git a/kivyblocks/graph/__init__.py b/kivyblocks/graph/__init__.py new file mode 100644 index 0000000..57e9f30 --- /dev/null +++ b/kivyblocks/graph/__init__.py @@ -0,0 +1,1753 @@ +''' +Graph +====== + +The :class:`Graph` widget is a widget for displaying plots. It supports +drawing multiple plot with different colors on the Graph. It also supports +axes titles, ticks, labeled ticks, grids and a log or linear representation on +both the x and y axis, independently. + +To display a plot. First create a graph which will function as a "canvas" for +the plots. Then create plot objects e.g. MeshLinePlot and add them to the +graph. + +To create a graph with x-axis between 0-100, y-axis between -1 to 1, x and y +labels of and X and Y, respectively, x major and minor ticks every 25, 5 units, +respectively, y major ticks every 1 units, full x and y grids and with +a red line plot containing a sin wave on this range:: + + from kivy_garden.graph import Graph, MeshLinePlot + graph = Graph(xlabel='X', ylabel='Y', x_ticks_minor=5, + x_ticks_major=25, y_ticks_major=1, + y_grid_label=True, x_grid_label=True, padding=5, + x_grid=True, y_grid=True, xmin=-0, xmax=100, ymin=-1, ymax=1) + plot = MeshLinePlot(color=[1, 0, 0, 1]) + plot.points = [(x, sin(x / 10.)) for x in range(0, 101)] + graph.add_plot(plot) + +The MeshLinePlot plot is a particular plot which draws a set of points using +a mesh object. The points are given as a list of tuples, with each tuple +being a (x, y) coordinate in the graph's units. + +You can create different types of plots other than MeshLinePlot by inheriting +from the Plot class and implementing the required functions. The Graph object +provides a "canvas" to which a Plot's instructions are added. The plot object +is responsible for updating these instructions to show within the bounding +box of the graph the proper plot. The Graph notifies the Plot when it needs +to be redrawn due to changes. See the MeshLinePlot class for how it is done. + +The current availables plots are: + + * `MeshStemPlot` + * `MeshLinePlot` + * `SmoothLinePlot` - require Kivy 1.8.1 + +.. note:: + + The graph uses a stencil view to clip the plots to the graph display area. + As with the stencil graphics instructions, you cannot stack more than 8 + stencil-aware widgets. + +''' + +__all__ = ('Graph', 'Plot', 'MeshLinePlot', 'MeshStemPlot', 'LinePlot', + 'SmoothLinePlot', 'ContourPlot', 'ScatterPlot', 'PointPlot', + 'BarPlot', 'HBar', 'VBar', + ) + +from kivy.uix.widget import Widget +from kivy.uix.label import Label +from kivy.uix.stencilview import StencilView +from kivy.properties import NumericProperty, BooleanProperty,\ + BoundedNumericProperty, StringProperty, ListProperty, ObjectProperty,\ + DictProperty, AliasProperty +from kivy.clock import Clock +from kivy.graphics import Mesh, Color, Rectangle, Point +from kivy.graphics import Fbo +from kivy.graphics.texture import Texture +from kivy.event import EventDispatcher +from kivy.lang import Builder +from kivy.logger import Logger +from kivy import metrics +from math import log10, floor, ceil +from decimal import Decimal +from itertools import chain +try: + import numpy as np +except ImportError as e: + np = None + +from ._version import __version__ + + +def identity(x): + return x + + +def exp10(x): + return 10 ** x + + +Builder.load_string(""" +: + canvas.before: + PushMatrix + Rotate: + angle: root.angle + axis: 0, 0, 1 + origin: root.center + canvas.after: + PopMatrix +""") + + +class GraphRotatedLabel(Label): + angle = NumericProperty(0) + + +class Axis(EventDispatcher): + pass + + +class XAxis(Axis): + pass + + +class YAxis(Axis): + pass + + +class Graph(Widget): + '''Graph class, see module documentation for more information. + ''' + + # triggers a full reload of graphics + _trigger = ObjectProperty(None) + # triggers only a repositioning of objects due to size/pos updates + _trigger_size = ObjectProperty(None) + # triggers only a update of colors, e.g. tick_color + _trigger_color = ObjectProperty(None) + # holds widget with the x-axis label + _xlabel = ObjectProperty(None) + # holds widget with the y-axis label + _ylabel = ObjectProperty(None) + # holds all the x-axis tick mark labels + _x_grid_label = ListProperty([]) + # holds all the y-axis tick mark labels + _y_grid_label = ListProperty([]) + # the mesh drawing all the ticks/grids + _mesh_ticks = ObjectProperty(None) + # the mesh which draws the surrounding rectangle + _mesh_rect = ObjectProperty(None) + # a list of locations of major and minor ticks. The values are not + # but is in the axis min - max range + _ticks_majorx = ListProperty([]) + _ticks_minorx = ListProperty([]) + _ticks_majory = ListProperty([]) + _ticks_minory = ListProperty([]) + + tick_color = ListProperty([.25, .25, .25, 1]) + '''Color of the grid/ticks, default to 1/4. grey. + ''' + + background_color = ListProperty([0, 0, 0, 0]) + '''Color of the background, defaults to transparent + ''' + + border_color = ListProperty([1, 1, 1, 1]) + '''Color of the border, defaults to white + ''' + + label_options = DictProperty() + '''Label options that will be passed to `:class:`kivy.uix.Label`. + ''' + + _with_stencilbuffer = BooleanProperty(True) + '''Whether :class:`Graph`'s FBO should use FrameBuffer (True) or not + (False). + + .. warning:: This property is internal and so should be used with care. + It can break some other graphic instructions used by the :class:`Graph`, + for example you can have problems when drawing :class:`SmoothLinePlot` + plots, so use it only when you know what exactly you are doing. + + :data:`_with_stencilbuffer` is a :class:`~kivy.properties.BooleanProperty`, + defaults to True. + ''' + + def __init__(self, **kwargs): + super(Graph, self).__init__(**kwargs) + + with self.canvas: + self._fbo = Fbo( + size=self.size, with_stencilbuffer=self._with_stencilbuffer) + + with self._fbo: + self._background_color = Color(*self.background_color) + self._background_rect = Rectangle(size=self.size) + self._mesh_ticks_color = Color(*self.tick_color) + self._mesh_ticks = Mesh(mode='lines') + self._mesh_rect_color = Color(*self.border_color) + self._mesh_rect = Mesh(mode='line_strip') + + with self.canvas: + Color(1, 1, 1) + self._fbo_rect = Rectangle( + size=self.size, texture=self._fbo.texture) + + mesh = self._mesh_rect + mesh.vertices = [0] * (5 * 4) + mesh.indices = range(5) + + self._plot_area = StencilView() + self.add_widget(self._plot_area) + + t = self._trigger = Clock.create_trigger(self._redraw_all) + ts = self._trigger_size = Clock.create_trigger(self._redraw_size) + tc = self._trigger_color = Clock.create_trigger(self._update_colors) + + self.bind(center=ts, padding=ts, precision=ts, plots=ts, x_grid=ts, + y_grid=ts, draw_border=ts) + self.bind(xmin=t, xmax=t, xlog=t, x_ticks_major=t, x_ticks_minor=t, + xlabel=t, x_grid_label=t, ymin=t, ymax=t, ylog=t, + y_ticks_major=t, y_ticks_minor=t, ylabel=t, y_grid_label=t, + font_size=t, label_options=t, x_ticks_angle=t) + self.bind(tick_color=tc, background_color=tc, border_color=tc) + self._trigger() + + def add_widget(self, widget): + if widget is self._plot_area: + canvas = self.canvas + self.canvas = self._fbo + super(Graph, self).add_widget(widget) + if widget is self._plot_area: + self.canvas = canvas + + def remove_widget(self, widget): + if widget is self._plot_area: + canvas = self.canvas + self.canvas = self._fbo + super(Graph, self).remove_widget(widget) + if widget is self._plot_area: + self.canvas = canvas + + def _get_ticks(self, major, minor, log, s_min, s_max): + if major and s_max > s_min: + if log: + s_min = log10(s_min) + s_max = log10(s_max) + # count the decades in min - max. This is in actual decades, + # not logs. + n_decades = floor(s_max - s_min) + # for the fractional part of the last decade, we need to + # convert the log value, x, to 10**x but need to handle + # differently if the last incomplete decade has a decade + # boundary in it + if floor(s_min + n_decades) != floor(s_max): + n_decades += 1 - (10 ** (s_min + n_decades + 1) - 10 ** + s_max) / 10 ** floor(s_max + 1) + else: + n_decades += ((10 ** s_max - 10 ** (s_min + n_decades)) / + 10 ** floor(s_max + 1)) + # this might be larger than what is needed, but we delete + # excess later + n_ticks_major = n_decades / float(major) + n_ticks = int(floor(n_ticks_major * (minor if minor >= + 1. else 1.0))) + 2 + # in decade multiples, e.g. 0.1 of the decade, the distance + # between ticks + decade_dist = major / float(minor if minor else 1.0) + + points_minor = [0] * n_ticks + points_major = [0] * n_ticks + k = 0 # position in points major + k2 = 0 # position in points minor + # because each decade is missing 0.1 of the decade, if a tick + # falls in < min_pos skip it + min_pos = 0.1 - 0.00001 * decade_dist + s_min_low = floor(s_min) + # first real tick location. value is in fractions of decades + # from the start we have to use decimals here, otherwise + # floating point inaccuracies results in bad values + start_dec = ceil((10 ** Decimal(s_min - s_min_low - 1)) / + Decimal(decade_dist)) * decade_dist + count_min = (0 if not minor else + floor(start_dec / decade_dist) % minor) + start_dec += s_min_low + count = 0 # number of ticks we currently have passed start + while True: + # this is the current position in decade that we are. + # e.g. -0.9 means that we're at 0.1 of the 10**ceil(-0.9) + # decade + pos_dec = start_dec + decade_dist * count + pos_dec_low = floor(pos_dec) + diff = pos_dec - pos_dec_low + zero = abs(diff) < 0.001 * decade_dist + if zero: + # the same value as pos_dec but in log scale + pos_log = pos_dec_low + else: + pos_log = log10((pos_dec - pos_dec_low + ) * 10 ** ceil(pos_dec)) + if pos_log > s_max: + break + count += 1 + if zero or diff >= min_pos: + if minor and not count_min % minor: + points_major[k] = pos_log + k += 1 + else: + points_minor[k2] = pos_log + k2 += 1 + count_min += 1 + else: + # distance between each tick + tick_dist = major / float(minor if minor else 1.0) + n_ticks = int(floor((s_max - s_min) / tick_dist) + 1) + points_major = [0] * int(floor((s_max - s_min) / float(major)) + + 1) + points_minor = [0] * (n_ticks - len(points_major) + 1) + k = 0 # position in points major + k2 = 0 # position in points minor + for m in range(0, n_ticks): + if minor and m % minor: + points_minor[k2] = m * tick_dist + s_min + k2 += 1 + else: + points_major[k] = m * tick_dist + s_min + k += 1 + del points_major[k:] + del points_minor[k2:] + else: + points_major = [] + points_minor = [] + return points_major, points_minor + + def _update_labels(self): + xlabel = self._xlabel + ylabel = self._ylabel + x = self.x + y = self.y + width = self.width + height = self.height + padding = self.padding + x_next = padding + x + y_next = padding + y + xextent = width + x + yextent = height + y + ymin = self.ymin + ymax = self.ymax + xmin = self.xmin + precision = self.precision + x_overlap = False + y_overlap = False + # set up x and y axis labels + if xlabel: + xlabel.text = self.xlabel + xlabel.texture_update() + xlabel.size = xlabel.texture_size + xlabel.pos = int( + x + width / 2. - xlabel.width / 2.), int(padding + y) + y_next += padding + xlabel.height + if ylabel: + ylabel.text = self.ylabel + ylabel.texture_update() + ylabel.size = ylabel.texture_size + ylabel.x = padding + x - (ylabel.width / 2. - ylabel.height / 2.) + x_next += padding + ylabel.height + xpoints = self._ticks_majorx + xlabels = self._x_grid_label + xlabel_grid = self.x_grid_label + ylabel_grid = self.y_grid_label + ypoints = self._ticks_majory + ylabels = self._y_grid_label + # now x and y tick mark labels + if len(ylabels) and ylabel_grid: + # horizontal size of the largest tick label, to have enough room + funcexp = exp10 if self.ylog else identity + funclog = log10 if self.ylog else identity + ylabels[0].text = precision % funcexp(ypoints[0]) + ylabels[0].texture_update() + y1 = ylabels[0].texture_size + y_start = y_next + (padding + y1[1] if len(xlabels) and xlabel_grid + else 0) + \ + (padding + y1[1] if not y_next else 0) + yextent = y + height - padding - y1[1] / 2. + + ymin = funclog(ymin) + ratio = (yextent - y_start) / float(funclog(ymax) - ymin) + y_start -= y1[1] / 2. + y1 = y1[0] + for k in range(len(ylabels)): + ylabels[k].text = precision % funcexp(ypoints[k]) + ylabels[k].texture_update() + ylabels[k].size = ylabels[k].texture_size + y1 = max(y1, ylabels[k].texture_size[0]) + ylabels[k].pos = ( + int(x_next), + int(y_start + (ypoints[k] - ymin) * ratio)) + if len(ylabels) > 1 and ylabels[0].top > ylabels[1].y: + y_overlap = True + else: + x_next += y1 + padding + if len(xlabels) and xlabel_grid: + funcexp = exp10 if self.xlog else identity + funclog = log10 if self.xlog else identity + # find the distance from the end that'll fit the last tick label + xlabels[0].text = precision % funcexp(xpoints[-1]) + xlabels[0].texture_update() + xextent = x + width - xlabels[0].texture_size[0] / 2. - padding + # find the distance from the start that'll fit the first tick label + if not x_next: + xlabels[0].text = precision % funcexp(xpoints[0]) + xlabels[0].texture_update() + x_next = padding + xlabels[0].texture_size[0] / 2. + xmin = funclog(xmin) + ratio = (xextent - x_next) / float(funclog(self.xmax) - xmin) + right = -1 + for k in range(len(xlabels)): + xlabels[k].text = precision % funcexp(xpoints[k]) + # update the size so we can center the labels on ticks + xlabels[k].texture_update() + xlabels[k].size = xlabels[k].texture_size + half_ts = xlabels[k].texture_size[0] / 2. + xlabels[k].pos = ( + int(x_next + (xpoints[k] - xmin) * ratio - half_ts), + int(y_next)) + if xlabels[k].x < right: + x_overlap = True + break + right = xlabels[k].right + if not x_overlap: + y_next += padding + xlabels[0].texture_size[1] + # now re-center the x and y axis labels + if xlabel: + xlabel.x = int( + x_next + (xextent - x_next) / 2. - xlabel.width / 2.) + if ylabel: + ylabel.y = int( + y_next + (yextent - y_next) / 2. - ylabel.height / 2.) + ylabel.angle = 90 + if x_overlap: + for k in range(len(xlabels)): + xlabels[k].text = '' + if y_overlap: + for k in range(len(ylabels)): + ylabels[k].text = '' + return x_next - x, y_next - y, xextent - x, yextent - y + + def _update_ticks(self, size): + # re-compute the positions of the bounding rectangle + mesh = self._mesh_rect + vert = mesh.vertices + if self.draw_border: + s0, s1, s2, s3 = size + vert[0] = s0 + vert[1] = s1 + vert[4] = s2 + vert[5] = s1 + vert[8] = s2 + vert[9] = s3 + vert[12] = s0 + vert[13] = s3 + vert[16] = s0 + vert[17] = s1 + else: + vert[0:18] = [0 for k in range(18)] + mesh.vertices = vert + # re-compute the positions of the x/y axis ticks + mesh = self._mesh_ticks + vert = mesh.vertices + start = 0 + xpoints = self._ticks_majorx + ypoints = self._ticks_majory + xpoints2 = self._ticks_minorx + ypoints2 = self._ticks_minory + ylog = self.ylog + xlog = self.xlog + xmin = self.xmin + xmax = self.xmax + if xlog: + xmin = log10(xmin) + xmax = log10(xmax) + ymin = self.ymin + ymax = self.ymax + if ylog: + ymin = log10(ymin) + ymax = log10(ymax) + if len(xpoints): + top = size[3] if self.x_grid else metrics.dp(12) + size[1] + ratio = (size[2] - size[0]) / float(xmax - xmin) + for k in range(start, len(xpoints) + start): + vert[k * 8] = size[0] + (xpoints[k - start] - xmin) * ratio + vert[k * 8 + 1] = size[1] + vert[k * 8 + 4] = vert[k * 8] + vert[k * 8 + 5] = top + start += len(xpoints) + if len(xpoints2): + top = metrics.dp(8) + size[1] + ratio = (size[2] - size[0]) / float(xmax - xmin) + for k in range(start, len(xpoints2) + start): + vert[k * 8] = size[0] + (xpoints2[k - start] - xmin) * ratio + vert[k * 8 + 1] = size[1] + vert[k * 8 + 4] = vert[k * 8] + vert[k * 8 + 5] = top + start += len(xpoints2) + if len(ypoints): + top = size[2] if self.y_grid else metrics.dp(12) + size[0] + ratio = (size[3] - size[1]) / float(ymax - ymin) + for k in range(start, len(ypoints) + start): + vert[k * 8 + 1] = size[1] + (ypoints[k - start] - ymin) * ratio + vert[k * 8 + 5] = vert[k * 8 + 1] + vert[k * 8] = size[0] + vert[k * 8 + 4] = top + start += len(ypoints) + if len(ypoints2): + top = metrics.dp(8) + size[0] + ratio = (size[3] - size[1]) / float(ymax - ymin) + for k in range(start, len(ypoints2) + start): + vert[k * 8 + 1] = size[1] + ( + ypoints2[k - start] - ymin) * ratio + vert[k * 8 + 5] = vert[k * 8 + 1] + vert[k * 8] = size[0] + vert[k * 8 + 4] = top + mesh.vertices = vert + + x_axis = ListProperty([None]) + y_axis = ListProperty([None]) + + def get_x_axis(self, axis=0): + if axis == 0: + return self.xlog, self.xmin, self.xmax + info = self.x_axis[axis] + return info["log"], info["min"], info["max"] + + def get_y_axis(self, axis=0): + if axis == 0: + return self.ylog, self.ymin, self.ymax + info = self.y_axis[axis] + return info["log"], info["min"], info["max"] + + def add_x_axis(self, xmin, xmax, xlog=False): + data = { + "log": xlog, + "min": xmin, + "max": xmax + } + self.x_axis.append(data) + return data + + def add_y_axis(self, ymin, ymax, ylog=False): + data = { + "log": ylog, + "min": ymin, + "max": ymax + } + self.y_axis.append(data) + return data + + def _update_plots(self, size): + for plot in self.plots: + xlog, xmin, xmax = self.get_x_axis(plot.x_axis) + ylog, ymin, ymax = self.get_y_axis(plot.y_axis) + plot._update(xlog, xmin, xmax, ylog, ymin, ymax, size) + + def _update_colors(self, *args): + self._mesh_ticks_color.rgba = tuple(self.tick_color) + self._background_color.rgba = tuple(self.background_color) + self._mesh_rect_color.rgba = tuple(self.border_color) + + def _redraw_all(self, *args): + # add/remove all the required labels + xpoints_major, xpoints_minor = self._redraw_x(*args) + ypoints_major, ypoints_minor = self._redraw_y(*args) + + mesh = self._mesh_ticks + n_points = (len(xpoints_major) + len(xpoints_minor) + + len(ypoints_major) + len(ypoints_minor)) + mesh.vertices = [0] * (n_points * 8) + mesh.indices = [k for k in range(n_points * 2)] + self._redraw_size() + + def _redraw_x(self, *args): + font_size = self.font_size + if self.xlabel: + xlabel = self._xlabel + if not xlabel: + xlabel = Label() + self.add_widget(xlabel) + self._xlabel = xlabel + + xlabel.font_size = font_size + for k, v in self.label_options.items(): + setattr(xlabel, k, v) + + else: + xlabel = self._xlabel + if xlabel: + self.remove_widget(xlabel) + self._xlabel = None + grids = self._x_grid_label + xpoints_major, xpoints_minor = self._get_ticks(self.x_ticks_major, + self.x_ticks_minor, + self.xlog, self.xmin, + self.xmax) + self._ticks_majorx = xpoints_major + self._ticks_minorx = xpoints_minor + + if not self.x_grid_label: + n_labels = 0 + else: + n_labels = len(xpoints_major) + + for k in range(n_labels, len(grids)): + self.remove_widget(grids[k]) + del grids[n_labels:] + + grid_len = len(grids) + grids.extend([None] * (n_labels - len(grids))) + for k in range(grid_len, n_labels): + grids[k] = GraphRotatedLabel( + font_size=font_size, angle=self.x_ticks_angle, + **self.label_options) + self.add_widget(grids[k]) + return xpoints_major, xpoints_minor + + def _redraw_y(self, *args): + font_size = self.font_size + if self.ylabel: + ylabel = self._ylabel + if not ylabel: + ylabel = GraphRotatedLabel() + self.add_widget(ylabel) + self._ylabel = ylabel + + ylabel.font_size = font_size + for k, v in self.label_options.items(): + setattr(ylabel, k, v) + else: + ylabel = self._ylabel + if ylabel: + self.remove_widget(ylabel) + self._ylabel = None + grids = self._y_grid_label + ypoints_major, ypoints_minor = self._get_ticks(self.y_ticks_major, + self.y_ticks_minor, + self.ylog, self.ymin, + self.ymax) + self._ticks_majory = ypoints_major + self._ticks_minory = ypoints_minor + + if not self.y_grid_label: + n_labels = 0 + else: + n_labels = len(ypoints_major) + + for k in range(n_labels, len(grids)): + self.remove_widget(grids[k]) + del grids[n_labels:] + + grid_len = len(grids) + grids.extend([None] * (n_labels - len(grids))) + for k in range(grid_len, n_labels): + grids[k] = Label(font_size=font_size, **self.label_options) + self.add_widget(grids[k]) + return ypoints_major, ypoints_minor + + def _redraw_size(self, *args): + # size a 4-tuple describing the bounding box in which we can draw + # graphs, it's (x0, y0, x1, y1), which correspond with the bottom left + # and top right corner locations, respectively + self._clear_buffer() + size = self._update_labels() + self.view_pos = self._plot_area.pos = (size[0], size[1]) + self.view_size = self._plot_area.size = ( + size[2] - size[0], size[3] - size[1]) + + if self.size[0] and self.size[1]: + self._fbo.size = self.size + else: + self._fbo.size = 1, 1 # gl errors otherwise + self._fbo_rect.texture = self._fbo.texture + self._fbo_rect.size = self.size + self._fbo_rect.pos = self.pos + self._background_rect.size = self.size + self._update_ticks(size) + self._update_plots(size) + + def _clear_buffer(self, *largs): + fbo = self._fbo + fbo.bind() + fbo.clear_buffer() + fbo.release() + + def add_plot(self, plot): + '''Add a new plot to this graph. + + :Parameters: + `plot`: + Plot to add to this graph. + + >>> graph = Graph() + >>> plot = MeshLinePlot(mode='line_strip', color=[1, 0, 0, 1]) + >>> plot.points = [(x / 10., sin(x / 50.)) for x in range(-0, 101)] + >>> graph.add_plot(plot) + ''' + if plot in self.plots: + return + add = self._plot_area.canvas.add + for instr in plot.get_drawings(): + add(instr) + plot.bind(on_clear_plot=self._clear_buffer) + self.plots.append(plot) + + def remove_plot(self, plot): + '''Remove a plot from this graph. + + :Parameters: + `plot`: + Plot to remove from this graph. + + >>> graph = Graph() + >>> plot = MeshLinePlot(mode='line_strip', color=[1, 0, 0, 1]) + >>> plot.points = [(x / 10., sin(x / 50.)) for x in range(-0, 101)] + >>> graph.add_plot(plot) + >>> graph.remove_plot(plot) + ''' + if plot not in self.plots: + return + remove = self._plot_area.canvas.remove + for instr in plot.get_drawings(): + remove(instr) + plot.unbind(on_clear_plot=self._clear_buffer) + self.plots.remove(plot) + self._clear_buffer() + + def collide_plot(self, x, y): + '''Determine if the given coordinates fall inside the plot area. Use + `x, y = self.to_widget(x, y, relative=True)` to first convert into + widget coordinates if it's in window coordinates because it's assumed + to be given in local widget coordinates, relative to the graph's pos. + + :Parameters: + `x, y`: + The coordinates to test. + ''' + adj_x, adj_y = x - self._plot_area.pos[0], y - self._plot_area.pos[1] + return 0 <= adj_x <= self._plot_area.size[0] \ + and 0 <= adj_y <= self._plot_area.size[1] + + def to_data(self, x, y): + '''Convert widget coords to data coords. Use + `x, y = self.to_widget(x, y, relative=True)` to first convert into + widget coordinates if it's in window coordinates because it's assumed + to be given in local widget coordinates, relative to the graph's pos. + + :Parameters: + `x, y`: + The coordinates to convert. + + If the graph has multiple axes, use :class:`Plot.unproject` instead. + ''' + adj_x = float(x - self._plot_area.pos[0]) + adj_y = float(y - self._plot_area.pos[1]) + norm_x = adj_x / self._plot_area.size[0] + norm_y = adj_y / self._plot_area.size[1] + if self.xlog: + xmin, xmax = log10(self.xmin), log10(self.xmax) + conv_x = 10.**(norm_x * (xmax - xmin) + xmin) + else: + conv_x = norm_x * (self.xmax - self.xmin) + self.xmin + if self.ylog: + ymin, ymax = log10(self.ymin), log10(self.ymax) + conv_y = 10.**(norm_y * (ymax - ymin) + ymin) + else: + conv_y = norm_y * (self.ymax - self.ymin) + self.ymin + return [conv_x, conv_y] + + xmin = NumericProperty(0.) + '''The x-axis minimum value. + + If :data:`xlog` is True, xmin must be larger than zero. + + :data:`xmin` is a :class:`~kivy.properties.NumericProperty`, defaults to 0. + ''' + + xmax = NumericProperty(100.) + '''The x-axis maximum value, larger than xmin. + + :data:`xmax` is a :class:`~kivy.properties.NumericProperty`, defaults to 0. + ''' + + xlog = BooleanProperty(False) + '''Determines whether the x-axis should be displayed logarithmically (True) + or linearly (False). + + :data:`xlog` is a :class:`~kivy.properties.BooleanProperty`, defaults + to False. + ''' + + x_ticks_major = BoundedNumericProperty(0, min=0) + '''Distance between major tick marks on the x-axis. + + Determines the distance between the major tick marks. Major tick marks + start from min and re-occur at every ticks_major until :data:`xmax`. + If :data:`xmax` doesn't overlap with a integer multiple of ticks_major, + no tick will occur at :data:`xmax`. Zero indicates no tick marks. + + If :data:`xlog` is true, then this indicates the distance between ticks + in multiples of current decade. E.g. if :data:`xmin` is 0.1 and + ticks_major is 0.1, it means there will be a tick at every 10th of the + decade, i.e. 0.1 ... 0.9, 1, 2... If it is 0.3, the ticks will occur at + 0.1, 0.3, 0.6, 0.9, 2, 5, 8, 10. You'll notice that it went from 8 to 10 + instead of to 20, that's so that we can say 0.5 and have ticks at every + half decade, e.g. 0.1, 0.5, 1, 5, 10, 50... Similarly, if ticks_major is + 1.5, there will be ticks at 0.1, 5, 100, 5,000... Also notice, that there's + always a major tick at the start. Finally, if e.g. :data:`xmin` is 0.6 + and this 0.5 there will be ticks at 0.6, 1, 5... + + :data:`x_ticks_major` is a + :class:`~kivy.properties.BoundedNumericProperty`, defaults to 0. + ''' + + x_ticks_minor = BoundedNumericProperty(0, min=0) + '''The number of sub-intervals that divide x_ticks_major. + + Determines the number of sub-intervals into which ticks_major is divided, + if non-zero. The actual number of minor ticks between the major ticks is + ticks_minor - 1. Only used if ticks_major is non-zero. If there's no major + tick at xmax then the number of minor ticks after the last major + tick will be however many ticks fit until xmax. + + If self.xlog is true, then this indicates the number of intervals the + distance between major ticks is divided. The result is the number of + multiples of decades between ticks. I.e. if ticks_minor is 10, then if + ticks_major is 1, there will be ticks at 0.1, 0.2...0.9, 1, 2, 3... If + ticks_major is 0.3, ticks will occur at 0.1, 0.12, 0.15, 0.18... Finally, + as is common, if ticks major is 1, and ticks minor is 5, there will be + ticks at 0.1, 0.2, 0.4... 0.8, 1, 2... + + :data:`x_ticks_minor` is a + :class:`~kivy.properties.BoundedNumericProperty`, defaults to 0. + ''' + + x_grid = BooleanProperty(False) + '''Determines whether the x-axis has tick marks or a full grid. + + If :data:`x_ticks_major` is non-zero, then if x_grid is False tick marks + will be displayed at every major tick. If x_grid is True, instead of ticks, + a vertical line will be displayed at every major tick. + + :data:`x_grid` is a :class:`~kivy.properties.BooleanProperty`, defaults + to False. + ''' + + x_grid_label = BooleanProperty(False) + '''Whether labels should be displayed beneath each major tick. If true, + each major tick will have a label containing the axis value. + + :data:`x_grid_label` is a :class:`~kivy.properties.BooleanProperty`, + defaults to False. + ''' + + xlabel = StringProperty('') + '''The label for the x-axis. If not empty it is displayed in the center of + the axis. + + :data:`xlabel` is a :class:`~kivy.properties.StringProperty`, + defaults to ''. + ''' + + ymin = NumericProperty(0.) + '''The y-axis minimum value. + + If :data:`ylog` is True, ymin must be larger than zero. + + :data:`ymin` is a :class:`~kivy.properties.NumericProperty`, defaults to 0. + ''' + + ymax = NumericProperty(100.) + '''The y-axis maximum value, larger than ymin. + + :data:`ymax` is a :class:`~kivy.properties.NumericProperty`, defaults to 0. + ''' + + ylog = BooleanProperty(False) + '''Determines whether the y-axis should be displayed logarithmically (True) + or linearly (False). + + :data:`ylog` is a :class:`~kivy.properties.BooleanProperty`, defaults + to False. + ''' + + y_ticks_major = BoundedNumericProperty(0, min=0) + '''Distance between major tick marks. See :data:`x_ticks_major`. + + :data:`y_ticks_major` is a + :class:`~kivy.properties.BoundedNumericProperty`, defaults to 0. + ''' + + y_ticks_minor = BoundedNumericProperty(0, min=0) + '''The number of sub-intervals that divide ticks_major. + See :data:`x_ticks_minor`. + + :data:`y_ticks_minor` is a + :class:`~kivy.properties.BoundedNumericProperty`, defaults to 0. + ''' + + y_grid = BooleanProperty(False) + '''Determines whether the y-axis has tick marks or a full grid. See + :data:`x_grid`. + + :data:`y_grid` is a :class:`~kivy.properties.BooleanProperty`, defaults + to False. + ''' + + y_grid_label = BooleanProperty(False) + '''Whether labels should be displayed beneath each major tick. If true, + each major tick will have a label containing the axis value. + + :data:`y_grid_label` is a :class:`~kivy.properties.BooleanProperty`, + defaults to False. + ''' + + ylabel = StringProperty('') + '''The label for the y-axis. If not empty it is displayed in the center of + the axis. + + :data:`ylabel` is a :class:`~kivy.properties.StringProperty`, + defaults to ''. + ''' + + padding = NumericProperty('5dp') + '''Padding distances between the labels, axes titles and graph, as + well between the widget and the objects near the boundaries. + + :data:`padding` is a :class:`~kivy.properties.NumericProperty`, defaults + to 5dp. + ''' + + font_size = NumericProperty('15sp') + '''Font size of the labels. + + :data:`font_size` is a :class:`~kivy.properties.NumericProperty`, defaults + to 15sp. + ''' + + x_ticks_angle = NumericProperty(0) + '''Rotate angle of the x-axis tick marks. + + :data:`x_ticks_angle` is a :class:`~kivy.properties.NumericProperty`, + defaults to 0. + ''' + + precision = StringProperty('%g') + '''Determines the numerical precision of the tick mark labels. This value + governs how the numbers are converted into string representation. Accepted + values are those listed in Python's manual in the + "String Formatting Operations" section. + + :data:`precision` is a :class:`~kivy.properties.StringProperty`, defaults + to '%g'. + ''' + + draw_border = BooleanProperty(True) + '''Whether a border is drawn around the canvas of the graph where the + plots are displayed. + + :data:`draw_border` is a :class:`~kivy.properties.BooleanProperty`, + defaults to True. + ''' + + plots = ListProperty([]) + '''Holds a list of all the plots in the graph. To add and remove plots + from the graph use :data:`add_plot` and :data:`add_plot`. Do not add + directly edit this list. + + :data:`plots` is a :class:`~kivy.properties.ListProperty`, + defaults to []. + ''' + + view_size = ObjectProperty((0, 0)) + '''The size of the graph viewing area - the area where the plots are + displayed, excluding labels etc. + ''' + + view_pos = ObjectProperty((0, 0)) + '''The pos of the graph viewing area - the area where the plots are + displayed, excluding labels etc. It is relative to the graph's pos. + ''' + + +class Plot(EventDispatcher): + '''Plot class, see module documentation for more information. + + :Events: + `on_clear_plot` + Fired before a plot updates the display and lets the fbo know that + it should clear the old drawings. + + ..versionadded:: 0.4 + ''' + + __events__ = ('on_clear_plot', ) + + # most recent values of the params used to draw the plot + params = DictProperty({'xlog': False, 'xmin': 0, 'xmax': 100, + 'ylog': False, 'ymin': 0, 'ymax': 100, + 'size': (0, 0, 0, 0)}) + + color = ListProperty([1, 1, 1, 1]) + '''Color of the plot. + ''' + + points = ListProperty([]) + '''List of (x, y) points to be displayed in the plot. + + The elements of points are 2-tuples, (x, y). The points are displayed + based on the mode setting. + + :data:`points` is a :class:`~kivy.properties.ListProperty`, defaults to + []. + ''' + + x_axis = NumericProperty(0) + '''Index of the X axis to use, defaults to 0 + ''' + + y_axis = NumericProperty(0) + '''Index of the Y axis to use, defaults to 0 + ''' + + def __init__(self, **kwargs): + super(Plot, self).__init__(**kwargs) + self.ask_draw = Clock.create_trigger(self.draw) + self.bind(params=self.ask_draw, points=self.ask_draw) + self._drawings = self.create_drawings() + + def funcx(self): + """Return a function that convert or not the X value according to plot + prameters""" + return log10 if self.params["xlog"] else lambda x: x + + def funcy(self): + """Return a function that convert or not the Y value according to plot + prameters""" + return log10 if self.params["ylog"] else lambda y: y + + def x_px(self): + """Return a function that convert the X value of the graph to the + pixel coordinate on the plot, according to the plot settings and axis + settings. It's relative to the graph pos. + """ + funcx = self.funcx() + params = self.params + size = params["size"] + xmin = funcx(params["xmin"]) + xmax = funcx(params["xmax"]) + ratiox = (size[2] - size[0]) / float(xmax - xmin) + return lambda x: (funcx(x) - xmin) * ratiox + size[0] + + def y_px(self): + """Return a function that convert the Y value of the graph to the + pixel coordinate on the plot, according to the plot settings and axis + settings. The returned value is relative to the graph pos. + """ + funcy = self.funcy() + params = self.params + size = params["size"] + ymin = funcy(params["ymin"]) + ymax = funcy(params["ymax"]) + ratioy = (size[3] - size[1]) / float(ymax - ymin) + return lambda y: (funcy(y) - ymin) * ratioy + size[1] + + def unproject(self, x, y): + """Return a function that unproject a pixel to a X/Y value on the plot + (works only for linear, not log yet). `x`, `y`, is relative to the + graph pos, so the graph's pos needs to be subtracted from x, y before + passing it in. + """ + params = self.params + size = params["size"] + xmin = params["xmin"] + xmax = params["xmax"] + ymin = params["ymin"] + ymax = params["ymax"] + ratiox = (size[2] - size[0]) / float(xmax - xmin) + ratioy = (size[3] - size[1]) / float(ymax - ymin) + x0 = (x - size[0]) / ratiox + xmin + y0 = (y - size[1]) / ratioy + ymin + return x0, y0 + + def get_px_bounds(self): + """Returns a dict containing the pixels bounds from the plot parameters. + The returned values are relative to the graph pos. + """ + params = self.params + x_px = self.x_px() + y_px = self.y_px() + return { + "xmin": x_px(params["xmin"]), + "xmax": x_px(params["xmax"]), + "ymin": y_px(params["ymin"]), + "ymax": y_px(params["ymax"]), + } + + def update(self, xlog, xmin, xmax, ylog, ymin, ymax, size): + '''Called by graph whenever any of the parameters + change. The plot should be recalculated then. + log, min, max indicate the axis settings. + size a 4-tuple describing the bounding box in which we can draw + graphs, it's (x0, y0, x1, y1), which correspond with the bottom left + and top right corner locations, respectively. + ''' + self.params.update({ + 'xlog': xlog, 'xmin': xmin, 'xmax': xmax, 'ylog': ylog, + 'ymin': ymin, 'ymax': ymax, 'size': size}) + + def get_group(self): + '''returns a string which is unique and is the group name given to all + the instructions returned by _get_drawings. Graph uses this to remove + these instructions when needed. + ''' + return '' + + def get_drawings(self): + '''returns a list of canvas instructions that will be added to the + graph's canvas. + ''' + if isinstance(self._drawings, (tuple, list)): + return self._drawings + return [] + + def create_drawings(self): + '''called once to create all the canvas instructions needed for the + plot + ''' + pass + + def draw(self, *largs): + '''draw the plot according to the params. It dispatches on_clear_plot + so derived classes should call super before updating. + ''' + self.dispatch('on_clear_plot') + + def iterate_points(self): + '''Iterate on all the points adjusted to the graph settings + ''' + x_px = self.x_px() + y_px = self.y_px() + for x, y in self.points: + yield x_px(x), y_px(y) + + def on_clear_plot(self, *largs): + pass + + # compatibility layer + _update = update + _get_drawings = get_drawings + _params = params + + +class MeshLinePlot(Plot): + '''MeshLinePlot class which displays a set of points similar to a mesh. + ''' + + def _set_mode(self, value): + if hasattr(self, '_mesh'): + self._mesh.mode = value + + mode = AliasProperty(lambda self: self._mesh.mode, _set_mode) + '''VBO Mode used for drawing the points. Can be one of: 'points', + 'line_strip', 'line_loop', 'lines', 'triangle_strip', 'triangle_fan'. + See :class:`~kivy.graphics.Mesh` for more details. + + Defaults to 'line_strip'. + ''' + + def create_drawings(self): + self._color = Color(*self.color) + self._mesh = Mesh(mode='line_strip') + self.bind( + color=lambda instr, value: setattr(self._color, "rgba", value)) + return [self._color, self._mesh] + + def draw(self, *args): + super(MeshLinePlot, self).draw(*args) + self.plot_mesh() + + def plot_mesh(self): + points = [p for p in self.iterate_points()] + mesh, vert, _ = self.set_mesh_size(len(points)) + for k, (x, y) in enumerate(points): + vert[k * 4] = x + vert[k * 4 + 1] = y + mesh.vertices = vert + + def set_mesh_size(self, size): + mesh = self._mesh + vert = mesh.vertices + ind = mesh.indices + diff = size - len(vert) // 4 + if diff < 0: + del vert[4 * size:] + del ind[size:] + elif diff > 0: + ind.extend(range(len(ind), len(ind) + diff)) + vert.extend([0] * (diff * 4)) + mesh.vertices = vert + return mesh, vert, ind + + +class MeshStemPlot(MeshLinePlot): + '''MeshStemPlot uses the MeshLinePlot class to draw a stem plot. The data + provided is graphed from origin to the data point. + ''' + + def plot_mesh(self): + points = [p for p in self.iterate_points()] + mesh, vert, _ = self.set_mesh_size(len(points) * 2) + y0 = self.y_px()(0) + for k, (x, y) in enumerate(self.iterate_points()): + vert[k * 8] = x + vert[k * 8 + 1] = y0 + vert[k * 8 + 4] = x + vert[k * 8 + 5] = y + mesh.vertices = vert + + +class LinePlot(Plot): + """LinePlot draws using a standard Line object. + """ + + line_width = NumericProperty(1) + + def create_drawings(self): + from kivy.graphics import Line, RenderContext + + self._grc = RenderContext( + use_parent_modelview=True, + use_parent_projection=True) + with self._grc: + self._gcolor = Color(*self.color) + self._gline = Line( + points=[], cap='none', + width=self.line_width, joint='round') + + return [self._grc] + + def draw(self, *args): + super(LinePlot, self).draw(*args) + # flatten the list + points = [] + for x, y in self.iterate_points(): + points += [x, y] + self._gline.points = points + + def on_line_width(self, *largs): + if hasattr(self, "_gline"): + self._gline.width = self.line_width + + +class SmoothLinePlot(Plot): + '''Smooth Plot class, see module documentation for more information. + This plot use a specific Fragment shader for a custom anti aliasing. + ''' + + SMOOTH_FS = ''' + $HEADER$ + + void main(void) { + float edgewidth = 0.015625 * 64.; + float t = texture2D(texture0, tex_coord0).r; + float e = smoothstep(0., edgewidth, t); + gl_FragColor = frag_color * vec4(1, 1, 1, e); + } + ''' + + # XXX This gradient data is a 64x1 RGB image, and + # values goes from 0 -> 255 -> 0. + GRADIENT_DATA = ( + b"\x00\x00\x00\x07\x07\x07\x0f\x0f\x0f\x17\x17\x17\x1f\x1f\x1f" + b"'''///777???GGGOOOWWW___gggooowww\x7f\x7f\x7f\x87\x87\x87" + b"\x8f\x8f\x8f\x97\x97\x97\x9f\x9f\x9f\xa7\xa7\xa7\xaf\xaf\xaf" + b"\xb7\xb7\xb7\xbf\xbf\xbf\xc7\xc7\xc7\xcf\xcf\xcf\xd7\xd7\xd7" + b"\xdf\xdf\xdf\xe7\xe7\xe7\xef\xef\xef\xf7\xf7\xf7\xff\xff\xff" + b"\xf6\xf6\xf6\xee\xee\xee\xe6\xe6\xe6\xde\xde\xde\xd5\xd5\xd5" + b"\xcd\xcd\xcd\xc5\xc5\xc5\xbd\xbd\xbd\xb4\xb4\xb4\xac\xac\xac" + b"\xa4\xa4\xa4\x9c\x9c\x9c\x94\x94\x94\x8b\x8b\x8b\x83\x83\x83" + b"{{{sssjjjbbbZZZRRRJJJAAA999111))) \x18\x18\x18\x10\x10\x10" + b"\x08\x08\x08\x00\x00\x00") + + def create_drawings(self): + from kivy.graphics import Line, RenderContext + + # very first time, create a texture for the shader + if not hasattr(SmoothLinePlot, '_texture'): + tex = Texture.create(size=(1, 64), colorfmt='rgb') + tex.add_reload_observer(SmoothLinePlot._smooth_reload_observer) + SmoothLinePlot._texture = tex + SmoothLinePlot._smooth_reload_observer(tex) + + self._grc = RenderContext( + fs=SmoothLinePlot.SMOOTH_FS, + use_parent_modelview=True, + use_parent_projection=True) + with self._grc: + self._gcolor = Color(*self.color) + self._gline = Line( + points=[], cap='none', width=2., + texture=SmoothLinePlot._texture) + + return [self._grc] + + @staticmethod + def _smooth_reload_observer(texture): + texture.blit_buffer(SmoothLinePlot.GRADIENT_DATA, colorfmt="rgb") + + def draw(self, *args): + super(SmoothLinePlot, self).draw(*args) + # flatten the list + points = [] + for x, y in self.iterate_points(): + points += [x, y] + self._gline.points = points + + +class ContourPlot(Plot): + """ + ContourPlot visualizes 3 dimensional data as an intensity map image. + The user must first specify 'xrange' and 'yrange' (tuples of min,max) and + then 'data', the intensity values. + `data`, is a MxN matrix, where the first dimension of size M specifies the + `y` values, and the second dimension of size N specifies the `x` values. + Axis Y and X values are assumed to be linearly spaced values from + xrange/yrange and the dimensions of 'data', `MxN`, respectively. + The color values are automatically scaled to the min and max z range of the + data set. + """ + _image = ObjectProperty(None) + data = ObjectProperty(None, force_dispatch=True) + xrange = ListProperty([0, 100]) + yrange = ListProperty([0, 100]) + + def __init__(self, **kwargs): + super(ContourPlot, self).__init__(**kwargs) + self.bind(data=self.ask_draw, xrange=self.ask_draw, + yrange=self.ask_draw) + + def create_drawings(self): + self._image = Rectangle() + self._color = Color([1, 1, 1, 1]) + self.bind( + color=lambda instr, value: setattr(self._color, 'rgba', value)) + return [self._color, self._image] + + def draw(self, *args): + super(ContourPlot, self).draw(*args) + data = self.data + xdim, ydim = data.shape + + # Find the minimum and maximum z values + zmax = data.max() + zmin = data.min() + rgb_scale_factor = 1.0 / (zmax - zmin) * 255 + # Scale the z values into RGB data + buf = np.array(data, dtype=float, copy=True) + np.subtract(buf, zmin, out=buf) + np.multiply(buf, rgb_scale_factor, out=buf) + # Duplicate into 3 dimensions (RGB) and convert to byte array + buf = np.asarray(buf, dtype=np.uint8) + buf = np.expand_dims(buf, axis=2) + buf = np.concatenate((buf, buf, buf), axis=2) + buf = np.reshape(buf, (xdim, ydim, 3)) + + charbuf = bytearray(np.reshape(buf, (buf.size))) + self._texture = Texture.create(size=(xdim, ydim), colorfmt='rgb') + self._texture.blit_buffer(charbuf, colorfmt='rgb', bufferfmt='ubyte') + image = self._image + image.texture = self._texture + + x_px = self.x_px() + y_px = self.y_px() + bl = x_px(self.xrange[0]), y_px(self.yrange[0]) + tr = x_px(self.xrange[1]), y_px(self.yrange[1]) + image.pos = bl + w = tr[0] - bl[0] + h = tr[1] - bl[1] + image.size = (w, h) + + +class BarPlot(Plot): + '''BarPlot class which displays a bar graph. + ''' + + bar_width = NumericProperty(1) + bar_spacing = NumericProperty(1.) + graph = ObjectProperty(allownone=True) + + def __init__(self, *ar, **kw): + super(BarPlot, self).__init__(*ar, **kw) + self.bind(bar_width=self.ask_draw) + self.bind(points=self.update_bar_width) + self.bind(graph=self.update_bar_width) + + def update_bar_width(self, *ar): + if not self.graph: + return + if len(self.points) < 2: + return + if self.graph.xmax == self.graph.xmin: + return + + point_width = ( + len(self.points) * + float(abs(self.graph.xmax) + abs(self.graph.xmin)) / + float(abs(max(self.points)[0]) + abs(min(self.points)[0]))) + + if not self.points: + self.bar_width = 1 + else: + self.bar_width = ( + (self.graph.width - self.graph.padding) / + point_width * self.bar_spacing) + + def create_drawings(self): + self._color = Color(*self.color) + self._mesh = Mesh() + self.bind( + color=lambda instr, value: setattr(self._color, 'rgba', value)) + return [self._color, self._mesh] + + def draw(self, *args): + super(BarPlot, self).draw(*args) + points = self.points + + # The mesh only supports (2^16) - 1 indices, so... + if len(points) * 6 > 65535: + Logger.error( + "BarPlot: cannot support more than 10922 points. " + "Ignoring extra points.") + points = points[:10922] + + point_len = len(points) + mesh = self._mesh + mesh.mode = 'triangles' + vert = mesh.vertices + ind = mesh.indices + diff = len(points) * 6 - len(vert) // 4 + if diff < 0: + del vert[24 * point_len:] + del ind[point_len:] + elif diff > 0: + ind.extend(range(len(ind), len(ind) + diff)) + vert.extend([0] * (diff * 4)) + + bounds = self.get_px_bounds() + x_px = self.x_px() + y_px = self.y_px() + ymin = y_px(0) + + bar_width = self.bar_width + if bar_width < 0: + bar_width = x_px(bar_width) - bounds["xmin"] + + for k in range(point_len): + p = points[k] + x1 = x_px(p[0]) + x2 = x1 + bar_width + y1 = ymin + y2 = y_px(p[1]) + + idx = k * 24 + # first triangle + vert[idx] = x1 + vert[idx + 1] = y2 + vert[idx + 4] = x1 + vert[idx + 5] = y1 + vert[idx + 8] = x2 + vert[idx + 9] = y1 + # second triangle + vert[idx + 12] = x1 + vert[idx + 13] = y2 + vert[idx + 16] = x2 + vert[idx + 17] = y2 + vert[idx + 20] = x2 + vert[idx + 21] = y1 + mesh.vertices = vert + + def _unbind_graph(self, graph): + graph.unbind(width=self.update_bar_width, + xmin=self.update_bar_width, + ymin=self.update_bar_width) + + def bind_to_graph(self, graph): + old_graph = self.graph + + if old_graph: + # unbind from the old one + self._unbind_graph(old_graph) + + # bind to the new one + self.graph = graph + graph.bind(width=self.update_bar_width, + xmin=self.update_bar_width, + ymin=self.update_bar_width) + + def unbind_from_graph(self): + if self.graph: + self._unbind_graph(self.graph) + + +class HBar(MeshLinePlot): + '''HBar draw horizontal bar on all the Y points provided + ''' + + def plot_mesh(self, *args): + points = self.points + mesh, vert, ind = self.set_mesh_size(len(points) * 2) + mesh.mode = "lines" + + bounds = self.get_px_bounds() + px_xmin = bounds["xmin"] + px_xmax = bounds["xmax"] + y_px = self.y_px() + for k, y in enumerate(points): + y = y_px(y) + vert[k * 8] = px_xmin + vert[k * 8 + 1] = y + vert[k * 8 + 4] = px_xmax + vert[k * 8 + 5] = y + mesh.vertices = vert + + +class VBar(MeshLinePlot): + '''VBar draw vertical bar on all the X points provided + ''' + + def plot_mesh(self, *args): + points = self.points + mesh, vert, ind = self.set_mesh_size(len(points) * 2) + mesh.mode = "lines" + + bounds = self.get_px_bounds() + px_ymin = bounds["ymin"] + px_ymax = bounds["ymax"] + x_px = self.x_px() + for k, x in enumerate(points): + x = x_px(x) + vert[k * 8] = x + vert[k * 8 + 1] = px_ymin + vert[k * 8 + 4] = x + vert[k * 8 + 5] = px_ymax + mesh.vertices = vert + + +class ScatterPlot(Plot): + """ + ScatterPlot draws using a standard Point object. + The pointsize can be controlled with :attr:`point_size`. + + >>> plot = ScatterPlot(color=[1, 0, 0, 1], point_size=5) + """ + + point_size = NumericProperty(1) + """The point size of the scatter points. Defaults to 1. + """ + + def create_drawings(self): + from kivy.graphics import Point, RenderContext + + self._points_context = RenderContext( + use_parent_modelview=True, + use_parent_projection=True) + with self._points_context: + self._gcolor = Color(*self.color) + self._gpts = Point(points=[], pointsize=self.point_size) + + return [self._points_context] + + def draw(self, *args): + super(ScatterPlot, self).draw(*args) + # flatten the list + self._gpts.points = list(chain(*self.iterate_points())) + + def on_point_size(self, *largs): + if hasattr(self, "_gpts"): + self._gpts.pointsize = self.point_size + + +class PointPlot(Plot): + '''Displays a set of points. + ''' + + point_size = NumericProperty(1) + ''' + Defaults to 1. + ''' + + _color = None + + _point = None + + def __init__(self, **kwargs): + super(PointPlot, self).__init__(**kwargs) + + def update_size(*largs): + if self._point: + self._point.pointsize = self.point_size + self.fbind('point_size', update_size) + + def update_color(*largs): + if self._color: + self._color.rgba = self.color + self.fbind('color', update_color) + + def create_drawings(self): + self._color = Color(*self.color) + self._point = Point(pointsize=self.point_size) + return [self._color, self._point] + + def draw(self, *args): + super(PointPlot, self).draw(*args) + self._point.points = [v for p in self.iterate_points() for v in p] + + +if __name__ == '__main__': + import itertools + from math import sin, cos, pi + from random import randrange + from kivy.utils import get_color_from_hex as rgb + from kivy.uix.boxlayout import BoxLayout + from kivy.app import App + + class TestApp(App): + + def build(self): + b = BoxLayout(orientation='vertical') + # example of a custom theme + colors = itertools.cycle([ + rgb('7dac9f'), rgb('dc7062'), rgb('66a8d4'), rgb('e5b060')]) + graph_theme = { + 'label_options': { + 'color': rgb('444444'), # color of tick labels and titles + 'bold': True}, + 'background_color': rgb('f8f8f2'), # canvas background color + 'tick_color': rgb('808080'), # ticks and grid + 'border_color': rgb('808080')} # border drawn around each graph + + graph = Graph( + xlabel='Cheese', + ylabel='Apples', + x_ticks_minor=5, + x_ticks_major=25, + y_ticks_major=1, + y_grid_label=True, + x_grid_label=True, + padding=5, + xlog=False, + ylog=False, + x_grid=True, + y_grid=True, + xmin=-50, + xmax=50, + ymin=-1, + ymax=1, + **graph_theme) + + plot = SmoothLinePlot(color=next(colors)) + plot.points = [(x / 10., sin(x / 50.)) for x in range(-500, 501)] + # for efficiency, the x range matches xmin, xmax + graph.add_plot(plot) + + plot = MeshLinePlot(color=next(colors)) + plot.points = [(x / 10., cos(x / 50.)) for x in range(-500, 501)] + graph.add_plot(plot) + self.plot = plot # this is the moving graph, so keep a reference + + plot = MeshStemPlot(color=next(colors)) + graph.add_plot(plot) + plot.points = [(x, x / 50.) for x in range(-50, 51)] + + plot = BarPlot(color=next(colors), bar_spacing=.72) + graph.add_plot(plot) + plot.bind_to_graph(graph) + plot.points = [(x, .1 + randrange(10) / 10.) for x in range(-50, 1)] + + Clock.schedule_interval(self.update_points, 1 / 60.) + + graph2 = Graph( + xlabel='Position (m)', + ylabel='Time (s)', + x_ticks_minor=0, + x_ticks_major=1, + y_ticks_major=10, + y_grid_label=True, + x_grid_label=True, + padding=5, + xlog=False, + ylog=False, + xmin=0, + ymin=0, + **graph_theme) + b.add_widget(graph) + + if np is not None: + (xbounds, ybounds, data) = self.make_contour_data() + # This is required to fit the graph to the data extents + graph2.xmin, graph2.xmax = xbounds + graph2.ymin, graph2.ymax = ybounds + + plot = ContourPlot() + plot.data = data + plot.xrange = xbounds + plot.yrange = ybounds + plot.color = [1, 0.7, 0.2, 1] + graph2.add_plot(plot) + + b.add_widget(graph2) + self.contourplot = plot + + Clock.schedule_interval(self.update_contour, 1 / 60.) + + # Test the scatter plot + plot = ScatterPlot(color=next(colors), pointsize=5) + graph.add_plot(plot) + plot.points = [(x, .1 + randrange(10) / 10.) for x in range(-50, 1)] + return b + + def make_contour_data(self, ts=0): + omega = 2 * pi / 30 + k = (2 * pi) / 2.0 + + ts = sin(ts * 2) + 1.5 # emperically determined 'pretty' values + npoints = 100 + data = np.ones((npoints, npoints)) + + position = [ii * 0.1 for ii in range(npoints)] + time = [(ii % 100) * 0.6 for ii in range(npoints)] + + for ii, t in enumerate(time): + for jj, x in enumerate(position): + data[ii, jj] = sin( + k * x + omega * t) + sin(-k * x + omega * t) / ts + return (0, max(position)), (0, max(time)), data + + def update_points(self, *args): + self.plot.points = [ + (x / 10., cos(Clock.get_time() + x / 50.)) + for x in range(-500, 501)] + + def update_contour(self, *args): + _, _, self.contourplot.data[:] = self.make_contour_data( + Clock.get_time()) + # this does not trigger an update, because we replace the + # values of the arry and do not change the object. + # However, we cannot do "...data = make_contour_data()" as + # kivy will try to check for the identity of the new and + # old values. In numpy, 'nd1 == nd2' leads to an error + # (you have to use np.all). Ideally, property should be patched + # for this. + self.contourplot.ask_draw() + + TestApp().run() diff --git a/kivyblocks/graph/_version.py b/kivyblocks/graph/_version.py new file mode 100644 index 0000000..717870e --- /dev/null +++ b/kivyblocks/graph/_version.py @@ -0,0 +1 @@ +__version__ = '0.4.1.dev0' diff --git a/kivyblocks/graph/tests/test_import.py b/kivyblocks/graph/tests/test_import.py new file mode 100644 index 0000000..7cf6828 --- /dev/null +++ b/kivyblocks/graph/tests/test_import.py @@ -0,0 +1,6 @@ +import pytest + + +def test_flower(): + from kivy_garden.graph import Graph + widget = Graph() diff --git a/kivyblocks/newvideo.py b/kivyblocks/newvideo.py index 7b60648..047fae2 100644 --- a/kivyblocks/newvideo.py +++ b/kivyblocks/newvideo.py @@ -22,8 +22,23 @@ class NewVideo(BGColorBehavior, Video): radius=radius) Window.allow_screensaver = False set_log_callback(self.ffplayerLog) + self.register_event_type('on_open_failed') + + def on_open_failed(self, o, source): + print('source=',source, 'open failed') def ffplayerLog(self, msg, level): + if 'Connection to tcp' in msg and 'failed' in msg: + self.dispatch('on_open_failed', self.source) + if 'Invalid data found when processing input' in msg: + self.dispatch('on_open_failed', self.source) + if 'End of file' in msg: + self.dispatch('on_open_failed', self.source) + if 'Server returned 404 Not Found' in msg: + self.dispatch('on_open_failed', self.source) + if 'Server returned 403 Forbidden' in msg: + self.dispatch('on_open_failed', self.source) + msg = msg.strip() if msg: logger_func[level]('yffpyplayer: {}'.format(msg)) diff --git a/kivyblocks/register.py b/kivyblocks/register.py index aef28d9..bc159b8 100644 --- a/kivyblocks/register.py +++ b/kivyblocks/register.py @@ -16,6 +16,9 @@ from .qrdata import QRCodeWidget # from .kivycamera import KivyCamera from .filebrowser import FileLoaderBrowser from .osc_server import OSCServer +from graph import Graph, MeshLinePlot, MeshStemPlot, LinePlot, \ + SmoothLinePlot, ContourPlot, BarPlot, HBar, VBar, ScatterPlot, \ + PointPlot r = Factory.register r('OSCServer',OSCServer)