def __init__(self, config, series, secondary_series, uuid, xml_filters): """Init the graph""" self.uuid = uuid self.__dict__.update(config.to_dict()) self.config = config self.series = series or [] self.secondary_series = secondary_series or [] self.xml_filters = xml_filters or [] self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self._x_labels = None self._y_labels = None self._x_2nd_labels = None self._y_2nd_labels = None self.nodes = {} self.margin = Margin(*([self.margin] * 4)) self._box = Box() self.view = None if self.logarithmic and self.zero == 0: # Explicit min to avoid interpolation dependency if self._dual: get = lambda x: x[1] or 1 else: get = lambda x: x positive_values = list(filter( lambda x: x > 0, [get(val) for serie in self.series for val in serie.safe_values])) self.zero = min(positive_values or (1,)) or 1 self._draw() self.svg.pre_render()
def __init__(self, config, series, secondary_series, uuid): """Init the graph""" self.uuid = uuid self.config = config self.series = series or [] self.secondary_series = secondary_series or [] self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self._x_labels = None self._y_labels = None self._x_2nd_labels = None self._y_2nd_labels = None self.nodes = {} self.margin = Margin(*([self.margin] * 4)) self._box = Box() self.view = None if self.logarithmic and self.zero == 0: # Explicit min to avoid interpolation dependency from pygal.graph.xy import XY if isinstance(self, XY): get = lambda x: x[1] else: get = lambda x: x positive_values = list( filter(lambda x: x > 0, [ get(val) for serie in self.series for val in serie.safe_values ])) self.zero = min(positive_values) if positive_values else 0 self._draw() self.svg.pre_render()
def __init__(self, config, series, secondary_series, uuid, xml_filters): """Init the graph""" self.uuid = uuid self.__dict__.update(config.to_dict()) self.config = config self.series = series or [] self.secondary_series = secondary_series or [] self.xml_filters = xml_filters or [] self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self._x_labels = None self._y_labels = None self._x_2nd_labels = None self._y_2nd_labels = None self.nodes = {} self.margin = Margin(*([self.margin] * 4)) self._box = Box() self.view = None if self.logarithmic and self.zero == 0: # Explicit min to avoid interpolation dependency if self._dual: get = lambda x: x[1] or 1 else: get = lambda x: x positive_values = list(filter( lambda x: x > 0, [get(val) for serie in self.series for val in serie.safe_values])) self.zero = min(positive_values or 1,) or 1 self._draw() self.svg.pre_render()
def __init__(self, config, series, secondary_series): """Init the graph""" self.config = config self.series = series or [] self.secondary_series = secondary_series or [] self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self._x_labels = None self._y_labels = None self._x_2nd_labels = None self._y_2nd_labels = None self.nodes = {} self.margin = Margin(*([20] * 4)) self._box = Box() self.view = None if self.logarithmic and self.zero == 0: # Explicit min to avoid interpolation dependency from pygal.graph.xy import XY if isinstance(self, XY): get = lambda x: x[1] else: get = lambda x: x positive_values = list(filter( lambda x: x > 0, [get(val) for serie in self.series for val in serie.safe_values])) self.zero = min(positive_values) if positive_values else 0 self._draw() self.svg.pre_render()
def setup(self, **kwargs): """Set up the transient state prior rendering""" # Keep labels in case of map if getattr(self, 'x_labels', None) is not None: self.x_labels = list(self.x_labels) if getattr(self, 'y_labels', None) is not None: self.y_labels = list(self.y_labels) self.state = State(self, **kwargs) if isinstance(self.style, type): self.style = self.style() self.series = self.prepare_values([ rs for rs in self.raw_series if not rs[1].get('secondary') ]) or [] self.secondary_series = self.prepare_values([ rs for rs in self.raw_series if rs[1].get('secondary') ], len(self.series)) or [] self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self._x_labels = None self._y_labels = None self._x_2nd_labels = None self._y_2nd_labels = None self.nodes = {} self.margin_box = Margin( self.margin_top or self.margin, self.margin_right or self.margin, self.margin_bottom or self.margin, self.margin_left or self.margin ) self._box = Box() self.view = None if self.logarithmic and self.zero == 0: # Explicit min to avoid interpolation dependency positive_values = list( filter( lambda x: x > 0, [ val[1] or 1 if self._dual else val for serie in self.series for val in serie.safe_values ] ) ) self.zero = min(positive_values or (1, )) or 1 if self._len < 3: self.interpolate = None self._draw() self.svg.pre_render()
def __init__(self, config, series): """Init the graph""" self.config = config self.series = series self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self._x_labels = None self._y_labels = None self.nodes = {} self.margin = Margin(*([20] * 4)) self._box = Box() self.view = None if self.series and self._has_data(): self._draw() else: self.svg.draw_no_data() self.svg.pre_render()
def __init__(self, config, series): """Init the graph""" self.config = config self.series = series or [] self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self._x_labels = None self._y_labels = None self.nodes = {} self.margin = Margin(*([20] * 4)) self._box = Box() self.view = None if self.logarithmic and self.zero == 0: # Explicit min to avoid interpolation dependency self.zero = min( filter(lambda x: x > 0, [ val for serie in self.series for val in serie.safe_values ])) self._draw() self.svg.pre_render()
def __init__(self, config=None, **kwargs): """Init the graph""" self.config = config or Config() self.config(**kwargs) self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self.series = [] self._x_labels = None self._y_labels = None self._box = None self.nodes = {} self.margin = None self.view = None
def __init__(self, config, series): """Init the graph""" self.config = config self.series = series self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self._x_labels = None self._y_labels = None self.nodes = {} self.margin = Margin(*([20] * 4)) self._box = Box() self.view = None if self.logarithmic and self.zero == 0: # Explicit min to avoid interpolation dependency self.zero = min(val for serie in self.series for val in serie.values) if self.series and self._has_data(): self._draw() else: self.svg.draw_no_data() self.svg.pre_render()
def __init__(self, config, series): """Init the graph""" self.config = config self.series = series or [] self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self._x_labels = None self._y_labels = None self.nodes = {} self.margin = Margin(*([20] * 4)) self._box = Box() self.view = None if self.logarithmic and self.zero == 0: # Explicit min to avoid interpolation dependency self.zero = min([x for x in [val for serie in self.series for val in serie.safe_values] if x > 0]) self._draw() self.svg.pre_render()
def setup(self, **kwargs): """Init the graph""" # Keep labels in case of map if getattr(self, 'x_labels', None) is not None: self.x_labels = list(map(to_unicode, self.x_labels)) if getattr(self, 'y_labels', None) is not None: self.y_labels = list(self.y_labels) self.state = State(self, **kwargs) self.series = self.prepare_values( self.raw_series) or [] self.secondary_series = self.prepare_values( self.raw_series2, len(self.series)) or [] self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self._x_labels = None self._y_labels = None self._x_2nd_labels = None self._y_2nd_labels = None self.nodes = {} self.margin_box = Margin( self.margin_top or self.margin, self.margin_right or self.margin, self.margin_bottom or self.margin, self.margin_left or self.margin) self._box = Box() self.view = None if self.logarithmic and self.zero == 0: # Explicit min to avoid interpolation dependency positive_values = list(filter( lambda x: x > 0, [val[1] or 1 if self._dual else val for serie in self.series for val in serie.safe_values])) self.zero = min(positive_values or (1,)) or 1 if self._len < 3: self.interpolate = None self._draw() self.svg.pre_render()
class BaseGraph(object): """Graphs commons""" __value__ = Value def __init__(self, config=None, **kwargs): """Init the graph""" self.config = config or Config() self.config(**kwargs) self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self.series = [] self._x_labels = None self._y_labels = None self._box = None self.nodes = {} self.margin = None self.view = None def add(self, title, values): """Add a serie to this graph""" self.series.append( Serie(title, values, len(self.series), self.__value__)) def reinit(self): """(Re-)Init the graph""" self.margin = Margin(*([20] * 4)) self._box = Box() if self.logarithmic and self.zero == 0: # If logarithmic, default zero to 1 self.zero = 1 def __getattr__(self, attr): """Search in config, then in self""" if attr in dir(self.config): return object.__getattribute__(self.config, attr) return object.__getattribute__(self, attr) @property def _format(self): """Return the value formatter for this graph""" return humanize if self.human_readable else str def _compute(self): """Initial computations to draw the graph""" def _plot(self): """Actual plotting of the graph""" def _compute_margin(self): """Compute graph margins from set texts""" if self.show_legend: h, w = get_texts_box( map(lambda x: truncate(x, self.truncate_legend or 15), cut(self.series, 'title')), self.legend_font_size) if self.legend_at_bottom: h_max = max(h, self.legend_box_size) self.margin.bottom += 10 + h_max * round( sqrt(len(self.series)) - 1) * 1.5 + h_max else: self.margin.right += 10 + w + self.legend_box_size if self.title: h, w = get_text_box(self.title, self.title_font_size) self.margin.top += 10 + h if self._x_labels: h, w = get_texts_box( cut(self._x_labels), self.label_font_size) self._x_labels_height = 10 + max( w * sin(rad(self.x_label_rotation)), h) self.margin.bottom += self._x_labels_height if self.x_label_rotation: self.margin.right = max( w * cos(rad(self.x_label_rotation)), self.margin.right) else: self._x_labels_height = 0 if self._y_labels: h, w = get_texts_box( cut(self._y_labels), self.label_font_size) self.margin.left += 10 + max( w * cos(rad(self.y_label_rotation)), h) @cached_property def _legends(self): """Getter for series title""" return [serie.title for serie in self.series] @cached_property def _values(self): """Getter for series values (flattened)""" return [val for serie in self.series for val in serie.values if val != None] @cached_property def _len(self): """Getter for the maximum series size""" return max([len(serie.values) for serie in self.series]) @cached_property def _min(self): """Getter for the minimum series value""" return (self.range and self.range[0]) or min(self._values) @cached_property def _max(self): """Getter for the maximum series value""" return (self.range and self.range[1]) or max(self._values) def _draw(self): """Draw all the things""" self._prepare_data() self._compute() self._compute_margin() self._decorate() self._plot() def _has_data(self): """Check if there is any data""" if len(self.series) == 0: return False if sum(map(len, map(lambda s: s.values, self.series))) == 0: return False return True def _prepare_data(self): """Remove aberrant values""" if self.logarithmic: for serie in self.series: for metadata in serie.metadata: if metadata.value <= 0: metadata.value = None def _uniformize_data(self): """Make all series to max len""" for serie in self.series: if len(serie.values) < self._len: diff = self._len - len(serie.values) serie.metadata += diff * [self.__value__(0)] for metadata in serie.metadata: if metadata.value == None: metadata.value = 0 def _render(self): """Make the graph internally""" self.reinit() self.svg.reinit() if self._has_data(): self._draw() self.svg.pre_render(False) else: self.svg.pre_render(True) def render(self, is_unicode=False): """Render the graph, and return the svg string""" self._render() return self.svg.render( is_unicode=is_unicode, pretty_print=self.pretty_print) def render_tree(self): """Render the graph, and return lxml tree""" self._render() return self.svg.root def render_pyquery(self): """Render the graph, and return a pyquery wrapped tree""" from pyquery import PyQuery as pq return pq(self.render_tree()) def render_in_browser(self): """Render the graph, open it in your browser with black magic""" from lxml.html import open_in_browser open_in_browser(self.render_tree(), encoding='utf-8') def render_response(self): """Render the graph, and return a Flask response""" from flask import Response return Response(self.render(), mimetype='image/svg+xml') def render_to_file(self, filename): """Render the graph, and write it to filename""" with io.open(filename, 'w', encoding='utf-8') as f: f.write(self.render(is_unicode=True)) def render_to_png(self, filename): """Render the graph, convert it to png and write it to filename""" import cairosvg from io import BytesIO fakefile = BytesIO() fakefile.write(self.render()) fakefile.seek(0) cairosvg.surface.PNGSurface.convert( file_obj=fakefile, write_to=filename)
class BaseGraph(object): """Graphs commons""" _adapters = [] def __init__(self, config, series): """Init the graph""" self.config = config self.series = series self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self._x_labels = None self._y_labels = None self.nodes = {} self.margin = Margin(*([20] * 4)) self._box = Box() self.view = None if self.series and self._has_data(): self._draw() else: self.svg.draw_no_data() self.svg.pre_render() def __getattr__(self, attr): """Search in config, then in self""" if attr in dir(self.config): return object.__getattribute__(self.config, attr) return object.__getattribute__(self, attr) @property def _format(self): """Return the value formatter for this graph""" return humanize if self.human_readable else str def _compute(self): """Initial computations to draw the graph""" def _plot(self): """Actual plotting of the graph""" def _compute_margin(self): """Compute graph margins from set texts""" if self.show_legend: h, w = get_texts_box( map(lambda x: truncate(x, self.truncate_legend or 15), cut(self.series, 'title')), self.legend_font_size) if self.legend_at_bottom: h_max = max(h, self.legend_box_size) self.margin.bottom += 10 + h_max * round( sqrt(self._order) - 1) * 1.5 + h_max else: self.margin.right += 10 + w + self.legend_box_size if self.title: h, w = get_text_box(self.title, self.title_font_size) self.margin.top += 10 + h if self._x_labels: h, w = get_texts_box(cut(self._x_labels), self.label_font_size) self._x_labels_height = 10 + max( w * sin(rad(self.x_label_rotation)), h) self.margin.bottom += self._x_labels_height if self.x_label_rotation: self.margin.right = max(w * cos(rad(self.x_label_rotation)), self.margin.right) else: self._x_labels_height = 0 if self._y_labels: h, w = get_texts_box(cut(self._y_labels), self.label_font_size) self.margin.left += 10 + max(w * cos(rad(self.y_label_rotation)), h) @cached_property def _legends(self): """Getter for series title""" return [serie.title for serie in self.series] @cached_property def _values(self): """Getter for series values (flattened)""" return [ val for serie in self.series for val in serie.values if val is not None ] @cached_property def _len(self): """Getter for the maximum series size""" return max([len(serie.values) for serie in self.series]) @cached_property def _min(self): """Getter for the minimum series value""" return (self.range and self.range[0]) or min(self._values) @cached_property def _max(self): """Getter for the maximum series value""" return (self.range and self.range[1]) or max(self._values) @cached_property def _order(self): """Getter for the maximum series value""" return len(self.series) def _draw(self): """Draw all the things""" self._compute() self._compute_margin() self._decorate() self._plot() def _has_data(self): """Check if there is any data""" return sum(map(len, map(lambda s: s.values, self.series))) != 0 def render(self, is_unicode): """Render the graph, and return the svg string""" return self.svg.render(is_unicode=is_unicode, pretty_print=self.pretty_print) def render_tree(self): """Render the graph, and return lxml tree""" return self.svg.root
class BaseGraph(object): """Graphs commons""" _adapters = [] def __init__(self, config, series): """Init the graph""" self.config = config self.series = series self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self._x_labels = None self._y_labels = None self.nodes = {} self.margin = Margin(*([20] * 4)) self._box = Box() self.view = None if self.series and self._has_data(): self._draw() else: self.svg.draw_no_data() self.svg.pre_render() def __getattr__(self, attr): """Search in config, then in self""" if attr in dir(self.config): return object.__getattribute__(self.config, attr) return object.__getattribute__(self, attr) @property def _format(self): """Return the value formatter for this graph""" return humanize if self.human_readable else str def _compute(self): """Initial computations to draw the graph""" def _plot(self): """Actual plotting of the graph""" def _compute_margin(self): """Compute graph margins from set texts""" if self.show_legend: h, w = get_texts_box( map(lambda x: truncate(x, self.truncate_legend or 15), cut(self.series, 'title')), self.legend_font_size) if self.legend_at_bottom: h_max = max(h, self.legend_box_size) self.margin.bottom += 10 + h_max * round( sqrt(self._order) - 1) * 1.5 + h_max else: self.margin.right += 10 + w + self.legend_box_size if self.title: h, w = get_text_box(self.title, self.title_font_size) self.margin.top += 10 + h if self._x_labels: h, w = get_texts_box( cut(self._x_labels), self.label_font_size) self._x_labels_height = 10 + max( w * sin(rad(self.x_label_rotation)), h) self.margin.bottom += self._x_labels_height if self.x_label_rotation: self.margin.right = max( w * cos(rad(self.x_label_rotation)), self.margin.right) else: self._x_labels_height = 0 if self._y_labels: h, w = get_texts_box( cut(self._y_labels), self.label_font_size) self.margin.left += 10 + max( w * cos(rad(self.y_label_rotation)), h) @cached_property def _legends(self): """Getter for series title""" return [serie.title for serie in self.series] @cached_property def _values(self): """Getter for series values (flattened)""" return [val for serie in self.series for val in serie.values if val is not None] @cached_property def _len(self): """Getter for the maximum series size""" return max([len(serie.values) for serie in self.series]) @cached_property def _min(self): """Getter for the minimum series value""" return (self.range and self.range[0]) or min(self._values) @cached_property def _max(self): """Getter for the maximum series value""" return (self.range and self.range[1]) or max(self._values) @cached_property def _order(self): """Getter for the maximum series value""" return len(self.series) def _draw(self): """Draw all the things""" self._compute() self._compute_margin() self._decorate() self._plot() def _has_data(self): """Check if there is any data""" return sum(map(len, map(lambda s: s.values, self.series))) != 0 def render(self, is_unicode): """Render the graph, and return the svg string""" return self.svg.render( is_unicode=is_unicode, pretty_print=self.pretty_print) def render_tree(self): """Render the graph, and return lxml tree""" return self.svg.root
class BaseGraph(object): """Chart internal behaviour related functions""" _adapters = [] def __init__(self, config=None, **kwargs): """Config preparation and various initialization""" if config: if isinstance(config, type): config = config() else: config = config.copy() else: config = Config() config(**kwargs) self.config = config self.state = None self.uuid = str(uuid4()) self.raw_series = [] self.xml_filters = [] def __setattr__(self, name, value): """Set an attribute on the class or in the state if there is one""" if name.startswith('__') or getattr(self, 'state', None) is None: super(BaseGraph, self).__setattr__(name, value) else: setattr(self.state, name, value) def __getattribute__(self, name): """Get an attribute from the class or from the state if there is one""" if name.startswith('__') or name == 'state' or getattr( self, 'state', None) is None or name not in self.state.__dict__: return super(BaseGraph, self).__getattribute__(name) return getattr(self.state, name) def prepare_values(self, raw, offset=0): """Prepare the values to start with sane values""" from pygal import Histogram from pygal.graph.map import BaseMap if self.zero == 0 and isinstance(self, BaseMap): self.zero = 1 if self.x_label_rotation: self.x_label_rotation %= 360 if self.y_label_rotation: self.y_label_rotation %= 360 for key in ('x_labels', 'y_labels'): if getattr(self, key): setattr(self, key, list(getattr(self, key))) if not raw: return adapters = list(self._adapters) or [lambda x: x] if self.logarithmic: for fun in not_zero, positive: if fun in adapters: adapters.remove(fun) adapters = adapters + [positive, not_zero] adapters = adapters + [decimal_to_float] self._adapt = reduce(compose, adapters) if not self.strict else ident self._x_adapt = reduce( compose, self._x_adapters ) if not self.strict and getattr(self, '_x_adapters', None) else ident series = [] raw = [( list(raw_values) if not isinstance(raw_values, dict) else raw_values, serie_config_kwargs ) for raw_values, serie_config_kwargs in raw] width = max([len(values) for values, _ in raw] + [len(self.x_labels or [])]) for raw_values, serie_config_kwargs in raw: metadata = {} values = [] if isinstance(raw_values, dict): if isinstance(self, BaseMap): raw_values = list(raw_values.items()) else: value_list = [None] * width for k, v in raw_values.items(): if k in (self.x_labels or []): value_list[self.x_labels.index(k)] = v raw_values = value_list for index, raw_value in enumerate(raw_values + ( (width - len(raw_values)) * [None] # aligning values if len(raw_values) < width else [])): if isinstance(raw_value, dict): raw_value = dict(raw_value) value = raw_value.pop('value', None) metadata[index] = raw_value else: value = raw_value # Fix this by doing this in charts class methods if isinstance(self, Histogram): if value is None: value = (None, None, None) elif not is_list_like(value): value = (value, self.zero, self.zero) elif len(value) == 2: value = (1, value[0], value[1]) value = list(map(self._adapt, value)) elif self._dual: if value is None: value = (None, None) elif not is_list_like(value): value = (value, self.zero) if self._x_adapt: value = ( self._x_adapt(value[0]), self._adapt(value[1]) ) if isinstance(self, BaseMap): value = (self._adapt(value[0]), value[1]) else: value = list(map(self._adapt, value)) else: value = self._adapt(value) values.append(value) serie_config = SerieConfig() serie_config( **dict((k, v) for k, v in self.state.__dict__.items() if k in dir(serie_config)) ) serie_config(**serie_config_kwargs) series.append( Serie(offset + len(series), values, serie_config, metadata) ) return series def setup(self, **kwargs): """Set up the transient state prior rendering""" # Keep labels in case of map if getattr(self, 'x_labels', None) is not None: self.x_labels = list(self.x_labels) if getattr(self, 'y_labels', None) is not None: self.y_labels = list(self.y_labels) self.state = State(self, **kwargs) if isinstance(self.style, type): self.style = self.style() self.series = self.prepare_values([ rs for rs in self.raw_series if not rs[1].get('secondary') ]) or [] self.secondary_series = self.prepare_values([ rs for rs in self.raw_series if rs[1].get('secondary') ], len(self.series)) or [] self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self._x_labels = None self._y_labels = None self._x_2nd_labels = None self._y_2nd_labels = None self.nodes = {} self.margin_box = Margin( self.margin_top or self.margin, self.margin_right or self.margin, self.margin_bottom or self.margin, self.margin_left or self.margin ) self._box = Box() self.view = None if self.logarithmic and self.zero == 0: # Explicit min to avoid interpolation dependency positive_values = list( filter( lambda x: x > 0, [ val[1] or 1 if self._dual else val for serie in self.series for val in serie.safe_values ] ) ) self.zero = min(positive_values or (1, )) or 1 if self._len < 3: self.interpolate = None self._draw() self.svg.pre_render() def teardown(self): """Remove the transient state after rendering""" if os.getenv('PYGAL_KEEP_STATE'): return del self.state self.state = None def _repr_svg_(self): """Display svg in IPython notebook""" return self.render(disable_xml_declaration=True) def _repr_png_(self): """Display png in IPython notebook""" return self.render_to_png()
class BaseGraph(object): """Graphs commons""" _adapters = [] def __init__(self, config, series, secondary_series, uuid): """Init the graph""" self.uuid = uuid self.__dict__.update(config.to_dict()) self.config = config self.series = series or [] self.secondary_series = secondary_series or [] self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self._x_labels = None self._y_labels = None self._x_2nd_labels = None self._y_2nd_labels = None self.nodes = {} self.margin = Margin(*([self.margin] * 4)) self._box = Box() self.view = None if self.logarithmic and self.zero == 0: # Explicit min to avoid interpolation dependency from pygal.graph.xy import XY if isinstance(self, XY): get = lambda x: x[1] else: get = lambda x: x positive_values = list(filter( lambda x: x > 0, [get(val) for serie in self.series for val in serie.safe_values])) self.zero = min(positive_values) if positive_values else 0 self._draw() self.svg.pre_render() @property def all_series(self): return self.series + self.secondary_series @property def _format(self): """Return the value formatter for this graph""" return self.value_formatter or ( humanize if self.human_readable else str) def _compute(self): """Initial computations to draw the graph""" def _plot(self): """Actual plotting of the graph""" def _compute_margin(self): """Compute graph margins from set texts""" self._legend_at_left_width = 0 for series_group in (self.series, self.secondary_series): if self.show_legend and series_group: h, w = get_texts_box( map(lambda x: truncate(x, self.truncate_legend or 15), cut(series_group, 'title')), self.legend_font_size) if self.legend_at_bottom: h_max = max(h, self.legend_box_size) self.margin.bottom += self.spacing + h_max * round( sqrt(self._order) - 1) * 1.5 + h_max else: if series_group is self.series: legend_width = self.spacing + w + self.legend_box_size self.margin.left += legend_width self._legend_at_left_width += legend_width else: self.margin.right += ( self.spacing + w + self.legend_box_size) for xlabels in (self._x_labels, self._x_2nd_labels): if xlabels: h, w = get_texts_box( map(lambda x: truncate(x, self.truncate_label or 25), cut(xlabels)), self.label_font_size) self._x_labels_height = self.spacing + max( w * sin(rad(self.x_label_rotation)), h) if xlabels is self._x_labels: self.margin.bottom += self._x_labels_height else: self.margin.top += self._x_labels_height if self.x_label_rotation: self.margin.right = max( w * cos(rad(self.x_label_rotation)), self.margin.right) if not self._x_labels: self._x_labels_height = 0 if self.show_y_labels: for ylabels in (self._y_labels, self._y_2nd_labels): if ylabels: h, w = get_texts_box( cut(ylabels), self.label_font_size) if ylabels is self._y_labels: self.margin.left += self.spacing + max( w * cos(rad(self.y_label_rotation)), h) else: self.margin.right += self.spacing + max( w * cos(rad(self.y_label_rotation)), h) self.title = split_title( self.title, self.width, self.title_font_size) if self.title: h, _ = get_text_box(self.title[0], self.title_font_size) self.margin.top += len(self.title) * (self.spacing + h) self.x_title = split_title( self.x_title, self.width - self.margin.x, self.title_font_size) self._x_title_height = 0 if self.x_title: h, _ = get_text_box(self.x_title[0], self.title_font_size) height = len(self.x_title) * (self.spacing + h) self.margin.bottom += height self._x_title_height = height + self.spacing self.y_title = split_title( self.y_title, self.height - self.margin.y, self.title_font_size) self._y_title_height = 0 if self.y_title: h, _ = get_text_box(self.y_title[0], self.title_font_size) height = len(self.y_title) * (self.spacing + h) self.margin.left += height self._y_title_height = height + self.spacing @cached_property def _legends(self): """Getter for series title""" return [serie.title for serie in self.series] @cached_property def _secondary_legends(self): """Getter for series title on secondary y axis""" return [serie.title for serie in self.secondary_series] @cached_property def _values(self): """Getter for series values (flattened)""" return [val for serie in self.series for val in serie.values if val is not None] @cached_property def _secondary_values(self): """Getter for secondary series values (flattened)""" return [val for serie in self.secondary_series for val in serie.values if val is not None] @cached_property def _len(self): """Getter for the maximum series size""" return max([ len(serie.values) for serie in self.all_series] or [0]) @cached_property def _secondary_min(self): """Getter for the minimum series value""" return (self.range and self.range[0]) or ( min(self._secondary_values) if self._secondary_values else None) @cached_property def _min(self): """Getter for the minimum series value""" return (self.range and self.range[0]) or ( min(self._values) if self._values else None) @cached_property def _max(self): """Getter for the maximum series value""" return (self.range and self.range[1]) or ( max(self._values) if self._values else None) @cached_property def _secondary_max(self): """Getter for the maximum series value""" return (self.range and self.range[1]) or ( max(self._secondary_values) if self._secondary_values else None) @cached_property def _order(self): """Getter for the number of series""" return len(self.all_series) def _draw(self): """Draw all the things""" self._compute() self._compute_secondary() self._post_compute() self._compute_margin() self._decorate() if self.series and self._has_data(): self._plot() else: self.svg.draw_no_data() def _has_data(self): """Check if there is any data""" return sum( map(len, map(lambda s: s.safe_values, self.series))) != 0 and ( sum(map(abs, self._values)) != 0) def render(self, is_unicode=False): """Render the graph, and return the svg string""" return self.svg.render( is_unicode=is_unicode, pretty_print=self.pretty_print) def render_tree(self): """Render the graph, and return lxml tree""" return self.svg.root
class BaseGraph(object): """Graphs commons""" _adapters = [] def __init__(self, config=None, **kwargs): if config: if isinstance(config, type): config = config() else: config = config.copy() else: config = Config() config(**kwargs) self.config = config self.state = None self.uuid = str(uuid4()) self.raw_series = [] self.raw_series2 = [] self.xml_filters = [] def __setattr__(self, name, value): if name.startswith('__') or getattr(self, 'state', None) is None: super(BaseGraph, self).__setattr__(name, value) else: setattr(self.state, name, value) def __getattribute__(self, name): if name.startswith('__') or name == 'state' or getattr( self, 'state', None ) is None or name not in self.state.__dict__: return super(BaseGraph, self).__getattribute__(name) return getattr(self.state, name) def add(self, title, values, **kwargs): """Add a serie to this graph""" if not is_list_like(values) and not isinstance(values, dict): values = [values] if kwargs.get('secondary', False): self.raw_series2.append((title, values, kwargs)) else: self.raw_series.append((title, values, kwargs)) def add_xml_filter(self, callback): self.xml_filters.append(callback) def prepare_values(self, raw, offset=0): """Prepare the values to start with sane values""" from pygal.graph.map import BaseMap from pygal import Histogram if self.zero == 0 and isinstance(self, BaseMap): self.zero = 1 for key in ('x_labels', 'y_labels'): if getattr(self, key): setattr(self, key, list(getattr(self, key))) if not raw: return adapters = list(self._adapters) or [lambda x:x] if self.logarithmic: for fun in not_zero, positive: if fun in adapters: adapters.remove(fun) adapters = adapters + [positive, not_zero] adapters = adapters + [decimal_to_float] adapter = reduce(compose, adapters) if not self.strict else ident x_adapter = reduce( compose, self._x_adapters) if getattr( self, '_x_adapters', None) else None series = [] raw = [( title, list(raw_values) if not isinstance( raw_values, dict) else raw_values, serie_config_kwargs ) for title, raw_values, serie_config_kwargs in raw] width = max([len(values) for _, values, _ in raw] + [len(self.x_labels or [])]) for title, raw_values, serie_config_kwargs in raw: metadata = {} values = [] if isinstance(raw_values, dict): if isinstance(self, BaseMap): raw_values = list(raw_values.items()) else: value_list = [None] * width for k, v in raw_values.items(): if k in self.x_labels: value_list[self.x_labels.index(k)] = v raw_values = value_list for index, raw_value in enumerate( raw_values + ( (width - len(raw_values)) * [None] # aligning values if len(raw_values) < width else [])): if isinstance(raw_value, dict): raw_value = dict(raw_value) value = raw_value.pop('value', None) metadata[index] = raw_value else: value = raw_value # Fix this by doing this in charts class methods if isinstance(self, Histogram): if value is None: value = (None, None, None) elif not is_list_like(value): value = (value, self.zero, self.zero) value = list(map(adapter, value)) elif self._dual: if value is None: value = (None, None) elif not is_list_like(value): value = (value, self.zero) if x_adapter: value = (x_adapter(value[0]), adapter(value[1])) if isinstance(self, BaseMap): value = (adapter(value[0]), value[1]) else: value = list(map(adapter, value)) else: value = adapter(value) values.append(value) serie_config = SerieConfig() serie_config(**dict((k, v) for k, v in self.state.__dict__.items() if k in dir(serie_config))) serie_config(**serie_config_kwargs) series.append( Serie(offset + len(series), title, values, serie_config, metadata)) return series def setup(self, **kwargs): """Init the graph""" # Keep labels in case of map if getattr(self, 'x_labels', None) is not None: self.x_labels = list(map(to_unicode, self.x_labels)) if getattr(self, 'y_labels', None) is not None: self.y_labels = list(self.y_labels) self.state = State(self, **kwargs) self.series = self.prepare_values( self.raw_series) or [] self.secondary_series = self.prepare_values( self.raw_series2, len(self.series)) or [] self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self._x_labels = None self._y_labels = None self._x_2nd_labels = None self._y_2nd_labels = None self.nodes = {} self.margin_box = Margin( self.margin_top or self.margin, self.margin_right or self.margin, self.margin_bottom or self.margin, self.margin_left or self.margin) self._box = Box() self.view = None if self.logarithmic and self.zero == 0: # Explicit min to avoid interpolation dependency positive_values = list(filter( lambda x: x > 0, [val[1] or 1 if self._dual else val for serie in self.series for val in serie.safe_values])) self.zero = min(positive_values or (1,)) or 1 if self._len < 3: self.interpolate = None self._draw() self.svg.pre_render() def teardown(self): if os.getenv('PYGAL_KEEP_STATE'): return del self.state self.state = None def render(self, is_unicode=False, **kwargs): """Render the graph, and return the svg string""" self.setup(**kwargs) svg = self.svg.render( is_unicode=is_unicode, pretty_print=self.pretty_print) self.teardown() return svg def render_tree(self): """Render the graph, and return (l)xml etree""" self.setup() svg = self.svg.root for f in self.xml_filters: svg = f(svg) self.teardown() return svg def render_table(self, **kwargs): # Import here to avoid lxml import try: from pygal.table import Table except ImportError: raise ImportError('You must install lxml to use render table') return Table(self).render(**kwargs) def render_pyquery(self): """Render the graph, and return a pyquery wrapped tree""" from pyquery import PyQuery as pq return pq(self.render(), parser='html') def render_in_browser(self, **kwargs): """Render the graph, open it in your browser with black magic""" try: from lxml.html import open_in_browser except ImportError: raise ImportError('You must install lxml to use render in browser') open_in_browser(self.render_tree(**kwargs), encoding='utf-8') def render_response(self, **kwargs): """Render the graph, and return a Flask response""" from flask import Response return Response(self.render(**kwargs), mimetype='image/svg+xml') def render_django_response(self, **kwargs): """Render the graph, and return a Django response""" from django.http import HttpResponse return HttpResponse( self.render(**kwargs), content_type='image/svg+xml') def render_to_file(self, filename, **kwargs): """Render the graph, and write it to filename""" with io.open(filename, 'w', encoding='utf-8') as f: f.write(self.render(is_unicode=True, **kwargs)) def render_to_png(self, filename=None, dpi=72, **kwargs): """Render the graph, convert it to png and write it to filename""" import cairosvg return cairosvg.svg2png( bytestring=self.render(**kwargs), write_to=filename, dpi=dpi) def render_sparktext(self, relative_to=None): """Make a mini text sparkline from chart""" bars = u('▁▂▃▄▅▆▇█') if len(self.raw_series) == 0: return u('') values = list(self.raw_series[0][1]) if len(values) == 0: return u('') chart = u('') values = list(map(lambda x: max(x, 0), values)) vmax = max(values) if relative_to is None: relative_to = min(values) if (vmax - relative_to) == 0: chart = bars[0] * len(values) return chart divisions = len(bars) - 1 for value in values: chart += bars[int(divisions * (value - relative_to) / (vmax - relative_to))] return chart def render_sparkline(self, **kwargs): spark_options = dict( width=200, height=50, show_dots=False, show_legend=False, show_x_labels=False, show_y_labels=False, spacing=0, margin=5, explicit_size=True ) spark_options.update(kwargs) return self.render(**spark_options) def _repr_svg_(self): """Display svg in IPython notebook""" return self.render(disable_xml_declaration=True) def _repr_png_(self): """Display png in IPython notebook""" return self.render_to_png()
class BaseGraph(object): """Chart internal behaviour related functions""" _adapters = [] def __init__(self, config=None, **kwargs): """Config preparation and various initialization""" if config: if isinstance(config, type): config = config() else: config = config.copy() else: config = Config() config(**kwargs) self.config = config self.state = None self.uuid = str(uuid4()) self.raw_series = [] self.xml_filters = [] def __setattr__(self, name, value): """Set an attribute on the class or in the state if there is one""" if name.startswith('__') or getattr(self, 'state', None) is None: super(BaseGraph, self).__setattr__(name, value) else: setattr(self.state, name, value) def __getattribute__(self, name): """Get an attribute from the class or from the state if there is one""" if name.startswith('__') or name == 'state' or getattr( self, 'state', None ) is None or name not in self.state.__dict__: return super(BaseGraph, self).__getattribute__(name) return getattr(self.state, name) def prepare_values(self, raw, offset=0): """Prepare the values to start with sane values""" from pygal.graph.map import BaseMap from pygal import Histogram if self.zero == 0 and isinstance(self, BaseMap): self.zero = 1 if self.x_label_rotation: self.x_label_rotation %= 360 if self.y_label_rotation: self.y_label_rotation %= 360 for key in ('x_labels', 'y_labels'): if getattr(self, key): setattr(self, key, list(getattr(self, key))) if not raw: return adapters = list(self._adapters) or [lambda x:x] if self.logarithmic: for fun in not_zero, positive: if fun in adapters: adapters.remove(fun) adapters = adapters + [positive, not_zero] adapters = adapters + [decimal_to_float] self._adapt = reduce(compose, adapters) if not self.strict else ident self._x_adapt = reduce( compose, self._x_adapters) if not self.strict and getattr( self, '_x_adapters', None) else ident series = [] raw = [( list(raw_values) if not isinstance( raw_values, dict) else raw_values, serie_config_kwargs ) for raw_values, serie_config_kwargs in raw] width = max([len(values) for values, _ in raw] + [len(self.x_labels or [])]) for raw_values, serie_config_kwargs in raw: metadata = {} values = [] if isinstance(raw_values, dict): if isinstance(self, BaseMap): raw_values = list(raw_values.items()) else: value_list = [None] * width for k, v in raw_values.items(): if k in (self.x_labels or []): value_list[self.x_labels.index(k)] = v raw_values = value_list for index, raw_value in enumerate( raw_values + ( (width - len(raw_values)) * [None] # aligning values if len(raw_values) < width else [])): if isinstance(raw_value, dict): raw_value = dict(raw_value) value = raw_value.pop('value', None) metadata[index] = raw_value else: value = raw_value # Fix this by doing this in charts class methods if isinstance(self, Histogram): if value is None: value = (None, None, None) elif not is_list_like(value): value = (value, self.zero, self.zero) elif len(value) == 2: value = (1, value[0], value[1]) value = list(map(self._adapt, value)) elif self._dual: if value is None: value = (None, None) elif not is_list_like(value): value = (value, self.zero) if self._x_adapt: value = ( self._x_adapt(value[0]), self._adapt(value[1])) if isinstance(self, BaseMap): value = (self._adapt(value[0]), value[1]) else: value = list(map(self._adapt, value)) else: value = self._adapt(value) values.append(value) serie_config = SerieConfig() serie_config(**dict((k, v) for k, v in self.state.__dict__.items() if k in dir(serie_config))) serie_config(**serie_config_kwargs) series.append( Serie(offset + len(series), values, serie_config, metadata)) return series def setup(self, **kwargs): """Set up the transient state prior rendering""" # Keep labels in case of map if getattr(self, 'x_labels', None) is not None: self.x_labels = list(self.x_labels) if getattr(self, 'y_labels', None) is not None: self.y_labels = list(self.y_labels) self.state = State(self, **kwargs) if isinstance(self.style, type): self.style = self.style() self.series = self.prepare_values( [rs for rs in self.raw_series if not rs[1].get('secondary')]) or [] self.secondary_series = self.prepare_values( [rs for rs in self.raw_series if rs[1].get('secondary')], len(self.series)) or [] self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self._x_labels = None self._y_labels = None self._x_2nd_labels = None self._y_2nd_labels = None self.nodes = {} self.margin_box = Margin( self.margin_top or self.margin, self.margin_right or self.margin, self.margin_bottom or self.margin, self.margin_left or self.margin) self._box = Box() self.view = None if self.logarithmic and self.zero == 0: # Explicit min to avoid interpolation dependency positive_values = list(filter( lambda x: x > 0, [val[1] or 1 if self._dual else val for serie in self.series for val in serie.safe_values])) self.zero = min(positive_values or (1,)) or 1 if self._len < 3: self.interpolate = None self._draw() self.svg.pre_render() def teardown(self): """Remove the transient state after rendering""" if os.getenv('PYGAL_KEEP_STATE'): return del self.state self.state = None def _repr_svg_(self): """Display svg in IPython notebook""" return self.render(disable_xml_declaration=True) def _repr_png_(self): """Display png in IPython notebook""" return self.render_to_png()
class BaseGraph(object): """Graphs commons""" _adapters = [] def __init__(self, config, series): """Init the graph""" self.config = config self.series = series or [] self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self._x_labels = None self._y_labels = None self.nodes = {} self.margin = Margin(*([20] * 4)) self._box = Box() self.view = None if self.logarithmic and self.zero == 0: # Explicit min to avoid interpolation dependency self.zero = min(filter( lambda x: x > 0, [val for serie in self.series for val in serie.safe_values])) self._draw() self.svg.pre_render() def __getattr__(self, attr): """Search in config, then in self""" if attr in dir(self.config): return object.__getattribute__(self.config, attr) return object.__getattribute__(self, attr) def _split_title(self): if not self.title: self.title = [] return size = reverse_text_len(self.width, self.title_font_size) title = self.title.strip() self.title = [] while len(title) > size: title_part = title[:size] i = title_part.rfind(' ') if i == -1: i = len(title_part) self.title.append(title_part[:i]) title = title[i:].strip() self.title.append(title) @property def _format(self): """Return the value formatter for this graph""" return humanize if self.human_readable else str def _compute(self): """Initial computations to draw the graph""" def _plot(self): """Actual plotting of the graph""" def _compute_margin(self): """Compute graph margins from set texts""" if self.show_legend and self.series: h, w = get_texts_box( map(lambda x: truncate(x, self.truncate_legend or 15), cut(self.series, 'title')), self.legend_font_size) if self.legend_at_bottom: h_max = max(h, self.legend_box_size) self.margin.bottom += 10 + h_max * round( sqrt(self._order) - 1) * 1.5 + h_max else: self.margin.right += 10 + w + self.legend_box_size if self.title: h, _ = get_text_box(self.title[0], self.title_font_size) self.margin.top += len(self.title) * (10 + h) if self._x_labels: h, w = get_texts_box( cut(self._x_labels), self.label_font_size) self._x_labels_height = 10 + max( w * sin(rad(self.x_label_rotation)), h) self.margin.bottom += self._x_labels_height if self.x_label_rotation: self.margin.right = max( w * cos(rad(self.x_label_rotation)), self.margin.right) else: self._x_labels_height = 0 if self._y_labels: h, w = get_texts_box( cut(self._y_labels), self.label_font_size) self.margin.left += 10 + max( w * cos(rad(self.y_label_rotation)), h) @cached_property def _legends(self): """Getter for series title""" return [serie.title for serie in self.series] @cached_property def _values(self): """Getter for series values (flattened)""" return [val for serie in self.series for val in serie.values if val is not None] @cached_property def _len(self): """Getter for the maximum series size""" return max([len(serie.values) for serie in self.series] or [0]) @cached_property def _min(self): """Getter for the minimum series value""" return (self.range and self.range[0]) or ( min(self._values) if self._values else None) @cached_property def _max(self): """Getter for the maximum series value""" return (self.range and self.range[1]) or ( max(self._values) if self._values else None) @cached_property def _order(self): """Getter for the maximum series value""" return len(self.series) def _draw(self): """Draw all the things""" self._compute() self._split_title() self._compute_margin() self._decorate() if self.series and self._has_data(): self._plot() else: self.svg.draw_no_data() def _has_data(self): """Check if there is any data""" return sum( map(len, map(lambda s: s.safe_values, self.series))) != 0 and ( sum(map(abs, self._values)) != 0) def render(self, is_unicode): """Render the graph, and return the svg string""" return self.svg.render( is_unicode=is_unicode, pretty_print=self.pretty_print) def render_tree(self): """Render the graph, and return lxml tree""" return self.svg.root
class BaseGraph(object): """Graphs commons""" _adapters = [] def __init__(self, config, series): """Init the graph""" self.config = config self.series = series or [] self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self._x_labels = None self._y_labels = None self.nodes = {} self.margin = Margin(*([20] * 4)) self._box = Box() self.view = None if self.logarithmic and self.zero == 0: # Explicit min to avoid interpolation dependency self.zero = min( filter(lambda x: x > 0, [ val for serie in self.series for val in serie.safe_values ])) self._draw() self.svg.pre_render() def __getattr__(self, attr): """Search in config, then in self""" if attr in dir(self.config): return object.__getattribute__(self.config, attr) return object.__getattribute__(self, attr) def _split_title(self): if not self.title: self.title = [] return size = reverse_text_len(self.width, self.title_font_size) title = self.title.strip() self.title = [] while len(title) > size: title_part = title[:size] i = title_part.rfind(' ') if i == -1: i = len(title_part) self.title.append(title_part[:i]) title = title[i:].strip() self.title.append(title) @property def _format(self): """Return the value formatter for this graph""" return humanize if self.human_readable else str def _compute(self): """Initial computations to draw the graph""" def _plot(self): """Actual plotting of the graph""" def _compute_margin(self): """Compute graph margins from set texts""" if self.show_legend and self.series: h, w = get_texts_box( map(lambda x: truncate(x, self.truncate_legend or 15), cut(self.series, 'title')), self.legend_font_size) if self.legend_at_bottom: h_max = max(h, self.legend_box_size) self.margin.bottom += 10 + h_max * round( sqrt(self._order) - 1) * 1.5 + h_max else: self.margin.right += 10 + w + self.legend_box_size if self.title: h, _ = get_text_box(self.title[0], self.title_font_size) self.margin.top += len(self.title) * (10 + h) if self._x_labels: h, w = get_texts_box(cut(self._x_labels), self.label_font_size) self._x_labels_height = 10 + max( w * sin(rad(self.x_label_rotation)), h) self.margin.bottom += self._x_labels_height if self.x_label_rotation: self.margin.right = max(w * cos(rad(self.x_label_rotation)), self.margin.right) else: self._x_labels_height = 0 if self._y_labels: h, w = get_texts_box(cut(self._y_labels), self.label_font_size) self.margin.left += 10 + max(w * cos(rad(self.y_label_rotation)), h) @cached_property def _legends(self): """Getter for series title""" return [serie.title for serie in self.series] @cached_property def _values(self): """Getter for series values (flattened)""" return [ val for serie in self.series for val in serie.values if val is not None ] @cached_property def _len(self): """Getter for the maximum series size""" return max([len(serie.values) for serie in self.series] or [0]) @cached_property def _min(self): """Getter for the minimum series value""" return (self.range and self.range[0]) or (min(self._values) if self._values else None) @cached_property def _max(self): """Getter for the maximum series value""" return (self.range and self.range[1]) or (max(self._values) if self._values else None) @cached_property def _order(self): """Getter for the maximum series value""" return len(self.series) def _draw(self): """Draw all the things""" self._compute() self._split_title() self._compute_margin() self._decorate() if self.series and self._has_data(): self._plot() else: self.svg.draw_no_data() def _has_data(self): """Check if there is any data""" return sum(map(len, map(lambda s: s.safe_values, self.series))) != 0 and (sum( map(abs, self._values)) != 0) def render(self, is_unicode): """Render the graph, and return the svg string""" return self.svg.render(is_unicode=is_unicode, pretty_print=self.pretty_print) def render_tree(self): """Render the graph, and return lxml tree""" return self.svg.root
class BaseGraph(object): """Graphs commons""" _adapters = [] def __init__(self, config, series, secondary_series, uuid, xml_filters): """Init the graph""" self.uuid = uuid self.__dict__.update(config.to_dict()) self.config = config self.series = series or [] self.secondary_series = secondary_series or [] self.xml_filters = xml_filters or [] self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self._x_labels = None self._y_labels = None self._x_2nd_labels = None self._y_2nd_labels = None self.nodes = {} self.margin = Margin(self.margin_top or self.margin, self.margin_right or self.margin, self.margin_bottom or self.margin, self.margin_left or self.margin) self._box = Box() self.view = None if self.logarithmic and self.zero == 0: # Explicit min to avoid interpolation dependency if self._dual: get = lambda x: x[1] or 1 else: get = lambda x: x positive_values = list(filter( lambda x: x > 0, [get(val) for serie in self.series for val in serie.safe_values])) self.zero = min(positive_values or (1,)) or 1 if self._len < 3: self.interpolate = None self._draw() self.svg.pre_render() @property def all_series(self): return self.series + self.secondary_series @property def _x_format(self): """Return the value formatter for this graph""" return self.config.x_value_formatter or ( humanize if self.human_readable else str) @property def _format(self): """Return the value formatter for this graph""" return self.config.value_formatter or ( humanize if self.human_readable else str) def _compute(self): """Initial computations to draw the graph""" def _compute_margin(self): """Compute graph margins from set texts""" self._legend_at_left_width = 0 for series_group in (self.series, self.secondary_series): if self.show_legend and series_group: h, w = get_texts_box( map(lambda x: truncate(x, self.truncate_legend or 15), cut(series_group, 'title')), self.legend_font_size) if self.legend_at_bottom: h_max = max(h, self.legend_box_size) cols = (self._order // self.legend_at_bottom_columns if self.legend_at_bottom_columns else ceil(sqrt(self._order)) or 1) self.margin.bottom += self.spacing + h_max * round( cols - 1) * 1.5 + h_max else: if series_group is self.series: legend_width = self.spacing + w + self.legend_box_size self.margin.left += legend_width self._legend_at_left_width += legend_width else: self.margin.right += ( self.spacing + w + self.legend_box_size) self._x_labels_height = 0 if (self._x_labels or self._x_2nd_labels) and self.show_x_labels: for xlabels in (self._x_labels, self._x_2nd_labels): if xlabels: h, w = get_texts_box( map(lambda x: truncate(x, self.truncate_label or 25), cut(xlabels)), self.label_font_size) self._x_labels_height = self.spacing + max( w * sin(rad(self.x_label_rotation)), h) if xlabels is self._x_labels: self.margin.bottom += self._x_labels_height else: self.margin.top += self._x_labels_height if self.x_label_rotation: self.margin.right = max( w * cos(rad(self.x_label_rotation)), self.margin.right) if self.show_y_labels: for ylabels in (self._y_labels, self._y_2nd_labels): if ylabels: h, w = get_texts_box( cut(ylabels), self.label_font_size) if ylabels is self._y_labels: self.margin.left += self.spacing + max( w * cos(rad(self.y_label_rotation)), h) else: self.margin.right += self.spacing + max( w * cos(rad(self.y_label_rotation)), h) self.title = split_title( self.title, self.width, self.title_font_size) if self.title: h, _ = get_text_box(self.title[0], self.title_font_size) self.margin.top += len(self.title) * (self.spacing + h) self.x_title = split_title( self.x_title, self.width - self.margin.x, self.title_font_size) self._x_title_height = 0 if self.x_title: h, _ = get_text_box(self.x_title[0], self.title_font_size) height = len(self.x_title) * (self.spacing + h) self.margin.bottom += height self._x_title_height = height + self.spacing self.y_title = split_title( self.y_title, self.height - self.margin.y, self.title_font_size) self._y_title_height = 0 if self.y_title: h, _ = get_text_box(self.y_title[0], self.title_font_size) height = len(self.y_title) * (self.spacing + h) self.margin.left += height self._y_title_height = height + self.spacing @cached_property def _legends(self): """Getter for series title""" return [serie.title for serie in self.series] @cached_property def _secondary_legends(self): """Getter for series title on secondary y axis""" return [serie.title for serie in self.secondary_series] @cached_property def _values(self): """Getter for series values (flattened)""" return [val for serie in self.series for val in serie.values if val is not None] @cached_property def _secondary_values(self): """Getter for secondary series values (flattened)""" return [val for serie in self.secondary_series for val in serie.values if val is not None] @cached_property def _len(self): """Getter for the maximum series size""" return max([ len(serie.values) for serie in self.all_series] or [0]) @cached_property def _secondary_min(self): """Getter for the minimum series value""" return (self.range[0] if (self.range and self.range[0] is not None) else (min(self._secondary_values) if self._secondary_values else None)) @cached_property def _min(self): """Getter for the minimum series value""" return (self.range[0] if (self.range and self.range[0] is not None) else (min(self._values) if self._values else None)) @cached_property def _max(self): """Getter for the maximum series value""" return (self.range[1] if (self.range and self.range[1] is not None) else (max(self._values) if self._values else None)) @cached_property def _secondary_max(self): """Getter for the maximum series value""" return (self.range[1] if (self.range and self.range[1] is not None) else (max(self._secondary_values) if self._secondary_values else None)) @cached_property def _order(self): """Getter for the number of series""" return len(self.all_series) @cached_property def _x_major_labels(self): """Getter for the x major label""" if self.x_labels_major: return self.x_labels_major if self.x_labels_major_every: return [self._x_labels[i][0] for i in range( 0, len(self._x_labels), self.x_labels_major_every)] if self.x_labels_major_count: label_count = len(self._x_labels) major_count = self.x_labels_major_count if (major_count >= label_count): return [label[0] for label in self._x_labels] return [self._x_labels[ int(i * (label_count - 1) / (major_count - 1))][0] for i in range(major_count)] return [] @cached_property def _y_major_labels(self): """Getter for the y major label""" if self.y_labels_major: return self.y_labels_major if self.y_labels_major_every: return [self._y_labels[i][1] for i in range( 0, len(self._y_labels), self.y_labels_major_every)] if self.y_labels_major_count: label_count = len(self._y_labels) major_count = self.y_labels_major_count if (major_count >= label_count): return [label[1] for label in self._y_labels] return [self._y_labels[ int(i * (label_count - 1) / (major_count - 1))][1] for i in range(major_count)] return majorize( cut(self._y_labels, 1) ) def _draw(self): """Draw all the things""" self._compute() self._compute_secondary() self._post_compute() self._compute_margin() self._decorate() if self.series and self._has_data(): self._plot() else: self.svg.draw_no_data() def _has_data(self): """Check if there is any data""" return sum( map(len, map(lambda s: s.safe_values, self.series))) != 0 and ( sum(map(abs, self._values)) != 0) def render(self, is_unicode=False): """Render the graph, and return the svg string""" return self.svg.render( is_unicode=is_unicode, pretty_print=self.pretty_print) def render_tree(self): """Render the graph, and return (l)xml etree""" svg = self.svg.root for f in self.xml_filters: svg = f(svg) return svg
class BaseGraph(object): """Graphs commons""" _adapters = [] def __init__(self, config, series, secondary_series, uuid): """Init the graph""" self.uuid = uuid self.config = config self.series = series or [] self.secondary_series = secondary_series or [] self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self._x_labels = None self._y_labels = None self._x_2nd_labels = None self._y_2nd_labels = None self.nodes = {} self.margin = Margin(*([self.margin] * 4)) self._box = Box() self.view = None if self.logarithmic and self.zero == 0: # Explicit min to avoid interpolation dependency from pygal.graph.xy import XY if isinstance(self, XY): get = lambda x: x[1] else: get = lambda x: x positive_values = list( filter(lambda x: x > 0, [ get(val) for serie in self.series for val in serie.safe_values ])) self.zero = min(positive_values) if positive_values else 0 self._draw() self.svg.pre_render() def __getattr__(self, attr): """Search in config, then in self""" if attr in dir(self.config): return object.__getattribute__(self.config, attr) return object.__getattribute__(self, attr) @property def all_series(self): return self.series + self.secondary_series @property def _format(self): """Return the value formatter for this graph""" return self.value_formatter or (humanize if self.human_readable else str) def _compute(self): """Initial computations to draw the graph""" def _plot(self): """Actual plotting of the graph""" def _compute_margin(self): """Compute graph margins from set texts""" self._legend_at_left_width = 0 for series_group in (self.series, self.secondary_series): if self.show_legend and series_group: h, w = get_texts_box( map(lambda x: truncate(x, self.truncate_legend or 15), cut(series_group, 'title')), self.legend_font_size) if self.legend_at_bottom: h_max = max(h, self.legend_box_size) self.margin.bottom += self.spacing + h_max * round( sqrt(self._order) - 1) * 1.5 + h_max else: if series_group is self.series: legend_width = self.spacing + w + self.legend_box_size self.margin.left += legend_width self._legend_at_left_width += legend_width else: self.margin.right += (self.spacing + w + self.legend_box_size) for xlabels in (self._x_labels, self._x_2nd_labels): if xlabels: h, w = get_texts_box( map(lambda x: truncate(x, self.truncate_label or 25), cut(xlabels)), self.label_font_size) self._x_labels_height = self.spacing + max( w * sin(rad(self.x_label_rotation)), h) if xlabels is self._x_labels: self.margin.bottom += self._x_labels_height else: self.margin.top += self._x_labels_height if self.x_label_rotation: self.margin.right = max( w * cos(rad(self.x_label_rotation)), self.margin.right) if not self._x_labels: self._x_labels_height = 0 if self.show_y_labels: for ylabels in (self._y_labels, self._y_2nd_labels): if ylabels: h, w = get_texts_box(cut(ylabels), self.label_font_size) if ylabels is self._y_labels: self.margin.left += self.spacing + max( w * cos(rad(self.y_label_rotation)), h) else: self.margin.right += self.spacing + max( w * cos(rad(self.y_label_rotation)), h) self.title = split_title(self.title, self.width, self.title_font_size) if self.title: h, _ = get_text_box(self.title[0], self.title_font_size) self.margin.top += len(self.title) * (self.spacing + h) self.x_title = split_title(self.x_title, self.width - self.margin.x, self.title_font_size) self._x_title_height = 0 if self.x_title: h, _ = get_text_box(self.x_title[0], self.title_font_size) height = len(self.x_title) * (self.spacing + h) self.margin.bottom += height self._x_title_height = height + self.spacing self.y_title = split_title(self.y_title, self.height - self.margin.y, self.title_font_size) self._y_title_height = 0 if self.y_title: h, _ = get_text_box(self.y_title[0], self.title_font_size) height = len(self.y_title) * (self.spacing + h) self.margin.left += height self._y_title_height = height + self.spacing @cached_property def _legends(self): """Getter for series title""" return [serie.title for serie in self.series] @cached_property def _secondary_legends(self): """Getter for series title on secondary y axis""" return [serie.title for serie in self.secondary_series] @cached_property def _values(self): """Getter for series values (flattened)""" return [ val for serie in self.series for val in serie.values if val is not None ] @cached_property def _secondary_values(self): """Getter for secondary series values (flattened)""" return [ val for serie in self.secondary_series for val in serie.values if val is not None ] @cached_property def _len(self): """Getter for the maximum series size""" return max([len(serie.values) for serie in self.all_series] or [0]) @cached_property def _secondary_min(self): """Getter for the minimum series value""" return (self.range and self.range[0]) or (min( self._secondary_values) if self._secondary_values else None) @cached_property def _min(self): """Getter for the minimum series value""" return (self.range and self.range[0]) or (min(self._values) if self._values else None) @cached_property def _max(self): """Getter for the maximum series value""" return (self.range and self.range[1]) or (max(self._values) if self._values else None) @cached_property def _secondary_max(self): """Getter for the maximum series value""" return (self.range and self.range[1]) or (max( self._secondary_values) if self._secondary_values else None) @cached_property def _order(self): """Getter for the number of series""" return len(self.all_series) def _draw(self): """Draw all the things""" self._compute() self._compute_secondary() self._post_compute() self._compute_margin() self._decorate() if self.series and self._has_data(): self._plot() else: self.svg.draw_no_data() def _has_data(self): """Check if there is any data""" return sum(map(len, map(lambda s: s.safe_values, self.series))) != 0 and (sum( map(abs, self._values)) != 0) def render(self, is_unicode=False): """Render the graph, and return the svg string""" return self.svg.render(is_unicode=is_unicode, pretty_print=self.pretty_print) def render_tree(self): """Render the graph, and return lxml tree""" return self.svg.root