From 443ca2f5b0fb08faa9c5eb73073d6028fa527f00 Mon Sep 17 00:00:00 2001 From: rahul-deepsource Date: Tue, 1 Sep 2020 20:47:50 +0530 Subject: [PATCH 1/7] Add ability to embed image to right (or) plot a trend line --- pybadges/__init__.py | 23 +++++++---- pybadges/__main__.py | 66 ++++++++++++++++++++++++++++--- pybadges/badge-template-full.svg | 19 ++++++--- pybadges/trend.py | 67 ++++++++++++++++++++++++++++++++ setup.py | 1 + 5 files changed, 158 insertions(+), 18 deletions(-) create mode 100644 pybadges/trend.py diff --git a/pybadges/__init__.py b/pybadges/__init__.py index e3c6d61..5f16c4a 100644 --- a/pybadges/__init__.py +++ b/pybadges/__init__.py @@ -119,13 +119,16 @@ def badge( right_link: Optional[str] = None, whole_link: Optional[str] = None, logo: Optional[str] = None, - left_color: str = '#555', + bg_color: str = '#555', + left_color: Optional[str] = None, right_color: str = '#007ec6', measurer: Optional[text_measurer.TextMeasurer] = None, embed_logo: bool = False, whole_title: Optional[str] = None, left_title: Optional[str] = None, right_title: Optional[str] = None, + right_image: Optional[str] = None, + embed_right_image: bool = False, ) -> str: """Creates a github-style badge as an SVG image. @@ -148,16 +151,13 @@ def badge( selected. If set then left_link and right_right may not be set. logo: A url representing a logo that will be displayed inside the badge. Can be a data URL e.g. "data:image/svg+xml;utf8, {% if whole_title %} @@ -16,7 +17,7 @@ - + {% if left_title %} {{ left_title }} {% endif %} @@ -26,20 +27,26 @@ {{ right_title }} {% endif %} + {% if left_color %} + + {% endif %} {% if logo %} - + + {% endif %} + {{ left_text }} + {{ left_text }} + {% if right_image %} + {% endif %} - {{ left_text }} - {{ left_text }} {{ right_text }} {{ right_text}} {% if left_link or whole_link %} - + {% endif %} diff --git a/pybadges/trend.py b/pybadges/trend.py new file mode 100644 index 0000000..7cb3160 --- /dev/null +++ b/pybadges/trend.py @@ -0,0 +1,67 @@ +from typing import Optional, List, Tuple + +import drawSvg as draw +import itertools +import numpy as np + +import pybadges + +HEIGHT = 13 +WIDTH = 110 +X_OFFSET = 7 +Y_OFFSET = 1 + + +def normalize(arr: np.ndarray) -> np.ndarray: + max_arr = np.max(arr) + if max_arr != 0: + arr /= max_arr + return arr + + +def repeat(samples: List[int], n: int) -> List[int]: + """Repeats a value n times in an array. + + Args: + samples: The list of all elements to be repeated. + n: Number of times to repeat each element in samples. + """ + return list( + itertools.chain.from_iterable( + itertools.repeat(sample, n) + for sample in samples + ) + ) + + +def fit_data(samples: List[int]) -> Tuple[List[int], List[int]]: + y = list( + itertools.chain.from_iterable( + itertools.repeat(sample, 10) + for sample in samples + ) + ) + xp = np.arange(len(y)) + yp = normalize(np.poly1d(np.polyfit(xp, y, 15))(xp)) + yp[yp>0] *= (HEIGHT-2) + return xp, yp + + +def trend( + samples: List[int], stroke_color: str, stroke_width: int + ) -> str: + canvas = draw.Drawing(WIDTH, HEIGHT, origin=(0, -Y_OFFSET)) + path = draw.Path( + fill="transparent", + stroke=pybadges._NAME_TO_COLOR.get(stroke_color, stroke_color), + stroke_width=stroke_width, + stroke_linejoin="round", + ) + + xp, yp = fit_data(samples) + path.M(X_OFFSET + xp[0], yp[0]) + for x, y in zip(xp[1:], yp[1:]): + path.L(X_OFFSET + x, y) + canvas.append(path) + + return canvas.asDataUri() diff --git a/setup.py b/setup.py index 0d8160b..ee958d9 100644 --- a/setup.py +++ b/setup.py @@ -69,6 +69,7 @@ def replace_relative_with_absolute(match): install_requires=['Jinja2>=2.9.0,<3', 'requests>=2.9.0,<3'], extras_require={ 'pil-measurement': ['Pillow>=5,<6'], + 'trend': ['numpy', 'drawSvg'], 'dev': [ 'fonttools>=3.26', 'nox', 'Pillow>=5', 'pytest>=3.6', 'xmldiff>=2.4' ], From 2bcecc01359bdc9485d262c90daa07cbaedf8460 Mon Sep 17 00:00:00 2001 From: rahul-deepsource Date: Tue, 1 Sep 2020 20:54:00 +0530 Subject: [PATCH 2/7] Format code with yapf --- pybadges/__main__.py | 63 +++++++++++++++++++++++--------------------- pybadges/trend.py | 18 ++++--------- 2 files changed, 38 insertions(+), 43 deletions(-) diff --git a/pybadges/__main__.py b/pybadges/__main__.py index e45076f..707a2b0 100644 --- a/pybadges/__main__.py +++ b/pybadges/__main__.py @@ -28,10 +28,13 @@ def main(): + def csv(values): return [int(value) for value in values.split(",")] + def boolean(value): return value.lower() in ['y', 'yes', 't', 'true', '1', ''] + parser = argparse.ArgumentParser( 'pybadges', description='generate a github-style badge given some text and colors') @@ -56,10 +59,9 @@ def boolean(value): default=None, help='the url to redirect to when the right-hand of the badge is ' + 'clicked') - parser.add_argument( - '--bg-color', - default='#555', - help='the background color of the badge') + parser.add_argument('--bg-color', + default='#555', + help='the background color of the badge') parser.add_argument( '--left-color', default='None', @@ -93,7 +95,8 @@ def boolean(value): type=boolean, const='yes', default='no', - help='embed right image into the badge. See embed-logo for more details') + help='embed right image into the badge. See embed-logo for more details' + ) parser.add_argument('--browser', action='store_true', default=False, @@ -137,11 +140,10 @@ def boolean(value): default=None, help='the color of the trend-line. if not supplied, it is plotted' ' in the same color as right-color') - parser.add_argument( - '--trend-width', - type=int, - default=1, - help='the width of the trend-line. default: 1') + parser.add_argument('--trend-width', + type=int, + default=1, + help='the width of the trend-line. default: 1') parser.add_argument( '-v', '--version', @@ -155,14 +157,14 @@ def boolean(value): file=sys.stderr) sys.exit(1) if args.show_trend and args.right_image: - print('argument --right-image: cannot be used with ' + - '--show-trend', + print('argument --right-image: cannot be used with ' + '--show-trend', file=sys.stderr) sys.exit(1) right_image = args.right_image if args.show_trend: - samples = args.show_trend if len(args.show_trend) < 10 else args.show_trend[:10] + samples = args.show_trend if len( + args.show_trend) < 10 else args.show_trend[:10] if len(samples) < 10: samples = [0] * (10 - len(samples)) + samples right_image = trend( @@ -181,23 +183,24 @@ def boolean(value): from pybadges import pil_text_measurer measurer = pil_text_measurer.PilMeasurer(args.deja_vu_sans_path) - badge = pybadges.badge(left_text=args.left_text, - right_text=args.right_text, - left_link=args.left_link, - right_link=args.right_link, - whole_link=args.whole_link, - bg_color=args.bg_color, - left_color=args.left_color, - right_color=args.right_color, - logo=args.logo, - measurer=measurer, - embed_logo=args.embed_logo, - whole_title=args.whole_title, - left_title=args.left_title, - right_title=args.right_title, - right_image=right_image, - embed_right_image=args.embed_right_image, - ) + badge = pybadges.badge( + left_text=args.left_text, + right_text=args.right_text, + left_link=args.left_link, + right_link=args.right_link, + whole_link=args.whole_link, + bg_color=args.bg_color, + left_color=args.left_color, + right_color=args.right_color, + logo=args.logo, + measurer=measurer, + embed_logo=args.embed_logo, + whole_title=args.whole_title, + left_title=args.left_title, + right_title=args.right_title, + right_image=right_image, + embed_right_image=args.embed_right_image, + ) if args.browser: _, badge_path = tempfile.mkstemp(suffix='.svg') diff --git a/pybadges/trend.py b/pybadges/trend.py index 7cb3160..ec86bcd 100644 --- a/pybadges/trend.py +++ b/pybadges/trend.py @@ -28,28 +28,20 @@ def repeat(samples: List[int], n: int) -> List[int]: """ return list( itertools.chain.from_iterable( - itertools.repeat(sample, n) - for sample in samples - ) - ) + itertools.repeat(sample, n) for sample in samples)) def fit_data(samples: List[int]) -> Tuple[List[int], List[int]]: y = list( - itertools.chain.from_iterable( - itertools.repeat(sample, 10) - for sample in samples - ) - ) + itertools.chain.from_iterable( + itertools.repeat(sample, 10) for sample in samples)) xp = np.arange(len(y)) yp = normalize(np.poly1d(np.polyfit(xp, y, 15))(xp)) - yp[yp>0] *= (HEIGHT-2) + yp[yp > 0] *= (HEIGHT - 2) return xp, yp -def trend( - samples: List[int], stroke_color: str, stroke_width: int - ) -> str: +def trend(samples: List[int], stroke_color: str, stroke_width: int) -> str: canvas = draw.Drawing(WIDTH, HEIGHT, origin=(0, -Y_OFFSET)) path = draw.Path( fill="transparent", From 8b5650d1740d3912598294a38d562cedd9f73549 Mon Sep 17 00:00:00 2001 From: rahul-deepsource Date: Tue, 1 Sep 2020 21:25:38 +0530 Subject: [PATCH 3/7] change left_color to bg_color in tests --- tests/test-badges.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test-badges.json b/tests/test-badges.json index f2bc92a..10bb3ae 100644 --- a/tests/test-badges.json +++ b/tests/test-badges.json @@ -29,7 +29,7 @@ "file_name": "complete.svg", "left_text": "complete", "right_text": "example", - "left_color": "green", + "bg_color": "green", "right_color": "#fb3", "left_link": "http://www.complete.com/", "right_link": "http://www.example.com", @@ -43,7 +43,7 @@ "file_name": "complete.svg", "left_text": "complete", "right_text": "example", - "left_color": "green", + "bg_color": "green", "right_color": "#fb3", "left_link": "http://www.complete.com/", "right_link": "http://www.example.com", @@ -121,7 +121,7 @@ "file_name": "tests.svg", "left_text": "tests", "right_text": "231 passed, 1 failed, 1 skipped", - "left_color": "blue", + "bg_color": "blue", "right_color": "orange" } ] From 22f0e14430a348921fea7dcef71de810745ee097 Mon Sep 17 00:00:00 2001 From: rahul-deepsource Date: Wed, 2 Sep 2020 11:49:58 +0530 Subject: [PATCH 4/7] Refactor design; pin dependencies --- pybadges/__init__.py | 24 +++++++++++++++++++++++- pybadges/__main__.py | 18 ++++-------------- setup.py | 2 +- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/pybadges/__init__.py b/pybadges/__init__.py index 5f16c4a..d9382c7 100644 --- a/pybadges/__init__.py +++ b/pybadges/__init__.py @@ -32,7 +32,7 @@ import base64 import imghdr import mimetypes -from typing import Optional +from typing import List, Optional import urllib.parse from xml.dom import minidom @@ -40,6 +40,7 @@ import requests from pybadges import text_measurer +from pybadges.trend import trend from pybadges import precalculated_text_measurer from pybadges.version import __version__ @@ -129,6 +130,9 @@ def badge( right_title: Optional[str] = None, right_image: Optional[str] = None, embed_right_image: bool = False, + show_trend: Optional[List[int]] = None, + trend_color: Optional[str] = None, + trend_width: Optional[int] = 1, ) -> str: """Creates a github-style badge as an SVG image. @@ -177,6 +181,10 @@ def badge( Can be a data URL e.g. "data:image/svg+xml;utf8,=2.9.0,<3', 'requests>=2.9.0,<3'], extras_require={ 'pil-measurement': ['Pillow>=5,<6'], - 'trend': ['numpy', 'drawSvg'], + 'trend': ['drawsvg>=1.6.0', 'numpy>=1.19.0'], 'dev': [ 'fonttools>=3.26', 'nox', 'Pillow>=5', 'pytest>=3.6', 'xmldiff>=2.4' ], From 83a015a6a7bb2295899f70ac7e3bc89cdf007f1e Mon Sep 17 00:00:00 2001 From: rahul-deepsource Date: Wed, 9 Sep 2020 10:46:46 +0530 Subject: [PATCH 5/7] Improve docstring --- pybadges/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pybadges/__init__.py b/pybadges/__init__.py index d9382c7..2180724 100644 --- a/pybadges/__init__.py +++ b/pybadges/__init__.py @@ -155,13 +155,13 @@ def badge( selected. If set then left_link and right_right may not be set. logo: A url representing a logo that will be displayed inside the badge. Can be a data URL e.g. "data:image/svg+xml;utf8, Date: Wed, 9 Sep 2020 10:47:09 +0530 Subject: [PATCH 6/7] Stretch plot rather than zero padding --- pybadges/__init__.py | 5 +---- pybadges/badge-template-full.svg | 2 +- pybadges/trend.py | 27 +++++++-------------------- 3 files changed, 9 insertions(+), 25 deletions(-) diff --git a/pybadges/__init__.py b/pybadges/__init__.py index 2180724..0f6cd7a 100644 --- a/pybadges/__init__.py +++ b/pybadges/__init__.py @@ -199,11 +199,8 @@ def badge( raise ValueError('right-image and trend cannot be used together.') if show_trend: - samples = show_trend if len(show_trend) <= 10 else show_trend[:10] - if len(samples) < 10: - samples = [0] * (10 - len(samples)) + samples right_image = trend( - samples=samples, + samples=show_trend, stroke_color=(trend_color or right_color), stroke_width=trend_width, ) diff --git a/pybadges/badge-template-full.svg b/pybadges/badge-template-full.svg index 4ae9b39..469a4d6 100644 --- a/pybadges/badge-template-full.svg +++ b/pybadges/badge-template-full.svg @@ -1,6 +1,6 @@ {% set logo_width = 14 if logo else 0 %} {% set logo_padding = 3 if (logo and left_text) else 0 %} -{% set image_width = 110 if right_image else 0 %} +{% set image_width = 107 if right_image else 0 %} {% set left_width = image_width + left_text_width + 10 + logo_width + logo_padding %} {% set right_width = right_text_width + 10 %} diff --git a/pybadges/trend.py b/pybadges/trend.py index ec86bcd..abb2a83 100644 --- a/pybadges/trend.py +++ b/pybadges/trend.py @@ -1,13 +1,12 @@ from typing import Optional, List, Tuple import drawSvg as draw -import itertools import numpy as np import pybadges HEIGHT = 13 -WIDTH = 110 +WIDTH = 107 X_OFFSET = 7 Y_OFFSET = 1 @@ -19,23 +18,11 @@ def normalize(arr: np.ndarray) -> np.ndarray: return arr -def repeat(samples: List[int], n: int) -> List[int]: - """Repeats a value n times in an array. - - Args: - samples: The list of all elements to be repeated. - n: Number of times to repeat each element in samples. - """ - return list( - itertools.chain.from_iterable( - itertools.repeat(sample, n) for sample in samples)) - - def fit_data(samples: List[int]) -> Tuple[List[int], List[int]]: - y = list( - itertools.chain.from_iterable( - itertools.repeat(sample, 10) for sample in samples)) - xp = np.arange(len(y)) + width = WIDTH - X_OFFSET + N = int(width / len(samples)) + y = np.repeat(samples, N) + xp = np.linspace(start=X_OFFSET, stop=width, num=len(y)) yp = normalize(np.poly1d(np.polyfit(xp, y, 15))(xp)) yp[yp > 0] *= (HEIGHT - 2) return xp, yp @@ -51,9 +38,9 @@ def trend(samples: List[int], stroke_color: str, stroke_width: int) -> str: ) xp, yp = fit_data(samples) - path.M(X_OFFSET + xp[0], yp[0]) + path.M(xp[0], yp[0]) for x, y in zip(xp[1:], yp[1:]): - path.L(X_OFFSET + x, y) + path.L(x, y) canvas.append(path) return canvas.asDataUri() From 72a7ccf55048e8a0b71f83fc685f52ebb2f86d08 Mon Sep 17 00:00:00 2001 From: rahul-deepsource Date: Wed, 9 Sep 2020 11:22:31 +0530 Subject: [PATCH 7/7] Import conditionally; Improve docstrings --- pybadges/__init__.py | 12 ++++++++++-- pybadges/__main__.py | 13 ++++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/pybadges/__init__.py b/pybadges/__init__.py index 0f6cd7a..6ae390c 100644 --- a/pybadges/__init__.py +++ b/pybadges/__init__.py @@ -40,10 +40,14 @@ import requests from pybadges import text_measurer -from pybadges.trend import trend from pybadges import precalculated_text_measurer from pybadges.version import __version__ +try: + from pybadges.trend import trend +except: + trend = None + _JINJA2_ENVIRONMENT = jinja2.Environment( trim_blocks=True, lstrip_blocks=True, @@ -183,7 +187,8 @@ def badge( itself and saves an additional HTTP request. See embed_logo. show_trend: accepts comma separated integers (least to most recent), and plots a trend line showing variation of that data. If both show_trend and right_image - are passed, ValueError is raised. + are passed, ValueError is raised. Needs additional dependencies installed: + numpy and drawSvg. trend_color: color of the trend line. If not supplied, right_color is used. trend_width: stroke width of the trend line. """ @@ -199,6 +204,9 @@ def badge( raise ValueError('right-image and trend cannot be used together.') if show_trend: + if trend is None: + raise ValueError('Additional dependencies not installed.') + right_image = trend( samples=show_trend, stroke_color=(trend_color or right_color), diff --git a/pybadges/__main__.py b/pybadges/__main__.py index c56a05e..4e05b12 100644 --- a/pybadges/__main__.py +++ b/pybadges/__main__.py @@ -60,7 +60,7 @@ def boolean(value): 'clicked') parser.add_argument('--bg-color', default='#555', - help='the background color of the badge') + help='the background color of the badge. Default: #555') parser.add_argument( '--left-color', default='None', @@ -69,7 +69,8 @@ def boolean(value): parser.add_argument( '--right-color', default='#007ec6', - help='the background color of the right-hand-side of the badge') + help='the background color of the right-hand-side of the badge.' + ' Default: #007ec6') parser.add_argument( '--logo', default=None, @@ -132,8 +133,10 @@ def boolean(value): '--show-trend', default=None, type=csv, - help='up to ten integral values to be plotted as a trend. If' - ' --show-trend is passed, right image should not be used.') + help='accepts comma separated integers (least to most recent) and' + ' plots a trend line showing variation of that data. If both' + ' --show-trend is passed, right image should not be used. It needs' + ' additional dependencies installed: drawSvg and numpy.') parser.add_argument( '--trend-color', default=None, @@ -156,7 +159,7 @@ def boolean(value): file=sys.stderr) sys.exit(1) if args.show_trend and args.right_image: - print('argument --right-image: cannot be used with ' + '--show-trend', + print('argument --right-image: cannot be used with --show-trend', file=sys.stderr) sys.exit(1)