|
|
@ -31,26 +31,35 @@ class BaseGraph(object): |
|
|
|
"""Graphs commons""" |
|
|
|
"""Graphs commons""" |
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, config=None, **kwargs): |
|
|
|
def __init__(self, config=None, **kwargs): |
|
|
|
|
|
|
|
"""Init the graph""" |
|
|
|
self.config = config or Config() |
|
|
|
self.config = config or Config() |
|
|
|
self.config(**kwargs) |
|
|
|
self.config(**kwargs) |
|
|
|
self.svg = Svg(self) |
|
|
|
self.svg = Svg(self) |
|
|
|
self.series = [] |
|
|
|
self.series = [] |
|
|
|
self._x_labels = self._y_labels = None |
|
|
|
self._x_labels = self._y_labels = None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add(self, title, values): |
|
|
|
|
|
|
|
"""Add a serie to this graph""" |
|
|
|
|
|
|
|
self.series.append(Serie(title, values, len(self.series))) |
|
|
|
|
|
|
|
|
|
|
|
def _init(self): |
|
|
|
def _init(self): |
|
|
|
|
|
|
|
"""Init the graph""" |
|
|
|
self.margin = Margin(*([20] * 4)) |
|
|
|
self.margin = Margin(*([20] * 4)) |
|
|
|
self._box = Box() |
|
|
|
self._box = Box() |
|
|
|
|
|
|
|
|
|
|
|
def __getattr__(self, attr): |
|
|
|
def __getattr__(self, attr): |
|
|
|
|
|
|
|
"""Search in config, then in self""" |
|
|
|
if attr in dir(self.config): |
|
|
|
if attr in dir(self.config): |
|
|
|
return object.__getattribute__(self.config, attr) |
|
|
|
return object.__getattribute__(self.config, attr) |
|
|
|
return object.__getattribute__(self, attr) |
|
|
|
return object.__getattribute__(self, attr) |
|
|
|
|
|
|
|
|
|
|
|
@property |
|
|
|
@property |
|
|
|
def format(self): |
|
|
|
def _format(self): |
|
|
|
|
|
|
|
"""Return the value formatter for this graph""" |
|
|
|
return humanize if self.human_readable else str |
|
|
|
return humanize if self.human_readable else str |
|
|
|
|
|
|
|
|
|
|
|
def _compute_logarithmic_scale(self, min_, max_): |
|
|
|
def _compute_logarithmic_scale(self, min_, max_): |
|
|
|
|
|
|
|
"""Compute an optimal scale for logarithmic""" |
|
|
|
min_order = int(floor(log10(min_))) |
|
|
|
min_order = int(floor(log10(min_))) |
|
|
|
max_order = int(ceil(log10(max_))) |
|
|
|
max_order = int(ceil(log10(max_))) |
|
|
|
positions = [] |
|
|
|
positions = [] |
|
|
@ -71,6 +80,7 @@ class BaseGraph(object): |
|
|
|
return positions |
|
|
|
return positions |
|
|
|
|
|
|
|
|
|
|
|
def _compute_scale(self, min_, max_, min_scale=4, max_scale=20): |
|
|
|
def _compute_scale(self, min_, max_, min_scale=4, max_scale=20): |
|
|
|
|
|
|
|
"""Compute an optimal scale between min and max""" |
|
|
|
if min_ == 0 and max_ == 0: |
|
|
|
if min_ == 0 and max_ == 0: |
|
|
|
return [0] |
|
|
|
return [0] |
|
|
|
if max_ - min_ == 0: |
|
|
|
if max_ - min_ == 0: |
|
|
@ -99,19 +109,26 @@ class BaseGraph(object): |
|
|
|
return positions |
|
|
|
return positions |
|
|
|
|
|
|
|
|
|
|
|
def _text_len(self, lenght, fs): |
|
|
|
def _text_len(self, lenght, fs): |
|
|
|
|
|
|
|
"""Approximation of text length""" |
|
|
|
return lenght * 0.6 * fs |
|
|
|
return lenght * 0.6 * fs |
|
|
|
|
|
|
|
|
|
|
|
def _get_text_box(self, text, fs): |
|
|
|
def _get_text_box(self, text, fs): |
|
|
|
|
|
|
|
"""Approximation of text bounds""" |
|
|
|
return (fs, self._text_len(len(text), fs)) |
|
|
|
return (fs, self._text_len(len(text), fs)) |
|
|
|
|
|
|
|
|
|
|
|
def _get_texts_box(self, texts, fs): |
|
|
|
def _get_texts_box(self, texts, fs): |
|
|
|
|
|
|
|
"""Approximation of multiple texts bounds""" |
|
|
|
max_len = max(map(len, texts)) |
|
|
|
max_len = max(map(len, texts)) |
|
|
|
return (fs, self._text_len(max_len, fs)) |
|
|
|
return (fs, self._text_len(max_len, fs)) |
|
|
|
|
|
|
|
|
|
|
|
def _compute(self): |
|
|
|
def _compute(self): |
|
|
|
"""Initial computations to draw the graph""" |
|
|
|
"""Initial computations to draw the graph""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _plot(self): |
|
|
|
|
|
|
|
"""Actual plotting of the graph""" |
|
|
|
|
|
|
|
|
|
|
|
def _compute_margin(self): |
|
|
|
def _compute_margin(self): |
|
|
|
|
|
|
|
"""Compute graph margins from set texts""" |
|
|
|
if self.show_legend: |
|
|
|
if self.show_legend: |
|
|
|
h, w = self._get_texts_box( |
|
|
|
h, w = self._get_texts_box( |
|
|
|
cut(self.series, 'title'), self.legend_font_size) |
|
|
|
cut(self.series, 'title'), self.legend_font_size) |
|
|
@ -138,10 +155,12 @@ class BaseGraph(object): |
|
|
|
|
|
|
|
|
|
|
|
@cached_property |
|
|
|
@cached_property |
|
|
|
def _legends(self): |
|
|
|
def _legends(self): |
|
|
|
|
|
|
|
"""Getter for series title""" |
|
|
|
return [serie.title for serie in self.series] |
|
|
|
return [serie.title for serie in self.series] |
|
|
|
|
|
|
|
|
|
|
|
@cached_property |
|
|
|
@cached_property |
|
|
|
def _values(self): |
|
|
|
def _values(self): |
|
|
|
|
|
|
|
"""Getter for series values (flattened)""" |
|
|
|
return [val |
|
|
|
return [val |
|
|
|
for serie in self.series |
|
|
|
for serie in self.series |
|
|
|
for val in serie.values |
|
|
|
for val in serie.values |
|
|
@ -149,18 +168,18 @@ class BaseGraph(object): |
|
|
|
|
|
|
|
|
|
|
|
@cached_property |
|
|
|
@cached_property |
|
|
|
def _len(self): |
|
|
|
def _len(self): |
|
|
|
|
|
|
|
"""Getter for the maximum series size""" |
|
|
|
return max([len(serie.values) for serie in self.series]) |
|
|
|
return max([len(serie.values) for serie in self.series]) |
|
|
|
|
|
|
|
|
|
|
|
def _draw(self): |
|
|
|
def _draw(self): |
|
|
|
|
|
|
|
"""Draw all the things""" |
|
|
|
self._compute() |
|
|
|
self._compute() |
|
|
|
self._compute_margin() |
|
|
|
self._compute_margin() |
|
|
|
self._decorate() |
|
|
|
self._decorate() |
|
|
|
self._plot() |
|
|
|
self._plot() |
|
|
|
|
|
|
|
|
|
|
|
def add(self, title, values): |
|
|
|
|
|
|
|
self.series.append(Serie(title, values, len(self.series))) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _has_data(self): |
|
|
|
def _has_data(self): |
|
|
|
|
|
|
|
"""Check if there is any data""" |
|
|
|
if len(self.series) == 0: |
|
|
|
if len(self.series) == 0: |
|
|
|
return False |
|
|
|
return False |
|
|
|
for serie in self.series: |
|
|
|
for serie in self.series: |
|
|
@ -171,6 +190,7 @@ class BaseGraph(object): |
|
|
|
return True |
|
|
|
return True |
|
|
|
|
|
|
|
|
|
|
|
def _render(self): |
|
|
|
def _render(self): |
|
|
|
|
|
|
|
"""Make the graph internally""" |
|
|
|
self._init() |
|
|
|
self._init() |
|
|
|
self.svg._init() |
|
|
|
self.svg._init() |
|
|
|
if self._has_data(): |
|
|
|
if self._has_data(): |
|
|
@ -180,30 +200,37 @@ class BaseGraph(object): |
|
|
|
self.svg._pre_render(True) |
|
|
|
self.svg._pre_render(True) |
|
|
|
|
|
|
|
|
|
|
|
def render(self, is_unicode=False): |
|
|
|
def render(self, is_unicode=False): |
|
|
|
|
|
|
|
"""Render the graph, and return the svg string""" |
|
|
|
self._render() |
|
|
|
self._render() |
|
|
|
return self.svg.render(is_unicode=is_unicode) |
|
|
|
return self.svg.render(is_unicode=is_unicode) |
|
|
|
|
|
|
|
|
|
|
|
def render_tree(self): |
|
|
|
def render_tree(self): |
|
|
|
|
|
|
|
"""Render the graph, and return lxml tree""" |
|
|
|
self._render() |
|
|
|
self._render() |
|
|
|
return self.svg.root |
|
|
|
return self.svg.root |
|
|
|
|
|
|
|
|
|
|
|
def render_pyquery(self): |
|
|
|
def render_pyquery(self): |
|
|
|
|
|
|
|
"""Render the graph, and return a pyquery wrapped tree""" |
|
|
|
from pyquery import PyQuery as pq |
|
|
|
from pyquery import PyQuery as pq |
|
|
|
return pq(self.render_tree()) |
|
|
|
return pq(self.render_tree()) |
|
|
|
|
|
|
|
|
|
|
|
def render_in_browser(self): |
|
|
|
def render_in_browser(self): |
|
|
|
|
|
|
|
"""Render the graph, open it in your browser with black magic""" |
|
|
|
from lxml.html import open_in_browser |
|
|
|
from lxml.html import open_in_browser |
|
|
|
open_in_browser(self.render_tree(), encoding='utf-8') |
|
|
|
open_in_browser(self.render_tree(), encoding='utf-8') |
|
|
|
|
|
|
|
|
|
|
|
def render_response(self): |
|
|
|
def render_response(self): |
|
|
|
|
|
|
|
"""Render the graph, and return a Flask response""" |
|
|
|
from flask import Response |
|
|
|
from flask import Response |
|
|
|
return Response(self.render(), mimetype='image/svg+xml') |
|
|
|
return Response(self.render(), mimetype='image/svg+xml') |
|
|
|
|
|
|
|
|
|
|
|
def render_to_file(self, filename): |
|
|
|
def render_to_file(self, filename): |
|
|
|
|
|
|
|
"""Render the graph, and write it to filename""" |
|
|
|
with io.open(filename, 'w', encoding='utf-8') as f: |
|
|
|
with io.open(filename, 'w', encoding='utf-8') as f: |
|
|
|
f.write(self.render(is_unicode=True)) |
|
|
|
f.write(self.render(is_unicode=True)) |
|
|
|
|
|
|
|
|
|
|
|
def render_to_png(self, filename): |
|
|
|
def render_to_png(self, filename): |
|
|
|
|
|
|
|
"""Render the graph, convert it to png and write it to filename""" |
|
|
|
import cairosvg |
|
|
|
import cairosvg |
|
|
|
from io import BytesIO |
|
|
|
from io import BytesIO |
|
|
|
fakefile = BytesIO() |
|
|
|
fakefile = BytesIO() |
|
|
|