From f78f88d7a376bb7ec49ae8c3277f18020a85d198 Mon Sep 17 00:00:00 2001 From: never-eat-yellow-snow Date: Sat, 6 Jun 2015 11:07:45 +0200 Subject: [PATCH 1/3] Remove explicit open() calls on .css and .svg map files ... and therefore allow py2exe to do a better job in packaging. --- pygal/config.py | 6 +-- pygal/css/__init__.py | 0 pygal/css/{base.css => base_css.py} | 2 + pygal/css/{graph.css => graph_css.py} | 2 + pygal/css/{style.css => style_css.py} | 2 + pygal/graph/frenchmap.py | 14 +----- pygal/graph/maps/__init__.py | 0 .../{ch.cantons.svg => ch_cantons_svg.py} | 3 +- ....departments.svg => fr_departments_svg.py} | 2 + .../{fr.regions.svg => fr_regions_svg.py} | 2 + .../maps/{worldmap.svg => worldmap_svg.py} | 2 + pygal/graph/swissmap.py | 7 +-- pygal/graph/worldmap.py | 7 +-- pygal/svg.py | 48 ++++++++++--------- setup.py | 2 +- 15 files changed, 48 insertions(+), 51 deletions(-) create mode 100644 pygal/css/__init__.py rename pygal/css/{base.css => base_css.py} (99%) rename pygal/css/{graph.css => graph_css.py} (99%) rename pygal/css/{style.css => style_css.py} (99%) create mode 100644 pygal/graph/maps/__init__.py rename pygal/graph/maps/{ch.cantons.svg => ch_cantons_svg.py} (99%) rename pygal/graph/maps/{fr.departments.svg => fr_departments_svg.py} (99%) rename pygal/graph/maps/{fr.regions.svg => fr_regions_svg.py} (99%) rename pygal/graph/maps/{worldmap.svg => worldmap_svg.py} (99%) diff --git a/pygal/config.py b/pygal/config.py index a89239cb..af2ef1e9 100644 --- a/pygal/config.py +++ b/pygal/config.py @@ -23,7 +23,7 @@ from copy import deepcopy from pygal.style import Style, DefaultStyle from pygal.interpolate import INTERPOLATIONS - +from pygal.css import style_css, graph_css CONFIG_ITEMS = [] @@ -170,9 +170,9 @@ class Config(CommonConfig): DefaultStyle, Style, "Style", "Style holding values injected in css") css = Key( - ('style.css', 'graph.css'), list, "Style", + (style_css, graph_css), list, "Style", "List of css file", - "It can be an absolute file path or an external link", + "It can be an absolute file path, an external link or a python module exporting the string variable data containing the .css style", str) # Look # diff --git a/pygal/css/__init__.py b/pygal/css/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pygal/css/base.css b/pygal/css/base_css.py similarity index 99% rename from pygal/css/base.css rename to pygal/css/base_css.py index 8837ca46..f1339907 100644 --- a/pygal/css/base.css +++ b/pygal/css/base_css.py @@ -1,3 +1,4 @@ +data = """\ /* * This file is part of pygal * @@ -55,3 +56,4 @@ {{ id }}text.no_data { font-size: {{ font_sizes.no_data }}; } +""" \ No newline at end of file diff --git a/pygal/css/graph.css b/pygal/css/graph_css.py similarity index 99% rename from pygal/css/graph.css rename to pygal/css/graph_css.py index 095e7bc8..a8dc0be9 100644 --- a/pygal/css/graph.css +++ b/pygal/css/graph_css.py @@ -1,3 +1,4 @@ +data="""\ /* * This file is part of pygal * @@ -126,3 +127,4 @@ {{ id }}.tooltip text tspan.label { fill-opacity: .8; } +""" \ No newline at end of file diff --git a/pygal/css/style.css b/pygal/css/style_css.py similarity index 99% rename from pygal/css/style.css rename to pygal/css/style_css.py index 2c3f7086..8c152b37 100644 --- a/pygal/css/style.css +++ b/pygal/css/style_css.py @@ -1,3 +1,4 @@ +data="""\ /* * This file is part of pygal * @@ -139,3 +140,4 @@ {{ colors }} +""" \ No newline at end of file diff --git a/pygal/graph/frenchmap.py b/pygal/graph/frenchmap.py index 78125bb3..e4479f40 100644 --- a/pygal/graph/frenchmap.py +++ b/pygal/graph/frenchmap.py @@ -169,12 +169,7 @@ '06': u("Mayotte") } - -with open(os.path.join( - os.path.dirname(__file__), 'maps', - 'fr.departments.svg')) as file: - DPT_MAP = file.read() - +from .maps.fr_departments_svg import data as DPT_MAP class IntCodeMixin(object): def adapt_code(self, area_code): @@ -194,12 +189,7 @@ class FrenchMapDepartments(IntCodeMixin, BaseMap): kind = 'departement' svg_map = DPT_MAP - -with open(os.path.join( - os.path.dirname(__file__), 'maps', - 'fr.regions.svg')) as file: - REG_MAP = file.read() - +from .maps.fr_regions_svg import data as REG_MAP class FrenchMapRegions(IntCodeMixin, BaseMap): """French regions map""" diff --git a/pygal/graph/maps/__init__.py b/pygal/graph/maps/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pygal/graph/maps/ch.cantons.svg b/pygal/graph/maps/ch_cantons_svg.py similarity index 99% rename from pygal/graph/maps/ch.cantons.svg rename to pygal/graph/maps/ch_cantons_svg.py index 7f016ade..ee0836e2 100644 --- a/pygal/graph/maps/ch.cantons.svg +++ b/pygal/graph/maps/ch_cantons_svg.py @@ -1,3 +1,4 @@ +data = """\ @@ -88,4 +89,4 @@ - +""" diff --git a/pygal/graph/maps/fr.departments.svg b/pygal/graph/maps/fr_departments_svg.py similarity index 99% rename from pygal/graph/maps/fr.departments.svg rename to pygal/graph/maps/fr_departments_svg.py index 0d02fbdc..c2b33849 100644 --- a/pygal/graph/maps/fr.departments.svg +++ b/pygal/graph/maps/fr_departments_svg.py @@ -1,3 +1,4 @@ +data="""\ @@ -326,3 +327,4 @@ +""" \ No newline at end of file diff --git a/pygal/graph/maps/fr.regions.svg b/pygal/graph/maps/fr_regions_svg.py similarity index 99% rename from pygal/graph/maps/fr.regions.svg rename to pygal/graph/maps/fr_regions_svg.py index 046c62d7..4121d274 100644 --- a/pygal/graph/maps/fr.regions.svg +++ b/pygal/graph/maps/fr_regions_svg.py @@ -1,3 +1,4 @@ +data="""\ @@ -89,3 +90,4 @@ +""" \ No newline at end of file diff --git a/pygal/graph/maps/worldmap.svg b/pygal/graph/maps/worldmap_svg.py similarity index 99% rename from pygal/graph/maps/worldmap.svg rename to pygal/graph/maps/worldmap_svg.py index b024ce1c..4546b284 100644 --- a/pygal/graph/maps/worldmap.svg +++ b/pygal/graph/maps/worldmap_svg.py @@ -1,3 +1,4 @@ +data="""\ @@ -2408,3 +2409,4 @@ +""" \ No newline at end of file diff --git a/pygal/graph/swissmap.py b/pygal/graph/swissmap.py index 384cf054..19cc4576 100644 --- a/pygal/graph/swissmap.py +++ b/pygal/graph/swissmap.py @@ -56,12 +56,7 @@ 'kt-ge': u("Genf"), } - -with open(os.path.join( - os.path.dirname(__file__), 'maps', - 'ch.cantons.svg')) as file: - CNT_MAP = file.read() - +from .maps.ch_cantons_svg import data as CNT_MAP class SwissMapCantons(BaseMap): """Swiss Cantons map""" diff --git a/pygal/graph/worldmap.py b/pygal/graph/worldmap.py index a98a2f73..9141bb95 100644 --- a/pygal/graph/worldmap.py +++ b/pygal/graph/worldmap.py @@ -27,12 +27,7 @@ from pygal.i18n import COUNTRIES, SUPRANATIONAL import os - -with open(os.path.join( - os.path.dirname(__file__), 'maps', - 'worldmap.svg')) as file: - WORLD_MAP = file.read() - +from .maps.worldmap_svg import data as WORLD_MAP class Worldmap(BaseMap): """Worldmap graph""" diff --git a/pygal/svg.py b/pygal/svg.py index 169f33dd..e98ebb84 100644 --- a/pygal/svg.py +++ b/pygal/svg.py @@ -32,6 +32,7 @@ from math import cos, sin, pi from pygal.util import template, coord_format, minify_css from pygal import __version__ +from pygal.css import base_css class Svg(object): @@ -83,36 +84,39 @@ def add_styles(self): """Add the css to the svg""" colors = self.graph.style.get_colors(self.id) all_css = [] - for css in ['base.css'] + list(self.graph.css): - if '://' in css: + for css in [base_css] + list(self.graph.css): + if type(css) == str and '://' in css: self.processing_instructions.append( etree.PI( u('xml-stylesheet'), u('href="%s"' % css))) else: - if css.startswith('inline:'): + if type(css) == str and css.startswith('inline:'): css_text = css[len('inline:'):] else: - if not os.path.exists(css): - css = os.path.join( - os.path.dirname(__file__), 'css', css) + if type(css) == str: + 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))) + 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.style, - colors=colors, - font_sizes=fs, - id=self.id) + with io.open(css, encoding='utf-8') as f: + css_text = template( + f.read(), + style=self.graph.style, + colors=colors, + font_sizes=fs, + id=self.id) + else: + css_text = css.data if not self.graph.pretty_print: css_text = minify_css(css_text) all_css.append(css_text) diff --git a/setup.py b/setup.py index 6aab2bd6..5b0564c3 100644 --- a/setup.py +++ b/setup.py @@ -64,7 +64,7 @@ def run_tests(self): "svg", "chart", "graph", "diagram", "plot", "histogram", "kiviat"], tests_require=["pytest", "pyquery", "flask", "cairosvg"], cmdclass={'test': PyTest}, - package_data={'pygal': ['css/*', 'graph/maps/*.svg']}, + #package_data={'pygal': ['css/*', 'graph/maps/*.svg']}, extras_require={ 'lxml': ['lxml'], 'png': ['cairosvg'] From e9ddb87df67f1cd279551180c0637ea83b4e5921 Mon Sep 17 00:00:00 2001 From: "never-eat-yellow-snow@gmx.net" Date: Sat, 6 Jun 2015 19:12:34 +0200 Subject: [PATCH 2/3] fix testcases (run on linux, python3.4 with equivalent results to master branch) --- pygal/config.py | 5 ++--- pygal/svg.py | 5 ++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pygal/config.py b/pygal/config.py index af2ef1e9..f818ab26 100644 --- a/pygal/config.py +++ b/pygal/config.py @@ -23,7 +23,6 @@ from copy import deepcopy from pygal.style import Style, DefaultStyle from pygal.interpolate import INTERPOLATIONS -from pygal.css import style_css, graph_css CONFIG_ITEMS = [] @@ -170,9 +169,9 @@ class Config(CommonConfig): DefaultStyle, Style, "Style", "Style holding values injected in css") css = Key( - (style_css, graph_css), list, "Style", + ("!pygal.css.style_css", "!pygal.css.graph_css"), list, "Style", "List of css file", - "It can be an absolute file path, an external link or a python module exporting the string variable data containing the .css style", + "It can be an absolute file path or an external link", str) # Look # diff --git a/pygal/svg.py b/pygal/svg.py index e98ebb84..586525c3 100644 --- a/pygal/svg.py +++ b/pygal/svg.py @@ -27,6 +27,7 @@ import io import os import json +import importlib from datetime import date, datetime from numbers import Number from math import cos, sin, pi @@ -93,7 +94,9 @@ def add_styles(self): if type(css) == str and css.startswith('inline:'): css_text = css[len('inline:'):] else: - if type(css) == str: + if type(css) == str and css.startswith("!"): + css_text = importlib.import_module(css[1:]).data + elif type(css) == str: if not os.path.exists(css): css = os.path.join( os.path.dirname(__file__), 'css', css) From 00b851ba0ebc4cbafbfb3920fdad9e0259d171c8 Mon Sep 17 00:00:00 2001 From: never-eat-yellow-snow Date: Sun, 7 Jun 2015 14:23:42 +0200 Subject: [PATCH 3/3] add possibility for background images --- pygal/config.py | 4 ++++ pygal/graph/base.py | 2 +- pygal/graph/graph.py | 25 +++++++++++++++++++++- pygal/svg.py | 39 ++++++++++++++++++----------------- pygal/view.py | 49 +++++++++++++++++++++++--------------------- 5 files changed, 75 insertions(+), 44 deletions(-) diff --git a/pygal/config.py b/pygal/config.py index f818ab26..2ee5d0b8 100644 --- a/pygal/config.py +++ b/pygal/config.py @@ -427,6 +427,10 @@ class Config(CommonConfig): "Don't prefix css") inverse_y_axis = Key(False, bool, "Misc", "Inverse Y axis direction") + + background_image = Key(None, str, "Misc", "Provide an optional background image to the plot") + + preserve_aspect = Key(False, bool, "Misc", "Preserve aspect ratio") class SerieConfig(CommonConfig): diff --git a/pygal/graph/base.py b/pygal/graph/base.py index 9d814391..97098cb0 100644 --- a/pygal/graph/base.py +++ b/pygal/graph/base.py @@ -1,4 +1,4 @@ - # -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- # This file is part of pygal # # A python svg graph plotting library diff --git a/pygal/graph/graph.py b/pygal/graph/graph.py index 19673759..0db7dd2a 100644 --- a/pygal/graph/graph.py +++ b/pygal/graph/graph.py @@ -65,7 +65,8 @@ def _set_view(self): self.view = view_class( self.width - self.margin_box.x, self.height - self.margin_box.y, - self._box) + self._box, + self.preserve_aspect) def _make_graph(self): """Init common graph svg structure""" @@ -87,6 +88,28 @@ def _make_graph(self): x=0, y=0, width=self.view.width, height=self.view.height) + if not self.background_image is None: + corners = list(map(self.view, ( (self._box.xmin,self._box.ymin), (self._box.xmax,self._box.ymax) ) )) + + x0 = min(corners[0][0], corners[1][0]) + y0 = min(corners[0][1], corners[1][1]) + x1 = max(corners[0][0], corners[1][0]) + y1 = max(corners[0][1], corners[1][1]) + w = x1-x0 + h = y1-y0 + x0 += self._box.margin*w + y0 += self._box.margin*h + w -= self._box.margin*2*w + h -= self._box.margin*2*h + self.nodes['bgimage'] = self.svg.node( + self.nodes['plot'], + tag='image', + attrib={'xlink:href' : self.background_image}, + x=x0, + y=y0, + width=w, + height=h, + preserveAspectRatio="none") self.nodes['title'] = self.svg.node( self.nodes['graph'], class_="titles") diff --git a/pygal/svg.py b/pygal/svg.py index 47d0f02d..0a219001 100644 --- a/pygal/svg.py +++ b/pygal/svg.py @@ -95,31 +95,32 @@ def add_styles(self): css_text = css[len('inline:'):] else: if type(css) == str and css.startswith("!"): - css_text = importlib.import_module(css[1:]).data + css_raw = importlib.import_module(css[1:]).data elif type(css) == str: 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.style, - colors=colors, - font_sizes=fs, - id=self.id) + css_raw = f.read() else: - css_text = css.data + css_raw = css.data + + 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))) + + css_text = template( + css_raw, + style=self.graph.style, + colors=colors, + font_sizes=fs, + id=self.id) if not self.graph.pretty_print: css_text = minify_css(css_text) all_css.append(css_text) diff --git a/pygal/view.py b/pygal/view.py index e0a6ff58..f2aa3dbe 100644 --- a/pygal/view.py +++ b/pygal/view.py @@ -130,24 +130,31 @@ def fix(self, with_margin=True): class View(object): """Projection base class""" - def __init__(self, width, height, box): + def __init__(self, width, height, box, preserve_aspect = False): self.width = width self.height = height self.box = box self.box.fix() + self.preserve_aspect = preserve_aspect + self.scalex = self.width / self.box.width + self.scaley = self.height / self.box.height + if preserve_aspect: + print("preserve aspect", self.scalex, self.scaley) + self.scalex = min(self.scalex, self.scaley) + self.scaley = self.scalex + print(" -> ", self.scalex, self.scaley) def x(self, x): """Project x""" if x is None: return None - return self.width * (x - self.box.xmin) / self.box.width - + return self.scalex * (x - self.box.xmin) + def y(self, y): """Project y""" if y is None: return None - return (self.height - self.height * - (y - self.box.ymin) / self.box.height) + return self.height - self.scaley * (y - self.box.ymin) def __call__(self, xy): """Project x and y""" @@ -159,18 +166,14 @@ class ReverseView(View): def y(self, y): if y is None: return None - return (self.height * (y - self.box.ymin) / self.box.height) + return self.scaley * (y - self.box.ymin) class HorizontalView(View): - def __init__(self, width, height, box): + def __init__(self, width, height, box, preserve_aspect = False): self._force_vertical = None - self.width = width - self.height = height - - self.box = box - self.box.fix() - self.box.swap() + box.swap() + super(HorizontalView, self).__init__(width, height, box, preserve_aspect) def x(self, x): """Project x""" @@ -203,8 +206,8 @@ def __call__(self, rhotheta): class PolarLogView(View): """Logarithmic polar projection""" - def __init__(self, width, height, box): - super(PolarLogView, self).__init__(width, height, box) + def __init__(self, width, height, box, preserve_aspect = False): + super(PolarLogView, self).__init__(width, height, box, preserve_aspect) if not hasattr(box, '_rmin') or not hasattr(box, '_rmax'): raise Exception( 'Box must be set with set_polar_box for polar charts') @@ -232,8 +235,8 @@ def __call__(self, rhotheta): class PolarThetaView(View): """Logarithmic polar projection""" - def __init__(self, width, height, box): - super(PolarThetaView, self).__init__(width, height, box) + def __init__(self, width, height, box, preserve_aspect = False): + super(PolarThetaView, self).__init__(width, height, box, preserve_aspect) if not hasattr(box, '_tmin') or not hasattr(box, '_tmax'): raise Exception( 'Box must be set with set_polar_box for polar charts') @@ -260,8 +263,8 @@ def __call__(self, rhotheta): class PolarThetaLogView(View): """Logarithmic polar projection""" - def __init__(self, width, height, box): - super(PolarThetaLogView, self).__init__(width, height, box) + def __init__(self, width, height, box, preserve_aspect = False): + super(PolarThetaLogView, self).__init__(width, height, box, preserve_aspect) if not hasattr(box, '_tmin') or not hasattr(box, '_tmax'): raise Exception( 'Box must be set with set_polar_box for polar charts') @@ -299,7 +302,7 @@ def __call__(self, rhotheta): class LogView(View): """Logarithmic projection """ # Do not want to call the parent here - def __init__(self, width, height, box): + def __init__(self, width, height, box, preserve_aspect = False): self.width = width self.height = height self.box = box @@ -321,7 +324,7 @@ def y(self, y): class XLogView(View): """Logarithmic projection """ # Do not want to call the parent here - def __init__(self, width, height, box): + def __init__(self, width, height, box, preserve_aspect = False): self.width = width self.height = height self.box = box @@ -339,7 +342,7 @@ def x(self, x): class XYLogView(XLogView, LogView): - def __init__(self, width, height, box): + def __init__(self, width, height, box, preserve_aspect = False): self.width = width self.height = height self.box = box @@ -353,7 +356,7 @@ def __init__(self, width, height, box): class HorizontalLogView(XLogView): """Logarithmic projection """ # Do not want to call the parent here - def __init__(self, width, height, box): + def __init__(self, width, height, box, preserve_aspect = False): self._force_vertical = None self.width = width self.height = height