From c273e4bb8a9df632504e148d17616fe7d56b60ff Mon Sep 17 00:00:00 2001 From: yumoqing Date: Tue, 28 Dec 2021 10:48:22 +0800 Subject: [PATCH] first commit --- kivycharts/__init__.py | 0 kivycharts/bar.py | 85 ++ kivycharts/chart.py | 79 ++ kivycharts/geometry.py | 81 ++ kivycharts/graph/__init__.py | 1759 +++++++++++++++++++++++++ kivycharts/graph/_version.py | 1 + kivycharts/graph/arc.py | 13 + kivycharts/graph/beziercurve.py | 14 + kivycharts/graph/circle.py | 10 + kivycharts/graph/compoundpath.py | 7 + kivycharts/graph/displayable.py | 5 + kivycharts/graph/droplet.py | 11 + kivycharts/graph/ellipse.py | 11 + kivycharts/graph/heart.py | 11 + kivycharts/graph/image.py | 12 + kivycharts/graph/line.py | 12 + kivycharts/graph/lsogon.py | 10 + kivycharts/graph/path.py | 14 + kivycharts/graph/polygon.py | 10 + kivycharts/graph/polyline.py | 10 + kivycharts/graph/rectangle.py | 12 + kivycharts/graph/rose.py | 12 + kivycharts/graph/sector.py | 14 + kivycharts/graph/star.py | 13 + kivycharts/graph/tests/test_import.py | 6 + kivycharts/graph/text.py | 9 + kivycharts/graph/touchoid.py | 12 + kivycharts/pie.py | 66 + requirements.txt | 10 + setup.py | 60 + version.py | 1 + 31 files changed, 2370 insertions(+) create mode 100644 kivycharts/__init__.py create mode 100644 kivycharts/bar.py create mode 100644 kivycharts/chart.py create mode 100644 kivycharts/geometry.py create mode 100644 kivycharts/graph/__init__.py create mode 100644 kivycharts/graph/_version.py create mode 100644 kivycharts/graph/arc.py create mode 100644 kivycharts/graph/beziercurve.py create mode 100644 kivycharts/graph/circle.py create mode 100644 kivycharts/graph/compoundpath.py create mode 100644 kivycharts/graph/displayable.py create mode 100644 kivycharts/graph/droplet.py create mode 100644 kivycharts/graph/ellipse.py create mode 100644 kivycharts/graph/heart.py create mode 100644 kivycharts/graph/image.py create mode 100644 kivycharts/graph/line.py create mode 100644 kivycharts/graph/lsogon.py create mode 100644 kivycharts/graph/path.py create mode 100644 kivycharts/graph/polygon.py create mode 100644 kivycharts/graph/polyline.py create mode 100644 kivycharts/graph/rectangle.py create mode 100644 kivycharts/graph/rose.py create mode 100644 kivycharts/graph/sector.py create mode 100644 kivycharts/graph/star.py create mode 100644 kivycharts/graph/tests/test_import.py create mode 100644 kivycharts/graph/text.py create mode 100644 kivycharts/graph/touchoid.py create mode 100644 kivycharts/pie.py create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 version.py diff --git a/kivycharts/__init__.py b/kivycharts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kivycharts/bar.py b/kivycharts/bar.py new file mode 100644 index 0000000..01f1b33 --- /dev/null +++ b/kivycharts/bar.py @@ -0,0 +1,85 @@ +from kivy.uix.widget import Widget +from kivy.graphics import Color, Rectangle +from kivyblocks.colorcalc import * +from kivyblocks.charts.chart import Chart,ChartPart + +class BarPart(ChartPart): + def __init__(self,widget,pos,size,color,data_offset): + self.widget = widget + self.pos = pos + self.size = size + self.color = color + self.data_offset = data_offset + + def collide_point(self,x,y): + minx = self.widget.pos[0] + self.pos[0] + maxx = minx + self.size[0] + miny = self.widget.pos[1] + self.pos[1] + maxy = miny + self.size[1] + if minx <= x and x <= maxx and \ + miny <= y and y <= maxy: + return True + return False + + def draw(self): + with self.widget.canvas: + Color(*self.color) + Rectangle(pos=(self.widget.pos[0] + self.pos[0], + self.widget.pos[1] + self.pos[1]), + size = self.size) + + def mark(self): + rcolor = reverseColor(self.color) + rcolor.append(0.8) + with self.widget.canvas.after: + Color(*reverseColor(self.color)) + Rectangle(pos=(self.widget.pos[0] + self.pos[0], + self.widget.pos[1] + self.pos[1]), + size = self.size) + +class Bar(Chart): + """ + a BAR class + """ + def __init__(self,**options): + """ + options={ + width, + height, + title, + keyField, + valueField, + color1: + color2: + data=[ + { + name, + value, + },{ + } + ] + } + """ + self.data = None + super().__init__(**options) + + def data2part(self): + data = self.data + kvs = [ [i[self.options['keyField']],i[self.options['valueField']]] for i in data ] + m = max([i[1] for i in kvs ]) + cnt = len(kvs) + points = divide([0,0],[self.width,0],cnt) + color1='8833ee' + color2='ed8234' + colors = divideColor(color1,color2,cnt-1) + for i in range(cnt): + h = self.height * kvs[i][1] / m + c = colors[i] + part = BarPart(self, + points[i], + (points[i+1][0] - points[i][0],h), + colors[i], + i + ) + self.chartparts.append(part) + diff --git a/kivycharts/chart.py b/kivycharts/chart.py new file mode 100644 index 0000000..f7aef87 --- /dev/null +++ b/kivycharts/chart.py @@ -0,0 +1,79 @@ +from kivy.app import App +from kivy.graphics import Color, Rectangle +from kivy.uix.widget import Widget +from kivyblocks.colorcalc import toArrayColor + +class ChartPart(object): + def __init__(self,pos,width,height): + self.pos = pos + self.width = width + self.height = height + + def collide_point(self,x,y): + raise NotImplementedError + + def draw(self): + raise NotImplementedError + + def mark(self): + raise NotImplementedError + +class Chart(Widget): + def __init__(self,**options): + super().__init__() + self.options = options + self.bg_color = toArrayColor(self.options.get('bg_color',[0.3,0.3,0.3,1])) + self.markpart = None + self.bind(size=self.onSize,pos=self.onSize) + self.register_event_type("on_press") + + def unmark(self): + self.canvas.after.clear() + + def getData(self): + url = self.options.get('url') + if url: + hc = App.get_running_app().hc + params = self.options.get('params',{}) + d = hc.get(url,parms=param) + self.data = d.get('data',[]) + else: + self.data = self.options.get('data',[]) + self.chartparts = [] + + def on_touch_down(self,touch): + if touch.is_mouse_scrolling: + return False + if not self.collide_point(touch.x,touch.y): + return False + self.markpart = None + self.unmark() + for part in self.chartparts: + if part.collide_point(touch.x,touch.y): + self.markPart(part) + self.dispatch('on_press',self,self.data[part.data_offset]) + return super(Chart, self).on_touch_down(touch) + + def on_press(self,o,data): + print('data=',data) + + def draw(self): + with self.canvas.before: + Color(*self.bg_color) + Rectangle(pos=self.pos,size=self.size) + + self.canvas.clear() + for part in self.chartparts: + part.draw() + + def markPart(self,part): + self.markpart = part + part.mark() + + def onSize(self,o,v): + self.build() + + def build(self): + self.getData() + self.data2part() + self.draw() diff --git a/kivycharts/geometry.py b/kivycharts/geometry.py new file mode 100644 index 0000000..166d640 --- /dev/null +++ b/kivycharts/geometry.py @@ -0,0 +1,81 @@ +import math +import numpy as np + +class Point(object): + def __init__(self, x=0, y=0): + super().__init__() + self.x = x + self.y = y + +class Line(object): # 直线由两个点组成 + def __init__(self, p1, p2): + if isinstance(p1,list) or isinstance(p1,tuple): + self.p1 = Point(*p1) + else: + self.p1 = p1 + if isinstance(p2,list) or isinstance(p2,tuple): + self.p2 = Point(*p2) + else: + self.p2 = p2 + + def vector(self): + return self.p1.x - self.p2.x,self.p1.y - self.p2.y + + def lenght(self): + return math.sqrt(pow((self.p1.x - self.p2.x), 2) + + pow((self.p1.y - self.p2.y), 2)) + + def get_cross_angle(self, l): + # 向量a + arr_a = np.array(self.vector()) + # 向量b + arr_b = np.array(l.vector()) + cos_value = float(arr_a.dot(arr_b)) / (np.sqrt(arr_a.dot(arr_a)) \ + * np.sqrt(arr_b.dot(arr_b))) + # 注意转成浮点数运算 + return np.arccos(cos_value) * (180 / np.pi) + # 两个向量的夹角的角度, + # 余弦值:cos_value, np.cos(para), + # 其中para是弧度,不是角度 + +class EllipseUtils(object): + def __init__(self,pos,size): + self.pos = Point(pos) + self.size = size + self.slides = [] + super().__init__() + + def split(self,data,colors): + self.slides = [] + kvs = [ [i[self.options['keyField']],i[self.options['valueField']]] for i in data ] + total = sum([i[1] for i in kvs ]) + start_degree = 0 + cnt = len(kvs) + for i in range(cnt): + degree = start_degree + 360 * kvs[i][1] / total + self.slides.append((degress,colors[i])) + start_degree = degree + + def isInside(self,a,b,x,y): + if a>b: + return x**x / a**a + y**y/b**b <= 1 + return x**x / b**b + y**y / a**a <= 1 + + def collide_point(self,x,y): + a = float(self.size[0] / 2) + b = float(self.size[1] / 2) + x = x - self.pos[0] + y = y - self.pos[1] + if not self.isInside(a,b,x,y): + return -1 + start_degress = 0 + l = Line((a,b),(a,b*2)) + l1 = Line((a,b),(x,y)) + degress = l.get_cross_angle(l1) + if x < a: + degress = 360 - degress + for i in range(len(self.slides)): + if start_degress <= degress and degress < self.slides[i][0]: + return i + start_degress += self.slides[i][0] + return -1 diff --git a/kivycharts/graph/__init__.py b/kivycharts/graph/__init__.py new file mode 100644 index 0000000..5c0841d --- /dev/null +++ b/kivycharts/graph/__init__.py @@ -0,0 +1,1759 @@ +''' +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: + if k2 >= len(points_minor): + continue + print('len(points_minor)=', len(points_minor), \ + 'len(points_major)=',len(points_major), \ + 'major=', major, 'minor=', minor or 1, \ + 'k2=',k2, 'n_ticks=', n_ticks) + 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), point_size=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/kivycharts/graph/_version.py b/kivycharts/graph/_version.py new file mode 100644 index 0000000..717870e --- /dev/null +++ b/kivycharts/graph/_version.py @@ -0,0 +1 @@ +__version__ = '0.4.1.dev0' diff --git a/kivycharts/graph/arc.py b/kivycharts/graph/arc.py new file mode 100644 index 0000000..715d9ec --- /dev/null +++ b/kivycharts/graph/arc.py @@ -0,0 +1,13 @@ +# arc.py +""" +名称 类型 默认值 描述 +opts Object 配置项,继承得到的配置项参见 zrender.Displayable。 +opts.shape Object 形状属性。 +opts.shape.cx number 0 圆心横坐标。 +opts.shape.cy number 0 圆心纵坐标。 +opts.shape.r number 0 半径。 +opts.shape.startAngle number 0 起始弧度。 +opts.shape.endAngle number Math.PI * 2 终止弧度。 +opts.shape.clockwise boolean true 顺时针方向。 +""" + diff --git a/kivycharts/graph/beziercurve.py b/kivycharts/graph/beziercurve.py new file mode 100644 index 0000000..1535558 --- /dev/null +++ b/kivycharts/graph/beziercurve.py @@ -0,0 +1,14 @@ +# briercurve.py +""" +名称 类型 默认值 描述 +opts Object 配置项,继承得到的配置项参见 zrender.Displayable。 +opts.shape Object 形状属性。 +opts.shape.x1 number 0 起始点横坐标。 +opts.shape.y1 number 0 起始点纵坐标。 +opts.shape.x2 number 0 终止点横坐标。 +opts.shape.y2 number 0 终止点纵坐标。 +opts.shape.cpx1 number 0 控制点横坐标。 +opts.shape.cpy1 number 0 控制点纵坐标。 +opts.shape.percent number 1 已显示的百分比,用于绘制动画。 +""" + diff --git a/kivycharts/graph/circle.py b/kivycharts/graph/circle.py new file mode 100644 index 0000000..dc15c8c --- /dev/null +++ b/kivycharts/graph/circle.py @@ -0,0 +1,10 @@ +# circle.py +""" +名称 类型 默认值 描述 +opts Object 配置项,继承得到的配置项参见 zrender.Displayable。 +opts.shape Object 形状属性。 +opts.shape.cx number 0 圆心横坐标。 +opts.shape.cy number 0 圆心纵坐标。 +opts.shape.r number 0 半径。 +""" + diff --git a/kivycharts/graph/compoundpath.py b/kivycharts/graph/compoundpath.py new file mode 100644 index 0000000..7f72431 --- /dev/null +++ b/kivycharts/graph/compoundpath.py @@ -0,0 +1,7 @@ +# compoundpath.py +""" +名称 类型 默认值 描述 +opts Object 配置项,继承得到的配置项参见 zrender.Displayable。 +opts.shape.paths Path[] null 路径数组。 +""" + diff --git a/kivycharts/graph/displayable.py b/kivycharts/graph/displayable.py new file mode 100644 index 0000000..1f40182 --- /dev/null +++ b/kivycharts/graph/displayable.py @@ -0,0 +1,5 @@ +# displayable.py +""" +base class for all the plot class +""" + diff --git a/kivycharts/graph/droplet.py b/kivycharts/graph/droplet.py new file mode 100644 index 0000000..30c98f8 --- /dev/null +++ b/kivycharts/graph/droplet.py @@ -0,0 +1,11 @@ +# droplet.py +""" +名称 类型 默认值 描述 +opts Object 配置项,继承得到的配置项参见 zrender.Displayable。 +opts.shape Object 形状属性。 +opts.shape.cx number 0 圆心横坐标。 +opts.shape.cy number 0 圆心纵坐标。 +opts.shape.width number 0 宽度。 +opts.shape.height number 0 高度。 +""" + diff --git a/kivycharts/graph/ellipse.py b/kivycharts/graph/ellipse.py new file mode 100644 index 0000000..50c3d72 --- /dev/null +++ b/kivycharts/graph/ellipse.py @@ -0,0 +1,11 @@ +# ellipse.py +""" +名称 类型 默认值 描述 +opts Object 配置项,继承得到的配置项参见 zrender.Displayable。 +opts.shape Object 形状属性。 +opts.shape.cx number 0 圆心横坐标。 +opts.shape.cy number 0 圆心纵坐标。 +opts.shape.rx number 0 横向半径。 +opts.shape.ry number 0 纵向半径。 +""" + diff --git a/kivycharts/graph/heart.py b/kivycharts/graph/heart.py new file mode 100644 index 0000000..df14fb7 --- /dev/null +++ b/kivycharts/graph/heart.py @@ -0,0 +1,11 @@ +# heart.py +""" +名称 类型 默认值 描述 +opts Object 配置项,继承得到的配置项参见 zrender.Displayable。 +opts.shape Object 形状属性。 +opts.shape.cx number 0 圆心横坐标。 +opts.shape.cy number 0 圆心纵坐标。 +opts.shape.width number 0 宽度。 +opts.shape.height number 0 高度。 +""" + diff --git a/kivycharts/graph/image.py b/kivycharts/graph/image.py new file mode 100644 index 0000000..6afc08f --- /dev/null +++ b/kivycharts/graph/image.py @@ -0,0 +1,12 @@ +# image.py +""" +名称 类型 默认值 描述 +opts Object 配置项,继承得到的配置项参见 zrender.Displayable。 +opts.style Object 样式。 +opts.style.image string|HTMLImageElement|HTMLCanvasElement 图片的内容,可以是图片的 URL,也可以是 dataURI。 +opts.style.x number 图片左上角相对于父节点的横坐标。 +opts.style.y number 图片左上角相对于父节点的纵坐标。 +opts.style.width number 图片宽度。 +opts.style.height number 图片高度。 +""" + diff --git a/kivycharts/graph/line.py b/kivycharts/graph/line.py new file mode 100644 index 0000000..6f74758 --- /dev/null +++ b/kivycharts/graph/line.py @@ -0,0 +1,12 @@ +# line.py +""" +名称 类型 默认值 描述 +opts Object 配置项,继承得到的配置项参见 zrender.Displayable。 +opts.shape Object 形状属性。 +opts.shape.x1 number 0 起始点横坐标。 +opts.shape.y1 number 0 起始点纵坐标。 +opts.shape.x2 number 0 终止点横坐标。 +opts.shape.y2 number 0 终止点纵坐标。 +opts.shape.percent number 1 已显示的百分比,用于绘制动画。 +""" + diff --git a/kivycharts/graph/lsogon.py b/kivycharts/graph/lsogon.py new file mode 100644 index 0000000..0dca14d --- /dev/null +++ b/kivycharts/graph/lsogon.py @@ -0,0 +1,10 @@ +# lsogon.py +""" +opts Object 配置项,继承得到的配置项参见 zrender.Displayable。 +opts.shape Object 形状属性。 +opts.shape.x number 0 圆心横坐标。 +opts.shape.y number 0 圆心纵坐标。 +opts.shape.r number 0 半径。 +opts.shape.n number 0 边数。 +""" + diff --git a/kivycharts/graph/path.py b/kivycharts/graph/path.py new file mode 100644 index 0000000..e2453a1 --- /dev/null +++ b/kivycharts/graph/path.py @@ -0,0 +1,14 @@ +# path.py +""" +名称 类型 默认值 描述 +opts Object 配置项,继承得到的配置项参见 zrender.Displayable。 +""" + + +""" +path.extend +opts Object 配置项,继承得到的配置项参见 zrender.Displayable。 +opts.type string 类型,自定义的名称。 +opts.init Function 初始化时调用的函数。 +opts.buildPath Function 如何构建 Path 的函数,在绘制时候会调用。 +""" diff --git a/kivycharts/graph/polygon.py b/kivycharts/graph/polygon.py new file mode 100644 index 0000000..33df3f5 --- /dev/null +++ b/kivycharts/graph/polygon.py @@ -0,0 +1,10 @@ +# polygon.py +""" +名称 类型 默认值 描述 +opts Object 配置项,继承得到的配置项参见 zrender.Displayable。 +opts.shape Object 形状属性。 +opts.shape.points number[][] 0 每个元素是一个横纵坐标的数组。 +opts.shape.smooth number|string 0 圆滑程度,取值范围为 0 到 1 之间的数字,0 表示不圆滑;也可以是特殊字符串 'spline' 表示用 Catmull-Rom spline 插值算法,否则默认用贝塞尔曲线插值算法。 +opts.shape.smoothConstraint number[][] 0 将计算出来的控制点约束在一个包围盒内。比如 [[0, 0], [100, 100]],这个包围盒会与整个折线的包围盒做一个并集用来约束控制点。 +""" + diff --git a/kivycharts/graph/polyline.py b/kivycharts/graph/polyline.py new file mode 100644 index 0000000..c72f638 --- /dev/null +++ b/kivycharts/graph/polyline.py @@ -0,0 +1,10 @@ +# polyline.py +""" +名称 类型 默认值 描述 +opts Object 配置项,继承得到的配置项参见 zrender.Displayable。 +opts.shape Object 形状属性。 +opts.shape.points number[][] 0 每个元素是一个横纵坐标的数组。 +opts.shape.smooth number|string 0 圆滑程度,取值范围为 0 到 1 之间的数字,0 表示不圆滑;也可以是特殊字符串 'spline' 表示用 Catmull-Rom spline 插值算法,否则默认用贝塞尔曲线插值算法。 +opts.shape.smoothConstraint number[][] 0 将计算出来的控制点约束在一个包围盒内。比如 [[0, 0], [100, 100]],这个包围盒会与整个折线的包围盒做一个并集用来约束控制点。 +""" + diff --git a/kivycharts/graph/rectangle.py b/kivycharts/graph/rectangle.py new file mode 100644 index 0000000..36e3083 --- /dev/null +++ b/kivycharts/graph/rectangle.py @@ -0,0 +1,12 @@ +# rectangle.py +""" +名称 类型 默认值 描述 +opts Object 配置项,继承得到的配置项参见 zrender.Displayable。 +opts.shape Object 形状属性。 +opts.shape.r number|number[] 0 用于创建圆角矩形。左上、右上、右下、左下角的半径依次为 r1、 r2、 r3、 r4。r 缩写为 1 相当于 [1, 1, 1, 1];r 缩写为 [1] 相当于 [1, 1, 1, 1];r 缩写为 [1, 2] 相当于 [1, 2, 1, 2];r 缩写为 [1, 2, 3] 相当于 [1, 2, 3, 2]。 +opts.shape.x number 0 左上角的横坐标。 +opts.shape.y number 0 左上角的纵坐标。 +opts.shape.width number 0 宽度。 +opts.shape.height number 0 高度。 +""" + diff --git a/kivycharts/graph/rose.py b/kivycharts/graph/rose.py new file mode 100644 index 0000000..b573447 --- /dev/null +++ b/kivycharts/graph/rose.py @@ -0,0 +1,12 @@ +# rose.py +""" +名称 类型 默认值 描述 +opts Object 配置项,继承得到的配置项参见 zrender.Displayable。 +opts.shape Object 形状属性。 +opts.shape.cx number 0 圆心横坐标。 +opts.shape.cy number 0 圆心纵坐标。 +opts.shape.r number[] [] 半径。 +opts.shape.k number 0 玫瑰线参数,参见 wiki。 +opts.shape.n number 1 花瓣数。 +""" + diff --git a/kivycharts/graph/sector.py b/kivycharts/graph/sector.py new file mode 100644 index 0000000..e91f888 --- /dev/null +++ b/kivycharts/graph/sector.py @@ -0,0 +1,14 @@ +# sector.py +""" +名称 类型 默认值 描述 +opts Object 配置项,继承得到的配置项参见 zrender.Displayable。 +opts.shape Object 形状属性。 +opts.shape.cx number 0 圆心横坐标。 +opts.shape.cy number 0 圆心纵坐标。 +opts.shape.r number 0 半径。 +opts.shape.r0 number 0 内半径。 +opts.shape.startAngle number 0 起始弧度。 +opts.shape.endAngle number Math.PI * 2 终止弧度。 +opts.shape.clockwise boolean true 顺时针方向。 +""" + diff --git a/kivycharts/graph/star.py b/kivycharts/graph/star.py new file mode 100644 index 0000000..e788dd4 --- /dev/null +++ b/kivycharts/graph/star.py @@ -0,0 +1,13 @@ +# star.py +""" + +名称 类型 默认值 描述 +opts Object 配置项,继承得到的配置项参见 zrender.Displayable。 +opts.shape Object 形状属性。 +opts.shape.cx number 0 圆心横坐标。 +opts.shape.cy number 0 圆心纵坐标。 +opts.shape.n number 3 瓣数,如 n 等于 5 时,是我们熟悉的五角星。 +opts.shape.r number 0 半径。 +opts.shape.r0 number 0 内半径。 +""" + diff --git a/kivycharts/graph/tests/test_import.py b/kivycharts/graph/tests/test_import.py new file mode 100644 index 0000000..7cf6828 --- /dev/null +++ b/kivycharts/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/kivycharts/graph/text.py b/kivycharts/graph/text.py new file mode 100644 index 0000000..9c24a40 --- /dev/null +++ b/kivycharts/graph/text.py @@ -0,0 +1,9 @@ +# text.py +""" +名称 类型 默认值 描述 +opts Object 配置项,继承得到的配置项参见 zrender.Displayable。 +?text +font_size +? +""" + diff --git a/kivycharts/graph/touchoid.py b/kivycharts/graph/touchoid.py new file mode 100644 index 0000000..405cd90 --- /dev/null +++ b/kivycharts/graph/touchoid.py @@ -0,0 +1,12 @@ +# touchoid.py +""" +名称 类型 默认值 描述 +opts Object 配置项,继承得到的配置项参见 zrender.Displayable。 +opts.shape Object 形状属性。 +opts.shape.cx number 0 圆心横坐标。 +opts.shape.cy number 0 圆心纵坐标。 +opts.shape.r number 0 半径。 +opts.shape.r0 number 0 内半径。 +opts.shape.d number 0 内外旋轮曲线参数,参见 wiki。 +opts.shape.n location 'out' out 或 in,表示曲线在内部还是外部。 +""" diff --git a/kivycharts/pie.py b/kivycharts/pie.py new file mode 100644 index 0000000..fc11ce8 --- /dev/null +++ b/kivycharts/pie.py @@ -0,0 +1,66 @@ + +import math +import numpy as np +from kivy.uix.widget import Widget +from kivyblocks.charts.geometry import EllipseUtils + +class PiePart(ChartPart): + def collide_point(self,x,y): + if not self.isInSideEllipse(x,y): + return False + return self.isInSidePart(x,y) + + def isInSideEllipse(self,x,y): + a = self.width / 2 + b = self.height / 2 + v = x ** x / a ** a + y**y / b ** b + if v <= 1: + return True + return False + +class Pie(Widget): + def __init__(self,**options): + """ + options={ + width, + height, + title, + keyField, + valueField, + data=[ + { + name, + value, + },{ + } + ] + } + """ + self.options = options + self.initflag = False + super().__init__() + self.bind(size=self.onSize,pos=self.onSize) + + def data2pie(self): + data = self.options.get('data',[]) + kvs = [ [i[self.options['keyField']],i[self.options['valueField']]] for i in data ] + total = sum([i[1] for i in kvs ]) + start_degree = 0 + cnt = len(kvs) + color1='8833ee' + color2='ed8234' + colors = divideColor(color1,color2,cnt-1) + self.canvas.clear() + for i in range(cnt): + degree = start_degree + 360 * kvs[i][1] / total + with self.canvas: + Color(*colors[i]) + Ellipse(pos=self.pos, + size=self.size, + angle_start=start_degree, + angle_end= degree) + start_degree = degree + + def onSize(self,o,v): + self.data2pie() + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..612fb27 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +jinja2 +kivy +ffpyplayer +qrcode +pillow +requests +plyer +python-osc +bs4 +lxml diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..61375e0 --- /dev/null +++ b/setup.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +import codecs +from distutils.core import setup +from setuptools import setup, find_packages +from Cython.Build import cythonize +from version import version + +# usage: +# python setup.py bdist_wininst generate a window executable file +# python setup.py bdist_egg generate a egg file +# Release information about eway + +# version = "0.0.4" +description = "kivy charts is a tool to build kivy data plot" +author = "yumoqing" +email = "yumoqing@icloud.com" + +depandent_packages = [] +with codecs.open('./requirements.txt', 'r', 'utf-8') as f: + b = f.read() + depandent_packages = b.split(',') + +packages=find_packages() +package_data = { + "kivyblocks":[ + 'imgs/*.png', + 'imgs/*.atlas', + 'imgs/*.gif', + 'imgs/*.jpg', + 'ttf/*.ttf', + 'ui/*.uidesc', + ], +} + +setup( + name="kivycharts", + ext_modules=cythonize( + [ + ]), + version=version, + + # uncomment the following lines if you fill them out in release.py + description=description, + author=author, + author_email=email, + + install_requires=depandent_packages, + packages=packages, + package_data=package_data, + keywords = [ + ], + classifiers = [ + 'Development Status :: 3 - Alpha', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3.5', + 'Topic :: Software Development :: Libraries :: Python Modules' + ], + platforms= 'any' +) diff --git a/version.py b/version.py new file mode 100644 index 0000000..1fb86c7 --- /dev/null +++ b/version.py @@ -0,0 +1 @@ +version = '0.0.5'