def __init__(self, series_set, scale, style=None, vertical_scale=True, stacked=True, zero_base=True): """ Constructor; creates a new LineGraph. @param mseries: The data to plot, as a MultiSeries @type mseries: graphication.series.MultiSeries @param style: The Style to apply to this graph @type style: graphication.style.Style @param scale: The Scale to use for the graph. @type scale: graphication.scales.BaseScale @param smoothed: If the graph is smoothed (not straight lines) @type smoothed: bool """ self.series_set = series_set self.style = default_css.merge(style) self.scale = scale self.stacked = stacked self.zero_base = zero_base self.vertical_scale = vertical_scale # Initialise the vertical scale if stacked: y_min, y_max = self.stacked_value_range() else: y_min, y_max = self.series_set.value_range() if self.zero_base: y_min = 0 self.y_scale = VerticalWavegraphScale(y_min, y_max)
def calc_rel_points(self): """Calculates the relative shapes of the sections""" # Get the style stuff y_offset = self.style['wavegraph'].get_align("vertical-align", 0.5) y_size = self.style['wavegraph'].get_align("height", 0.9) # Work out our extents y_total = max([total for (key, total) in self.series_set.totals()]) self.y_scale = VerticalWavegraphScale(0, y_total) # Calculate the points cols = [] self.xs = [] for x, stack in self.series_set.stacks(): self.xs.append(self.scale.get_point(x)) # Collect the points ys = [] total = 0 for ser, val in stack: y = self.y_scale.get_point(val) * y_size ys.append(total) total += y ys.append(total) shift = 1 - total # Shift them down to center them ys = [a + (shift * y_offset) for a in ys] cols.append(ys) self.rows = list(zip(*cols))
def calc_rel_points(self): """Calculates the relative shapes of the sections""" if isinstance(self.vertical_scale, BaseScale): self.y_scale = self.vertical_scale else: # Work out our extents y_min, y_max = self.series_set.value_range() if self.zero_base: y_min = 0 self.y_scale = VerticalWavegraphScale(y_min, y_max)
def calc_rel_points(self): """Calculates the relative shapes of the sections""" # Get the style stuff y_offset = self.style['wavegraph'].get_align("vertical-align", 0.5) y_size = self.style['wavegraph'].get_align("height", 0.9) # Work out our extents y_total = max([total for (key, total) in self.series_set.totals()]) self.y_scale = VerticalWavegraphScale(0, y_total) # Calculate the points cols = [] self.xs = [] for x, stack in self.series_set.stacks(): self.xs.append(self.scale.get_point(x)) # Collect the points ys = [] total = 0 for ser, val in stack: y = self.y_scale.get_point(val) * y_size ys.append(total) total += y ys.append(total) shift = 1 - total # Shift them down to center them ys = map(lambda a: a + (shift * y_offset), ys) cols.append(ys) self.rows = zip(*cols)
def __init__(self, series_set, scale, style=None, vertical_scale=None, zero_base=True, label_on=True, stacked=True, sharp_edges=True, top_only=False, border_only=False): """ Constructor; creates a new CurvyBarChart. """ self.series_set = series_set self.style = default_css.merge(style) self.scale = scale self.label_height = style['curvybarchart label'].get_float("height", 30) self.label_on = label_on self.zero_base = zero_base self.stacked = stacked self.sharp_edges = sharp_edges self.top_only = top_only self.border_only = border_only if vertical_scale is None: if stacked: y_min, y_max = self.stacked_value_range() else: y_min, y_max = self.series_set.value_range() if self.zero_base: y_min = 0 vertical_scale = VerticalWavegraphScale(y_min, y_max) self.y_scale = vertical_scale
class BarChart(Graph): def __init__(self, series_set, scale, style=None, vertical_scale=True, stacked=True, zero_base=True): """ Constructor; creates a new LineGraph. @param mseries: The data to plot, as a MultiSeries @type mseries: graphication.series.MultiSeries @param style: The Style to apply to this graph @type style: graphication.style.Style @param scale: The Scale to use for the graph. @type scale: graphication.scales.BaseScale @param smoothed: If the graph is smoothed (not straight lines) @type smoothed: bool """ self.series_set = series_set self.style = default_css.merge(style) self.scale = scale self.stacked = stacked self.zero_base = zero_base self.vertical_scale = vertical_scale # Initialise the vertical scale if stacked: y_min, y_max = self.stacked_value_range() else: y_min, y_max = self.series_set.value_range() if self.zero_base: y_min = 0 self.y_scale = VerticalWavegraphScale(y_min, y_max) def stacked_value_range(self): y_max = 0 for key, total in self.series_set.totals(): y_max = max(total, y_max) return 0, y_max def calc_plot_size(self): self.plot_height = self.height - self.calc_label_dimension( self.scale, is_height=True, major_selector="barchart grid#x.major", minor_selector="barchart grid#x.minor", ) self.plot_width = self.width self.plot_left = 0 self.plot_top = 0 def render(self, context, debug=False): context.save() # Draw the bars # Get width per bar block keys = list(self.series_set.keys()) per_bar = self.plot_width / len(keys) # Some more drawing parameters zero_line = self.plot_top + self.plot_height bar_style = self.style["barchart bar"] bar_padding = bar_style.get_float("padding") bar_padding_top = bar_style.get_float("padding-top") border_width = bar_style.get_float("border-width") border_color = bar_style.get_color("border-color") # Draw the bars at each location left = self.plot_left for key in keys: stack = self.series_set.stack(key) bottom = zero_line inner_left = 0 for series, value in stack: height = 0 - self.y_scale.get_point(value) * self.plot_height if self.stacked: x, y, w, h = ( left + bar_padding, bottom, per_bar - bar_padding * 2, height + bar_padding_top, ) else: x, y, w, h = ( left + inner_left + bar_padding, bottom, per_bar / len(stack) - bar_padding * 2, height + bar_padding_top, ) inner_left += per_bar / len(stack) context.rectangle(x, y, w, h) context.set_source_rgba(*series.color_as_rgba()) context.fill() # Draw outer border if needed if border_width: context.rectangle( x + 0.5, y - 0.5, w - 1, h + 1, ) context.set_source_rgba(*border_color) context.stroke() if self.stacked: bottom += height # TODO: See if we need to draw a label here, too left += per_bar context.restore()
class LineGraph(object): def __init__(self, series_set, scale, style=None, vertical_scale=True, zero_base=True, smoothed=True, bottom_scale=False, no_bottom_labels=False, vertical_label="", peak_highlight=None, two_passes=False): """ Constructor; creates a new LineGraph. @param mseries: The data to plot, as a MultiSeries @type mseries: graphication.series.MultiSeries @param style: The Style to apply to this graph @type style: graphication.style.Style @param scale: The Scale to use for the graph. @type scale: graphication.scales.BaseScale @param smoothed: If the graph is smoothed (not straight lines) @type smoothed: bool """ self.series_set = series_set self.style = default_css.merge(style) self.scale = scale self.zero_base = zero_base self.vertical_scale = vertical_scale self.smoothed = smoothed self.no_bottom_labels = no_bottom_labels self.vertical_label = vertical_label self.peak_highlight = peak_highlight self.two_passes = two_passes self.first_pass = False self.calc_rel_points() def calc_rel_points(self): """Calculates the relative shapes of the sections""" if isinstance(self.vertical_scale, BaseScale): self.y_scale = self.vertical_scale else: # Work out our extents y_min, y_max = self.series_set.value_range() if self.zero_base: y_min = 0 self.y_scale = VerticalWavegraphScale(y_min, y_max) def set_size(self, width, height): self.width = width self.height = height self.calc_plot_height() def get_vertical_scale(self): return self.y_scale def calc_plot_height(self): # Are we ignoring the bottom labels? if self.no_bottom_labels: self.plot_height = self.height return major_style = self.style['linegraph grid.major'] minor_style = self.style['linegraph grid.minor'] # Work out the maxiumum label width max_height = 0 for linepos, title, ismajor in self.scale.get_lines(): if ismajor: this_style = major_style else: this_style = minor_style label_style = this_style.sub('label') width, height = text_bounds( title, label_style.get_float("font-size"), label_style.get_font(), label_style.get_cairo_font_style(), label_style.get_cairo_font_weight(), ) padding = label_style.get_float("padding") max_height = max(max_height, height + padding) self.plot_height = self.height - max_height def render(self, context, debug=False): if self.two_passes: self.first_pass = True self._render(context) self.first_pass = False self._render(context) def _render(self, context, debug=False): context.save() fascent, fdescent, fheight, fxadvance, fyadvance = context.font_extents( ) # Render the labels and lines if not self.first_pass: major_style = self.style['linegraph grid#x.major'] minor_style = self.style['linegraph grid#x.minor'] for linepos, title, ismajor in self.scale.get_lines(): if ismajor: this_style = major_style else: this_style = minor_style line_style = this_style.sub('line') label_style = this_style.sub('label') context.select_font_face( label_style.get_font(), label_style.get_cairo_font_style(), label_style.get_cairo_font_weight(), ) context.set_font_size(label_style.get_float("font-size")) x = linepos * self.width if title: x_bearing, y_bearing, width, height = context.text_extents( title)[:4] padding = label_style.get_float("padding") align = label_style.get_align("text-align") context.move_to( x - (align * width), self.plot_height + padding + fheight / 2.0 - fdescent) context.set_source_rgba(*label_style.get_color("color")) if not self.no_bottom_labels: context.show_text(title) context.fill() context.set_line_width(line_style.get_float("width", 1)) context.set_source_rgba(*line_style.get_color("color", "#aaa")) context.move_to(x, 0) context.line_to( x, self.plot_height + line_style.get_float("padding", 0)) context.stroke() # Draw the lines smooth = self.style['linegraph line'].get_float("smoothness", 0.5) y_size = self.style['linegraph'].get_align("height", 1) for series in self.series_set: # Get the line's points points = [ (self.scale.get_point(x) * self.width, (1 - (self.y_scale.get_point(y) * y_size)) * self.plot_height) for x, y in series.items() ] xs = [ self.scale.get_value(self.scale.get_point(x)) for x, y in series.items() ] # Get style infos line_style = self.style['linegraph line'] if series.line_width: context.set_line_width(series.line_width) else: context.set_line_width(line_style.get_float("width", 2)) context.set_source_rgba(*series.color_as_rgba()) # Finish off a line stylishly def stroke(nx=None, ox=None): curve = context.copy_path() # If there's a peak highlight line, we need to draw it if self.peak_highlight: context.save() peak_height, colour = self.peak_highlight context.set_source_rgba(*hex_to_rgba(colour)) # Now, use the curve as a mask for that context.line_to(nx, self.plot_height) context.line_to(ox, self.plot_height) context.clip() # Draw a rectangle at the right height for highlight bottom = (1 - self.y_scale.get_point(peak_height) ) * self.plot_height context.rectangle(0, 0, self.width, bottom) context.fill() context.restore() context.append_path(curve) if prev_style == Series.STYLE_DASHED: context.set_source_rgba(*series.color_as_rgba()) context.set_dash([3, 2], 2) context.stroke() context.set_dash([], 0) elif prev_style == Series.STYLE_LIGHT: r, g, b, a = series.color_as_rgba() context.set_source_rgba(r, g, b, a * 0.5) elif prev_style == Series.STYLE_VLIGHT: r, g, b, a = series.color_as_rgba() context.set_source_rgba(r, g, b, a * 0.4) elif prev_style in [ Series.STYLE_LINETOP, Series.STYLE_DOUBLEFILL, Series.STYLE_WHOLEFILL ]: r, g, b, a = series.color_as_rgba() if self.two_passes and self.first_pass: # Now fill in under the curve context.line_to(nx, self.plot_height) context.line_to(ox, self.plot_height) if series.fill_color: context.set_source_rgba( *series.fill_color_as_rgba()) else: context.set_source_rgba(r, g, b, a * 0.5) context.fill() if prev_style == Series.STYLE_DOUBLEFILL: # Now fill in over the curve context.append_path(curve) context.line_to(nx, 0) context.line_to(ox, 0) if series.fill_color: r, g, b, a = series.fill_color_as_rgba() context.set_source_rgba(r, g, b, a * 0.4) else: context.set_source_rgba(r, g, b, a * 0.20) context.fill() elif prev_style == Series.STYLE_WHOLEFILL: # Now fill in over the curve context.append_path(curve) context.line_to(nx, 0) context.line_to(ox, 0) if series.fill_color: r, g, b, a = series.fill_color_as_rgba() context.set_source_rgba(r, g, b, a) else: context.set_source_rgba(r, g, b, a * 0.5) context.fill() context.append_path(curve) context.set_source_rgba(*series.color_as_rgba()) else: context.set_source_rgba(*series.color_as_rgba()) if self.first_pass: context.new_path() return context.stroke() # Draw the line prev_style = series.style_at(0) last_change = 0 context.move_to(*points[0]) for j in range(1, len(points)): # Get the drawstyle for this coord draw_style = series.style_at(xs[j]) ox, oy = points[j - 1] nx, ny = points[j] dx = (nx - ox) * smooth if self.smoothed: context.curve_to(ox + dx, oy, nx - dx, ny, nx, ny) else: context.line_to(nx, ny) # If we have a new draw style, we need to end this segment and begin another if draw_style != prev_style: stroke(nx, last_change) prev_style = draw_style context.move_to(*points[j]) last_change = nx # Now close the line overall stroke(nx, last_change) context.restore()
class LineGraph(object): def __init__(self, series_set, scale, style=None, vertical_scale=True, zero_base=True, smoothed=True, bottom_scale=False, no_bottom_labels=False, vertical_label="", peak_highlight=None, two_passes=False): """ Constructor; creates a new LineGraph. @param mseries: The data to plot, as a MultiSeries @type mseries: graphication.series.MultiSeries @param style: The Style to apply to this graph @type style: graphication.style.Style @param scale: The Scale to use for the graph. @type scale: graphication.scales.BaseScale @param smoothed: If the graph is smoothed (not straight lines) @type smoothed: bool """ self.series_set = series_set self.style = default_css.merge(style) self.scale = scale self.zero_base = zero_base self.vertical_scale = vertical_scale self.smoothed = smoothed self.no_bottom_labels = no_bottom_labels self.vertical_label = vertical_label self.peak_highlight = peak_highlight self.two_passes = two_passes self.first_pass = False self.calc_rel_points() def calc_rel_points(self): """Calculates the relative shapes of the sections""" if isinstance(self.vertical_scale, BaseScale): self.y_scale = self.vertical_scale else: # Work out our extents y_min, y_max = self.series_set.value_range() if self.zero_base: y_min = 0 self.y_scale = VerticalWavegraphScale(y_min, y_max) def set_size(self, width, height): self.width = width self.height = height self.calc_plot_height() def get_vertical_scale(self): return self.y_scale def calc_plot_height(self): # Are we ignoring the bottom labels? if self.no_bottom_labels: self.plot_height = self.height return major_style = self.style['linegraph grid.major'] minor_style = self.style['linegraph grid.minor'] # Work out the maxiumum label width max_height = 0 for linepos, title, ismajor in self.scale.get_lines(): if ismajor: this_style = major_style else: this_style = minor_style label_style = this_style.sub('label') width, height = text_bounds( title, label_style.get_float("font-size"), label_style.get_font(), label_style.get_cairo_font_style(), label_style.get_cairo_font_weight(), ) padding = label_style.get_float("padding") max_height = max(max_height, height + padding) self.plot_height = self.height - max_height def render(self, context, debug=False): if self.two_passes: self.first_pass = True self._render(context) self.first_pass = False self._render(context) def _render(self, context, debug=False): context.save() fascent, fdescent, fheight, fxadvance, fyadvance = context.font_extents() # Render the labels and lines if not self.first_pass: major_style = self.style['linegraph grid#x.major'] minor_style = self.style['linegraph grid#x.minor'] for linepos, title, ismajor in self.scale.get_lines(): if ismajor: this_style = major_style else: this_style = minor_style line_style = this_style.sub('line') label_style = this_style.sub('label') context.select_font_face( label_style.get_font(), label_style.get_cairo_font_style(), label_style.get_cairo_font_weight(), ) context.set_font_size( label_style.get_float("font-size") ) x = linepos * self.width if title: x_bearing, y_bearing, width, height = context.text_extents(title)[:4] padding = label_style.get_float("padding") align = label_style.get_align("text-align") context.move_to(x - (align * width), self.plot_height + padding + fheight / 2.0 - fdescent) context.set_source_rgba(*label_style.get_color("color")) if not self.no_bottom_labels: context.show_text(title) context.fill() context.set_line_width(line_style.get_float("width", 1)) context.set_source_rgba(*line_style.get_color("color", "#aaa")) context.move_to(x, 0) context.line_to(x, self.plot_height + line_style.get_float("padding", 0)) context.stroke() # Draw the lines smooth = self.style['linegraph line'].get_float("smoothness", 0.5) y_size = self.style['linegraph'].get_align("height", 1) # Make sure we clip to the region so we can't draw over the edge. context.rectangle(0, 0, self.width, self.plot_height) context.clip() for series in self.series_set: # Get the line's points points = [(self.scale.get_point(x)*self.width, (1-(self.y_scale.get_point(y)*y_size))*self.plot_height) for x, y in series.items()] xs = [self.scale.get_value(self.scale.get_point(x)) for x,y in series.items()] # Get style infos line_style = self.style['linegraph line'] if series.line_width: context.set_line_width(series.line_width) else: context.set_line_width(line_style.get_float("width", 2)) context.set_source_rgba(*series.color_as_rgba()) # Finish off a line stylishly def stroke(nx=None, ox=None): curve = context.copy_path() # If there's a peak highlight line, we need to draw it if self.peak_highlight: context.save() peak_height, colour = self.peak_highlight context.set_source_rgba(*hex_to_rgba(colour)) # Now, use the curve as a mask for that context.line_to(nx, self.plot_height) context.line_to(ox, self.plot_height) context.clip() # Draw a rectangle at the right height for highlight bottom = (1 - self.y_scale.get_point(peak_height)) * self.plot_height context.rectangle(0, 0, self.width, bottom) context.fill() context.restore() context.append_path(curve) if prev_style == Series.STYLE_DASHED: context.set_source_rgba(*series.color_as_rgba()) context.set_dash([3, 2], 2) context.stroke() context.set_dash([], 0) elif prev_style == Series.STYLE_LIGHT: r,g,b,a = series.color_as_rgba() context.set_source_rgba(r,g,b,a*0.5) elif prev_style == Series.STYLE_VLIGHT: r,g,b,a = series.color_as_rgba() context.set_source_rgba(r,g,b,a*0.4) elif prev_style in [Series.STYLE_LINETOP, Series.STYLE_DOUBLEFILL, Series.STYLE_WHOLEFILL]: r,g,b,a = series.color_as_rgba() if self.two_passes and self.first_pass: # Now fill in under the curve context.line_to(nx, self.plot_height) context.line_to(ox, self.plot_height) if series.fill_color: context.set_source_rgba(*series.fill_color_as_rgba()) else: context.set_source_rgba(r,g,b,a*0.5) context.fill() if prev_style == Series.STYLE_DOUBLEFILL: # Now fill in over the curve context.append_path(curve) context.line_to(nx, 0) context.line_to(ox, 0) if series.fill_color: r,g,b,a = series.fill_color_as_rgba() context.set_source_rgba(r,g,b,a*0.4) else: context.set_source_rgba(r,g,b,a*0.20) context.fill() elif prev_style == Series.STYLE_WHOLEFILL: # Now fill in over the curve context.append_path(curve) context.line_to(nx, 0) context.line_to(ox, 0) if series.fill_color: r,g,b,a = series.fill_color_as_rgba() context.set_source_rgba(r,g,b,a) else: context.set_source_rgba(r,g,b,a*0.5) context.fill() context.append_path(curve) context.set_source_rgba(*series.color_as_rgba()) else: context.set_source_rgba(*series.color_as_rgba()) if self.first_pass: context.new_path() return context.stroke() # Draw the line if points: prev_style = series.style_at(0) last_change = 0 context.move_to(*points[0]) for j in range(1, len(points)): # Get the drawstyle for this coord draw_style = series.style_at(xs[j]) ox, oy = points[j-1] nx, ny = points[j] dx = (nx - ox) * smooth if self.smoothed: context.curve_to(ox+dx, oy, nx-dx, ny, nx, ny) else: context.line_to(nx, ny) # If we have a new draw style, we need to end this segment and begin another if draw_style != prev_style: stroke(nx, last_change) prev_style = draw_style context.move_to(*points[j]) last_change = nx # Now close the line overall stroke(nx, last_change) context.reset_clip() context.restore()
class WaveGraph(object): def __init__(self, series_set, scale, style=None, label_curves=True, vertical_scale=False, debug=False, textfix=False): """ Constructor; creates a new WaveGraph. @param mseries: The data to plot, as a MultiSeries @type mseries: graphication.series.MultiSeries @param style: The Style to apply to this graph @type style: graphication.style.Style @param scale: The Scale to use for the graph. @type scale: graphication.scales.BaseScale @param label_curves: If the curves should have labels written directly on top of them, fitted into their shape. Note: Takes a while to render. @type label_curves: bool @param vertical_axis: If a vertical scale should be drawn on the graph @type vertical_axis: bool """ self.series_set = series_set self.style = default_css.merge(style) self.scale = scale self.debug = debug self.label_curves = label_curves self.vertical_scale = vertical_scale self.textfix = textfix self.calc_rel_points() def calc_rel_points(self): """Calculates the relative shapes of the sections""" # Get the style stuff y_offset = self.style['wavegraph'].get_align("vertical-align", 0.5) y_size = self.style['wavegraph'].get_align("height", 0.9) # Work out our extents y_total = max([total for (key, total) in self.series_set.totals()]) self.y_scale = VerticalWavegraphScale(0, y_total) # Calculate the points cols = [] self.xs = [] for x, stack in self.series_set.stacks(): self.xs.append(self.scale.get_point(x)) # Collect the points ys = [] total = 0 for ser, val in stack: y = self.y_scale.get_point(val) * y_size ys.append(total) total += y ys.append(total) shift = 1 - total # Shift them down to center them ys = map(lambda a: a + (shift * y_offset), ys) cols.append(ys) self.rows = zip(*cols) def set_size(self, width, height): self.width = width self.height = height self.calc_plot_height() self.points = [[(x*self.width, y*self.plot_height) for x, y in zip(self.xs, ys)] for ys in self.rows] if self.label_curves: self.calc_text_positions() def interpolate(self, points, accuracy): """ Adds extra points into a list of points, interpolated linearly. @param points: The points to interpolate @type points: list (of 2-tuples) @param accuracy: How many extra points to create between each pair. @type accuracy: int @rtype: list """ newpoints = [] fractions = [x/float(accuracy+1) for x in range(1, accuracy+1)] for i in range(len(points)-1): oldx, oldy = points[i] #oldx = self.xs[i] newx, newy = points[i+1] #newx = self.xs[i+1] newpoints.append((oldx, oldy)) for fraction in fractions: newpoints.append((oldx+(fraction*(newx-oldx)), oldy+(fraction*(newy-oldy)))) newpoints.append((newx, newy)) return newpoints def rect_union(self, (rect1, rect2)): a1, b1, a2, b2 = rect1 x1, y1, x2, y2 = rect2 left = min(a1, x1) right = max(a2, x2) top = max(b1, y1) if (b1 > y2) or (y1 > b2): bottom = top else: bottom = min(b2, y2) return (left, top, right, bottom)
class BarChart(Graph): def __init__(self, series_set, scale, style=None, vertical_scale=True, stacked=True, zero_base=True): """ Constructor; creates a new LineGraph. @param mseries: The data to plot, as a MultiSeries @type mseries: graphication.series.MultiSeries @param style: The Style to apply to this graph @type style: graphication.style.Style @param scale: The Scale to use for the graph. @type scale: graphication.scales.BaseScale @param smoothed: If the graph is smoothed (not straight lines) @type smoothed: bool """ self.series_set = series_set self.style = default_css.merge(style) self.scale = scale self.stacked = stacked self.zero_base = zero_base self.vertical_scale = vertical_scale # Initialise the vertical scale if stacked: y_min, y_max = self.stacked_value_range() else: y_min, y_max = self.series_set.value_range() if self.zero_base: y_min = 0 self.y_scale = VerticalWavegraphScale(y_min, y_max) def stacked_value_range(self): y_min, y_max = sys.maxint, 0 print self.series_set, self.series_set.series for series in self.series_set: s_min, s_max = series.value_range() y_min = min(s_min, y_min) y_max += s_max if not self.zero_base: y_max -= y_min print y_min, y_max return y_min, y_max def calc_plot_size(self): self.plot_height = self.height - self.calc_label_dimension( self.scale, is_height = True, major_selector = "barchart grid#x.major", minor_selector = "barchart grid#x.minor", ) self.plot_width = self.width self.plot_left = 0 self.plot_top = 0 def render(self, context, debug=False): context.save() ### Draw the bars # Get width per bar block keys = self.series_set.keys() per_bar = self.plot_width / len(keys) # Some more drawing parameters zero_line = self.plot_top + self.plot_height bar_style = self.style["barchart bar"] bar_padding = bar_style.get_float("padding") bar_padding_top = bar_style.get_float("padding-top") border_width = bar_style.get_float("border-width") border_color = bar_style.get_color("border-color") # Draw the bars at each location left = self.plot_left for key in keys: stack = self.series_set.stack(key) bottom = zero_line inner_left = 0 for series, value in stack: height = 0 - self.y_scale.get_point(value) * self.plot_height if self.stacked: x, y, w, h = ( left + bar_padding, bottom, per_bar - bar_padding*2, height + bar_padding_top, ) else: x, y, w, h = ( left + inner_left + bar_padding, bottom, per_bar / len(stack) - bar_padding*2, height + bar_padding_top, ) inner_left += per_bar / len(stack) context.rectangle(x, y, w, h) context.set_source_rgba(*series.color_as_rgba()) context.fill() # Draw outer border if needed if border_width: context.rectangle( x + 0.5, y - 0.5, w - 1, h + 1, ) context.set_source_rgba(*border_color) context.stroke() if self.stacked: bottom += height # TODO: See if we need to draw a label here, too left += per_bar context.restore()
class WaveGraph(object): def __init__(self, series_set, scale, style=None, label_curves=True, vertical_scale=False, debug=False, textfix=False): """ Constructor; creates a new WaveGraph. @param mseries: The data to plot, as a MultiSeries @type mseries: graphication.series.MultiSeries @param style: The Style to apply to this graph @type style: graphication.style.Style @param scale: The Scale to use for the graph. @type scale: graphication.scales.BaseScale @param label_curves: If the curves should have labels written directly on top of them, fitted into their shape. Note: Takes a while to render. @type label_curves: bool @param vertical_axis: If a vertical scale should be drawn on the graph @type vertical_axis: bool """ self.series_set = series_set self.style = default_css.merge(style) self.scale = scale self.debug = debug self.label_curves = label_curves self.vertical_scale = vertical_scale self.textfix = textfix self.calc_rel_points() def calc_rel_points(self): """Calculates the relative shapes of the sections""" # Get the style stuff y_offset = self.style['wavegraph'].get_align("vertical-align", 0.5) y_size = self.style['wavegraph'].get_align("height", 0.9) # Work out our extents y_total = max([total for (key, total) in self.series_set.totals()]) self.y_scale = VerticalWavegraphScale(0, y_total) # Calculate the points cols = [] self.xs = [] for x, stack in self.series_set.stacks(): self.xs.append(self.scale.get_point(x)) # Collect the points ys = [] total = 0 for ser, val in stack: y = self.y_scale.get_point(val) * y_size ys.append(total) total += y ys.append(total) shift = 1 - total # Shift them down to center them ys = [a + (shift * y_offset) for a in ys] cols.append(ys) self.rows = list(zip(*cols)) def set_size(self, width, height): self.width = width self.height = height self.calc_plot_height() self.points = [[(x * self.width, y * self.plot_height) for x, y in zip(self.xs, ys)] for ys in self.rows] if self.label_curves: self.calc_text_positions() def interpolate(self, points, accuracy): """ Adds extra points into a list of points, interpolated linearly. @param points: The points to interpolate @type points: list (of 2-tuples) @param accuracy: How many extra points to create between each pair. @type accuracy: int @rtype: list """ newpoints = [] fractions = [x / float(accuracy + 1) for x in range(1, accuracy + 1)] for i in range(len(points) - 1): oldx, oldy = points[i] #oldx = self.xs[i] newx, newy = points[i + 1] #newx = self.xs[i+1] newpoints.append((oldx, oldy)) for fraction in fractions: newpoints.append((oldx + (fraction * (newx - oldx)), oldy + (fraction * (newy - oldy)))) newpoints.append((newx, newy)) return newpoints def rect_union(self, xxx_todo_changeme): (rect1, rect2) = xxx_todo_changeme a1, b1, a2, b2 = rect1 x1, y1, x2, y2 = rect2 left = min(a1, x1) right = max(a2, x2) top = max(b1, y1) if (b1 > y2) or (y1 > b2): bottom = top else: bottom = min(b2, y2) return (left, top, right, bottom) def get_text_ratio(self, text): from graphication.text import text_bounds w, h = text_bounds(text, 10, self.style['wavegraph curve label'].get_font()) if h and w: return (w / float(h)) else: return 1 def get_text_size(self, ratio, xxx_todo_changeme1): (x1, y1, x2, y2) = xxx_todo_changeme1 width = abs(x2 - x1) height = abs(y2 - y1) if (width == 0) or (height == 0): return 0 this_ratio = width / float(height) if this_ratio > ratio: return height else: return width / float(ratio) def calc_text_positions(self, accuracy=5, max_per_curve=20, spacing=200): """ Calculates the positions of the text. """ self.labels = [] # For each series... for i in range(len(self.series_set)): # Get the series, and the label's width/height ratio series = self.series_set.get_series(i) ratio = self.get_text_ratio(series.title) # Get the curve points, and interpolate along them tops = self.interpolate(self.points[i], accuracy) bottoms = self.interpolate(self.points[i + 1], accuracy) # Work out bounding boxes for these points boxes = [] for j in range(len(tops)): tl = tops[j] #tr = tops[j+1] bl = bottoms[j] #br = bottoms[j+1] #boxes.append((tl[0], max(tl[1], tr[1]), br[0], min(bl[1], br[1]))) boxes.append((tl[0], tl[1], bl[0], bl[1])) # Go through and union them to collect a set of bigger boxes bigboxes = [boxes] for j in range(accuracy * 2): bigboxes.append( list(map(self.rect_union, off_zip(bigboxes[-1])))) if i == 1 and self.debug: self.labels.extend([((0, b), "") for b in bigboxes[1]]) # Reduce that into a single list, rather than a list of lists bigboxes = reduce(lambda a, b: a + b, bigboxes) # Associate each box with its appropriate text size, then sort bigboxes = [(self.get_text_size(ratio, rect), rect) for rect in bigboxes] bigboxes.sort() bigboxes.reverse() # Choose boxes in order of decending size, so they don't overlap taken = [] for box in bigboxes: # If we have enough boxes, break if len(taken) >= max_per_curve: break # Check this isn't too close text_size, (left, top, right, bottom) = box try: for taken_left, taken_right in taken: if abs(left - taken_left) < spacing: raise TooClose elif abs(right - taken_right) < spacing: raise TooClose elif abs(right - taken_left) < spacing: raise TooClose elif abs(left - taken_right) < spacing: raise TooClose except TooClose: continue # OK, use this one taken.append((left, right)) self.labels.append((box, series.title)) def calc_plot_height(self): major_style = self.style['wavegraph grid#x.major'] minor_style = self.style['wavegraph grid#x.minor'] # Work out the maxiumum label width max_height = 0 for linepos, title, ismajor in self.scale.get_lines(): if ismajor: this_style = major_style else: this_style = minor_style label_style = this_style.sub('label') width, height = text_bounds( title, label_style.get_float("font-size"), label_style.get_font(), label_style.get_cairo_font_style(), label_style.get_cairo_font_weight(), ) padding = label_style.get_float("padding") max_height = max(max_height, height + padding) self.plot_height = self.height - max_height def render_debug(self, context): """Renders the calculation rectangles""" context.save() for (size, (x1, y1, x2, y2)), title in self.labels: if size: context.set_source_rgba(*hex_to_rgba("#f006")) context.set_line_width(1) else: context.set_source_rgba(*hex_to_rgba("#0f06")) context.set_line_width(0.5) context.rectangle(x1, y1, (x2 - x1), (y2 - y1)) context.stroke() context.restore() def render(self, context, debug=False): debug = debug or self.debug context.save() # Draw the vertical scale, if necessary if self.vertical_scale: major_style = self.style['wavegraph grid#y.major'] minor_style = self.style['wavegraph grid#y.minor'] for linepos, title, ismajor in self.y_scale.get_lines(): if ismajor: this_style = major_style else: this_style = minor_style line_style = this_style.sub('line') label_style = this_style.sub('label') context.select_font_face( label_style.get_font(), label_style.get_cairo_font_style(), label_style.get_cairo_font_weight(), ) context.set_font_size(label_style.get_float("font-size")) y = linepos * self.plot_height fascent, fdescent, fheight, fxadvance, fyadvance = context.font_extents( ) x_bearing, y_bearing, width, height = context.text_extents( title)[:4] padding = label_style.get_float("padding") align = label_style.get_align("text-align") context.move_to(0 - padding - (align * width), y + fheight / 2.0 - fdescent) context.set_source_rgba(*label_style.get_color("color")) context.show_text(title) context.set_line_width(line_style.get_float("width", 1)) context.set_source_rgba(*line_style.get_color("color", "#aaa")) context.move_to(0 - line_style.get_float("padding", 0), y) context.line_to(self.width, y) context.stroke() # Render the labels and lines major_style = self.style['wavegraph grid#x.major'] minor_style = self.style['wavegraph grid#x.minor'] for linepos, title, ismajor in self.scale.get_lines(): if ismajor: this_style = major_style else: this_style = minor_style line_style = this_style.sub('line') label_style = this_style.sub('label') context.select_font_face( label_style.get_font(), label_style.get_cairo_font_style(), label_style.get_cairo_font_weight(), ) context.set_font_size(label_style.get_float("font-size")) x = linepos * self.width if title: fascent, fdescent, fheight, fxadvance, fyadvance = context.font_extents( ) x_bearing, y_bearing, width, height = context.text_extents( title)[:4] padding = label_style.get_float("padding") align = label_style.get_align("text-align") context.move_to( x - (align * width), self.plot_height + padding + fheight / 2.0 - fdescent) context.set_source_rgba(*label_style.get_color("color")) if self.textfix: context.text_path(title) else: context.show_text(title) context.fill() context.set_line_width(line_style.get_float("width", 1)) context.set_source_rgba(*line_style.get_color("color", "#aaa")) context.move_to(x, 0) context.line_to( x, self.plot_height + line_style.get_float("padding", 0)) context.stroke() # Draw the strips smooth = self.style['wavegraph curve'].get_float("smoothness") i = -1 for series in self.series_set: i += 1 # Get the two lists of points tops = self.points[i] bottoms = self.points[i + 1] prev_style = series.style_at(0) context.move_to(*tops[0]) bottom_stack = [bottoms[0]] def close_path(): if prev_style == Series.STYLE_LINETOP: context.set_source_rgba(*series.color_as_rgba()) context.set_line_width(2) context.stroke_preserve() ppoint = bottom_stack.pop() context.line_to(*ppoint) while bottom_stack: npoint = bottom_stack.pop() context.curve_to(ppoint[0] - dx, ppoint[1], npoint[0] + dx, npoint[1], npoint[0], npoint[1]) ppoint = npoint context.close_path() if prev_style == Series.STYLE_DASHED: r, g, b, a = series.color_as_rgba() import cairo linear = cairo.LinearGradient(0, 0, self.width, self.height) for i in range(0, self.width, 5): dt = 1.5 / float(self.width) mid = i / float(self.width) linear.add_color_stop_rgba(mid - dt, r, g, b, a * 0.34) linear.add_color_stop_rgba(mid - (dt - 0.001), r, g, b, a * 0.8) linear.add_color_stop_rgba(mid + (dt - 0.001), r, g, b, a * 0.8) linear.add_color_stop_rgba(mid + dt, r, g, b, a * 0.34) context.set_source(linear) elif prev_style == Series.STYLE_LIGHT: r, g, b, a = series.color_as_rgba() context.set_source_rgba(r, g, b, a * 0.5) elif prev_style == Series.STYLE_VLIGHT: r, g, b, a = series.color_as_rgba() context.set_source_rgba(r, g, b, a * 0.4) elif prev_style == Series.STYLE_LINETOP: r, g, b, a = series.color_as_rgba() context.set_source_rgba(r, g, b, a * 0.2) else: context.set_source_rgba(*series.color_as_rgba()) context.fill() # Draw the tops for j in range(1, len(tops)): # Get the drawstyle for this coord draw_style = series.style_at(self.scale.get_value(self.xs[j])) ox, oy = tops[j - 1] nx, ny = tops[j] dx = (nx - ox) * smooth context.curve_to(ox + dx, oy, nx - dx, ny, nx, ny) bottom_stack.append(bottoms[j]) # If we have a new draw style, we need to end this segment and begin another if prev_style and draw_style != prev_style: close_path() prev_style = draw_style context.move_to(*tops[j]) bottom_stack.append(bottoms[j]) # Now close the line overall close_path() # Draw the on-curve labels if self.label_curves: label_style = self.style['wavegraph curve label'] dimming_top = label_style.get_float('dimming-top', 1) dimming_bottom = label_style.get_float('dimming-bottom', 0) r, g, b, a = label_style.get_color('color', '#fff') context.select_font_face( label_style.get_font(), label_style.get_cairo_font_style(), label_style.get_cairo_font_weight(), ) # Draw the labels for (size, (x1, y1, x2, y2)), title in self.labels: if size > 0: # Set the colour, including dimming if size >= dimming_top: dim = 1 elif size < dimming_bottom: dim = 0 else: dim = (size - dimming_bottom) / float(dimming_top - dimming_bottom) context.set_source_rgba(r, g, b, a * dim) # Position outselves context.set_font_size(size * 0.9) x_bearing, y_bearing, width, height = context.text_extents( title)[:4] context.move_to(((x2 + x1) / 2.0) - width / 2 - x_bearing, ((y2 + y1) / 2.0) - height / 2 - y_bearing) # Draw the text. We might use text_path because it looks prettier # (on image surfaces, show_text coerces font paths to fit inside pixels) if self.textfix: context.text_path(title) else: context.show_text(title) context.fill() # If uncommented, will show the used text boxes # self.render_debug(context) # Do we need labels on the bottom? # if self.x_labels_major: # context.save() #context.translate(0, self.cheight) # self.x_labels_major.render(context, # color=self.style['wavegraph:label_color'], # size=self.style['wavegraph:label_size'], # font=self.style['wavegraph:label_font'], # ) # context.restore() # Render the curve labels if needed # if self.curve_labels: # if self.style['wavegraph:debug']: # self.curve_labels.render_debug(context) # self.curve_labels.render(context, # color=self.style['wavegraph:curve_label_color'], # font=self.style['wavegraph:curve_label_font'], # weight=self.style['wavegraph:curve_label_font_weight'], # dimming_top=self.style['wavegraph:dimming_top'], # dimming_bottom=self.style['wavegraph:dimming_bottom'], # ) if debug: self.render_debug(context) context.restore()
class WaveGraph(object): def __init__(self, series_set, scale, style=None, label_curves=True, vertical_scale=False, debug=False, textfix=False): """ Constructor; creates a new WaveGraph. @param mseries: The data to plot, as a MultiSeries @type mseries: graphication.series.MultiSeries @param style: The Style to apply to this graph @type style: graphication.style.Style @param scale: The Scale to use for the graph. @type scale: graphication.scales.BaseScale @param label_curves: If the curves should have labels written directly on top of them, fitted into their shape. Note: Takes a while to render. @type label_curves: bool @param vertical_axis: If a vertical scale should be drawn on the graph @type vertical_axis: bool """ self.series_set = series_set self.style = default_css.merge(style) self.scale = scale self.debug = debug self.label_curves = label_curves self.vertical_scale = vertical_scale self.textfix = textfix self.calc_rel_points() def calc_rel_points(self): """Calculates the relative shapes of the sections""" # Get the style stuff y_offset = self.style['wavegraph'].get_align("vertical-align", 0.5) y_size = self.style['wavegraph'].get_align("height", 0.9) # Work out our extents y_total = max([total for (key, total) in self.series_set.totals()]) self.y_scale = VerticalWavegraphScale(0, y_total) # Calculate the points cols = [] self.xs = [] for x, stack in self.series_set.stacks(): self.xs.append(self.scale.get_point(x)) # Collect the points ys = [] total = 0 for ser, val in stack: y = self.y_scale.get_point(val) * y_size ys.append(total) total += y ys.append(total) shift = 1 - total # Shift them down to center them ys = map(lambda a: a + (shift * y_offset), ys) cols.append(ys) self.rows = zip(*cols) def set_size(self, width, height): self.width = width self.height = height self.calc_plot_height() self.points = [[(x * self.width, y * self.plot_height) for x, y in zip(self.xs, ys)] for ys in self.rows] if self.label_curves: self.calc_text_positions() def interpolate(self, points, accuracy): """ Adds extra points into a list of points, interpolated linearly. @param points: The points to interpolate @type points: list (of 2-tuples) @param accuracy: How many extra points to create between each pair. @type accuracy: int @rtype: list """ newpoints = [] fractions = [x / float(accuracy + 1) for x in range(1, accuracy + 1)] for i in range(len(points) - 1): oldx, oldy = points[i] #oldx = self.xs[i] newx, newy = points[i + 1] #newx = self.xs[i+1] newpoints.append((oldx, oldy)) for fraction in fractions: newpoints.append((oldx + (fraction * (newx - oldx)), oldy + (fraction * (newy - oldy)))) newpoints.append((newx, newy)) return newpoints def rect_union(self, (rect1, rect2)): a1, b1, a2, b2 = rect1 x1, y1, x2, y2 = rect2 left = min(a1, x1) right = max(a2, x2) top = max(b1, y1) if (b1 > y2) or (y1 > b2): bottom = top else: bottom = min(b2, y2) return (left, top, right, bottom)