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 all 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
53 changes: 45 additions & 8 deletions pybadges/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -43,6 +43,11 @@
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,
Expand Down Expand Up @@ -119,13 +124,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 +159,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. Default: #555. 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. Default: #007ec6 (blue)
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 +181,16 @@ 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: 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. 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.
"""
if measurer is None:
measurer = (
Expand All @@ -181,11 +199,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:
if trend is None:
raise ValueError('Additional dependencies not installed.')

right_image = trend(
samples=show_trend,
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 +230,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
88 changes: 70 additions & 18 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,28 +58,45 @@ 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. Default: #555')
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',
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,
help='a URI reference to a logo to display in the badge')
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 +129,23 @@ 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='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,
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 +158,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',
file=sys.stderr)
sys.exit(1)

measurer = None
if args.use_pil_text_measurer:
Expand All @@ -129,19 +173,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.
46 changes: 46 additions & 0 deletions pybadges/trend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from typing import Optional, List, Tuple

import drawSvg as draw
import numpy as np

import pybadges

HEIGHT = 13
WIDTH = 107
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 fit_data(samples: List[int]) -> Tuple[List[int], List[int]]:
width = WIDTH - X_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 to explain what this does?

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))
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(xp[0], yp[0])
for x, y in zip(xp[1:], yp[1:]):
path.L(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"
}
]