Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for displaying a trend line and embedding a right image #24

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 39 additions & 8 deletions pybadges/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,15 @@
import base64
import imghdr
import mimetypes
from typing import Optional
from typing import List, Optional
import urllib.parse
from xml.dom import minidom

import jinja2
import requests

from pybadges import text_measurer
from pybadges.trend import trend
rahul-deepsource marked this conversation as resolved.
Show resolved Hide resolved
from pybadges import precalculated_text_measurer
from pybadges.version import __version__

Expand Down Expand Up @@ -119,13 +120,19 @@ 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,
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.

Expand All @@ -148,16 +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,<svg..."
left_color: The color of the part of the badge containing the left-hand
text. Can be an valid CSS color
bg_color: The background color of the badge. Can be an valid CSS color
(see https://developer.mozilla.org/en-US/docs/Web/CSS/color) or a
color name defined here:
https://github.com/badges/shields/blob/master/lib/colorscheme.json
left_color: The color of the part of the badge containing the left text. If not specified, bg_color is used
right_color: The color of the part of the badge containing the
right-hand text. Can be an valid CSS color
(see https://developer.mozilla.org/en-US/docs/Web/CSS/color) or a
color name defined here:
https://github.com/badges/shields/blob/master/lib/colorscheme.json
right-hand text.
rahul-deepsource marked this conversation as resolved.
Show resolved Hide resolved
measurer: A text_measurer.TextMeasurer that can be used to measure the
width of left_text and right_text.
embed_logo: If True then embed the logo image directly in the badge.
Expand All @@ -173,6 +177,14 @@ def badge(
right_title: The title attribute to associate with the right part of
the badge.
See https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title.
right_image: A url representing a image which can be embedded before right_text.
Can be a data URL e.g. "data:image/svg+xml;utf8,<svg..."
embed_right_image: If True, the right image is embedded into the badge
itself and saves an additional HTTP request. See embed_logo.
show_trend: plot a trend with not more than 10 data points passed. if both
show_trend and right_image are passed, ValueError is raised.
trend_color: color of the trend line. If not supplied, right_color is used.
trend_width: stroke width of the trend line.
"""
if measurer is None:
measurer = (
Expand All @@ -181,11 +193,28 @@ def badge(
if (left_link or right_link) and whole_link:
raise ValueError(
'whole_link may not bet set with left_link or right_link')

if show_trend and right_image:
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
rahul-deepsource marked this conversation as resolved.
Show resolved Hide resolved
right_image = trend(
samples=samples,
stroke_color=(trend_color or right_color),
stroke_width=trend_width,
)

template = _JINJA2_ENVIRONMENT.get_template('badge-template-full.svg')

if logo and embed_logo:
logo = _embed_image(logo)

if right_image and embed_right_image:
right_image = _embed_image(right_image)

svg = template.render(
left_text=left_text,
right_text=right_text,
Expand All @@ -195,11 +224,13 @@ def badge(
right_link=right_link,
whole_link=whole_link,
logo=logo,
bg_color=_NAME_TO_COLOR.get(bg_color, bg_color),
left_color=_NAME_TO_COLOR.get(left_color, left_color),
right_color=_NAME_TO_COLOR.get(right_color, right_color),
whole_title=whole_title,
left_title=left_title,
right_title=right_title,
right_image=right_image,
)
xml = minidom.parseString(svg)
_remove_blanks(xml)
Expand Down
83 changes: 66 additions & 17 deletions pybadges/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,16 @@


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')

parser.add_argument(
'--left-text',
default='license',
Expand All @@ -52,10 +58,14 @@ def main():
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(
'--left-color',
default='#555',
help='the background color of the left-hand-side of the badge')
default='None',
help='the background color of the badge containing the left text.'
' If not provided, bg-color is used')
parser.add_argument(
'--right-color',
default='#007ec6',
Expand All @@ -67,13 +77,25 @@ def main():
parser.add_argument(
'--embed-logo',
nargs='?',
type=lambda x: x.lower() in ['y', 'yes', 't', 'true', '1', ''],
type=boolean,
const='yes',
default='no',
help='if the logo is specified then include the image data directly in '
'the badge (this will prevent a URL fetch and may work around the '
'fact that some browsers do not fetch external image references); '
'only works if --logo is a HTTP/HTTPS URI or a file path')
parser.add_argument(
'--right-image',
default=None,
help='a URI reference to an extra image to be displayed in the badge')
parser.add_argument(
'--embed-right-image',
nargs='?',
type=boolean,
const='yes',
default='no',
help='embed right image into the badge. See embed-logo for more details'
)
parser.add_argument('--browser',
action='store_true',
default=False,
Expand Down Expand Up @@ -106,6 +128,21 @@ def main():
default=None,
help='the title to associate with the right part of the badge. See '
'https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title')
parser.add_argument(
'--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.')
parser.add_argument(
'--trend-color',
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(
'-v',
'--version',
Expand All @@ -118,6 +155,10 @@ def main():
'--left-link or --right-link',
file=sys.stderr)
sys.exit(1)
if args.show_trend and args.right_image:
print('argument --right-image: cannot be used with ' + '--show-trend',
rahul-deepsource marked this conversation as resolved.
Show resolved Hide resolved
file=sys.stderr)
sys.exit(1)

measurer = None
if args.use_pil_text_measurer:
Expand All @@ -129,19 +170,27 @@ def main():
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,
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)
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=args.right_image,
embed_right_image=args.embed_right_image,
show_trend=args.show_trend,
trend_color=args.trend_color,
trend_width=args.trend_width,
)

if args.browser:
_, badge_path = tempfile.mkstemp(suffix='.svg')
Expand Down
19 changes: 13 additions & 6 deletions pybadges/badge-template-full.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
59 changes: 59 additions & 0 deletions pybadges/trend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you write a doc string explaining what this does?

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(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to do this or to interpolate?
e.g. with numpy.interp

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason behind repeating these numbers it that the more samples we have, we can increase the polyfit order more liberally which smoothens the curve considerably.

I don't think that it would make much sense to first interpolate the curve and then fit it again.

Also, I found out about np.repeat. Removing this hacky repeat method.

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))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the bottom is always zero?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahm, I don't quite get the question.

If all the values in yp are equal, then we'd have an array of all 1's after normalization. Similarly for all zeroes, we'd have a flat line.

Polyfit might return negative numbers sometime (-1 at most). But, since we only scale up positive numbers (yp[yp > 0] *= ...), and use an offset of -1 when defining the origin, it shouldn't be a problem.

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))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you write a doc string explaining what this does?

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()
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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': ['drawsvg>=1.6.0', 'numpy>=1.19.0'],
'dev': [
'fonttools>=3.26', 'nox', 'Pillow>=5', 'pytest>=3.6', 'xmldiff>=2.4'
],
Expand Down
6 changes: 3 additions & 3 deletions tests/test-badges.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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"
}
]