From 26abcc6662ea79eab33a58edeaa10c688435eedd Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Tue, 17 Feb 2015 17:00:22 +0100 Subject: [PATCH] Try to make it work without ghost. Not really possible as is. --- demo/moulinrouge/__init__.py | 3 +- demo/moulinrouge/tests.py | 49 +--- pygal/__init__.py | 42 ++- pygal/config.py | 19 -- pygal/ghost.py | 220 --------------- pygal/graph/__init__.py | 28 -- pygal/graph/base.py | 506 ++++++++++++++++++----------------- pygal/graph/frenchmap.py | 6 - pygal/graph/graph.py | 260 ++++++++++++++++-- pygal/graph/time.py | 19 +- pygal/serie.py | 2 +- pygal/state.py | 28 ++ pygal/style.py | 10 +- pygal/svg.py | 43 ++- pygal/table.py | 5 +- pygal/test/__init__.py | 17 +- pygal/test/test_config.py | 20 +- pygal/test/test_frenchmap.py | 6 +- pygal/test/test_graph.py | 12 +- pygal/util.py | 129 ++------- 20 files changed, 659 insertions(+), 765 deletions(-) delete mode 100644 pygal/ghost.py create mode 100644 pygal/state.py diff --git a/demo/moulinrouge/__init__.py b/demo/moulinrouge/__init__.py index 1c50ea4..37de447 100644 --- a/demo/moulinrouge/__init__.py +++ b/demo/moulinrouge/__init__.py @@ -20,7 +20,6 @@ from flask import Flask, render_template, Response, request import pygal from pygal.config import Config from pygal.util import cut -from pygal.graph import CHARTS_NAMES from pygal.etree import etree from pygal.style import styles, parametric_styles from base64 import ( @@ -95,7 +94,7 @@ def create_app(): 'index.jinja2', styles=styles, parametric_styles=parametric_styles, parametric_colors=( '#ff5995', '#b6e354', '#feed6c', '#8cedff', '#9e6ffe'), - links=links, charts_name=CHARTS_NAMES) + links=links, charts_name=pygal.CHARTS_NAMES) @app.route("/svg///") def svg(type, series, config): diff --git a/demo/moulinrouge/tests.py b/demo/moulinrouge/tests.py index 15e7870..d89c238 100644 --- a/demo/moulinrouge/tests.py +++ b/demo/moulinrouge/tests.py @@ -2,8 +2,8 @@ # This file is part of pygal from pygal import ( Bar, Gauge, Pyramid, Funnel, Dot, StackedBar, StackedLine, XY, - CHARTS_BY_NAME, Config, Line, DateY, Worldmap, Histogram, Box, - FrenchMap_Departments, FrenchMap_Regions, Pie, Treemap, TimeLine, DateLine) + CHARTS_BY_NAME, Config, Line, Worldmap, Histogram, Box, + FrenchMapDepartments, FrenchMapRegions, Pie, Treemap, TimeLine, DateLine) from pygal.style import styles, Style, RotateStyle from pygal.colors import rotate from pygal.graph.frenchmap import DEPARTMENTS, REGIONS @@ -225,12 +225,6 @@ def get_test_routes(app): graph.add('Single', [(1, 1)]) return graph.render_response() - @app.route('/test/datey_single') - def test_datey_single(): - graph = DateY(interpolate='cubic') - graph.add('Single', [(datetime.now(), 1)]) - return graph.render_response() - @app.route('/test/no_data/at_all/') def test_no_data_at_all_for(chart): graph = CHARTS_BY_NAME[chart]() @@ -287,6 +281,7 @@ def get_test_routes(app): bar = Bar() bar.add('1', [1, 2, 3]) bar.add('2', [4, 5, 6]) + bar.x_labels = ['a'] return bar.render_response() @app.route('/test/histogram') @@ -390,35 +385,7 @@ def get_test_routes(app): datey.x_label_rotation = 25 return datey.render_response() - @app.route('/test/datey') - def test_datey(): - from datetime import datetime - datey = DateY(show_dots=False) - datey.add('1', [ - (datetime(2011, 12, 21), 10), - (datetime(2014, 4, 8), 12), - (datetime(2010, 2, 28), 2) - ]) - datey.add('2', [(12, 4), (219, 8), (928, 6)]) - datey.x_label_rotation = 25 - return datey.render_response() - - @app.route('/test/datexy') - def test_datexy(): - from datetime import datetime, date, timedelta - datey = DateY() - datey.add('1', [ - (datetime(2011, 12, 21), 10), - (datetime(2014, 4, 8), 12), - (datetime(2010, 2, 28), 2) - ]) - datey.add('2', map( - lambda t: (date.today() + timedelta(days=t[0]), t[1]), - [(12, 4), (219, 8), (928, 6)])) - datey.x_label_rotation = 25 - return datey.render_response() - - @app.route('/test/timexy') + @app.route('/test/timeline') def test_timexy(): from datetime import time datey = TimeLine() @@ -427,8 +394,8 @@ def get_test_routes(app): (time(21, 2, 29), 10), (time(12, 30, 59), 7) ]) - datey.add('2', - [(time(12, 12, 12), 4), (time(), 8), (time(23, 59, 59), 6)]) + datey.add( + '2', [(time(12, 12, 12), 4), (time(), 8), (time(23, 59, 59), 6)]) datey.x_label_rotation = 25 return datey.render_response() @@ -458,7 +425,7 @@ def get_test_routes(app): @app.route('/test/frenchmapdepartments') def test_frenchmapdepartments(): - fmap = FrenchMap_Departments(style=choice(list(styles.values()))) + fmap = FrenchMapDepartments(style=choice(list(styles.values()))) for i in range(10): fmap.add('s%d' % i, [ (choice(list(DEPARTMENTS.keys())), randint(0, 100)) @@ -478,7 +445,7 @@ def get_test_routes(app): @app.route('/test/frenchmapregions') def test_frenchmapregions(): - fmap = FrenchMap_Regions(style=choice(list(styles.values()))) + fmap = FrenchMapRegions(style=choice(list(styles.values()))) for i in range(10): fmap.add('s%d' % i, [ (choice(list(REGIONS.keys())), randint(0, 100)) diff --git a/pygal/__init__.py b/pygal/__init__.py index 0d16844..f37110e 100644 --- a/pygal/__init__.py +++ b/pygal/__init__.py @@ -21,20 +21,36 @@ Pygal - A python svg graph plotting library """ -__version__ = '1.7.0' -import sys -from pygal.config import Config -from pygal.ghost import Ghost, REAL_CHARTS +__version__ = '1.9.9' -CHARTS = [] -CHARTS_BY_NAME = {} +from pygal.graph.bar import Bar +from pygal.graph.box import Box +from pygal.graph.dot import Dot +from pygal.graph.frenchmap import FrenchMapDepartments, FrenchMapRegions +from pygal.graph.funnel import Funnel +from pygal.graph.gauge import Gauge +from pygal.graph.histogram import Histogram +from pygal.graph.horizontalbar import HorizontalBar +from pygal.graph.horizontalstackedbar import HorizontalStackedBar +from pygal.graph.line import Line +from pygal.graph.pie import Pie +from pygal.graph.pyramid import Pyramid +from pygal.graph.radar import Radar +from pygal.graph.stackedbar import StackedBar +from pygal.graph.stackedline import StackedLine +from pygal.graph.supranationalworldmap import SupranationalWorldmap +from pygal.graph.time import DateLine, DateTimeLine, TimeLine, TimeDeltaLine +from pygal.graph.treemap import Treemap +from pygal.graph.verticalpyramid import VerticalPyramid +from pygal.graph.worldmap import Worldmap +from pygal.graph.xy import XY +from pygal.graph.graph import Graph +from pygal.config import Config -for NAME in REAL_CHARTS.keys(): - _CHART = type(NAME, (Ghost,), {}) - CHARTS.append(_CHART) - CHARTS_BY_NAME[NAME] = _CHART - setattr(sys.modules[__name__], NAME, _CHART) +CHARTS_BY_NAME = dict( + [(k, v) for k, v in locals().items() + if isinstance(v, type) and issubclass(v, Graph) and v != Graph]) -__all__ = list(CHARTS_BY_NAME.keys()) + [ - Config.__name__, 'CHARTS', 'CHARTS_BY_NAME'] +CHARTS_NAMES = list(CHARTS_BY_NAME.keys()) +CHARTS = list(CHARTS_BY_NAME.values()) diff --git a/pygal/config.py b/pygal/config.py index 9e91fad..9713bf3 100644 --- a/pygal/config.py +++ b/pygal/config.py @@ -25,9 +25,6 @@ from pygal.style import Style, DefaultStyle from pygal.interpolate import INTERPOLATIONS -class FontSizes(object): - """Container for font sizes""" - CONFIG_ITEMS = [] @@ -127,18 +124,6 @@ class BaseConfig(MetaConfig('ConfigBase', (object,), {})): dict([(k, v) for (k, v) in kwargs.items() if not k.startswith('_') and k in dir(self)])) - def font_sizes(self, with_unit=True): - """Getter for all font size configs""" - fs = FontSizes() - for name in dir(self): - if name.endswith('_font_size'): - setattr( - fs, - name.replace('_font_size', ''), - ('%dpx' % getattr(self, name)) - if with_unit else getattr(self, name)) - return fs - def to_dict(self): config = {} for attr in dir(self): @@ -321,10 +306,6 @@ class Config(CommonConfig): y_label_rotation = Key( 0, int, "Label", "Specify y labels rotation angles", "in degrees") - x_label_format = Key( - "%Y-%m-%d %H:%M:%S.%f", str, "Label", - "Date format for strftime to display the DateY X labels") - missing_value_fill_truncation = Key( "x", str, "Look", "Filled series with missing x and/or y values at the end of a series " diff --git a/pygal/ghost.py b/pygal/ghost.py deleted file mode 100644 index 9d43e64..0000000 --- a/pygal/ghost.py +++ /dev/null @@ -1,220 +0,0 @@ -# -*- coding: utf-8 -*- -# This file is part of pygal -# -# A python svg graph plotting library -# Copyright © 2012-2014 Kozea -# -# This library is free software: you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) any -# later version. -# -# This library is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with pygal. If not, see . -""" -Ghost container - -It is used to delegate rendering to real objects but keeping config in place - -""" - -from __future__ import division -import io -import sys -from pygal._compat import u, is_list_like -from pygal.graph import CHARTS_NAMES -from pygal.config import Config, CONFIG_ITEMS -from pygal.util import prepare_values -from uuid import uuid4 - - -class ChartCollection(object): - pass - - -REAL_CHARTS = { - 'DateY': 'pygal.graph.time.DateY', - 'DateTimeLine': 'pygal.graph.time.DateTimeLine', - 'DateLine': 'pygal.graph.time.DateLine', - 'TimeLine': 'pygal.graph.time.TimeLine', - 'TimeDeltaLine': 'pygal.graph.time.TimeDeltaLine' -} - -for NAME in CHARTS_NAMES: - if NAME in REAL_CHARTS: - mod_name = 'pygal.graph.time' - else: - mod_name = 'pygal.graph.%s' % NAME.lower() - - __import__(mod_name) - mod = sys.modules[mod_name] - chart = getattr(mod, NAME) - if issubclass(chart, ChartCollection): - for name, chart in chart.__dict__.items(): - if name.startswith('_'): - continue - REAL_CHARTS['%s_%s' % (NAME, name)] = chart - else: - REAL_CHARTS[NAME] = chart - - -class Ghost(object): - - def __init__(self, config=None, **kwargs): - """Init config""" - name = self.__class__.__name__ - self.cls = REAL_CHARTS[name] - self.uuid = str(uuid4()) - if config and isinstance(config, type): - config = config() - - if config: - config = config.copy() - else: - config = Config() - - config(**kwargs) - self.config = config - self.raw_series = [] - self.raw_series2 = [] - self.xml_filters = [] - - 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 make_series(self, series, offset=0): - return prepare_values(series, self.config, self.cls, offset) - - def make_instance(self, overrides=None): - for conf_key in CONFIG_ITEMS: - if conf_key.is_list: - if getattr(self, conf_key.name, None): - setattr(self, conf_key.name, - list(getattr(self, conf_key.name))) - - self.config(**self.__dict__) - self.config.__dict__.update(overrides or {}) - series = self.make_series(self.raw_series) - secondary_series = self.make_series( - self.raw_series2, len(series or [])) - self._last__inst = self.cls( - self.config, series, secondary_series, self.uuid, - self.xml_filters) - return self._last__inst - - # Rendering - def render(self, is_unicode=False, **kwargs): - return (self - .make_instance(overrides=kwargs) - .render(is_unicode=is_unicode)) - - def render_tree(self, **kwargs): - return self.make_instance(overrides=kwargs).render_tree() - - 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') - real_cls, self.cls = self.cls, Table - rv = self.make_instance().render(**kwargs) - self.cls = real_cls - return rv - - 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.make_instance(spark_options).render() - - 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() diff --git a/pygal/graph/__init__.py b/pygal/graph/__init__.py index 0a05b2a..c8a3583 100644 --- a/pygal/graph/__init__.py +++ b/pygal/graph/__init__.py @@ -20,31 +20,3 @@ Graph modules """ - -CHARTS_NAMES = [ - 'Line', - 'StackedLine', - 'XY', - 'Bar', - 'HorizontalBar', - 'StackedBar', - 'HorizontalStackedBar', - 'Pie', - 'Radar', - 'Funnel', - 'Pyramid', - 'VerticalPyramid', - 'Dot', - 'Gauge', - 'Worldmap', - 'SupranationalWorldmap', - 'Histogram', - 'Box', - 'FrenchMap', - 'Treemap', - 'DateY', - 'DateTimeLine', - 'DateLine', - 'TimeLine', - 'TimeDeltaLine' -] diff --git a/pygal/graph/base.py b/pygal/graph/base.py index fd4b131..2ac28a4 100644 --- a/pygal/graph/base.py +++ b/pygal/graph/base.py @@ -22,12 +22,18 @@ Base for pygal charts """ from __future__ import division +from pygal._compat import u, is_list_like, to_unicode from pygal.view import Margin, Box -from pygal.util import ( - get_text_box, get_texts_box, cut, rad, humanize, truncate, split_title) +from pygal.config import Config +from pygal.state import State +from pygal.util import compose, ident from pygal.svg import Svg -from pygal.util import cached_property, majorize -from math import sin, cos, sqrt, ceil +from pygal.serie import Serie +from pygal.config import SerieConfig +from pygal.adapters import ( + not_zero, positive, decimal_to_float) +from functools import reduce +from uuid import uuid4 class BaseGraph(object): @@ -35,14 +41,149 @@ class BaseGraph(object): _adapters = [] - def __init__(self, config, series, secondary_series, uuid, xml_filters): - """Init the graph""" - self.uuid = uuid - self.__dict__.update(config.to_dict()) + 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.series = series or [] - self.secondary_series = secondary_series or [] - self.xml_filters = xml_filters or [] + 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 import Worldmap, FrenchMapDepartments, Histogram + if self.x_labels is not None: + self.x_labels = list(map(to_unicode, self.x_labels)) + if self.zero == 0 and isinstance( + self, (Worldmap, FrenchMapDepartments)): + 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, (Worldmap, FrenchMapDepartments)): + 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, (Worldmap, FrenchMapDepartments)): + value = (adapter(value[0]), value[1]) + else: + value = list(map(adapter, value)) + else: + value = adapter(value) + + values.append(value) + serie_config = SerieConfig() + serie_config(**{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): + """Init the graph""" + self.state = State(self) + 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 @@ -50,22 +191,18 @@ class BaseGraph(object): 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.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) + [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 @@ -74,237 +211,17 @@ class BaseGraph(object): 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 teardown(self): + del self.state + self.state = None - 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): + def render(self, is_unicode=False, **kwargs): """Render the graph, and return the svg string""" - return self.svg.render( + self.setup() + 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""" @@ -312,3 +229,94 @@ class BaseGraph(object): for f in self.xml_filters: svg = f(svg) 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 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.make_instance(spark_options).render() + + 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""" diff --git a/pygal/graph/frenchmap.py b/pygal/graph/frenchmap.py index 6cffd17..f70a2d1 100644 --- a/pygal/graph/frenchmap.py +++ b/pygal/graph/frenchmap.py @@ -23,7 +23,6 @@ Worldmap chart from __future__ import division from collections import defaultdict -from pygal.ghost import ChartCollection from pygal.util import cut, cached_property, decorate from pygal.graph.graph import Graph from pygal._compat import u @@ -282,11 +281,6 @@ class FrenchMapRegions(FrenchMapDepartments): kind = 'region' -class FrenchMap(ChartCollection): - Regions = FrenchMapRegions - Departments = FrenchMapDepartments - - DEPARTMENTS_REGIONS = { "01": "82", "02": "22", diff --git a/pygal/graph/graph.py b/pygal/graph/graph.py index 5a26986..b695b5c 100644 --- a/pygal/graph/graph.py +++ b/pygal/graph/graph.py @@ -26,8 +26,10 @@ from pygal.interpolate import INTERPOLATIONS from pygal.graph.base import BaseGraph from pygal.view import View, LogView, XYLogView from pygal.util import ( - truncate, reverse_text_len, get_texts_box, cut, rad, decorate) -from math import sqrt, ceil, cos + cached_property, majorize, humanize, split_title, + truncate, reverse_text_len, get_text_box, get_texts_box, cut, rad, + decorate) +from math import sqrt, ceil, cos, sin from itertools import repeat, chain @@ -41,9 +43,9 @@ class Graph(BaseGraph): self._make_graph() self._axes() self._legend() - self._title() - self._x_title() - self._y_title() + self._make_title() + self._make_x_title() + self._make_y_title() def _axes(self): """Draw axes""" @@ -350,7 +352,7 @@ class Graph(BaseGraph): width=self.legend_box_size, height=self.legend_box_size, class_="color-%d reactive" % ( - global_serie_number % len(self.style['colors'])) + global_serie_number % len(self.style.colors)) ) if isinstance(title, dict): @@ -369,22 +371,22 @@ class Graph(BaseGraph): if truncated != title: self.svg.node(legend, 'title').text = title - def _title(self): + def _make_title(self): """Make the title""" - if self.title: - for i, title_line in enumerate(self.title, 1): + if self._title: + for i, title_line in enumerate(self._title, 1): self.svg.node( self.nodes['title'], 'text', class_='title plot_title', x=self.width / 2, y=i * (self.title_font_size + self.spacing) ).text = title_line - def _x_title(self): + def _make_x_title(self): """Make the X-Axis title""" y = (self.height - self.margin.bottom + self._x_labels_height) - if self.x_title: - for i, title_line in enumerate(self.x_title, 1): + if self._x_title: + for i, title_line in enumerate(self._x_title, 1): text = self.svg.node( self.nodes['title'], 'text', class_='title', x=self.margin.left + self.view.width / 2, @@ -392,11 +394,11 @@ class Graph(BaseGraph): ) text.text = title_line - def _y_title(self): + def _make_y_title(self): """Make the Y-Axis title""" - if self.y_title: + if self._y_title: yc = self.margin.top + self.view.height / 2 - for i, title_line in enumerate(self.y_title, 1): + for i, title_line in enumerate(self._y_title, 1): text = self.svg.node( self.nodes['title'], 'text', class_='title', x=self._legend_at_left_width, @@ -488,3 +490,231 @@ class Graph(BaseGraph): def _post_compute(self): pass + + @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.x_value_formatter or ( + humanize if self.human_readable else str) + + @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 _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) diff --git a/pygal/graph/time.py b/pygal/graph/time.py index f273840..2c10f60 100644 --- a/pygal/graph/time.py +++ b/pygal/graph/time.py @@ -57,8 +57,8 @@ class DateTimeLine(XY): """Return the value formatter for this graph""" def datetime_to_str(x): dt = datetime.fromtimestamp(x) - if self.config.x_value_formatter: - return self.config.x_value_formatter(dt) + if self.x_value_formatter: + return self.x_value_formatter(dt) return dt.isoformat() return datetime_to_str @@ -70,8 +70,8 @@ class DateLine(DateTimeLine): """Return the value formatter for this graph""" def date_to_str(x): d = date.fromtimestamp(x) - if self.config.x_value_formatter: - return self.config.x_value_formatter(d) + if self.x_value_formatter: + return self.x_value_formatter(d) return d.isoformat() return date_to_str @@ -84,8 +84,8 @@ class TimeLine(DateTimeLine): """Return the value formatter for this graph""" def date_to_str(x): t = datetime.fromtimestamp(x).time() - if self.config.x_value_formatter: - return self.config.x_value_formatter(t) + if self.x_value_formatter: + return self.x_value_formatter(t) return t.isoformat() return date_to_str @@ -98,11 +98,8 @@ class TimeDeltaLine(XY): """Return the value formatter for this graph""" def timedelta_to_str(x): td = timedelta(seconds=x) - if self.config.x_value_formatter: - return self.config.x_value_formatter(td) + if self.x_value_formatter: + return self.x_value_formatter(td) return str(td) return timedelta_to_str - -# Old pygal compat -DateY = DateTimeLine diff --git a/pygal/serie.py b/pygal/serie.py index 7e06021..8a5d2a1 100644 --- a/pygal/serie.py +++ b/pygal/serie.py @@ -30,7 +30,7 @@ class Serie(object): self.title = title self.values = values self.config = config - self.__dict__.update(config.to_dict()) + self.__dict__.update(config.__dict__) self.metadata = metadata or {} @cached_property diff --git a/pygal/state.py b/pygal/state.py new file mode 100644 index 0000000..a6545d9 --- /dev/null +++ b/pygal/state.py @@ -0,0 +1,28 @@ + # -*- coding: utf-8 -*- +# This file is part of pygal +# +# A python svg graph plotting library +# Copyright © 2012-2014 Kozea +# +# This library is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with pygal. If not, see . +""" +Class holding state during render + +""" + + +class State(object): + def __init__(self, graph): + self.__dict__.update(**graph.config.__dict__) + self.__dict__.update(**graph.__dict__) diff --git a/pygal/style.py b/pygal/style.py index 59157a0..592c517 100644 --- a/pygal/style.py +++ b/pygal/style.py @@ -28,6 +28,7 @@ import re re_dasharray_delimiters = re.compile(r'[\.|,|x|\||\- ]+', re.I) + class Style(object): """Styling class containing colors for the css generation""" def __init__( @@ -42,7 +43,7 @@ class Style(object): opacity_hover='.9', stroke_width='1', stroke_style='round', - stroke_dasharray=(0,0), + stroke_dasharray=(0, 0), transition='250ms', colors=( '#ff5995', '#b6e354', '#feed6c', '#8cedff', '#9e6ffe', @@ -79,11 +80,12 @@ class Style(object): self.stroke_dasharray = '%d,%d' % self.stroke_dasharray if isinstance(self.stroke_dasharray, str): - self.stroke_dasharray = re.sub(re_dasharray_delimiters, ',', self.stroke_dasharray) + self.stroke_dasharray = re.sub( + re_dasharray_delimiters, ',', self.stroke_dasharray) if not isinstance(self.stroke_dasharray, str): - raise ValueError('stroke_dasharray not in proper form: tuple(int,int)') - + raise ValueError( + 'stroke_dasharray not in proper form: tuple(int, int)') def get_colors(self, prefix): """Get the css color list""" diff --git a/pygal/svg.py b/pygal/svg.py index 94d4f3f..3e223d5 100644 --- a/pygal/svg.py +++ b/pygal/svg.py @@ -81,7 +81,7 @@ class Svg(object): def add_styles(self): """Add the css to the svg""" - colors = self.graph.config.style.get_colors(self.id) + colors = self.graph.style.get_colors(self.id) all_css = [] for css in ['base.css'] + list(self.graph.css): if '://' in css: @@ -95,12 +95,23 @@ class Svg(object): if not os.path.exists(css): css = os.path.join( os.path.dirname(__file__), 'css', css) + + class FontSizes(object): + """Container for font sizes""" + fs = FontSizes() + for name in dir(self.graph.state): + if name.endswith('_font_size'): + setattr( + fs, + name.replace('_font_size', ''), + ('%dpx' % getattr(self.graph, name))) + with io.open(css, encoding='utf-8') as f: css_text = template( f.read(), - style=self.graph.config.style, + style=self.graph.style, colors=colors, - font_sizes=self.graph.config.font_sizes(), + font_sizes=fs, id=self.id) if not self.graph.pretty_print: css_text = minify_css(css_text) @@ -111,13 +122,23 @@ class Svg(object): def add_scripts(self): """Add the js to the svg""" common_script = self.node(self.defs, 'script', type='text/javascript') + + def get_js_dict(): + return dict((k, getattr(self.graph, k)) for k in dir(self) + if not k.startswith('_') and + k in dir(self.graph.config)) + + def json_default(o): + if isinstance(o, (datetime, date)): + return o.isoformat() + if hasattr(o, 'to_dict'): + o = o.to_dict() + print(o) + return json.JSONEncoder().default(o) + common_script.text = " = ".join( ("window.config", json.dumps( - self.graph.config.to_dict(), - default=lambda o: ( - o.isoformat() if isinstance(o, (datetime, date)) - else json.JSONEncoder().default(o)) - ))) + get_js_dict(), default=json_default))) for js in self.graph.js: if '://' in js: @@ -174,17 +195,17 @@ class Svg(object): self.graph.nodes['plot'], class_='series serie-%d color-%d' % ( serie.index, serie.index % len( - self.graph.style['colors']))), + self.graph.style.colors))), overlay=self.node( self.graph.nodes['overlay'], class_='series serie-%d color-%d' % ( serie.index, serie.index % len( - self.graph.style['colors']))), + self.graph.style.colors))), text_overlay=self.node( self.graph.nodes['text_overlay'], class_='series serie-%d color-%d' % ( serie.index, serie.index % len( - self.graph.style['colors'])))) + self.graph.style.colors)))) def line(self, node, coords, close=False, **kwargs): """Draw a svg line""" diff --git a/pygal/table.py b/pygal/table.py index 6d8d2cf..3466bab 100644 --- a/pygal/table.py +++ b/pygal/table.py @@ -35,14 +35,13 @@ class HTML(object): class Table(BaseGraph): _dual = None - def __init__(self, config, series, secondary_series, uuid, xml_filters): + def __init__(self, chart, series, secondary_series, uuid, xml_filters): "Init the table" self.uuid = uuid self.series = series or [] self.secondary_series = secondary_series or [] self.xml_filters = xml_filters or [] - self.__dict__.update(config.to_dict()) - self.config = config + self.__dict__.update(chart.state) def render(self, total=False, transpose=False, style=False): html = HTML() diff --git a/pygal/test/__init__.py b/pygal/test/__init__.py index 312583b..d4de7ac 100644 --- a/pygal/test/__init__.py +++ b/pygal/test/__init__.py @@ -19,7 +19,6 @@ import pygal from pygal.util import cut -from datetime import datetime from pygal.i18n import COUNTRIES from pygal.graph.frenchmap import DEPARTMENTS, REGIONS from decimal import Decimal @@ -34,12 +33,12 @@ def get_data(i): def adapt(chart, data): - if isinstance(chart, pygal.DateY): - # Convert to a credible datetime - return list(map( - lambda t: - (datetime.fromtimestamp(1360000000 + t[0] * 987654) - if t[0] is not None else None, t[1]), data)) + # if isinstance(chart, pygal.DateY): + # # Convert to a credible datetime + # return list(map( + # lambda t: + # (datetime.fromtimestamp(1360000000 + t[0] * 987654) + # if t[0] is not None else None, t[1]), data)) if isinstance(chart, pygal.XY): return data @@ -51,13 +50,13 @@ def adapt(chart, data): COUNTRIES.keys())[ int(x) % len(COUNTRIES)] if x is not None else None, data)) - elif isinstance(chart, pygal.FrenchMap_Regions): + elif isinstance(chart, pygal.FrenchMapRegions): return list( map(lambda x: list( REGIONS.keys())[ int(x) % len(REGIONS)] if x is not None else None, data)) - elif isinstance(chart, pygal.FrenchMap_Departments): + elif isinstance(chart, pygal.FrenchMapDepartments): return list( map(lambda x: list( DEPARTMENTS.keys())[ diff --git a/pygal/test/test_config.py b/pygal/test/test_config.py index d773ea9..422f4ab 100644 --- a/pygal/test/test_config.py +++ b/pygal/test/test_config.py @@ -20,8 +20,8 @@ from pygal import ( Line, Dot, Pie, Treemap, Radar, Config, Bar, Funnel, Worldmap, SupranationalWorldmap, Histogram, Gauge, Box, XY, - Pyramid, DateY, HorizontalBar, HorizontalStackedBar, - FrenchMap_Regions, FrenchMap_Departments, + Pyramid, HorizontalBar, HorizontalStackedBar, + FrenchMapRegions, FrenchMapDepartments, DateTimeLine, TimeLine, DateLine, TimeDeltaLine) from pygal._compat import u from pygal.test.utils import texts @@ -275,9 +275,9 @@ def test_include_x_axis(Chart): chart = Chart() if Chart in (Pie, Treemap, Radar, Funnel, Dot, Gauge, Worldmap, SupranationalWorldmap, Histogram, Box, - FrenchMap_Regions, FrenchMap_Departments): + FrenchMapRegions, FrenchMapDepartments): return - if not chart.cls._dual: + if not chart._dual: data = 100, 200, 150 else: data = (1, 100), (3, 200), (2, 150) @@ -285,8 +285,8 @@ def test_include_x_axis(Chart): q = chart.render_pyquery() # Ghost thing yaxis = ".axis.%s .guides text" % ( - 'y' if not chart._last__inst.horizontal else 'x') - if not issubclass(chart.cls, Bar().cls): + 'y' if not getattr(chart, 'horizontal', False) else 'x') + if not isinstance(chart, Bar): assert '0.0' not in q(yaxis).map(texts) else: assert '0.0' in q(yaxis).map(texts) @@ -362,8 +362,8 @@ def test_x_label_major(Chart): if Chart in ( Pie, Treemap, Funnel, Dot, Gauge, Worldmap, SupranationalWorldmap, Histogram, Box, - FrenchMap_Regions, FrenchMap_Departments, - Pyramid, DateY, DateTimeLine, TimeLine, DateLine, + FrenchMapRegions, FrenchMapDepartments, + Pyramid, DateTimeLine, TimeLine, DateLine, TimeDeltaLine): return chart = Chart() @@ -407,10 +407,10 @@ def test_y_label_major(Chart): if Chart in ( Pie, Treemap, Funnel, Dot, Gauge, Worldmap, SupranationalWorldmap, Histogram, Box, - FrenchMap_Regions, FrenchMap_Departments, + FrenchMapRegions, FrenchMapDepartments, HorizontalBar, HorizontalStackedBar, Pyramid, DateTimeLine, TimeLine, DateLine, - TimeDeltaLine, DateY): + TimeDeltaLine): return chart = Chart() data = range(12) diff --git a/pygal/test/test_frenchmap.py b/pygal/test/test_frenchmap.py index 1cf9c72..19b917f 100644 --- a/pygal/test/test_frenchmap.py +++ b/pygal/test/test_frenchmap.py @@ -18,7 +18,7 @@ # along with pygal. If not, see . from pygal import ( - FrenchMap_Regions, FrenchMap_Departments) + FrenchMapRegions, FrenchMapDepartments) from pygal.graph.frenchmap import REGIONS, DEPARTMENTS, aggregate_regions @@ -27,14 +27,14 @@ def test_frenchmaps(): for dept in DEPARTMENTS.keys(): datas[dept] = int(''.join([x for x in dept if x.isdigit()])) * 10 - fmap = FrenchMap_Departments() + fmap = FrenchMapDepartments() fmap.add('departements', datas) q = fmap.render_pyquery() assert len( q('#departements .departement,#dom-com .departement') ) == len(DEPARTMENTS) - fmap = FrenchMap_Regions() + fmap = FrenchMapRegions() fmap.add('regions', aggregate_regions(datas)) q = fmap.render_pyquery() assert len(q('#regions .region,#dom-com .region')) == len(REGIONS) diff --git a/pygal/test/test_graph.py b/pygal/test/test_graph.py index 1d223ee..309a08b 100644 --- a/pygal/test/test_graph.py +++ b/pygal/test/test_graph.py @@ -82,9 +82,9 @@ def test_metadata(Chart): v = list(map(lambda x: (x, x + 1), v)) elif Chart == pygal.Worldmap or Chart == pygal.SupranationalWorldmap: v = [(i, k) for k, i in enumerate(i18n.COUNTRIES.keys())] - elif Chart == pygal.FrenchMap_Regions: + elif Chart == pygal.FrenchMapRegions: v = [(i, k) for k, i in enumerate(REGIONS.keys())] - elif Chart == pygal.FrenchMap_Departments: + elif Chart == pygal.FrenchMapDepartments: v = [(i, k) for k, i in enumerate(DEPARTMENTS.keys())] chart.add('Serie with metadata', [ @@ -110,7 +110,7 @@ def test_metadata(Chart): assert len(v) - 1 == len(q('.tooltip-trigger').siblings('.value')) elif Chart not in ( pygal.Worldmap, pygal.SupranationalWorldmap, - pygal.FrenchMap_Regions, pygal.FrenchMap_Departments): + pygal.FrenchMapRegions, pygal.FrenchMapDepartments): # Tooltip are not working on maps assert len(v) == len(q('.tooltip-trigger').siblings('.value')) @@ -192,8 +192,8 @@ def test_values_by_dict(Chart): if not issubclass(Chart, ( pygal.Worldmap, - pygal.FrenchMap_Departments, - pygal.FrenchMap_Regions)): + pygal.FrenchMapDepartments, + pygal.FrenchMapRegions)): chart1.add('A', {'red': 10, 'green': 12, 'blue': 14}) chart1.add('B', {'green': 11, 'red': 7}) chart1.add('C', {'blue': 7}) @@ -376,7 +376,7 @@ def test_labels_with_links(Chart): q = chart.render_pyquery() links = q('a') - if issubclass(chart.cls, + if isinstance(chart, (pygal.graph.worldmap.Worldmap, pygal.graph.frenchmap.FrenchMapDepartments)): # No country is found in this case so: diff --git a/pygal/util.py b/pygal/util.py index 2e9521f..563fda7 100644 --- a/pygal/util.py +++ b/pygal/util.py @@ -26,9 +26,6 @@ import re from decimal import Decimal from math import floor, pi, log, log10, ceil from itertools import cycle -from functools import reduce -from pygal.adapters import ( - not_zero, positive, decimal_to_float) ORDERS = u("yzafpnµm kMGTPEZY") @@ -272,21 +269,21 @@ def truncate(string, index): string = string[:index - 1] + u('…') return string - -# Stolen from brownie http://packages.python.org/Brownie/ -class cached_property(object): - """Optimize a static property""" - def __init__(self, getter, doc=None): - self.getter = getter - self.__module__ = getter.__module__ - self.__name__ = getter.__name__ - self.__doc__ = doc or getter.__doc__ - - def __get__(self, obj, type_=None): - if obj is None: - return self - value = obj.__dict__[self.__name__] = self.getter(obj) - return value +cached_property = property +# # Stolen from brownie http://packages.python.org/Brownie/ +# class cached_property(object): +# """Optimize a static property""" +# def __init__(self, getter, doc=None): +# self.getter = getter +# self.__module__ = getter.__module__ +# self.__name__ = getter.__name__ +# self.__doc__ = doc or getter.__doc__ + +# def __get__(self, obj, type_=None): +# if obj is None: +# return self +# value = obj.__dict__[self.__name__] = self.getter(obj) +# return value css_comments = re.compile(r'/\*.*?\*/', re.MULTILINE | re.DOTALL) @@ -331,102 +328,6 @@ def safe_enumerate(iterable): yield i, v -def prepare_values(raw, config, cls, offset=0): - """Prepare the values to start with sane values""" - from pygal.serie import Serie - from pygal.config import SerieConfig - from pygal.graph.time import DateY - from pygal.graph.histogram import Histogram - from pygal.graph.worldmap import Worldmap - from pygal.graph.frenchmap import FrenchMapDepartments - if config.x_labels is None and hasattr(cls, 'x_labels'): - config.x_labels = list(map(to_unicode, cls.x_labels)) - if config.zero == 0 and issubclass(cls, (Worldmap, FrenchMapDepartments)): - config.zero = 1 - - for key in ('x_labels', 'y_labels'): - if getattr(config, key): - setattr(config, key, list(getattr(config, key))) - if not raw: - return - - adapters = list(cls._adapters) or [lambda x:x] - if config.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 config.strict else ident - x_adapter = reduce( - compose, cls._x_adapters) if getattr( - cls, '_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(config.x_labels or [])]) - - for title, raw_values, serie_config_kwargs in raw: - metadata = {} - values = [] - if isinstance(raw_values, dict): - if issubclass(cls, (Worldmap, FrenchMapDepartments)): - raw_values = list(raw_values.items()) - else: - value_list = [None] * width - for k, v in raw_values.items(): - if k in config.x_labels: - value_list[config.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 issubclass(cls, Histogram): - if value is None: - value = (None, None, None) - elif not is_list_like(value): - value = (value, config.zero, config.zero) - value = list(map(adapter, value)) - elif cls._dual: - if value is None: - value = (None, None) - elif not is_list_like(value): - value = (value, config.zero) - if x_adapter: - value = (x_adapter(value[0]), adapter(value[1])) - if issubclass( - cls, (Worldmap, FrenchMapDepartments)): - value = (adapter(value[0]), value[1]) - else: - value = list(map(adapter, value)) - else: - value = adapter(value) - - values.append(value) - serie_config = SerieConfig() - serie_config(**config.to_dict()) - serie_config(**serie_config_kwargs) - series.append( - Serie(offset + len(series), title, values, serie_config, metadata)) - return series - - def split_title(title, width, title_fs): titles = [] if not title: