def draw_axis_overlay_y(self, context): graph_left = round(self.xmin * context.xfactor) graph_right = round(self.xmax * context.xfactor) for tickPos in self.ytickmarks[:-1]: tickY = tickPos * context.yfactor p1 = context.trpoint(Point(graph_left, tickY)) p2 = context.trpoint(Point(graph_right, tickY)) self.view.draw_line(p1, p2, PenID.AxisOverlay)
def draw_axis_overlay_x(self, context): graph_bottom = round(self.ymin * context.yfactor) if graph_bottom < 0: graph_bottom -= self.YAXIS_EXTRA_SPACE_ON_NEGATIVE graph_top = round(self.ymax * context.yfactor) for tickPos in self.xtickmarks[:-1]: tickX = tickPos * context.xfactor p1 = context.trpoint(Point(tickX, graph_bottom)) p2 = context.trpoint(Point(tickX, graph_top)) self.view.draw_line(p1, p2, PenID.AxisOverlay)
def point_in_circle(center, radius, angle): # Returns the point at the edge of a circle with specified center/radius/angle # a/sin(A) = b/sin(B) = c/sin(C) = 2R # the start point is (center.x + radius, center.y) and goes counterclockwise angle = angle % 360 C = radians(90) A = radians(angle % 90) B = C - A c = radius ratio = c / sin(C) b = ratio * sin(B) a = ratio * sin(A) if angle > 270: return Point(center.x + a, center.y - b) elif angle > 180: return Point(center.x - b, center.y - a) elif angle > 90: return Point(center.x - a, center.y + b) else: return Point(center.x + b, center.y + a)
def draw_graph(self, context): if len(self.data) < 2: return points = [ Point(x * context.xfactor, y * context.yfactor) for x, y in self.data ] # close the polygons and fill them. # The closing point depends if we have a positive graph, a negative one or a mixed up if self.ymin >= 0: # positive yClose = round(self.ymin * context.yfactor) elif self.ymax < 0: # negative yClose = round(self.ymax * context.yfactor) else: # mixed up yClose = 0 # painter.setPen(QPen(Qt.NoPen)) xTodayfactored = self._offset_xpos(date.today().toordinal() + 1) * context.xfactor pastPoints = [p for p in points if p.x <= xTodayfactored] futurePoints = [p for p in points if p.x > xTodayfactored] if pastPoints and futurePoints: meetingPoint = Point(xTodayfactored, pastPoints[-1].y) pastPoints.append(meetingPoint) futurePoints.insert(0, meetingPoint) else: meetingPoint = None # start with past if pastPoints: firstPoint = pastPoints[0] lastPoint = pastPoints[-1] pastPoints.append(Point(lastPoint.x, yClose)) pastPoints.append(Point(firstPoint.x, yClose)) self.view.draw_polygon(context.trpoints(pastPoints), None, BrushID.GraphNormal) if futurePoints: firstPoint = futurePoints[0] lastPoint = futurePoints[-1] futurePoints.append(Point(lastPoint.x, yClose)) futurePoints.append(Point(firstPoint.x, yClose)) self.view.draw_polygon(context.trpoints(futurePoints), None, BrushID.GraphFuture) if meetingPoint is not None: p1 = context.trpoint(Point(xTodayfactored, yClose)) p2 = context.trpoint(meetingPoint) self.view.draw_line(p1, p2, PenID.TodayLine) self.draw_axis_overlay_y(context) self.draw_axis_overlay_x(context) # draw the main graph line. It looks better when that line is drawn after the overlay. self.view.draw_polygon(context.trpoints(points), PenID.Graph, None)
def draw_graph(self, context): for x1, x2, h1, h2 in self.data: x1 *= context.xfactor x2 *= context.xfactor h1 *= context.yfactor h2 *= context.yfactor # Compute and fill past and future rectangles different_side = (h1 >= 0) != (h2 >= 0) past_rect = Rect(x1, 0, x2 - x1, abs(h1)) if h2: future_height = abs(h2 if different_side else h2 - h1) else: future_height = 0 future_rect = Rect(x1, 0, x2 - x1, future_height) if h1 >= 0: past_rect.bottom = h1 else: past_rect.top = h1 if h2 >= 0: future_rect.bottom = h2 else: future_rect.top = h2 self.view.draw_rect(context.trrect(past_rect), None, BrushID.NormalBar) self.view.draw_rect(context.trrect(future_rect), None, BrushID.FutureBar) # Compute and draw rect lines union = past_rect.united(future_rect) if (union.top < 0) and (union.bottom > 0): # we draw 4 sides instead of 3 self.view.draw_rect(context.trrect(union), PenID.Bar, None) else: # One of bottom and top is 0. Use the other one. We're working with floats here, # comparison with 0 are hazardous, so I'm avoiding them. h = union.top if abs(union.top) >= abs( union.bottom) else union.bottom points = [ Point(x1, 0), Point(x1, h), Point(x2, h), Point(x2, 0) ] self.view.draw_polygon(context.trpoints(points), PenID.Bar, None) # draw red line if (h1 != 0) and (h2 != 0): lineY = 0 if different_side else h1 p1 = context.trpoint(Point(x1, lineY)) p2 = context.trpoint(Point(x2, lineY)) context.today_line = ( p1, p2) # will be drawn in draw_graph_after_axis() # We don't draw the X overlay in a bar graph self.draw_axis_overlay_y(context)
def trpoint(self, p): x, y = p x += self.xoffset y += self.yoffset return Point(x, y)
def draw_chart(self): if not hasattr(self, 'xmax'): # we haven't computed yet return view_rect = Rect(0, 0, *self.view_size) data_width = self.xmax - self.xmin data_height = self.ymax - self.ymin y_labels_width = max( self.view.text_size(label['text'], FontID.AxisLabel)[0] for label in self.ylabels) labels_height = self.view.text_size('', FontID.AxisLabel)[1] title = "{} ({})".format(self.title, self.currency.code) title_width, title_height = self.view.text_size(title, FontID.Title) titley = view_rect.h - self.TITLE_PADDING - title_height graphx = y_labels_width + self.PADDING graphy = labels_height + self.PADDING graph_width = view_rect.w - graphx - self.PADDING graph_height = view_rect.h - graphy - title_height - self.TITLE_PADDING graph_rect = Rect(graphx, graphy, graph_width, graph_height) xfactor = graph_width / data_width yfactor = graph_height / data_height graph_left = round(self.xmin * xfactor) graph_bottom = round(self.ymin * yfactor) if graph_bottom < 0: # We have a graph with negative values and we need some extra space to draw the lowest values graph_bottom -= self.YAXIS_EXTRA_SPACE_ON_NEGATIVE graph_top = round(self.ymax * yfactor) xoffset = graph_rect.left yoffset = -(graph_bottom - graph_rect.y) context = GraphContext(xfactor, yfactor, xoffset, yoffset) self.draw_graph(context) # X/Y axis p1 = context.trpoint(Point(0, graph_bottom)) p2 = context.trpoint(Point(graph_width, graph_bottom)) p3 = context.trpoint(Point(0, graph_top)) self.view.draw_line(p1, p2, PenID.Axis) self.view.draw_line(p1, p3, PenID.Axis) if graph_bottom < 0: p1 = context.trpoint(Point(0, 0)) p2 = context.trpoint(Point(graph_width, 0)) self.view.draw_line(p1, p2, PenID.Axis) # X tickmarks tickBottomY = graph_bottom - self.TICKMARKS_LENGTH for tickPos in self.xtickmarks: tickX = tickPos * xfactor p1 = context.trpoint(Point(tickX, graph_bottom)) p2 = context.trpoint(Point(tickX, tickBottomY)) self.view.draw_line(p1, p2, PenID.Axis) # Y tickmarks tickLeftX = graph_left - self.TICKMARKS_LENGTH for tickPos in self.ytickmarks: tickY = tickPos * yfactor p1 = context.trpoint(Point(graph_left, tickY)) p2 = context.trpoint(Point(tickLeftX, tickY)) self.view.draw_line(p1, p2, PenID.Axis) # X Labels labelY = graph_bottom - labels_height - self.XLABELS_PADDING for label in self.xlabels: labelText = label['text'] labelWidth = self.view.text_size(labelText, FontID.AxisLabel)[0] labelX = (label['pos'] * xfactor) - (labelWidth / 2) text_rect = context.trrect( Rect(labelX, labelY, labelWidth, labels_height)) self.view.draw_text(labelText, text_rect, FontID.AxisLabel) # Y Labels for label in self.ylabels: labelText = label['text'] labelWidth = self.view.text_size(labelText, FontID.AxisLabel)[0] labelX = graph_left - self.YLABELS_PADDING - labelWidth labelY = (label['pos'] * yfactor) - (labels_height / 2) text_rect = context.trrect( Rect(labelX, labelY, labelWidth, labels_height)) self.view.draw_text(labelText, text_rect, FontID.AxisLabel) # Title self.view.draw_text(title, Rect(0, titley, view_rect.w, title_height), FontID.Title) self.draw_graph_after_axis(context)
def mouse_move(self, x, y): # only call when the mouse button is currently down self._last_mouse_pos = Point(x, y) self.view.refresh()
def mouse_down(self, x, y): self._last_mouse_down = Point(x, y) self._last_mouse_pos = Point(x, y) self.view.refresh()