Browse Source

Yapf code style

pull/340/merge
Florian Mounier 7 years ago
parent
commit
79b33bec70
  1. 198
      .style.yapf
  2. 2
      demo/cabaret.py
  3. 10
      demo/cabaret/__init__.py
  4. 183
      demo/moulinrouge/__init__.py
  5. 14
      demo/moulinrouge/data.py
  6. 803
      demo/moulinrouge/tests.py
  7. 46
      docs/conf.py
  8. 94
      docs/ext/pygal_sphinx_directives.py
  9. 16
      pygal/__init__.py
  10. 2
      pygal/_compat.py
  11. 18
      pygal/colors.py
  12. 5
      pygal/etree.py
  13. 8
      pygal/formatters.py
  14. 1
      pygal/graph/__init__.py
  15. 43
      pygal/graph/bar.py
  16. 70
      pygal/graph/base.py
  17. 134
      pygal/graph/box.py
  18. 70
      pygal/graph/dot.py
  19. 12
      pygal/graph/dual.py
  20. 42
      pygal/graph/funnel.py
  21. 67
      pygal/graph/gauge.py
  22. 597
      pygal/graph/graph.py
  23. 55
      pygal/graph/histogram.py
  24. 16
      pygal/graph/horizontal.py
  25. 2
      pygal/graph/horizontalbar.py
  26. 2
      pygal/graph/horizontalline.py
  27. 2
      pygal/graph/horizontalstackedbar.py
  28. 2
      pygal/graph/horizontalstackedline.py
  29. 75
      pygal/graph/line.py
  30. 24
      pygal/graph/map.py
  31. 24
      pygal/graph/pie.py
  32. 20
      pygal/graph/public.py
  33. 40
      pygal/graph/pyramid.py
  34. 90
      pygal/graph/radar.py
  35. 53
      pygal/graph/solidgauge.py
  36. 71
      pygal/graph/stackedbar.py
  37. 19
      pygal/graph/stackedline.py
  38. 25
      pygal/graph/time.py
  39. 52
      pygal/graph/treemap.py
  40. 60
      pygal/graph/xy.py
  41. 70
      pygal/interpolate.py
  42. 1
      pygal/maps/__init__.py
  43. 1
      pygal/serie.py
  44. 2
      pygal/state.py
  45. 16
      pygal/stats.py
  46. 166
      pygal/style.py
  47. 296
      pygal/svg.py
  48. 26
      pygal/table.py
  49. 14
      pygal/test/__init__.py
  50. 9
      pygal/test/conftest.py
  51. 1
      pygal/test/test_bar.py
  52. 49
      pygal/test/test_box.py
  53. 4
      pygal/test/test_colors.py
  54. 95
      pygal/test/test_config.py
  55. 168
      pygal/test/test_date.py
  56. 1
      pygal/test/test_formatters.py
  57. 202
      pygal/test/test_graph.py
  58. 9
      pygal/test/test_histogram.py
  59. 62
      pygal/test/test_interpolate.py
  60. 23
      pygal/test/test_line.py
  61. 1
      pygal/test/test_maps.py
  62. 1
      pygal/test/test_pie.py
  63. 1
      pygal/test/test_serie_config.py
  64. 1
      pygal/test/test_sparktext.py
  65. 13
      pygal/test/test_stacked.py
  66. 9
      pygal/test/test_style.py
  67. 1
      pygal/test/test_table.py
  68. 42
      pygal/test/test_util.py
  69. 2
      pygal/test/test_view.py
  70. 16
      pygal/test/test_xml_filters.py
  71. 1
      pygal/test/utils.py
  72. 62
      pygal/util.py
  73. 78
      pygal/view.py

198
.style.yapf

@ -0,0 +1,198 @@
[style]
based_on_style = pep8
# █████ ██ ██
# ██ ██ ██ ██
# ███████ ██ ██
# ██ ██ ██ ██
# ██ ██ ███████ ███████
# Align closing bracket with visual indentation.
# align_closing_bracket_with_visual_indent=True
# Allow dictionary keys to exist on multiple lines. For example:
#
# x = {
# ('this is the first element of a tuple',
# 'this is the second element of a tuple'):
# value,
# }
# allow_multiline_dictionary_keys=False
# Allow lambdas to be formatted on more than one line.
# allow_multiline_lambdas=False
# Insert a blank line before a class-level docstring.
# blank_line_before_class_docstring=False
# Insert a blank line before a 'def' or 'class' immediately nested
# within another 'def' or 'class'. For example:
#
# class Foo:
# # <------ this blank line
# def method():
# ...
# blank_line_before_nested_class_or_def=False
# Do not split consecutive brackets. Only relevant when
# dedent_closing_brackets is set. For example:
#
# call_func_that_takes_a_dict(
# {
# 'key1': 'value1',
# 'key2': 'value2',
# }
# )
#
# would reformat to:
#
# call_func_that_takes_a_dict({
# 'key1': 'value1',
# 'key2': 'value2',
# })
coalesce_brackets=True
# The column limit.
# column_limit=79
# Indent width used for line continuations.
# continuation_indent_width=4
# Put closing brackets on a separate line, dedented, if the bracketed
# expression can't fit in a single line. Applies to all kinds of brackets,
# including function definitions and calls. For example:
#
# config = {
# 'key1': 'value1',
# 'key2': 'value2',
# } # <--- this bracket is dedented and on a separate line
#
# time_series = self.remote_client.query_entity_counters(
# entity='dev3246.region1',
# key='dns.query_latency_tcp',
# transform=Transformation.AVERAGE(window=timedelta(seconds=60)),
# start_ts=now()-timedelta(days=3),
# end_ts=now(),
# ) # <--- this bracket is dedented and on a separate line
dedent_closing_brackets=True
# Place each dictionary entry onto its own line.
# each_dict_entry_on_separate_line=True
# The regex for an i18n comment. The presence of this comment stops
# reformatting of that line, because the comments are required to be
# next to the string they translate.
# i18n_comment=
# The i18n function call names. The presence of this function stops
# reformattting on that line, because the string it has cannot be moved
# away from the i18n comment.
# i18n_function_call=
# Indent the dictionary value if it cannot fit on the same line as the
# dictionary key. For example:
#
# config = {
# 'key1':
# 'value1',
# 'key2': value1 +
# value2,
# }
indent_dictionary_value=True
# The number of columns to use for indentation.
# indent_width=4
# Join short lines into one line. E.g., single line 'if' statements.
join_multiple_lines=False
# Do not include spaces around selected binary operators. For example:
#
# 1 + 2 * 3 - 4 / 5
#
# will be formatted as follows when configured with a value "*,/":
#
# 1 + 2*3 - 4/5
#
# no_spaces_around_selected_binary_operators=set()
# Use spaces around default or named assigns.
# spaces_around_default_or_named_assign=False
# Use spaces around the power operator.
# spaces_around_power_operator=False
# The number of spaces required before a trailing comment.
# spaces_before_comment=2
# Insert a space between the ending comma and closing bracket of a list,
# etc.
# space_between_ending_comma_and_closing_bracket=True
# Split before arguments if the argument list is terminated by a
# comma.
# split_arguments_when_comma_terminated=False
# Set to True to prefer splitting before '&', '|' or '^' rather than
# after.
# split_before_bitwise_operator=True
# Split before a dictionary or set generator (comp_for). For example, note
# the split before the 'for':
#
# foo = {
# variable: 'Hello world, have a nice day!'
# for variable in bar if variable != 42
# }
# split_before_dict_set_generator=True
# If an argument / parameter list is going to be split, then split before
# the first argument.
split_before_first_argument=True
# Set to True to prefer splitting before 'and' or 'or' rather than
# after.
# split_before_logical_operator=True
# Split named assignments onto individual lines.
# split_before_named_assigns=True
# The penalty for splitting right after the opening bracket.
# split_penalty_after_opening_bracket=30
# The penalty for splitting the line after a unary operator.
# split_penalty_after_unary_operator=10000
# The penalty for splitting right before an if expression.
# split_penalty_before_if_expr=0
# The penalty of splitting the line around the '&', '|', and '^'
# operators.
# split_penalty_bitwise_operator=300
# The penalty for characters over the column limit.
# split_penalty_excess_character=4500
# The penalty incurred by adding a line split to the unwrapped line. The
# more line splits added the higher the penalty.
# split_penalty_for_added_line_split=30
# The penalty of splitting a list of "import as" names. For example:
#
# from a_very_long_or_indented_module_name_yada_yad import (long_argument_1,
# long_argument_2,
# long_argument_3)
#
# would reformat to something like:
#
# from a_very_long_or_indented_module_name_yada_yad import (
# long_argument_1, long_argument_2, long_argument_3)
# split_penalty_import_names=0
# The penalty of splitting the line around the 'and' and 'or'
# operators.
# split_penalty_logical_operator=300
# Use the Tab character for indentation.
# use_tabs=False

2
demo/cabaret.py

@ -39,7 +39,6 @@ try:
except:
pass
try:
import wsreload
except ImportError:
@ -49,6 +48,7 @@ else:
def log(httpserver):
app.logger.debug('WSReloaded after server restart')
wsreload.monkey_patch_http_server({'url': url}, callback=log)
app.logger.debug('HTTPServer monkey patched for url %s' % url)

10
demo/cabaret/__init__.py

@ -34,10 +34,14 @@ def create_app():
@app.route("/")
def index():
return render_template(
'index.jinja2', charts_names=CHARTS_NAMES, configs=CONFIG_ITEMS,
interpolations=INTERPOLATIONS, styles_names=styles.keys())
'index.jinja2',
charts_names=CHARTS_NAMES,
configs=CONFIG_ITEMS,
interpolations=INTERPOLATIONS,
styles_names=styles.keys()
)
@app.route("/svg", methods=('POST',))
@app.route("/svg", methods=('POST', ))
def svg():
values = request.values
config = loads(values['opts'])

183
demo/moulinrouge/__init__.py

@ -23,8 +23,8 @@ from pygal.util import cut
from pygal.etree import etree
from pygal.style import styles, parametric_styles
from base64 import (
urlsafe_b64encode as b64encode,
urlsafe_b64decode as b64decode)
urlsafe_b64encode as b64encode, urlsafe_b64decode as b64decode
)
import string
import random
import pickle
@ -39,9 +39,9 @@ def get(type):
def random_label():
chars = string.ascii_letters + string.digits + u' àéèçêâäëï'
return ''.join(
[random.choice(chars)
for i in range(random.randrange(4, 30))])
return ''.join([
random.choice(chars) for i in range(random.randrange(4, 30))
])
def random_value(min=0, max=15):
@ -61,8 +61,8 @@ def create_app():
etree.to_lxml()
def _random(data, order):
max = 10 ** order
min = 10 ** random.randrange(0, order)
max = 10**order
min = 10**random.randrange(0, order)
series = []
for i in range(random.randrange(1, 10)):
@ -74,8 +74,8 @@ def create_app():
return series
def _random_series(type, data, order):
max = 10 ** order
min = 10 ** random.randrange(0, order)
max = 10**order
min = 10**random.randrange(0, order)
with_secondary = bool(random.randint(0, 1))
series = []
for i in range(random.randrange(1, 10)):
@ -84,11 +84,13 @@ def create_app():
elif type == 'XY':
values = [(
random_value((-max, min)[random.randrange(0, 2)], max),
random_value((-max, min)[random.randrange(0, 2)], max))
for i in range(data)]
random_value((-max, min)[random.randrange(0, 2)], max)
) for i in range(data)]
else:
values = [random_value((-max, min)[random.randrange(1, 2)],
max) for i in range(data)]
values = [
random_value((-max, min)[random.randrange(1, 2)], max)
for i in range(data)
]
config = {
'secondary': with_secondary and bool(random.randint(0, 1))
}
@ -101,25 +103,29 @@ def create_app():
@app.route("/")
def index():
return render_template(
'index.jinja2', styles=styles, parametric_styles=parametric_styles,
'index.jinja2',
styles=styles,
parametric_styles=parametric_styles,
parametric_colors=(
'#ff5995', '#b6e354', '#feed6c', '#8cedff', '#9e6ffe'),
links=links, charts_name=pygal.CHARTS_NAMES)
'#ff5995', '#b6e354', '#feed6c', '#8cedff', '#9e6ffe'
),
links=links,
charts_name=pygal.CHARTS_NAMES
)
@app.route("/svg/<type>/<series>/<config>")
def svg(type, series, config):
graph = get(type)(
pickle.loads(b64decode(str(config))))
for title, values, serie_config in pickle.loads(
b64decode(str(series))):
graph = get(type)(pickle.loads(b64decode(str(config))))
for title, values, serie_config in pickle.loads(b64decode(
str(series))):
graph.add(title, values, **serie_config)
return graph.render_response()
@app.route("/table/<type>/<series>/<config>")
def table(type, series, config):
graph = get(type)(pickle.loads(b64decode(str(config))))
for title, values, serie_config in pickle.loads(
b64decode(str(series))):
for title, values, serie_config in pickle.loads(b64decode(
str(series))):
graph.add(title, values, **serie_config)
return graph.render_table()
@ -134,17 +140,19 @@ def create_app():
line = pygal.Line(style=style, pretty_print=True)
line.add('_', [random.randrange(0, 10) for _ in range(25)])
return Response(
line.render_sparkline(height=40), mimetype='image/svg+xml')
line.render_sparkline(height=40), mimetype='image/svg+xml'
)
@app.route("/with/table/<type>")
def with_table(type):
chart = pygal.StackedBar(
disable_xml_declaration=True,
x_label_rotation=35)
disable_xml_declaration=True, x_label_rotation=35
)
chart.title = (
'What Linux distro do you primarily use'
' on your server computers? (Desktop'
' users vs Server Users)')
' users vs Server Users)'
)
if type == 'series':
chart.add('Debian', [1775, 82])
@ -170,14 +178,18 @@ def create_app():
'Red Hat Enterprise Linux', 'Gentoo', 'Fedora', 'Amazon Linux',
'OpenSUSE', 'Slackware', 'Xubuntu', 'Rasbian',
'SUSE Linux Enterprise Server', 'Linux Mint',
'Scientific Linux', 'Other']
chart.add('Desktop Users', [
1775, 1515, 807, 549, 247, 129, 91, 60, 58, 50, 38, 33, 33,
30, 32, 187
])
chart.add('Server Users', [
82, 80, 60, 12, 10, 7, 6, 0, 0, 3, 1, 4, 1, 4, 0, 5
])
'Scientific Linux', 'Other'
]
chart.add(
'Desktop Users', [
1775, 1515, 807, 549, 247, 129, 91, 60, 58, 50, 38, 33, 33,
30, 32, 187
]
)
chart.add(
'Server Users',
[82, 80, 60, 12, 10, 7, 6, 0, 0, 3, 1, 4, 1, 4, 0, 5]
)
return render_template('table.jinja2', chart=chart)
@ -194,13 +206,13 @@ def create_app():
style = styles[style]
else:
style = parametric_styles[style](
color, base_style=styles[base_style or 'default'])
color, base_style=styles[base_style or 'default']
)
xy_series = _random(data, order)
other_series = []
for title, values, config in xy_series:
other_series.append(
(title, cut(values, 1), config))
other_series.append((title, cut(values, 1), config))
xy_series = b64encode(pickle.dumps(xy_series))
other_series = b64encode(pickle.dumps(other_series))
config = Config()
@ -216,14 +228,15 @@ def create_app():
config.x_labels = None
else:
config.x_labels = [random_label() for i in range(data)]
svgs.append({'type': type,
'series': xy_series if chart._dual else other_series,
'config': b64encode(pickle.dumps(config))})
svgs.append({
'type': type,
'series': xy_series if chart._dual else other_series,
'config': b64encode(pickle.dumps(config))
})
return render_template('svgs.jinja2',
svgs=svgs,
width=width,
height=height)
return render_template(
'svgs.jinja2', svgs=svgs, width=width, height=height
)
@app.route("/rotation")
def rotation():
@ -244,14 +257,15 @@ def create_app():
config.x_labels = labels
config.x_label_rotation = angle
config.y_label_rotation = angle
svgs.append({'type': 'pygal.Bar',
'series': series,
'config': b64encode(pickle.dumps(config))})
svgs.append({
'type': 'pygal.Bar',
'series': series,
'config': b64encode(pickle.dumps(config))
})
return render_template('svgs.jinja2',
svgs=svgs,
width=width,
height=height)
return render_template(
'svgs.jinja2', svgs=svgs, width=width, height=height
)
@app.route("/interpolation")
def interpolation():
@ -268,51 +282,56 @@ def create_app():
for interpolation in 'quadratic', 'cubic', 'lagrange', 'trigonometric':
config.title = "%s interpolation" % interpolation
config.interpolate = interpolation
svgs.append({'type': 'pygal.StackedLine',
'series': series,
'config': b64encode(pickle.dumps(config))})
for params in [
{'type': 'catmull_rom'},
{'type': 'finite_difference'},
{'type': 'cardinal', 'c': .25},
{'type': 'cardinal', 'c': .5},
{'type': 'cardinal', 'c': .75},
{'type': 'cardinal', 'c': 1.5},
{'type': 'cardinal', 'c': 2},
{'type': 'cardinal', 'c': 5},
{'type': 'kochanek_bartels', 'b': 1, 'c': 1, 't': 1},
{'type': 'kochanek_bartels', 'b': -1, 'c': 1, 't': 1},
{'type': 'kochanek_bartels', 'b': 1, 'c': -1, 't': 1},
{'type': 'kochanek_bartels', 'b': 1, 'c': 1, 't': -1},
{'type': 'kochanek_bartels', 'b': -1, 'c': 1, 't': -1},
{'type': 'kochanek_bartels', 'b': -1, 'c': -1, 't': 1},
{'type': 'kochanek_bartels', 'b': -1, 'c': -1, 't': -1}
]:
svgs.append({
'type': 'pygal.StackedLine',
'series': series,
'config': b64encode(pickle.dumps(config))
})
for params in [{'type': 'catmull_rom'}, {'type': 'finite_difference'},
{'type': 'cardinal',
'c': .25}, {'type': 'cardinal',
'c': .5}, {'type': 'cardinal', 'c': .75},
{'type': 'cardinal',
'c': 1.5}, {'type': 'cardinal',
'c': 2}, {'type': 'cardinal', 'c': 5},
{'type': 'kochanek_bartels', 'b': 1, 'c': 1,
't': 1}, {'type': 'kochanek_bartels', 'b': -1, 'c': 1,
't': 1}, {'type': 'kochanek_bartels', 'b': 1,
'c': -1, 't': 1},
{'type': 'kochanek_bartels', 'b': 1, 'c': 1, 't': -1}, {
'type': 'kochanek_bartels', 'b': -1, 'c': 1, 't': -1
}, {'type': 'kochanek_bartels', 'b': -1, 'c': -1,
't': 1}, {'type': 'kochanek_bartels', 'b': -1,
'c': -1, 't': -1}]:
config.title = "Hermite interpolation with params %r" % params
config.interpolate = 'hermite'
config.interpolation_parameters = params
svgs.append({'type': 'pygal.StackedLine',
'series': series,
'config': b64encode(pickle.dumps(config))})
svgs.append({
'type': 'pygal.StackedLine',
'series': series,
'config': b64encode(pickle.dumps(config))
})
return render_template('svgs.jinja2',
svgs=svgs,
width=width,
height=height)
return render_template(
'svgs.jinja2', svgs=svgs, width=width, height=height
)
@app.route("/raw_svgs/")
def raw_svgs():
svgs = []
for color in styles['neon'].colors:
chart = pygal.Pie(style=parametric_styles['rotate'](color),
width=400, height=300)
chart = pygal.Pie(
style=parametric_styles['rotate'](color),
width=400,
height=300
)
chart.title = color
chart.disable_xml_declaration = True
chart.explicit_size = True
chart.js = ['http://l:2343/2.0.x/pygal-tooltips.js']
for i in range(6):
chart.add(str(i), 2 ** i)
chart.add(str(i), 2**i)
svgs.append(chart.render())
return render_template('raw_svgs.jinja2', svgs=svgs)

14
demo/moulinrouge/data.py

@ -17,12 +17,8 @@
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
labels = ['AURSAUTRAUIA',
'dpvluiqhu enuie',
'su sru a nanan a',
'09_28_3023_98120398',
u'éàé瀮ð{æə|&']
series = {
'Female': [4, 2, 3, 0, 2],
'Male': [5, 1, 1, 3, 2]
}
labels = [
'AURSAUTRAUIA', 'dpvluiqhu enuie', 'su sru a nanan a',
'09_28_3023_98120398', u'éàé瀮ð{æə|&'
]
series = {'Female': [4, 2, 3, 0, 2], 'Male': [5, 1, 1, 3, 2]}

803
demo/moulinrouge/tests.py

File diff suppressed because it is too large Load Diff

46
docs/conf.py

@ -32,12 +32,8 @@ sys.path.insert(0, os.path.join(os.path.abspath('.'), 'ext'))
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.doctest',
'sphinx.ext.intersphinx',
'sphinx.ext.coverage',
'sphinx.ext.viewcode',
'pygal_sphinx_directives'
'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx',
'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'pygal_sphinx_directives'
]
# Add any paths that contain templates here, relative to this directory.
@ -112,7 +108,6 @@ pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# on_rtd is whether we are on readthedocs.org
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
@ -223,25 +218,27 @@ htmlhelp_basename = 'pygaldoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# Latex figure (float) alignment
#'figure_align': 'htbp',
# Latex figure (float) alignment
#'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'pygal.tex', 'pygal Documentation',
'Florian Mounier', 'manual'),
(
master_doc, 'pygal.tex', 'pygal Documentation', 'Florian Mounier',
'manual'
),
]
# The name of an image file (relative to this directory) to place at the top of
@ -264,29 +261,25 @@ latex_documents = [
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'pygal', 'pygal Documentation',
[author], 1)
]
man_pages = [(master_doc, 'pygal', 'pygal Documentation', [author], 1)]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'pygal', 'pygal Documentation',
author, 'pygal', 'One line description of project.',
'Miscellaneous'),
(
master_doc, 'pygal', 'pygal Documentation', author, 'pygal',
'One line description of project.', 'Miscellaneous'
),
]
# Documents to append as an appendix to all manuals.
@ -301,6 +294,5 @@ texinfo_documents = [
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'https://docs.python.org/': None}

94
docs/ext/pygal_sphinx_directives.py

@ -17,7 +17,6 @@
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
from traceback import format_exc, print_exc
import docutils.core
@ -37,7 +36,8 @@ pygal.config.Config.style.value = pygal.style.RotateStyle(
foreground_subtle='#909090',
opacity='.8',
opacity_hover='.9',
transition='400ms ease-in')
transition='400ms ease-in'
)
class PygalDirective(Directive):
@ -48,8 +48,8 @@ class PygalDirective(Directive):
has_content = True
def run(self):
width, height = map(int, self.arguments[:2]) if len(
self.arguments) >= 2 else (600, 400)
width, height = map(int, self.arguments[:2]
) if len(self.arguments) >= 2 else (600, 400)
if len(self.arguments) == 1:
self.render_fix = bool(self.arguments[0])
elif len(self.arguments) == 3:
@ -67,10 +67,15 @@ class PygalDirective(Directive):
except Exception:
print(code)
print_exc()
return [docutils.nodes.system_message(
'An exception as occured during code parsing:'
' \n %s' % format_exc(), type='ERROR', source='/',
level=3)]
return [
docutils.nodes.system_message(
'An exception as occured during code parsing:'
' \n %s' % format_exc(),
type='ERROR',
source='/',
level=3
)
]
if self.render_fix:
rv = scope['rv']
else:
@ -81,9 +86,14 @@ class PygalDirective(Directive):
self.content.append(key + '.render()')
break
if chart is None:
return [docutils.nodes.system_message(
'No instance of graph found', level=3,
type='ERROR', source='/')]
return [
docutils.nodes.system_message(
'No instance of graph found',
level=3,
type='ERROR',
source='/'
)
]
chart.config.width = width
chart.config.height = height
chart.explicit_size = True
@ -91,27 +101,28 @@ class PygalDirective(Directive):
try:
svg = '<embed src="%s" />' % chart.render_data_uri()
except Exception:
return [docutils.nodes.system_message(
'An exception as occured during graph generation:'
' \n %s' % format_exc(), type='ERROR', source='/',
level=3)]
return [
docutils.nodes.system_message(
'An exception as occured during graph generation:'
' \n %s' % format_exc(),
type='ERROR',
source='/',
level=3
)
]
return [docutils.nodes.raw('', svg, format='html')]
class PygalWithCode(PygalDirective):
def run(self):
node_list = super(PygalWithCode, self).run()
node_list.extend(CodeBlock(
self.name,
['python'],
self.options,
self.content,
self.lineno,
self.content_offset,
self.block_text,
self.state,
self.state_machine).run())
node_list.extend(
CodeBlock(
self.name, ['python'], self.options, self.content, self.lineno,
self.content_offset, self.block_text, self.state,
self.state_machine
).run()
)
return [docutils.nodes.compound('', *node_list)]
@ -134,29 +145,30 @@ class PygalTable(Directive):
exec(code, scope)
except Exception:
print_exc()
return [docutils.nodes.system_message(
'An exception as occured during code parsing:'
' \n %s' % format_exc(), type='ERROR', source='/',
level=3)]
return [
docutils.nodes.system_message(
'An exception as occured during code parsing:'
' \n %s' % format_exc(),
type='ERROR',
source='/',
level=3
)
]
rv = scope['rv']
return [docutils.nodes.raw('', rv, format='html')]
class PygalTableWithCode(PygalTable):
def run(self):
node_list = super(PygalTableWithCode, self).run()
node_list.extend(CodeBlock(
self.name,
['python'],
self.options,
self.content,
self.lineno,
self.content_offset,
self.block_text,
self.state,
self.state_machine).run())
node_list.extend(
CodeBlock(
self.name, ['python'], self.options, self.content, self.lineno,
self.content_offset, self.block_text, self.state,
self.state_machine
).run()
)
return [docutils.nodes.compound('', *node_list)]

16
pygal/__init__.py

@ -54,19 +54,20 @@ from pygal.graph.graph import Graph
from pygal.config import Config
from pygal import maps
CHARTS_BY_NAME = dict(
[(k, v) for k, v in locals().items()
if isinstance(v, type) and issubclass(v, Graph) and v != Graph])
CHARTS_BY_NAME = dict([
(k, v) for k, v in locals().items()
if isinstance(v, type) and issubclass(v, Graph) and v != Graph
])
from pygal.graph.map import BaseMap
for entry in pkg_resources.iter_entry_points('pygal.maps'):
try:
module = entry.load()
except Exception:
warnings.warn('Unable to load %s pygal plugin \n\n%s' % (
entry, traceback.format_exc()), Warning)
warnings.warn(
'Unable to load %s pygal plugin \n\n%s' %
(entry, traceback.format_exc()), Warning
)
continue
setattr(maps, entry.name, module)
for k, v in module.__dict__.items():
@ -78,7 +79,6 @@ CHARTS = list(CHARTS_BY_NAME.values())
class PluginImportFixer(object):
"""
Allow external map plugins to be imported from pygal.maps package.

2
pygal/_compat.py

@ -73,6 +73,7 @@ try:
from datetime import timezone
utc = timezone.utc
except ImportError:
class UTC(tzinfo):
def tzname(self, dt):
return 'UTC'
@ -82,6 +83,7 @@ except ImportError:
def dst(self, dt):
return None
utc = UTC()

18
pygal/colors.py

@ -81,8 +81,10 @@ def hsl_to_rgb(h, s, l):
if 3 * h < 2:
return m1 + 6 * (2 / 3 - h) * (m2 - m1)
return m1
r, g, b = map(lambda x: round(x * 255),
map(h_to_rgb, (h + 1 / 3, h, h - 1 / 3)))
r, g, b = map(
lambda x: round(x * 255), map(h_to_rgb, (h + 1 / 3, h, h - 1 / 3))
)
return r, g, b
@ -107,7 +109,8 @@ def parse_color(color):
assert len(color) == 8
type = type or '#rrggbbaa'
r, g, b, a = [
int(''.join(c), 16) for c in zip(color[::2], color[1::2])]
int(''.join(c), 16) for c in zip(color[::2], color[1::2])
]
a /= 255
elif color.startswith('rgb('):
type = 'rgb'
@ -116,8 +119,8 @@ def parse_color(color):
elif color.startswith('rgba('):
type = 'rgba'
color = color[5:-1]
r, g, b, a = [int(c) for c in color.split(',')[:-1]] + [
float(color.split(',')[-1])]
r, g, b, a = [int(c) for c in color.split(',')[:-1]
] + [float(color.split(',')[-1])]
return r, g, b, a, type
@ -134,8 +137,9 @@ def unparse_color(r, g, b, a, type):
if type == '#rgba':
if r % 17 == 0 and g % 17 == 0 and b % 17 == 0:
return '#%x%x%x%x' % (int(r / 17), int(g / 17), int(b / 17),
int(a * 15))
return '#%x%x%x%x' % (
int(r / 17), int(g / 17), int(b / 17), int(a * 15)
)
type = '#rrggbbaa'
if type == '#rrggbb':

5
pygal/etree.py

@ -25,7 +25,6 @@ import os
class Etree(object):
"""Etree wrapper using lxml.etree or standard xml.etree"""
def __init__(self):
@ -46,8 +45,8 @@ class Etree(object):
def __getattribute__(self, attr):
"""Retrieve attr from current active etree implementation"""
if (attr not in object.__getattribute__(self, '__dict__') and
attr not in Etree.__dict__):
if (attr not in object.__getattribute__(self, '__dict__')
and attr not in Etree.__dict__):
return object.__getattribute__(self._etree, attr)
return object.__getattribute__(self, attr)

8
pygal/formatters.py

@ -46,14 +46,16 @@ class HumanReadable(Formatter):
order = val and int(floor(log(abs(val)) / log(1000)))
orders = self.ORDERS.split(" ")[int(order > 0)]
if order == 0 or order > len(orders):
return float_format(val / (1000 ** int(order)))
return float_format(val / (1000**int(order)))
return (
float_format(val / (1000 ** int(order))) +
orders[int(order) - int(order > 0)])
float_format(val / (1000**int(order))) +
orders[int(order) - int(order > 0)]
)
class Significant(Formatter):
"""Show precision significant digit of float"""
def __init__(self, precision=10):
self.format = '%%.%dg' % precision

1
pygal/graph/__init__.py

@ -16,5 +16,4 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Graph package containing all builtin charts"""

43
pygal/graph/bar.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""
Bar chart that presents grouped data with rectangular bars with lengths
proportional to the values that they represent.
@ -29,7 +28,6 @@ from pygal.util import alter, decorate, ident, swap
class Bar(Graph):
"""Bar graph class"""
_series_margin = .06
@ -54,15 +52,25 @@ class Bar(Graph):
width -= 2 * serie_margin
height = self.view.y(zero) - y
r = serie.rounded_bars * 1 if serie.rounded_bars else 0
alter(self.svg.transposable_node(
parent, 'rect',
x=x, y=y, rx=r, ry=r, width=width, height=height,
class_='rect reactive tooltip-trigger'), serie.metadata.get(i))
alter(
self.svg.transposable_node(
parent,
'rect',
x=x,
y=y,
rx=r,
ry=r,
width=width,
height=height,
class_='rect reactive tooltip-trigger'
), serie.metadata.get(i)
)
return x, y, width, height
def _tooltip_and_print_values(
self, serie_node, serie, parent, i, val, metadata,
x, y, width, height):
self, serie_node, serie, parent, i, val, metadata, x, y, width,
height
):
transpose = swap if self.horizontal else ident
x_center, y_center = transpose((x + width / 2, y + height / 2))
x_top, y_top = transpose((x + width, y + height))
@ -73,8 +81,8 @@ class Bar(Graph):
v = serie.values[i]
sign = -1 if v < self.zero else 1
self._tooltip_data(
parent, val, x_center, y_center, "centered",
self._get_x_label(i))
parent, val, x_center, y_center, "centered", self._get_x_label(i)
)
if self.print_values_position == 'top':
if self.horizontal:
@ -111,20 +119,21 @@ class Bar(Graph):
val = self._format(serie, i)
bar = decorate(
self.svg,
self.svg.node(bars, class_='bar'),
metadata)
self.svg, self.svg.node(bars, class_='bar'), metadata
)
x_, y_, width, height = self._bar(
serie, bar, x, y, i, self.zero, secondary=rescale)
serie, bar, x, y, i, self.zero, secondary=rescale
)
self._confidence_interval(
serie_node['overlay'], x_ + width / 2, y_, serie.values[i],
metadata)
metadata
)
self._tooltip_and_print_values(
serie_node, serie, bar, i, val, metadata,
x_, y_, width, height)
serie_node, serie, bar, i, val, metadata, x_, y_, width, height
)
def _compute(self):
"""Compute y min and max and y scale and set labels"""

70
pygal/graph/base.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Base for pygal charts"""
from __future__ import division
@ -36,7 +35,6 @@ from pygal.view import Box, Margin
class BaseGraph(object):
"""Chart internal behaviour related functions"""
_adapters = []
@ -93,7 +91,7 @@ class BaseGraph(object):
if not raw:
return
adapters = list(self._adapters) or [lambda x:x]
adapters = list(self._adapters) or [lambda x: x]
if self.logarithmic:
for fun in not_zero, positive:
if fun in adapters:
@ -103,19 +101,18 @@ class BaseGraph(object):
self._adapt = reduce(compose, adapters) if not self.strict else ident
self._x_adapt = reduce(
compose, self._x_adapters) if not self.strict and getattr(
self, '_x_adapters', None) else ident
compose, self._x_adapters
) if not self.strict and getattr(self, '_x_adapters', None) else ident
series = []
raw = [(
list(raw_values) if not isinstance(
raw_values, dict) else raw_values,
serie_config_kwargs
list(raw_values) if not isinstance(raw_values, dict) else
raw_values, serie_config_kwargs
) for raw_values, serie_config_kwargs in raw]
width = max([len(values) for values, _ in raw] +
[len(self.x_labels or [])])
width = max([len(values)
for values, _ in raw] + [len(self.x_labels or [])])
for raw_values, serie_config_kwargs in raw:
metadata = {}
@ -130,10 +127,9 @@ class BaseGraph(object):
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 [])):
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)
@ -157,8 +153,8 @@ class BaseGraph(object):
value = (value, self.zero)
if self._x_adapt:
value = (
self._x_adapt(value[0]),
self._adapt(value[1]))
self._x_adapt(value[0]), self._adapt(value[1])
)
if isinstance(self, BaseMap):
value = (self._adapt(value[0]), value[1])
else:
@ -168,11 +164,14 @@ class BaseGraph(object):
values.append(value)
serie_config = SerieConfig()
serie_config(**dict((k, v) for k, v in self.state.__dict__.items()
if k in dir(serie_config)))
serie_config(
**dict((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), values, serie_config, metadata))
Serie(offset + len(series), values, serie_config, metadata)
)
return series
def setup(self, **kwargs):
@ -185,11 +184,13 @@ class BaseGraph(object):
self.state = State(self, **kwargs)
if isinstance(self.style, type):
self.style = self.style()
self.series = self.prepare_values(
[rs for rs in self.raw_series if not rs[1].get('secondary')]) or []
self.series = self.prepare_values([
rs for rs in self.raw_series if not rs[1].get('secondary')
]) or []
self.secondary_series = self.prepare_values(
[rs for rs in self.raw_series if rs[1].get('secondary')],
len(self.series)) or []
[rs for rs in self.raw_series
if rs[1].get('secondary')], len(self.series)
) or []
self.horizontal = getattr(self, 'horizontal', False)
self.svg = Svg(self)
self._x_labels = None
@ -198,20 +199,23 @@ class BaseGraph(object):
self._y_2nd_labels = None
self.nodes = {}
self.margin_box = 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_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
positive_values = list(filter(
lambda x: x > 0,
[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
positive_values = list(
filter(
lambda x: x > 0, [
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
if self._len < 3:
self.interpolate = None
self._draw()

134
pygal/graph/box.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""
Box plot: a convenient way to display series as box with whiskers and outliers
Different types are available throught the box_mode option
@ -31,7 +30,6 @@ from pygal.util import alter, decorate
class Box(Graph):
"""
Box plot
For each series, shows the median value, the 25th and 75th percentiles,
@ -49,17 +47,20 @@ class Box(Graph):
"""
if self.box_mode == "extremes":
return (
'Min: %s\nQ1 : %s\nQ2 : %s\nQ3 : %s\nMax: %s' % tuple(
map(self._y_format, serie.points[1:6])))
'Min: %s\nQ1 : %s\nQ2 : %s\nQ3 : %s\nMax: %s' %
tuple(map(self._y_format, serie.points[1:6]))
)
elif self.box_mode in ["tukey", "stdev", "pstdev"]:
return (
'Min: %s\nLower Whisker: %s\nQ1: %s\nQ2: %s\nQ3: %s\n'
'Upper Whisker: %s\nMax: %s' % tuple(map(
self._y_format, serie.points)))
'Upper Whisker: %s\nMax: %s' %
tuple(map(self._y_format, serie.points))
)
elif self.box_mode == '1.5IQR':
# 1.5IQR mode
return 'Q1: %s\nQ2: %s\nQ3: %s' % tuple(map(
self._y_format, serie.points[2:5]))
return 'Q1: %s\nQ2: %s\nQ3: %s' % tuple(
map(self._y_format, serie.points[2:5])
)
else:
return self._y_format(serie.points)
@ -72,8 +73,7 @@ class Box(Graph):
serie.points, serie.outliers = \
self._box_points(serie.values, self.box_mode)
self._x_pos = [
(i + .5) / self._order for i in range(self._order)]
self._x_pos = [(i + .5) / self._order for i in range(self._order)]
if self._min:
self._box.ymin = min(self._min, self.zero)
@ -100,17 +100,17 @@ class Box(Graph):
metadata = serie.metadata.get(0)
box = decorate(
self.svg,
self.svg.node(boxes, class_='box'),
metadata)
box = decorate(self.svg, self.svg.node(boxes, class_='box'), metadata)
val = self._format(serie, 0)
x_center, y_center = self._draw_box(
box, serie.points[1:6], serie.outliers, serie.index, metadata)
self._tooltip_data(box, val, x_center, y_center, "centered",
self._get_x_label(serie.index))
box, serie.points[1:6], serie.outliers, serie.index, metadata
)
self._tooltip_data(
box, val, x_center, y_center, "centered",
self._get_x_label(serie.index)
)
self._static_value(serie_node, val, x_center, y_center, metadata)
def _draw_box(self, parent_node, quartiles, outliers, box_index, metadata):
@ -124,55 +124,78 @@ class Box(Graph):
width -= 2 * series_margin
# draw lines for whiskers - bottom, median, and top
for i, whisker in enumerate(
(quartiles[0], quartiles[2], quartiles[4])):
for i, whisker in enumerate((quartiles[0], quartiles[2],
quartiles[4])):
whisker_width = width if i == 1 else width / 2
shift = (width - whisker_width) / 2
xs = left_edge + shift
xe = left_edge + width - shift
alter(self.svg.line(
parent_node,
coords=[(xs, self.view.y(whisker)),
(xe, self.view.y(whisker))],
class_='reactive tooltip-trigger',
attrib={'stroke-width': 3}), metadata)
alter(
self.svg.line(
parent_node,
coords=[(xs, self.view.y(whisker)),
(xe, self.view.y(whisker))],
class_='reactive tooltip-trigger',
attrib={
'stroke-width': 3
}
), metadata
)
# draw lines connecting whiskers to box (Q1 and Q3)
alter(self.svg.line(
parent_node,
coords=[(left_edge + width / 2, self.view.y(quartiles[0])),
(left_edge + width / 2, self.view.y(quartiles[1]))],
class_='reactive tooltip-trigger',
attrib={'stroke-width': 2}), metadata)
alter(self.svg.line(
parent_node,
coords=[(left_edge + width / 2, self.view.y(quartiles[4])),
(left_edge + width / 2, self.view.y(quartiles[3]))],
class_='reactive tooltip-trigger',
attrib={'stroke-width': 2}), metadata)
alter(
self.svg.line(
parent_node,
coords=[(left_edge + width / 2, self.view.y(quartiles[0])),
(left_edge + width / 2, self.view.y(quartiles[1]))],
class_='reactive tooltip-trigger',
attrib={
'stroke-width': 2
}
), metadata
)
alter(
self.svg.line(
parent_node,
coords=[(left_edge + width / 2, self.view.y(quartiles[4])),
(left_edge + width / 2, self.view.y(quartiles[3]))],
class_='reactive tooltip-trigger',
attrib={
'stroke-width': 2
}
), metadata
)
# box, bounded by Q1 and Q3
alter(self.svg.node(
parent_node,
tag='rect',
x=left_edge,
y=self.view.y(quartiles[1]),
height=self.view.y(quartiles[3]) - self.view.y(quartiles[1]),
width=width,
class_='subtle-fill reactive tooltip-trigger'), metadata)
alter(
self.svg.node(
parent_node,
tag='rect',
x=left_edge,
y=self.view.y(quartiles[1]),
height=self.view.y(quartiles[3]) - self.view.y(quartiles[1]),
width=width,
class_='subtle-fill reactive tooltip-trigger'
), metadata
)
# draw outliers
for o in outliers:
alter(self.svg.node(
parent_node,
tag='circle',
cx=left_edge + width / 2,
cy=self.view.y(o),
r=3,
class_='subtle-fill reactive tooltip-trigger'), metadata)
return (left_edge + width / 2, self.view.y(
sum(quartiles) / len(quartiles)))
alter(
self.svg.node(
parent_node,
tag='circle',
cx=left_edge + width / 2,
cy=self.view.y(o),
r=3,
class_='subtle-fill reactive tooltip-trigger'
), metadata
)
return (
left_edge + width / 2,
self.view.y(sum(quartiles) / len(quartiles))
)
@staticmethod
def _box_points(values, mode='extremes'):
@ -199,6 +222,7 @@ class Box(Graph):
Sincich, T. L. Statistics for Engineering and the
Sciences, 4th ed. Prentice-Hall, 1995.
"""
def median(seq):
n = len(seq)
if n % 2 == 0: # seq has an even length

70
pygal/graph/dot.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""
Dot chart displaying values as a grid of dots, the bigger the value
the bigger the dot
@ -33,7 +32,6 @@ from pygal.view import ReverseView, View
class Dot(Graph):
"""Dot graph class"""
def dot(self, serie, r_max):
@ -48,10 +46,8 @@ class Dot(Graph):
log10max = log10(self._max or 1)
if value != 0:
size = r_max * (
(log10(abs(value)) - log10min) /
(log10max - log10min)
)
size = r_max * ((log10(abs(value)) - log10min) /
(log10max - log10min))
else:
size = 0
else:
@ -59,19 +55,25 @@ class Dot(Graph):
metadata = serie.metadata.get(i)
dots = decorate(
self.svg,
self.svg.node(serie_node['plot'], class_="dots"),
metadata)
alter(self.svg.node(
dots, 'circle',
cx=x, cy=y, r=size,
class_='dot reactive tooltip-trigger' + (
' negative' if value < 0 else '')), metadata)
self.svg, self.svg.node(serie_node['plot'], class_="dots"),
metadata
)
alter(
self.svg.node(
dots,
'circle',
cx=x,
cy=y,
r=size,
class_='dot reactive tooltip-trigger' +
(' negative' if value < 0 else '')
), metadata
)
val = self._format(serie, i)
self._tooltip_data(
dots, val, x, y, 'centered',
self._get_x_label(i))
dots, val, x, y, 'centered', self._get_x_label(i)
)
self._static_value(serie_node, val, x, y, metadata)
def _compute(self):
@ -85,26 +87,28 @@ class Dot(Graph):
self._y_pos = [n / 2 for n in reversed(range(1, 2 * y_len, 2))]
for j, serie in enumerate(self.series):
serie.points = [
(self._x_pos[i], self._y_pos[j])
for i in range(x_len)]
serie.points = [(self._x_pos[i], self._y_pos[j])
for i in range(x_len)]
def _compute_y_labels(self):
self._y_labels = list(zip(
self.y_labels and map(to_str, self.y_labels) or [
serie.title['title']
if isinstance(serie.title, dict)
else serie.title or '' for serie in self.series],
self._y_pos))
self._y_labels = list(
zip(
self.y_labels and map(to_str, self.y_labels) or [
serie.title['title']
if isinstance(serie.title, dict) else serie.title or ''
for serie in self.series
], self._y_pos
)
)
def _set_view(self):
"""Assign a view to current graph"""
view_class = ReverseView if self.inverse_y_axis else View
self.view = view_class(
self.width - self.margin_box.x,
self.height - self.margin_box.y,
self._box)
self.width - self.margin_box.x, self.height - self.margin_box.y,
self._box
)
@cached_property
def _values(self):
@ -114,14 +118,16 @@ class Dot(Graph):
@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(map(abs, self._values)) if self._values else None))
return (
self.range[1] if (self.range and self.range[1] is not None) else
(max(map(abs, self._values)) if self._values else None)
)
def _plot(self):
"""Plot all dots for series"""
r_max = min(
self.view.x(1) - self.view.x(0),
(self.view.y(0) or 0) - self.view.y(1)) / (
2 * 1.05)
(self.view.y(0) or 0) - self.view.y(1)
) / (2 * 1.05)
for serie in self.series:
self.dot(serie, r_max)

12
pygal/graph/dual.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Dual chart base. Dual means a chart with 2 scaled axis like xy"""
from pygal._compat import is_str
@ -31,14 +30,12 @@ class Dual(Graph):
"""
Format value for dual value display.
"""
return '%s: %s' % (
self._x_format(value[0]),
self._y_format(value[1]))
return '%s: %s' % (self._x_format(value[0]), self._y_format(value[1]))
def _compute_x_labels(self):
x_pos = compute_scale(
self._box.xmin, self._box.xmax, self.logarithmic,
self.order_min, self.min_scale, self.max_scale
self._box.xmin, self._box.xmax, self.logarithmic, self.order_min,
self.min_scale, self.max_scale
)
if self.x_labels:
self._x_labels = []
@ -63,7 +60,8 @@ class Dual(Graph):
def _compute_x_labels_major(self):
# In case of dual, x labels must adapters and so majors too
self.x_labels_major = self.x_labels_major and list(
map(self._x_adapt, self.x_labels_major))
map(self._x_adapt, self.x_labels_major)
)
super(Dual, self)._compute_x_labels_major()
def _get_x_label(self, i):

42
pygal/graph/funnel.py

@ -26,7 +26,6 @@ from pygal.util import alter, cut, decorate
class Funnel(Graph):
"""Funnel graph class"""
_adapters = [positive, none_to_zero]
@ -44,22 +43,27 @@ class Funnel(Graph):
val = self._format(serie, i)
funnels = decorate(
self.svg,
self.svg.node(serie_node['plot'], class_="funnels"),
metadata)
alter(self.svg.node(
funnels, 'polygon',
points=' '.join(map(fmt, map(self.view, poly))),
class_='funnel reactive tooltip-trigger'), metadata)
self.svg, self.svg.node(serie_node['plot'], class_="funnels"),
metadata
)
alter(
self.svg.node(
funnels,
'polygon',
points=' '.join(map(fmt, map(self.view, poly))),
class_='funnel reactive tooltip-trigger'
), metadata
)
# Poly center from label
x, y = self.view((
self._center(self._x_pos[serie.index]),
sum([point[1] for point in poly]) / len(poly)))
sum([point[1] for point in poly]) / len(poly)
))
self._tooltip_data(
funnels, val, x, y, 'centered',
self._get_x_label(serie.index))
funnels, val, x, y, 'centered', self._get_x_label(serie.index)
)
self._static_value(serie_node, val, x, y, metadata)
def _center(self, x):
@ -73,7 +77,7 @@ class Funnel(Graph):
previous = [[self.zero, self.zero] for i in range(self._len)]
for i, serie in enumerate(self.series):
y_height = - sum(serie.safe_values) / 2
y_height = -sum(serie.safe_values) / 2
all_x_pos = [0] + self._x_pos
serie.points = []
for j, value in enumerate(serie.values):
@ -98,12 +102,14 @@ class Funnel(Graph):
def _compute_x_labels(self):
self._x_labels = list(
zip(self.x_labels and
map(self._x_format, self.x_labels) or [
zip(
self.x_labels and map(self._x_format, self.x_labels) or [
serie.title['title']
if isinstance(serie.title, dict)
else serie.title or '' for serie in self.series],
map(self._center, self._x_pos)))
if isinstance(serie.title, dict) else serie.title or ''
for serie in self.series
], map(self._center, self._x_pos)
)
)
def _plot(self):
"""Plot the funnel"""

67
pygal/graph/gauge.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Gauge chart representing values as needles on a polar scale"""
from __future__ import division
@ -28,7 +27,6 @@ from pygal.view import PolarThetaLogView, PolarThetaView
class Gauge(Graph):
"""Gauge graph class"""
needle_width = 1 / 20
@ -41,9 +39,9 @@ class Gauge(Graph):
view_class = PolarThetaView
self.view = view_class(
self.width - self.margin_box.x,
self.height - self.margin_box.y,
self._box)
self.width - self.margin_box.x, self.height - self.margin_box.y,
self._box
)
def needle(self, serie):
"""Draw a needle for each value"""
@ -58,9 +56,9 @@ class Gauge(Graph):
val = self._format(serie, i)
metadata = serie.metadata.get(i)
gauges = decorate(
self.svg,
self.svg.node(serie_node['plot'], class_="dots"),
metadata)
self.svg, self.svg.node(serie_node['plot'], class_="dots"),
metadata
)
tolerance = 1.15
@ -73,23 +71,24 @@ class Gauge(Graph):
w = (self._box._tmax - self._box._tmin + self.view.aperture) / 4
if self.logarithmic:
w = min(w, self._min - self._min * 10 ** -10)
w = min(w, self._min - self._min * 10**-10)
alter(
self.svg.node(
gauges, 'path', d='M %s L %s A %s 1 0 1 %s Z' % (
gauges,
'path',
d='M %s L %s A %s 1 0 1 %s Z' % (
point(.85, theta),
point(self.needle_width, theta - w),
'%f %f' % (self.needle_width, self.needle_width),
point(self.needle_width, theta + w),
),
class_='line reactive tooltip-trigger'),
metadata)
class_='line reactive tooltip-trigger'
), metadata
)
x, y = self.view((.75, theta))
self._tooltip_data(
gauges, val, x, y,
xlabel=self._get_x_label(i))
self._tooltip_data(gauges, val, x, y, xlabel=self._get_x_label(i))
self._static_value(serie_node, val, x, y, metadata)
def _y_axis(self, draw_axes=True):
@ -100,26 +99,26 @@ class Gauge(Graph):
guides = self.svg.node(axis, class_='guides')
self.svg.line(
guides, [self.view((.95, theta)), self.view((1, theta))],
guides, [self.view((.95, theta)),
self.view((1, theta))],
close=True,
class_='line')
class_='line'
)
self.svg.line(
guides, [self.view((0, theta)), self.view((.95, theta))],
guides, [self.view((0, theta)),
self.view((.95, theta))],
close=True,
class_='guide line %s' % (
'major' if i in (0, len(self._y_labels) - 1)
else ''))
class_='guide line %s' %
('major' if i in (0, len(self._y_labels) - 1) else '')
)
x, y = self.view((.9, theta))
self.svg.node(
guides, 'text',
x=x,
y=y
).text = label
self.svg.node(guides, 'text', x=x, y=y).text = label
self.svg.node(
guides, 'title',
guides,
'title',
).text = self._y_format(theta)
def _x_axis(self, draw_axes=True):
@ -136,18 +135,15 @@ class Gauge(Graph):
self.min_ -= 1
self.max_ += 1
self._box.set_polar_box(
0, 1,
self.min_,
self.max_)
self._box.set_polar_box(0, 1, self.min_, self.max_)
def _compute_x_labels(self):
pass
def _compute_y_labels(self):
y_pos = compute_scale(
self.min_, self.max_, self.logarithmic,
self.order_min, self.min_scale, self.max_scale
self.min_, self.max_, self.logarithmic, self.order_min,
self.min_scale, self.max_scale
)
if self.y_labels:
self._y_labels = []
@ -164,10 +160,7 @@ class Gauge(Graph):
self._y_labels.append((title, pos))
self.min_ = min(self.min_, min(cut(self._y_labels, 1)))
self.max_ = max(self.max_, max(cut(self._y_labels, 1)))
self._box.set_polar_box(
0, 1,
self.min_,
self.max_)
self._box.set_polar_box(0, 1, self.min_, self.max_)
else:
self._y_labels = list(zip(map(self._y_format, y_pos), y_pos))

597
pygal/graph/graph.py

@ -28,12 +28,12 @@ from pygal.graph.public import PublicApi
from pygal.interpolate import INTERPOLATIONS
from pygal.util import (
cached_property, compute_scale, cut, decorate, filter_kwargs, get_text_box,
get_texts_box, majorize, rad, reverse_text_len, split_title, truncate)
get_texts_box, majorize, rad, reverse_text_len, split_title, truncate
)
from pygal.view import LogView, ReverseView, View, XYLogView
class Graph(PublicApi):
"""Graph super class containing generic common functions"""
_dual = False
@ -64,65 +64,93 @@ class Graph(PublicApi):
view_class = ReverseView if self.inverse_y_axis else View
self.view = view_class(
self.width - self.margin_box.x,
self.height - self.margin_box.y,
self._box)
self.width - self.margin_box.x, self.height - self.margin_box.y,
self._box
)
def _make_graph(self):
"""Init common graph svg structure"""
self.nodes['graph'] = self.svg.node(
class_='graph %s-graph %s' % (
self.__class__.__name__.lower(),
'horizontal' if self.horizontal else 'vertical'))
self.svg.node(self.nodes['graph'], 'rect',
class_='background',
x=0, y=0,
width=self.width,
height=self.height)
'horizontal' if self.horizontal else 'vertical'
)
)
self.svg.node(
self.nodes['graph'],
'rect',
class_='background',
x=0,
y=0,
width=self.width,
height=self.height
)
self.nodes['plot'] = self.svg.node(
self.nodes['graph'], class_="plot",
transform="translate(%d, %d)" % (
self.margin_box.left, self.margin_box.top))
self.svg.node(self.nodes['plot'], 'rect',
class_='background',
x=0, y=0,
width=self.view.width,
height=self.view.height)
self.nodes['title'] = self.svg.node(
self.nodes['graph'],
class_="titles")
class_="plot",
transform="translate(%d, %d)" %
(self.margin_box.left, self.margin_box.top)
)
self.svg.node(
self.nodes['plot'],
'rect',
class_='background',
x=0,
y=0,
width=self.view.width,
height=self.view.height
)
self.nodes['title'] = self.svg.node(
self.nodes['graph'], class_="titles"
)
self.nodes['overlay'] = self.svg.node(
self.nodes['graph'], class_="plot overlay",
transform="translate(%d, %d)" % (
self.margin_box.left, self.margin_box.top))
self.nodes['graph'],
class_="plot overlay",
transform="translate(%d, %d)" %
(self.margin_box.left, self.margin_box.top)
)
self.nodes['text_overlay'] = self.svg.node(
self.nodes['graph'], class_="plot text-overlay",
transform="translate(%d, %d)" % (
self.margin_box.left, self.margin_box.top))
self.nodes['graph'],
class_="plot text-overlay",
transform="translate(%d, %d)" %
(self.margin_box.left, self.margin_box.top)
)
self.nodes['tooltip_overlay'] = self.svg.node(
self.nodes['graph'], class_="plot tooltip-overlay",
transform="translate(%d, %d)" % (
self.margin_box.left, self.margin_box.top))
self.nodes['graph'],
class_="plot tooltip-overlay",
transform="translate(%d, %d)" %
(self.margin_box.left, self.margin_box.top)
)
self.nodes['tooltip'] = self.svg.node(
self.nodes['tooltip_overlay'],
transform='translate(0 0)',
style="opacity: 0",
**{'class': 'tooltip'})
**{
'class': 'tooltip'
}
)
self.svg.node(self.nodes['tooltip'], 'rect',
rx=self.tooltip_border_radius,
ry=self.tooltip_border_radius,
width=0, height=0,
**{'class': 'tooltip-box'})
self.svg.node(
self.nodes['tooltip'],
'rect',
rx=self.tooltip_border_radius,
ry=self.tooltip_border_radius,
width=0,
height=0,
**{
'class': 'tooltip-box'
}
)
self.svg.node(self.nodes['tooltip'], 'g', class_='text')
def _x_axis(self):
"""Make the x axis: labels and guides"""
if not self._x_labels or not self.show_x_labels:
return
axis = self.svg.node(self.nodes['plot'], class_="axis x%s" % (
' always_show' if self.show_x_guides else ''
))
axis = self.svg.node(
self.nodes['plot'],
class_="axis x%s" % (' always_show' if self.show_x_guides else '')
)
truncation = self.truncate_label
if not truncation:
if self.x_label_rotation or len(self._x_labels) <= 1:
@ -130,18 +158,21 @@ class Graph(PublicApi):
else:
first_label_position = self.view.x(self._x_labels[0][1]) or 0
last_label_position = self.view.x(self._x_labels[-1][1]) or 0
available_space = (
last_label_position - first_label_position) / (
len(self._x_labels) - 1)
available_space = (last_label_position - first_label_position
) / (len(self._x_labels) - 1)
truncation = reverse_text_len(
available_space, self.style.label_font_size)
available_space, self.style.label_font_size
)
truncation = max(truncation, 1)
lastlabel = self._x_labels[-1][0]
if 0 not in [label[1] for label in self._x_labels]:
self.svg.node(axis, 'path',
d='M%f %f v%f' % (0, 0, self.view.height),
class_='line')
self.svg.node(
axis,
'path',
d='M%f %f v%f' % (0, 0, self.view.height),
class_='line'
)
lastlabel = None
for label, position in self._x_labels:
@ -158,18 +189,18 @@ class Graph(PublicApi):
y = self.view.height + 5
last_guide = (self._y_2nd_labels and label == lastlabel)
self.svg.node(
guides, 'path',
guides,
'path',
d='M%f %f v%f' % (x or 0, 0, self.view.height),
class_='%s%s%sline' % (
'axis ' if label == "0" else '',
'major ' if major else '',
'guide ' if position != 0 and not last_guide else ''))
'axis ' if label == "0" else '', 'major '
if major else '', 'guide '
if position != 0 and not last_guide else ''
)
)
y += .5 * self.style.label_font_size + 5
text = self.svg.node(
guides, 'text',
x=x,
y=y,
class_='major' if major else ''
guides, 'text', x=x, y=y, class_='major' if major else ''
)
text.text = truncate(label, truncation)
@ -177,29 +208,35 @@ class Graph(PublicApi):
self.svg.node(guides, 'title').text = label
elif self._dual:
self.svg.node(
guides, 'title',
guides,
'title',
).text = self._x_format(position)
if self.x_label_rotation:
text.attrib['transform'] = "rotate(%d %f %f)" % (
self.x_label_rotation, x, y)
self.x_label_rotation, x, y
)
if self.x_label_rotation >= 180:
text.attrib['class'] = ' '.join(
(text.attrib['class'] and text.attrib['class'].split(
' ') or []) + ['backwards'])
text.attrib['class'] = ' '.join((
text.attrib['class']
and text.attrib['class'].split(' ') or []
) + ['backwards'])
if self._y_2nd_labels and 0 not in [
label[1] for label in self._x_labels]:
self.svg.node(axis, 'path',
d='M%f %f v%f' % (
self.view.width, 0, self.view.height),
class_='line')
if self._y_2nd_labels and 0 not in [label[1]
for label in self._x_labels]:
self.svg.node(
axis,
'path',
d='M%f %f v%f' % (self.view.width, 0, self.view.height),
class_='line'
)
if self._x_2nd_labels:
secondary_ax = self.svg.node(
self.nodes['plot'], class_="axis x x2%s" % (
' always_show' if self.show_x_guides else ''
))
self.nodes['plot'],
class_="axis x x2%s" %
(' always_show' if self.show_x_guides else '')
)
for label, position in self._x_2nd_labels:
major = label in self._x_labels_major
if not (self.show_minor_x_labels or major):
@ -209,37 +246,38 @@ class Graph(PublicApi):
x = self.view.x(position)
y = -5
text = self.svg.node(
guides, 'text',
x=x,
y=y,
class_='major' if major else ''
guides, 'text', x=x, y=y, class_='major' if major else ''
)
text.text = label
if self.x_label_rotation:
text.attrib['transform'] = "rotate(%d %f %f)" % (
-self.x_label_rotation, x, y)
-self.x_label_rotation, x, y
)
if self.x_label_rotation >= 180:
text.attrib['class'] = ' '.join((
text.attrib['class'] and
text.attrib['class'].split(
' ') or []) + ['backwards'])
text.attrib['class']
and text.attrib['class'].split(' ') or []
) + ['backwards'])
def _y_axis(self):
"""Make the y axis: labels and guides"""
if not self._y_labels or not self.show_y_labels:
return
axis = self.svg.node(self.nodes['plot'], class_="axis y%s" % (
' always_show' if self.show_y_guides else ''
))
axis = self.svg.node(
self.nodes['plot'],
class_="axis y%s" % (' always_show' if self.show_y_guides else '')
)
if (0 not in [label[1] for label in self._y_labels] and
self.show_y_guides):
if (0 not in [label[1] for label in self._y_labels]
and self.show_y_guides):
self.svg.node(
axis, 'path',
axis,
'path',
d='M%f %f h%f' % (
0, 0 if self.inverse_y_axis else self.view.height,
self.view.width),
self.view.width
),
class_='line'
)
@ -251,23 +289,28 @@ class Graph(PublicApi):
if not (self.show_minor_y_labels or major):
continue
guides = self.svg.node(axis, class_='%sguides' % (
'logarithmic ' if self.logarithmic else ''
))
guides = self.svg.node(
axis,
class_='%sguides' %
('logarithmic ' if self.logarithmic else '')
)
x = -5
y = self.view.y(position)
if not y:
continue
if self.show_y_guides:
self.svg.node(
guides, 'path',
guides,
'path',
d='M%f %f h%f' % (0, y, self.view.width),
class_='%s%s%sline' % (
'axis ' if label == "0" else '',
'major ' if major else '',
'guide ' if position != 0 else ''))
'axis ' if label == "0" else '', 'major '
if major else '', 'guide ' if position != 0 else ''
)
)
text = self.svg.node(
guides, 'text',
guides,
'text',
x=x,
y=y + .35 * self.style.label_font_size,
class_='major' if major else ''
@ -277,18 +320,20 @@ class Graph(PublicApi):
if self.y_label_rotation:
text.attrib['transform'] = "rotate(%d %f %f)" % (
self.y_label_rotation, x, y)
self.y_label_rotation, x, y
)
if 90 < self.y_label_rotation < 270:
text.attrib['class'] = ' '.join(
(text.attrib['class'] and text.attrib['class'].split(
' ') or []) + ['backwards'])
text.attrib['class'] = ' '.join((
text.attrib['class']
and text.attrib['class'].split(' ') or []
) + ['backwards'])
self.svg.node(
guides, 'title',
guides,
'title',
).text = self._y_format(position)
if self._y_2nd_labels:
secondary_ax = self.svg.node(
self.nodes['plot'], class_="axis y2")
secondary_ax = self.svg.node(self.nodes['plot'], class_="axis y2")
for label, position in self._y_2nd_labels:
major = position in self._y_labels_major
if not (self.show_minor_y_labels or major):
@ -298,7 +343,8 @@ class Graph(PublicApi):
x = self.view.width + 5
y = self.view.y(position)
text = self.svg.node(
guides, 'text',
guides,
'text',
x=x,
y=y + .35 * self.style.label_font_size,
class_='major' if major else ''
@ -306,12 +352,13 @@ class Graph(PublicApi):
text.text = label
if self.y_label_rotation:
text.attrib['transform'] = "rotate(%d %f %f)" % (
self.y_label_rotation, x, y)
self.y_label_rotation, x, y
)
if 90 < self.y_label_rotation < 270:
text.attrib['class'] = ' '.join(
(text.attrib['class'] and
text.attrib['class'].split(
' ') or []) + ['backwards'])
text.attrib['class'] = ' '.join((
text.attrib['class']
and text.attrib['class'].split(' ') or []
) + ['backwards'])
def _legend(self):
"""Make the legend box"""
@ -320,17 +367,20 @@ class Graph(PublicApi):
truncation = self.truncate_legend
if self.legend_at_bottom:
x = self.margin_box.left + self.spacing
y = (self.margin_box.top + self.view.height +
self._x_title_height +
self._x_labels_height + self.spacing)
cols = self.legend_at_bottom_columns or ceil(
sqrt(self._order)) or 1
y = (
self.margin_box.top + self.view.height + self._x_title_height +
self._x_labels_height + self.spacing
)
cols = self.legend_at_bottom_columns or ceil(sqrt(self._order)
) or 1
if not truncation:
available_space = self.view.width / cols - (
self.legend_box_size + 5)
self.legend_box_size + 5
)
truncation = reverse_text_len(
available_space, self.style.legend_font_size)
available_space, self.style.legend_font_size
)
else:
x = self.spacing
y = self.margin_box.top + self.spacing
@ -339,8 +389,10 @@ class Graph(PublicApi):
truncation = 15
legends = self.svg.node(
self.nodes['graph'], class_='legends',
transform='translate(%d, %d)' % (x, y))
self.nodes['graph'],
class_='legends',
transform='translate(%d, %d)' % (x, y)
)
h = max(self.legend_box_size, self.style.legend_font_size)
x_step = self.view.width / cols
@ -352,22 +404,25 @@ class Graph(PublicApi):
x = self.margin_box.left + self.view.width + self.spacing
if self._y_2nd_labels:
h, w = get_texts_box(
cut(self._y_2nd_labels), self.style.label_font_size)
x += self.spacing + max(w * abs(cos(rad(
self.y_label_rotation))), h)
cut(self._y_2nd_labels), self.style.label_font_size
)
x += self.spacing + max(
w * abs(cos(rad(self.y_label_rotation))), h
)
y = self.margin_box.top + self.spacing
secondary_legends = self.svg.node(
self.nodes['graph'], class_='legends',
transform='translate(%d, %d)' % (x, y))
self.nodes['graph'],
class_='legends',
transform='translate(%d, %d)' % (x, y)
)
serie_number = -1
i = 0
for titles, is_secondary in (
(self._legends, False),
(self._secondary_legends, True)):
for titles, is_secondary in ((self._legends, False),
(self._secondary_legends, True)):
if not self.legend_at_bottom and is_secondary:
i = 0
@ -381,9 +436,11 @@ class Graph(PublicApi):
legend = self.svg.node(
secondary_legends if is_secondary else legends,
class_='legend reactive activate-serie',
id="activate-serie-%d" % serie_number)
id="activate-serie-%d" % serie_number
)
self.svg.node(
legend, 'rect',
legend,
'rect',
x=col * x_step,
y=1.5 * row * h + (
self.style.legend_font_size - self.legend_box_size
@ -403,7 +460,8 @@ class Graph(PublicApi):
truncated = truncate(title, truncation)
self.svg.node(
node, 'text',
node,
'text',
x=col * x_step + self.legend_box_size + 5,
y=1.5 * row * h + .5 * h + .3 * self.style.legend_font_size
).text = truncated
@ -418,19 +476,22 @@ class Graph(PublicApi):
if self._title:
for i, title_line in enumerate(self._title, 1):
self.svg.node(
self.nodes['title'], 'text', class_='title plot_title',
self.nodes['title'],
'text',
class_='title plot_title',
x=self.width / 2,
y=i * (self.style.title_font_size + self.spacing)
).text = title_line
def _make_x_title(self):
"""Make the X-Axis title"""
y = (self.height - self.margin_box.bottom +
self._x_labels_height)
y = (self.height - self.margin_box.bottom + self._x_labels_height)
if self._x_title:
for i, title_line in enumerate(self._x_title, 1):
text = self.svg.node(
self.nodes['title'], 'text', class_='title',
self.nodes['title'],
'text',
class_='title',
x=self.margin_box.left + self.view.width / 2,
y=y + i * (self.style.title_font_size + self.spacing)
)
@ -442,12 +503,15 @@ class Graph(PublicApi):
yc = self.margin_box.top + self.view.height / 2
for i, title_line in enumerate(self._y_title, 1):
text = self.svg.node(
self.nodes['title'], 'text', class_='title',
self.nodes['title'],
'text',
class_='title',
x=self._legend_at_left_width,
y=i * (self.style.title_font_size + self.spacing) + yc
)
text.attrib['transform'] = "rotate(%d %f %f)" % (
-90, self._legend_at_left_width, yc)
-90, self._legend_at_left_width, yc
)
text.text = title_line
def _interpolate(self, xs, ys):
@ -461,16 +525,19 @@ class Graph(PublicApi):
interpolate = INTERPOLATIONS[self.interpolate]
return list(interpolate(
x, y, self.interpolation_precision,
**self.interpolation_parameters))
return list(
interpolate(
x, y, self.interpolation_precision,
**self.interpolation_parameters
)
)
def _rescale(self, points):
"""Scale for secondary"""
return [
(x, self._scale_diff + (y - self._scale_min_2nd) * self._scale
if y is not None else None)
for x, y in points]
return [(
x, self._scale_diff + (y - self._scale_min_2nd) * self._scale
if y is not None else None
) for x, y in points]
def _tooltip_data(self, node, value, x, y, classes=None, xlabel=None):
"""Insert in desc tags informations for the javascript tooltip"""
@ -483,16 +550,21 @@ class Graph(PublicApi):
classes.append('top')
classes = ' '.join(classes)
self.svg.node(node, 'desc',
class_="x " + classes).text = to_str(x)
self.svg.node(node, 'desc',
class_="y " + classes).text = to_str(y)
self.svg.node(node, 'desc', class_="x " + classes).text = to_str(x)
self.svg.node(node, 'desc', class_="y " + classes).text = to_str(y)
if xlabel:
self.svg.node(node, 'desc',
class_="x_label").text = to_str(xlabel)
def _static_value(self, serie_node, value, x, y, metadata,
align_text='left', classes=None):
self.svg.node(node, 'desc', class_="x_label").text = to_str(xlabel)
def _static_value(
self,
serie_node,
value,
x,
y,
metadata,
align_text='left',
classes=None
):
"""Write the print value"""
label = metadata and metadata.get('label')
classes = classes and [classes] or []
@ -502,7 +574,8 @@ class Graph(PublicApi):
if self.print_values:
y -= self.style.value_font_size / 2
self.svg.node(
serie_node['text_overlay'], 'text',
serie_node['text_overlay'],
'text',
class_=' '.join(label_cls),
x=x,
y=y + self.style.value_font_size / 3
@ -515,11 +588,14 @@ class Graph(PublicApi):
val_cls.append('showable')
self.svg.node(
serie_node['text_overlay'], 'text',
serie_node['text_overlay'],
'text',
class_=' '.join(val_cls),
x=x,
y=y + self.style.value_font_size / 3,
attrib={'text-anchor': align_text}
attrib={
'text-anchor': align_text
}
).text = value if self.print_zeroes or value != '0' else ''
def _points(self, x_pos):
@ -528,9 +604,7 @@ class Graph(PublicApi):
and interpolated points if interpolate option is specified
"""
for serie in self.all_series:
serie.points = [
(x_pos[i], v)
for i, v in enumerate(serie.values)]
serie.points = [(x_pos[i], v) for i, v in enumerate(serie.values)]
if serie.points and self.interpolate:
serie.interpolated = self._interpolate(x_pos, serie.values)
else:
@ -600,33 +674,18 @@ class Graph(PublicApi):
value = serie.values[i]
metadata = serie.metadata.get(i)
kwargs = {
'chart': self,
'serie': serie,
'index': i
}
formatter = (
(metadata and metadata.get('formatter')) or
serie.formatter or
self.formatter or
self._value_format
)
kwargs = {'chart': self, 'serie': serie, 'index': i}
formatter = ((metadata and metadata.get('formatter'))
or serie.formatter or self.formatter
or self._value_format)
kwargs = filter_kwargs(formatter, kwargs)
return formatter(value, **kwargs)
def _serie_format(self, serie, value):
"""Format an independent value for the serie"""
kwargs = {
'chart': self,
'serie': serie,
'index': None
}
formatter = (
serie.formatter or
self.formatter or
self._value_format
)
kwargs = {'chart': self, 'serie': serie, 'index': None}
formatter = (serie.formatter or self.formatter or self._value_format)
kwargs = filter_kwargs(formatter, kwargs)
return formatter(value, **kwargs)
@ -639,18 +698,24 @@ class Graph(PublicApi):
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),
[serie.title['title']
if isinstance(serie.title, dict)
else serie.title or '' for serie in series_group]),
self.style.legend_font_size)
map(
lambda x: truncate(x, self.truncate_legend or 15), [
serie.title['title']
if isinstance(serie.title, dict) else serie.title
or '' for serie in series_group
]
), self.style.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)
cols = (
self._order // self.legend_at_bottom_columns
if self.legend_at_bottom_columns else
ceil(sqrt(self._order)) or 1
)
self.margin_box.bottom += self.spacing + h_max * round(
cols - 1) * 1.5 + h_max
cols - 1
) * 1.5 + h_max
else:
if series_group is self.series:
legend_width = self.spacing + w + self.legend_box_size
@ -658,18 +723,22 @@ class Graph(PublicApi):
self._legend_at_left_width += legend_width
else:
self.margin_box.right += (
self.spacing + w + self.legend_box_size)
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.style.label_font_size)
map(
lambda x: truncate(x, self.truncate_label or 25),
cut(xlabels)
), self.style.label_font_size
)
self._x_labels_height = self.spacing + max(
w * abs(sin(rad(self.x_label_rotation))), h)
w * abs(sin(rad(self.x_label_rotation))), h
)
if xlabels is self._x_labels:
self.margin_box.bottom += self._x_labels_height
else:
@ -678,26 +747,32 @@ class Graph(PublicApi):
if self.x_label_rotation % 180 < 90:
self.margin_box.right = max(
w * abs(cos(rad(self.x_label_rotation))),
self.margin_box.right)
self.margin_box.right
)
else:
self.margin_box.left = max(
w * abs(cos(rad(self.x_label_rotation))),
self.margin_box.left)
self.margin_box.left
)
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.style.label_font_size)
cut(ylabels), self.style.label_font_size
)
if ylabels is self._y_labels:
self.margin_box.left += self.spacing + max(
w * abs(cos(rad(self.y_label_rotation))), h)
w * abs(cos(rad(self.y_label_rotation))), h
)
else:
self.margin_box.right += self.spacing + max(
w * abs(cos(rad(self.y_label_rotation))), h)
w * abs(cos(rad(self.y_label_rotation))), h
)
self._title = split_title(
self.title, self.width, self.style.title_font_size)
self.title, self.width, self.style.title_font_size
)
if self.title:
h, _ = get_text_box(self._title[0], self.style.title_font_size)
@ -705,7 +780,8 @@ class Graph(PublicApi):
self._x_title = split_title(
self.x_title, self.width - self.margin_box.x,
self.style.title_font_size)
self.style.title_font_size
)
self._x_title_height = 0
if self._x_title:
@ -716,7 +792,8 @@ class Graph(PublicApi):
self._y_title = split_title(
self.y_title, self.height - self.margin_box.y,
self.style.title_font_size)
self.style.title_font_size
)
self._y_title_height = 0
if self._y_title:
@ -741,15 +818,16 @@ class Graph(PublicApi):
ci['point_estimate'] = value
low, high = getattr(
stats,
'confidence_interval_%s' % ci.get('type', 'manual')
stats, 'confidence_interval_%s' % ci.get('type', 'manual')
)(**ci)
self.svg.confidence_interval(
node, x,
node,
x,
# Respect some charts y modifications (pyramid, stackbar)
y + (self.view.y(low) - self.view.y(value)),
y + (self.view.y(high) - self.view.y(value)))
y + (self.view.y(high) - self.view.y(value))
)
@cached_property
def _legends(self):
@ -764,54 +842,59 @@ class Graph(PublicApi):
@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]
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]
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])
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.secondary_range[0] if (
self.secondary_range and self.secondary_range[0] is not None)
else (min(self._secondary_values)
if self._secondary_values else None))
return (
self.secondary_range[0]
if (self.secondary_range
and self.secondary_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))
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))
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.secondary_range[1] if (
self.secondary_range and self.secondary_range[1] is not None)
else (max(self._secondary_values)
if self._secondary_values else None))
return (
self.secondary_range[1]
if (self.secondary_range
and self.secondary_range[1] is not None) else
(max(self._secondary_values) if self._secondary_values else None)
)
@cached_property
def _order(self):
@ -825,13 +908,18 @@ class Graph(PublicApi):
def _compute_x_labels(self):
self._x_labels = self.x_labels and list(
zip(map(self._x_label_format_if_value, self.x_labels),
self._x_pos))
zip(
map(self._x_label_format_if_value, self.x_labels), self._x_pos
)
)
def _compute_x_labels_major(self):
if self.x_labels_major_every:
self._x_labels_major = [self._x_labels[i][0] for i in range(
0, len(self._x_labels), self.x_labels_major_every)]
self._x_labels_major = [
self._x_labels[i][0]
for i in
range(0, len(self._x_labels), self.x_labels_major_every)
]
elif self.x_labels_major_count:
label_count = len(self._x_labels)
@ -840,17 +928,20 @@ class Graph(PublicApi):
self._x_labels_major = [label[0] for label in self._x_labels]
else:
self._x_labels_major = [self._x_labels[
int(i * (label_count - 1) / (major_count - 1))][0]
for i in range(major_count)]
self._x_labels_major = [
self._x_labels[int(
i * (label_count - 1) / (major_count - 1)
)][0] for i in range(major_count)
]
else:
self._x_labels_major = self.x_labels_major and list(
map(self._x_label_format_if_value, self.x_labels_major)) or []
map(self._x_label_format_if_value, self.x_labels_major)
) or []
def _compute_y_labels(self):
y_pos = compute_scale(
self._box.ymin, self._box.ymax, self.logarithmic,
self.order_min, self.min_scale, self.max_scale
self._box.ymin, self._box.ymax, self.logarithmic, self.order_min,
self.min_scale, self.max_scale
)
if self.y_labels:
self._y_labels = []
@ -872,8 +963,11 @@ class Graph(PublicApi):
def _compute_y_labels_major(self):
if self.y_labels_major_every:
self._y_labels_major = [self._y_labels[i][1] for i in range(
0, len(self._y_labels), self.y_labels_major_every)]
self._y_labels_major = [
self._y_labels[i][1]
for i in
range(0, len(self._y_labels), self.y_labels_major_every)
]
elif self.y_labels_major_count:
label_count = len(self._y_labels)
@ -881,9 +975,11 @@ class Graph(PublicApi):
if (major_count >= label_count):
self._y_labels_major = [label[1] for label in self._y_labels]
else:
self._y_labels_major = [self._y_labels[
int(i * (label_count - 1) / (major_count - 1))][1]
for i in range(major_count)]
self._y_labels_major = [
self._y_labels[int(
i * (label_count - 1) / (major_count - 1)
)][1] for i in range(major_count)
]
elif self.y_labels_major:
self._y_labels_major = list(map(self._adapt, self.y_labels_major))
@ -902,19 +998,22 @@ class Graph(PublicApi):
for line in range(x_lines):
_current_x += (self.width - self.margin_box.x) / squares[0]
self.svg.node(
self.nodes['plot'], 'path',
self.nodes['plot'],
'path',
class_='bg-lines',
d='M%s %s L%s %s' % (
_current_x, 0, _current_x,
self.height - self.margin_box.y))
d='M%s %s L%s %s' %
(_current_x, 0, _current_x, self.height - self.margin_box.y)
)
for line in range(y_lines):
_current_y += (self.height - self.margin_box.y) / squares[1]
self.svg.node(
self.nodes['plot'], 'path',
self.nodes['plot'],
'path',
class_='bg-lines',
d='M%s %s L%s %s' % (
0, _current_y, self.width - self.margin_box.x, _current_y))
d='M%s %s L%s %s' %
(0, _current_y, self.width - self.margin_box.x, _current_y)
)
return ((self.width - self.margin_box.x) / squares[0],
(self.height - self.margin_box.y) / squares[1])
@ -938,8 +1037,8 @@ class Graph(PublicApi):
"""Check if there is any data"""
return any([
len([
v for a in (s[0] if is_list_like(s) else [s])
for v in (a if is_list_like(a) else [a])
if v is not None])
for s in self.raw_series
v
for a in (s[0] if is_list_like(s) else [s])
for v in (a if is_list_like(a) else [a]) if v is not None
]) for s in self.raw_series
])

55
pygal/graph/histogram.py

@ -29,7 +29,6 @@ from pygal.util import alter, cached_property, decorate
class Histogram(Dual, Bar):
"""Histogram chart class"""
_series_margin = 0
@ -41,27 +40,27 @@ class Histogram(Dual, Bar):
@cached_property
def _secondary_values(self):
"""Getter for secondary series values (flattened)"""
return [val[0]
for serie in self.secondary_series
for val in serie.values
if val[0] is not None]
return [
val[0] for serie in self.secondary_series for val in serie.values
if val[0] is not None
]
@cached_property
def xvals(self):
"""All x values"""
return [val
for serie in self.all_series
for dval in serie.values
for val in dval[1:3]
if val is not None]
return [
val
for serie in self.all_series for dval in serie.values
for val in dval[1:3] if val is not None
]
@cached_property
def yvals(self):
"""All y values"""
return [val[0]
for serie in self.series
for val in serie.values
if val[0] is not None]
return [
val[0] for serie in self.series for val in serie.values
if val[0] is not None
]
def _bar(self, serie, parent, x0, x1, y, i, zero, secondary=False):
"""Internal bar drawing function"""
@ -74,10 +73,19 @@ class Histogram(Dual, Bar):
width -= 2 * series_margin
r = serie.rounded_bars * 1 if serie.rounded_bars else 0
alter(self.svg.transposable_node(
parent, 'rect',
x=x, y=y, rx=r, ry=r, width=width, height=height,
class_='rect reactive tooltip-trigger'), serie.metadata.get(i))
alter(
self.svg.transposable_node(
parent,
'rect',
x=x,
y=y,
rx=r,
ry=r,
width=width,
height=height,
class_='rect reactive tooltip-trigger'
), serie.metadata.get(i)
)
return x, y, width, height
def bar(self, serie, rescale=False):
@ -92,15 +100,16 @@ class Histogram(Dual, Bar):
metadata = serie.metadata.get(i)
bar = decorate(
self.svg,
self.svg.node(bars, class_='histbar'),
metadata)
self.svg, self.svg.node(bars, class_='histbar'), metadata
)
val = self._format(serie, i)
bounds = self._bar(
serie, bar, x0, x1, y, i, self.zero, secondary=rescale)
serie, bar, x0, x1, y, i, self.zero, secondary=rescale
)
self._tooltip_and_print_values(
serie_node, serie, bar, i, val, metadata, *bounds)
serie_node, serie, bar, i, val, metadata, *bounds
)
def _compute(self):
"""Compute x/y min and max and x/y scale and set labels"""

16
pygal/graph/horizontal.py

@ -23,7 +23,6 @@ from pygal.view import HorizontalLogView, HorizontalView
class HorizontalGraph(Graph):
"""Horizontal graph mixin"""
def __init__(self, *args, **kwargs):
@ -35,11 +34,14 @@ class HorizontalGraph(Graph):
"""After computations transpose labels"""
self._x_labels, self._y_labels = self._y_labels, self._x_labels
self._x_labels_major, self._y_labels_major = (
self._y_labels_major, self._x_labels_major)
self._y_labels_major, self._x_labels_major
)
self._x_2nd_labels, self._y_2nd_labels = (
self._y_2nd_labels, self._x_2nd_labels)
self._y_2nd_labels, self._x_2nd_labels
)
self.show_y_guides, self.show_x_guides = (
self.show_x_guides, self.show_y_guides)
self.show_x_guides, self.show_y_guides
)
def _axes(self):
"""Set the _force_vertical flag when rendering axes"""
@ -55,9 +57,9 @@ class HorizontalGraph(Graph):
view_class = HorizontalView
self.view = view_class(
self.width - self.margin_box.x,
self.height - self.margin_box.y,
self._box)
self.width - self.margin_box.x, self.height - self.margin_box.y,
self._box
)
def _get_x_label(self, i):
"""Convenience function to get the x_label of a value index"""

2
pygal/graph/horizontalbar.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Horizontal bar graph"""
from pygal.graph.bar import Bar
@ -24,7 +23,6 @@ from pygal.graph.horizontal import HorizontalGraph
class HorizontalBar(HorizontalGraph, Bar):
"""Horizontal Bar graph"""
def _plot(self):

2
pygal/graph/horizontalline.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Horizontal line graph"""
from pygal.graph.horizontal import HorizontalGraph
@ -24,7 +23,6 @@ from pygal.graph.line import Line
class HorizontalLine(HorizontalGraph, Line):
"""Horizontal Line graph"""
def _plot(self):

2
pygal/graph/horizontalstackedbar.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Horizontal stacked graph"""
from pygal.graph.horizontal import HorizontalGraph
@ -24,5 +23,4 @@ from pygal.graph.stackedbar import StackedBar
class HorizontalStackedBar(HorizontalGraph, StackedBar):
"""Horizontal Stacked Bar graph"""

2
pygal/graph/horizontalstackedline.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Horizontal Stacked Line graph"""
from pygal.graph.horizontal import HorizontalGraph
@ -24,7 +23,6 @@ from pygal.graph.stackedline import StackedLine
class HorizontalStackedLine(HorizontalGraph, StackedLine):
"""Horizontal Stacked Line graph"""
def _plot(self):

75
pygal/graph/line.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""
Line chart: Display series of data as markers (dots)
connected by straight segments
@ -29,7 +28,6 @@ from pygal.util import alter, cached_property, decorate
class Line(Graph):
"""Line graph class"""
def __init__(self, *args, **kwargs):
@ -42,20 +40,20 @@ class Line(Graph):
"""Getter for series values (flattened)"""
return [
val[1]
for serie in self.series
for val in (serie.interpolated
if self.interpolate else serie.points)
if val[1] is not None and (not self.logarithmic or val[1] > 0)]
for serie in self.series for val in
(serie.interpolated if self.interpolate else serie.points)
if val[1] is not None and (not self.logarithmic or val[1] > 0)
]
@cached_property
def _secondary_values(self):
"""Getter for secondary series values (flattened)"""
return [
val[1]
for serie in self.secondary_series
for val in (serie.interpolated
if self.interpolate else serie.points)
if val[1] is not None and (not self.logarithmic or val[1] > 0)]
for serie in self.secondary_series for val in
(serie.interpolated if self.interpolate else serie.points)
if val[1] is not None and (not self.logarithmic or val[1] > 0)
]
def _fill(self, values):
"""Add extra values to fill the line"""
@ -80,12 +78,12 @@ class Line(Graph):
"Invalid value ({}) for config key "
"'missing_value_fill_truncation';"
" Use 'x', 'y' or 'either'".format(
self.missing_value_fill_truncation))
self.missing_value_fill_truncation
)
)
end -= 1
return ([(values[0][0], zero)] +
values +
[(values[end][0], zero)])
return ([(values[0][0], zero)] + values + [(values[end][0], zero)])
def line(self, serie, rescale=False):
"""Draw the line serie"""
@ -102,9 +100,9 @@ class Line(Graph):
if self.logarithmic:
if points[i][1] is None or points[i][1] <= 0:
continue
if (serie.show_only_major_dots and
self.x_labels and i < len(self.x_labels) and
self.x_labels[i] not in self._x_labels_major):
if (serie.show_only_major_dots and self.x_labels
and i < len(self.x_labels)
and self.x_labels[i] not in self._x_labels_major):
continue
metadata = serie.metadata.get(i)
@ -116,25 +114,33 @@ class Line(Graph):
classes = ' '.join(classes)
self._confidence_interval(
serie_node['overlay'], x, y, serie.values[i], metadata)
serie_node['overlay'], x, y, serie.values[i], metadata
)
dots = decorate(
self.svg,
self.svg.node(serie_node['overlay'], class_="dots"),
metadata)
metadata
)
val = self._format(serie, i)
alter(self.svg.transposable_node(
dots, 'circle', cx=x, cy=y, r=serie.dots_size,
class_='dot reactive tooltip-trigger'), metadata)
alter(
self.svg.transposable_node(
dots,
'circle',
cx=x,
cy=y,
r=serie.dots_size,
class_='dot reactive tooltip-trigger'
), metadata
)
self._tooltip_data(
dots, val, x, y,
xlabel=self._get_x_label(i))
dots, val, x, y, xlabel=self._get_x_label(i)
)
self._static_value(
serie_node, val,
x + self.style.value_font_size,
y + self.style.value_font_size,
metadata)
serie_node, val, x + self.style.value_font_size,
y + self.style.value_font_size, metadata
)
if serie.stroke:
if self.interpolate:
@ -157,12 +163,12 @@ class Line(Graph):
# emit current subsequence
sequences.append(cur_sequence)
cur_sequence = []
elif y is None: # just discard
elif y is None: # just discard
continue
else:
cur_sequence.append((x, y)) # append the element
cur_sequence.append((x, y)) # append the element
if len(cur_sequence) > 0: # emit last possible sequence
if len(cur_sequence) > 0: # emit last possible sequence
sequences.append(cur_sequence)
else:
# plain vanilla rendering
@ -175,9 +181,12 @@ class Line(Graph):
del seq[seq.index(ele)]
for seq in sequences:
self.svg.line(
serie_node['plot'], seq, close=self._self_close,
serie_node['plot'],
seq,
close=self._self_close,
class_='line reactive' +
(' nofill' if not serie.fill else ''))
(' nofill' if not serie.fill else '')
)
def _compute(self):
"""Compute y min and max and y scale and set labels"""

24
pygal/graph/map.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""
pygal contains no map but a base class to create extension
see the pygal_maps_world package to get an exemple.
@ -31,7 +30,6 @@ from pygal.util import alter, cached_property, cut, decorate
class BaseMap(Graph):
"""Base class for maps"""
_dual = True
@ -39,10 +37,10 @@ class BaseMap(Graph):
@cached_property
def _values(self):
"""Getter for series values (flattened)"""
return [val[1]
for serie in self.series
for val in serie.values
if val[1] is not None]
return [
val[1] for serie in self.series for val in serie.values
if val[1] is not None
]
def enumerate_values(self, serie):
"""Hook to replace default enumeration on values"""
@ -58,7 +56,8 @@ class BaseMap(Graph):
"""
return '%s: %s' % (
self.area_names.get(self.adapt_code(value[0]), '?'),
self._y_format(value[1]))
self._y_format(value[1])
)
def _plot(self):
"""Insert a map in the chart and apply data on it"""
@ -67,8 +66,9 @@ class BaseMap(Graph):
map.set('height', str(self.view.height))
for i, serie in enumerate(self.series):
safe_vals = list(filter(
lambda x: x is not None, cut(serie.values, 1)))
safe_vals = list(
filter(lambda x: x is not None, cut(serie.values, 1))
)
if not safe_vals:
continue
min_ = min(safe_vals)
@ -83,9 +83,9 @@ class BaseMap(Graph):
ratio = .3 + .7 * (value - min_) / (max_ - min_)
areae = map.findall(
".//*[@class='%s%s %s map-element']" % (
self.area_prefix, area_code,
self.kind))
".//*[@class='%s%s %s map-element']" %
(self.area_prefix, area_code, self.kind)
)
if not areae:
continue

24
pygal/graph/pie.py

@ -31,7 +31,6 @@ from pygal.util import alter, decorate
class Pie(Graph):
"""Pie graph class"""
_adapters = [positive, none_to_zero]
@ -62,9 +61,8 @@ class Pie(Graph):
val = self._format(serie, i)
metadata = serie.metadata.get(i)
slice_ = decorate(
self.svg,
self.svg.node(slices, class_="slice"),
metadata)
self.svg, self.svg.node(slices, class_="slice"), metadata
)
if dual:
small_radius = radius * .9
big_radius = radius
@ -72,17 +70,21 @@ class Pie(Graph):
big_radius = radius * .9
small_radius = radius * serie.inner_radius
alter(self.svg.slice(
serie_node, slice_, big_radius, small_radius,
angle, start_angle, center, val, i, metadata), metadata)
alter(
self.svg.slice(
serie_node, slice_, big_radius, small_radius, angle,
start_angle, center, val, i, metadata
), metadata
)
start_angle += angle
if dual:
val = self._serie_format(serie, sum(serie.values))
self.svg.slice(serie_node,
self.svg.node(slices, class_="big_slice"),
radius * .9, 0, serie_angle,
original_start_angle, center, val, i, metadata)
self.svg.slice(
serie_node, self.svg.node(slices,
class_="big_slice"), radius * .9, 0,
serie_angle, original_start_angle, center, val, i, metadata
)
return serie_angle
def _compute_x_labels(self):

20
pygal/graph/public.py

@ -26,7 +26,6 @@ from pygal.graph.base import BaseGraph
class PublicApi(BaseGraph):
"""Chart public functions"""
def add(self, title, values, **kwargs):
@ -51,7 +50,8 @@ class PublicApi(BaseGraph):
"""Render the graph, and return the svg string"""
self.setup(**kwargs)
svg = self.svg.render(
is_unicode=is_unicode, pretty_print=self.pretty_print)
is_unicode=is_unicode, pretty_print=self.pretty_print
)
self.teardown()
return svg
@ -96,16 +96,16 @@ class PublicApi(BaseGraph):
"""Render the graph, and return a Django response"""
from django.http import HttpResponse
return HttpResponse(
self.render(**kwargs), content_type='image/svg+xml')
self.render(**kwargs), content_type='image/svg+xml'
)
def render_data_uri(self, **kwargs):
"""Output a base 64 encoded data uri"""
# Force protocol as data uri have none
kwargs.setdefault('force_uri_protocol', 'https')
return "data:image/svg+xml;charset=utf-8;base64,%s" % (
base64.b64encode(
self.render(**kwargs)
).decode('utf-8').replace('\n', '')
base64.b64encode(self.render(**kwargs)
).decode('utf-8').replace('\n', '')
)
def render_to_file(self, filename, **kwargs):
@ -117,7 +117,8 @@ class PublicApi(BaseGraph):
"""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)
bytestring=self.render(**kwargs), write_to=filename, dpi=dpi
)
def render_sparktext(self, relative_to=None):
"""Make a mini text sparkline from chart"""
@ -141,8 +142,9 @@ class PublicApi(BaseGraph):
divisions = len(bars) - 1
for value in values:
chart += bars[int(divisions *
(value - relative_to) / (vmax - relative_to))]
chart += bars[int(
divisions * (value - relative_to) / (vmax - relative_to)
)]
return chart
def render_sparkline(self, **kwargs):

40
pygal/graph/pyramid.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""
Pyramid chart: Stacked bar chart containing only positive values divided by two
axes, generally gender for age pyramid.
@ -30,7 +29,6 @@ from pygal.graph.stackedbar import StackedBar
class VerticalPyramid(StackedBar):
"""Vertical Pyramid graph class"""
_adapters = [positive]
@ -42,26 +40,37 @@ class VerticalPyramid(StackedBar):
def _get_separated_values(self, secondary=False):
"""Separate values between odd and even series stacked"""
series = self.secondary_series if secondary else self.series
positive_vals = map(sum, zip(
*[serie.safe_values
for index, serie in enumerate(series)
if index % 2]))
negative_vals = map(sum, zip(
*[serie.safe_values
for index, serie in enumerate(series)
if not index % 2]))
positive_vals = map(
sum,
zip(
*[
serie.safe_values for index, serie in enumerate(series)
if index % 2
]
)
)
negative_vals = map(
sum,
zip(
*[
serie.safe_values for index, serie in enumerate(series)
if not index % 2
]
)
)
return list(positive_vals), list(negative_vals)
def _compute_box(self, positive_vals, negative_vals):
"""Compute Y min and max"""
max_ = max(
max(positive_vals or [self.zero]),
max(negative_vals or [self.zero]))
max(negative_vals or [self.zero])
)
if self.range and self.range[0] is not None:
self._box.ymin = self.range[0]
else:
self._box.ymin = - max_
self._box.ymin = -max_
if self.range and self.range[1] is not None:
self._box.ymax = self.range[1]
@ -71,16 +80,15 @@ class VerticalPyramid(StackedBar):
def _pre_compute_secondary(self, positive_vals, negative_vals):
"""Compute secondary y min and max"""
self._secondary_max = max(max(positive_vals), max(negative_vals))
self._secondary_min = - self._secondary_max
self._secondary_min = -self._secondary_max
def _bar(self, serie, parent, x, y, i, zero, secondary=False):
"""Internal stacking bar drawing function"""
if serie.index % 2:
y = -y
return super(VerticalPyramid, self)._bar(
serie, parent, x, y, i, zero, secondary)
return super(VerticalPyramid,
self)._bar(serie, parent, x, y, i, zero, secondary)
class Pyramid(HorizontalGraph, VerticalPyramid):
"""Horizontal Pyramid graph class like the one used by age pyramid"""

90
pygal/graph/radar.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""
Radar chart: As known as kiviat chart or spider chart is a polar line chart
useful for multivariate observation.
@ -34,7 +33,6 @@ from pygal.view import PolarLogView, PolarView
class Radar(Line):
"""Rada graph class"""
_adapters = [positive, none_to_zero]
@ -52,8 +50,9 @@ class Radar(Line):
def _values(self):
"""Getter for series values (flattened)"""
if self.interpolate:
return [val[0] for serie in self.series
for val in serie.interpolated]
return [
val[0] for serie in self.series for val in serie.interpolated
]
else:
return super(Line, self)._values
@ -65,18 +64,20 @@ class Radar(Line):
view_class = PolarView
self.view = view_class(
self.width - self.margin_box.x,
self.height - self.margin_box.y,
self._box)
self.width - self.margin_box.x, self.height - self.margin_box.y,
self._box
)
def _x_axis(self, draw_axes=True):
"""Override x axis to make it polar"""
if not self._x_labels or not self.show_x_labels:
return
axis = self.svg.node(self.nodes['plot'], class_="axis x web%s" % (
' always_show' if self.show_x_guides else ''
))
axis = self.svg.node(
self.nodes['plot'],
class_="axis x web%s" %
(' always_show' if self.show_x_guides else '')
)
format_ = lambda x: '%f %f' % x
center = self.view((0, 0))
r = self._rmax
@ -92,32 +93,37 @@ class Radar(Line):
end = self.view((r, theta))
self.svg.node(
guides, 'path',
guides,
'path',
d='M%s L%s' % (format_(center), format_(end)),
class_='%s%sline' % (
'axis ' if label == "0" else '',
'major ' if major else ''))
class_='%s%sline' %
('axis ' if label == "0" else '', 'major ' if major else '')
)
r_txt = (1 - self._box.__class__.margin) * self._box.ymax
pos_text = self.view((r_txt, theta))
text = self.svg.node(
guides, 'text',
guides,
'text',
x=pos_text[0],
y=pos_text[1],
class_='major' if major else '')
class_='major' if major else ''
)
text.text = truncate(label, truncation)
if text.text != label:
self.svg.node(guides, 'title').text = label
else:
self.svg.node(
guides, 'title',
guides,
'title',
).text = self._x_format(theta)
angle = - theta + pi / 2
angle = -theta + pi / 2
if cos(angle) < 0:
angle -= pi
text.attrib['transform'] = 'rotate(%f %s)' % (
self.x_label_rotation or deg(angle), format_(pos_text))
self.x_label_rotation or deg(angle), format_(pos_text)
)
def _y_axis(self, draw_axes=True):
"""Override y axis to make it polar"""
@ -130,31 +136,32 @@ class Radar(Line):
major = r in self._y_labels_major
if not (self.show_minor_y_labels or major):
continue
guides = self.svg.node(axis, class_='%sguides' % (
'logarithmic ' if self.logarithmic else ''
))
guides = self.svg.node(
axis,
class_='%sguides' %
('logarithmic ' if self.logarithmic else '')
)
if self.show_y_guides:
self.svg.line(
guides, [self.view((r, theta)) for theta in self._x_pos],
close=True,
class_='%sguide line' % (
'major ' if major else ''))
class_='%sguide line' % ('major ' if major else '')
)
x, y = self.view((r, self._x_pos[0]))
x -= 5
text = self.svg.node(
guides, 'text',
x=x,
y=y,
class_='major' if major else ''
guides, 'text', x=x, y=y, class_='major' if major else ''
)
text.text = label
if self.y_label_rotation:
text.attrib['transform'] = "rotate(%d %f %f)" % (
self.y_label_rotation, x, y)
self.y_label_rotation, x, y
)
self.svg.node(
guides, 'title',
guides,
'title',
).text = self._y_format(r)
def _compute(self):
@ -162,19 +169,20 @@ class Radar(Line):
delta = 2 * pi / self._len if self._len else 0
self._x_pos = [.5 * pi + i * delta for i in range(self._len + 1)]
for serie in self.all_series:
serie.points = [
(v, self._x_pos[i])
for i, v in enumerate(serie.values)]
serie.points = [(v, self._x_pos[i])
for i, v in enumerate(serie.values)]
if self.interpolate:
extended_x_pos = (
[.5 * pi - delta] + self._x_pos)
extended_vals = (serie.values[-1:] +
serie.values)
extended_x_pos = ([.5 * pi - delta] + self._x_pos)
extended_vals = (serie.values[-1:] + serie.values)
serie.interpolated = list(
map(tuple,
map(reversed,
self._interpolate(
extended_x_pos, extended_vals))))
map(
tuple,
map(
reversed,
self._interpolate(extended_x_pos, extended_vals)
)
)
)
# x labels space
self._box.margin *= 2

53
pygal/graph/solidgauge.py

@ -16,8 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""
Solid Guage
For each series a solid guage is shown on the plot area.
@ -31,24 +29,21 @@ from pygal.util import alter, decorate
class SolidGauge(Graph):
def gaugify(self, serie, squares, sq_dimensions, current_square):
serie_node = self.svg.serie(serie)
if self.half_pie:
start_angle = 3 * pi / 2
center = (
(current_square[1] * sq_dimensions[0]) - (
sq_dimensions[0] / 2.),
(current_square[0] * sq_dimensions[1]) - (
sq_dimensions[1] / 4))
center = ((current_square[1] * sq_dimensions[0]) -
(sq_dimensions[0] / 2.),
(current_square[0] * sq_dimensions[1]) -
(sq_dimensions[1] / 4))
end_angle = pi / 2
else:
start_angle = 0
center = (
(current_square[1] * sq_dimensions[0]) - (
sq_dimensions[0] / 2.),
(current_square[0] * sq_dimensions[1]) - (
sq_dimensions[1] / 2.))
center = ((current_square[1] * sq_dimensions[0]) -
(sq_dimensions[0] / 2.),
(current_square[0] * sq_dimensions[1]) -
(sq_dimensions[1] / 2.))
end_angle = 2 * pi
max_value = serie.metadata.get(0, {}).get('max_value', 100)
@ -57,7 +52,8 @@ class SolidGauge(Graph):
self.svg.gauge_background(
serie_node, start_angle, center, radius, small_radius, end_angle,
self.half_pie, self._serie_format(serie, max_value))
self.half_pie, self._serie_format(serie, max_value)
)
sum_ = 0
for i, value in enumerate(serie.values):
@ -73,27 +69,30 @@ class SolidGauge(Graph):
metadata = serie.metadata.get(i)
gauge_ = decorate(
self.svg,
self.svg.node(serie_node['plot'], class_="gauge"),
metadata)
self.svg, self.svg.node(serie_node['plot'], class_="gauge"),
metadata
)
alter(
self.svg.solid_gauge(
serie_node, gauge_, radius, small_radius,
angle, start_angle, center, val, i, metadata,
self.half_pie, end_angle,
self._serie_format(serie, max_value)),
metadata)
serie_node, gauge_, radius, small_radius, angle,
start_angle, center, val, i, metadata, self.half_pie,
end_angle, self._serie_format(serie, max_value)
), metadata
)
start_angle += angle
sum_ += value
x, y = center
self.svg.node(
serie_node['text_overlay'], 'text',
serie_node['text_overlay'],
'text',
class_='value gauge-sum',
x=x,
y=y + self.style.value_font_size / 3,
attrib={'text-anchor': 'middle'}
attrib={
'text-anchor': 'middle'
}
).text = self._serie_format(serie, sum_)
def _compute_x_labels(self):
@ -109,8 +108,7 @@ class SolidGauge(Graph):
for index, serie in enumerate(self.series):
current_square = self._current_square(squares, index)
self.gaugify(
serie, squares, sq_dimensions, current_square)
self.gaugify(serie, squares, sq_dimensions, current_square)
def _squares(self):
@ -150,4 +148,5 @@ class SolidGauge(Graph):
else:
return tuple(current_square)
raise Exception(
'Something went wrong with the current square assignment.')
'Something went wrong with the current square assignment.'
)

71
pygal/graph/stackedbar.py

@ -28,7 +28,6 @@ from pygal.graph.bar import Bar
class StackedBar(Bar):
"""Stacked Bar graph class"""
_adapters = [none_to_zero]
@ -37,15 +36,14 @@ class StackedBar(Bar):
"""Separate values between positives and negatives stacked"""
series = self.secondary_series if secondary else self.series
transposed = list(zip(*[serie.values for serie in series]))
positive_vals = [sum([
val for val in vals
if val is not None and val >= self.zero])
for vals in transposed]
negative_vals = [sum([
val
for val in vals
if val is not None and val < self.zero])
for vals in transposed]
positive_vals = [
sum([val for val in vals if val is not None and val >= self.zero])
for vals in transposed
]
negative_vals = [
sum([val for val in vals if val is not None and val < self.zero])
for vals in transposed
]
return positive_vals, negative_vals
def _compute_box(self, positive_vals, negative_vals):
@ -54,22 +52,26 @@ class StackedBar(Bar):
self._box.ymin = self.range[0]
else:
self._box.ymin = negative_vals and min(
min(negative_vals), self.zero) or self.zero
min(negative_vals), self.zero
) or self.zero
if self.range and self.range[1] is not None:
self._box.ymax = self.range[1]
else:
self._box.ymax = positive_vals and max(
max(positive_vals), self.zero) or self.zero
max(positive_vals), self.zero
) or self.zero
def _compute(self):
"""Compute y min and max and y scale and set labels"""
positive_vals, negative_vals = self._get_separated_values()
if self.logarithmic:
positive_vals = list(filter(
lambda x: x > self.zero, positive_vals))
negative_vals = list(filter(
lambda x: x > self.zero, negative_vals))
positive_vals = list(
filter(lambda x: x > self.zero, positive_vals)
)
negative_vals = list(
filter(lambda x: x > self.zero, negative_vals)
)
self._compute_box(positive_vals, negative_vals)
positive_vals = positive_vals or [self.zero]
@ -96,21 +98,25 @@ class StackedBar(Bar):
def _pre_compute_secondary(self, positive_vals, negative_vals):
"""Compute secondary y min and max"""
self._secondary_min = (negative_vals and min(
min(negative_vals), self.zero)) or self.zero
self._secondary_max = (positive_vals and max(
max(positive_vals), self.zero)) or self.zero
self._secondary_min = (
negative_vals and min(min(negative_vals), self.zero)
) or self.zero
self._secondary_max = (
positive_vals and max(max(positive_vals), self.zero)
) or self.zero
def _bar(self, serie, parent, x, y, i, zero, secondary=False):
"""Internal stacking bar drawing function"""
if secondary:
cumulation = (self.secondary_negative_cumulation
if y < self.zero else
self.secondary_positive_cumulation)
cumulation = (
self.secondary_negative_cumulation
if y < self.zero else self.secondary_positive_cumulation
)
else:
cumulation = (self.negative_cumulation
if y < self.zero else
self.positive_cumulation)
cumulation = (
self.negative_cumulation
if y < self.zero else self.positive_cumulation
)
zero = cumulation[i]
cumulation[i] = zero + y
if zero == 0:
@ -133,9 +139,16 @@ class StackedBar(Bar):
height = self.view.y(zero) - y
r = serie.rounded_bars * 1 if serie.rounded_bars else 0
self.svg.transposable_node(
parent, 'rect',
x=x, y=y, rx=r, ry=r, width=width, height=height,
class_='rect reactive tooltip-trigger')
parent,
'rect',
x=x,
y=y,
rx=r,
ry=r,
width=width,
height=height,
class_='rect reactive tooltip-trigger'
)
return x, y, width, height
def _plot(self):

19
pygal/graph/stackedline.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""
Stacked Line chart: Like a line chart but with all lines stacking
on top of the others. Used along fill=True option.
@ -29,7 +28,6 @@ from pygal.graph.line import Line
class StackedLine(Line):
"""Stacked Line graph class"""
_adapters = [none_to_zero]
@ -45,15 +43,11 @@ class StackedLine(Line):
"""
sum_ = serie.points[index][1]
if serie in self.series and (
self.stack_from_top and
self.series.index(serie) == self._order - 1 or
not self.stack_from_top and
self.series.index(serie) == 0):
self.stack_from_top
and self.series.index(serie) == self._order - 1
or not self.stack_from_top and self.series.index(serie) == 0):
return super(StackedLine, self)._value_format(value)
return '%s (+%s)' % (
self._y_format(sum_),
self._y_format(value)
)
return '%s (+%s)' % (self._y_format(sum_), self._y_format(value))
def _fill(self, values):
"""Add extra values to fill the line"""
@ -73,9 +67,8 @@ class StackedLine(Line):
accumulation = [0] * self._len
for serie in series_group[::-1 if self.stack_from_top else 1]:
accumulation = list(map(sum, zip(accumulation, serie.values)))
serie.points = [
(x_pos[i], v)
for i, v in enumerate(accumulation)]
serie.points = [(x_pos[i], v)
for i, v in enumerate(accumulation)]
if serie.points and self.interpolate:
serie.interpolated = self._interpolate(x_pos, accumulation)
else:

25
pygal/graph/time.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""
XY time extensions: handle convertion of date, time, datetime, timedelta
into float for xy plot and back to their type for display
@ -67,21 +66,20 @@ def timedelta_to_seconds(x):
def time_to_seconds(x):
"""Convert a time in a seconds sum"""
if isinstance(x, time):
return ((
((x.hour * 60) + x.minute) * 60 + x.second
) * 10 ** 6 + x.microsecond) / 10 ** 6
return ((((x.hour * 60) + x.minute) * 60 + x.second) * 10**6 +
x.microsecond) / 10**6
if is_str(x):
return x
# Clamp to valid time
return x and max(0, min(x, 24 * 3600 - 10 ** -6))
return x and max(0, min(x, 24 * 3600 - 10**-6))
def seconds_to_time(x):
"""Convert a number of second into a time"""
t = int(x * 10 ** 6)
ms = t % 10 ** 6
t = t // 10 ** 6
t = int(x * 10**6)
ms = t % 10**6
t = t // 10**6
s = t % 60
t = t // 60
m = t % 60
@ -91,7 +89,6 @@ def seconds_to_time(x):
class DateTimeLine(XY):
"""DateTime abscissa xy graph class"""
_x_adapters = [datetime_to_timestamp, date_to_datetime]
@ -99,27 +96,29 @@ class DateTimeLine(XY):
@property
def _x_format(self):
"""Return the value formatter for this graph"""
def datetime_to_str(x):
dt = datetime.utcfromtimestamp(x)
return self.x_value_formatter(dt)
return datetime_to_str
class DateLine(DateTimeLine):
"""Date abscissa xy graph class"""
@property
def _x_format(self):
"""Return the value formatter for this graph"""
def date_to_str(x):
d = datetime.utcfromtimestamp(x).date()
return self.x_value_formatter(d)
return date_to_str
class TimeLine(DateTimeLine):
"""Time abscissa xy graph class"""
_x_adapters = [positive, time_to_seconds, datetime_to_time]
@ -127,14 +126,15 @@ class TimeLine(DateTimeLine):
@property
def _x_format(self):
"""Return the value formatter for this graph"""
def date_to_str(x):
t = seconds_to_time(x)
return self.x_value_formatter(t)
return date_to_str
class TimeDeltaLine(XY):
"""TimeDelta abscissa xy graph class"""
_x_adapters = [timedelta_to_seconds]
@ -142,6 +142,7 @@ class TimeDeltaLine(XY):
@property
def _x_format(self):
"""Return the value formatter for this graph"""
def timedelta_to_str(x):
td = timedelta(seconds=x)
return self.x_value_formatter(td)

52
pygal/graph/treemap.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Treemap chart: Visualize data using nested recangles"""
from __future__ import division
@ -27,7 +26,6 @@ from pygal.util import alter, cut, decorate
class Treemap(Graph):
"""Treemap graph class"""
_adapters = [positive, none_to_zero]
@ -43,31 +41,26 @@ class Treemap(Graph):
val = self._format(serie, i)
rect = decorate(
self.svg,
self.svg.node(rects, class_="rect"),
metadata)
self.svg, self.svg.node(rects, class_="rect"), metadata
)
alter(
self.svg.node(
rect, 'rect',
rect,
'rect',
x=rx,
y=ry,
width=rw,
height=rh,
class_='rect reactive tooltip-trigger'),
metadata)
class_='rect reactive tooltip-trigger'
), metadata
)
self._tooltip_data(
rect, val,
rx + rw / 2,
ry + rh / 2,
'centered',
self._get_x_label(i))
self._static_value(
serie_node, val,
rx + rw / 2,
ry + rh / 2,
metadata)
rect, val, rx + rw / 2, ry + rh / 2, 'centered',
self._get_x_label(i)
)
self._static_value(serie_node, val, rx + rw / 2, ry + rh / 2, metadata)
def _binary_tree(self, data, total, x, y, w, h, parent=None):
if total == 0:
@ -81,10 +74,11 @@ class Treemap(Graph):
datum = data[0]
serie_node = self.svg.serie(datum)
self._binary_tree(
list(enumerate(datum.values)),
total, x, y, w, h,
(datum, serie_node,
self.svg.node(serie_node['plot'], class_="rects")))
list(enumerate(datum.values)), total, x, y, w, h, (
datum, serie_node,
self.svg.node(serie_node['plot'], class_="rects")
)
)
return
midpoint = total / 2
@ -110,16 +104,16 @@ class Treemap(Graph):
if h > w:
y_pivot = pivot_pct * h
self._binary_tree(half1, half1_sum, x, y, w, y_pivot, parent)
self._binary_tree(
half1, half1_sum, x, y, w, y_pivot, parent)
self._binary_tree(
half2, half2_sum, x, y + y_pivot, w, h - y_pivot, parent)
half2, half2_sum, x, y + y_pivot, w, h - y_pivot, parent
)
else:
x_pivot = pivot_pct * w
self._binary_tree(half1, half1_sum, x, y, x_pivot, h, parent)
self._binary_tree(
half1, half1_sum, x, y, x_pivot, h, parent)
self._binary_tree(
half2, half2_sum, x + x_pivot, y, w - x_pivot, h, parent)
half2, half2_sum, x + x_pivot, y, w - x_pivot, h, parent
)
def _compute_x_labels(self):
pass
@ -136,7 +130,7 @@ class Treemap(Graph):
gh = self.height - self.margin_box.y
self.view.box.xmin = self.view.box.ymin = x = y = 0
self.view.box.xmax = w = (total * gw / gh) ** .5
self.view.box.xmax = w = (total * gw / gh)**.5
self.view.box.ymax = h = total / w
self.view.box.fix()

60
pygal/graph/xy.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""
XY Line graph: Plot a set of couple data points (x, y) connected by
straight segments.
@ -32,7 +31,6 @@ from pygal.util import cached_property, compose, ident
class XY(Line, Dual):
"""XY Line graph class"""
_x_adapters = []
@ -40,38 +38,42 @@ class XY(Line, Dual):
@cached_property
def xvals(self):
"""All x values"""
return [val[0]
for serie in self.all_series
for val in serie.values
if val[0] is not None]
return [
val[0] for serie in self.all_series for val in serie.values
if val[0] is not None
]
@cached_property
def yvals(self):
"""All y values"""
return [val[1]
for serie in self.series
for val in serie.values
if val[1] is not None]
return [
val[1] for serie in self.series for val in serie.values
if val[1] is not 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.yvals) if self.yvals else None))
return (
self.range[0] if (self.range and self.range[0] is not None) else
(min(self.yvals) if self.yvals 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.yvals) if self.yvals else None))
return (
self.range[1] if (self.range and self.range[1] is not None) else
(max(self.yvals) if self.yvals else None)
)
def _compute(self):
"""Compute x/y min and max and x/y scale and set labels"""
if self.xvals:
if self.xrange:
x_adapter = reduce(
compose, self._x_adapters) if getattr(
self, '_x_adapters', None) else ident
x_adapter = reduce(compose, self._x_adapters) if getattr(
self, '_x_adapters', None
) else ident
xmin = x_adapter(self.xrange[0])
xmax = x_adapter(self.xrange[1])
@ -98,18 +100,24 @@ class XY(Line, Dual):
for serie in self.all_series:
serie.points = serie.values
if self.interpolate:
vals = list(zip(*sorted(
filter(lambda t: None not in t,
serie.points), key=lambda x: x[0])))
vals = list(
zip(
*sorted(
filter(lambda t: None not in t, serie.points),
key=lambda x: x[0]
)
)
)
serie.interpolated = self._interpolate(vals[0], vals[1])
if self.interpolate:
self.xvals = [val[0]
for serie in self.all_series
for val in serie.interpolated]
self.yvals = [val[1]
for serie in self.series
for val in serie.interpolated]
self.xvals = [
val[0] for serie in self.all_series
for val in serie.interpolated
]
self.yvals = [
val[1] for serie in self.series for val in serie.interpolated
]
if self.xvals:
xmin = min(self.xvals)
xmax = max(self.xvals)

70
pygal/interpolate.py

@ -102,8 +102,9 @@ def cubic_interpolate(x, y, precision=250, **kwargs):
yield x[i] + X, a[i] + b[i] * X + c[i] * X2 + d[i] * X3
def hermite_interpolate(x, y, precision=250,
type='cardinal', c=None, b=None, t=None):
def hermite_interpolate(
x, y, precision=250, type='cardinal', c=None, b=None, t=None
):
"""
Interpolate x, y using the hermite method.
See https://en.wikipedia.org/wiki/Cubic_Hermite_spline
@ -132,11 +133,9 @@ def hermite_interpolate(x, y, precision=250,
c = 0
if type == 'finite_difference':
for i in range(1, n):
m[i] = w[i] = .5 * (
(y[i + 1] - y[i]) / (x[i + 1] - x[i]) +
(y[i] - y[i - 1]) / (
x[i] - x[i - 1])
) if x[i + 1] - x[i] and x[i] - x[i - 1] else 0
m[i] = w[i] = .5 * ((y[i + 1] - y[i]) / (x[i + 1] - x[i]) +
(y[i] - y[i - 1]) / (x[i] - x[i - 1])
) if x[i + 1] - x[i] and x[i] - x[i - 1] else 0
elif type == 'kochanek_bartels':
c = c or 0
@ -151,9 +150,9 @@ def hermite_interpolate(x, y, precision=250,
if type == 'cardinal':
c = c or 0
for i in range(1, n):
m[i] = w[i] = (1 - c) * (
y[i + 1] - y[i - 1]) / (
x[i + 1] - x[i - 1]) if x[i + 1] - x[i - 1] else 0
m[i] = w[i] = (1 - c) * (y[i + 1] - y[i - 1]) / (
x[i + 1] - x[i - 1]
) if x[i + 1] - x[i - 1] else 0
def p(i, x_):
t = (x_ - x[i]) / delta_x[i]
@ -162,13 +161,13 @@ def hermite_interpolate(x, y, precision=250,
h00 = 2 * t3 - 3 * t2 + 1
h10 = t3 - 2 * t2 + t
h01 = - 2 * t3 + 3 * t2
h01 = -2 * t3 + 3 * t2
h11 = t3 - t2
return (h00 * y[i] +
h10 * m[i] * delta_x[i] +
h01 * y[i + 1] +
h11 * w[i + 1] * delta_x[i])
return (
h00 * y[i] + h10 * m[i] * delta_x[i] + h01 * y[i + 1] +
h11 * w[i + 1] * delta_x[i]
)
for i in range(n + 1):
yield x[i], y[i]
@ -239,7 +238,6 @@ INTERPOLATIONS = {
'trigonometric': trigonometric_interpolate
}
if __name__ == '__main__':
from pygal import XY
points = [(.1, 7), (.3, -4), (.6, 10), (.9, 8), (1.4, 3), (1.7, 1)]
@ -249,16 +247,32 @@ if __name__ == '__main__':
xy.add('cubic', cubic_interpolate(*zip(*points)))
xy.add('lagrange', lagrange_interpolate(*zip(*points)))
xy.add('trigonometric', trigonometric_interpolate(*zip(*points)))
xy.add('hermite catmul_rom', hermite_interpolate(
*zip(*points), type='catmul_rom'))
xy.add('hermite finite_difference', hermite_interpolate(
*zip(*points), type='finite_difference'))
xy.add('hermite cardinal -.5', hermite_interpolate(
*zip(*points), type='cardinal', c=-.5))
xy.add('hermite cardinal .5', hermite_interpolate(
*zip(*points), type='cardinal', c=.5))
xy.add('hermite kochanek_bartels .5 .75 -.25', hermite_interpolate(
*zip(*points), type='kochanek_bartels', c=.5, b=.75, t=-.25))
xy.add('hermite kochanek_bartels .25 -.75 .5', hermite_interpolate(
*zip(*points), type='kochanek_bartels', c=.25, b=-.75, t=.5))
xy.add(
'hermite catmul_rom',
hermite_interpolate(*zip(*points), type='catmul_rom')
)
xy.add(
'hermite finite_difference',
hermite_interpolate(*zip(*points), type='finite_difference')
)
xy.add(
'hermite cardinal -.5',
hermite_interpolate(*zip(*points), type='cardinal', c=-.5)
)
xy.add(
'hermite cardinal .5',
hermite_interpolate(*zip(*points), type='cardinal', c=.5)
)
xy.add(
'hermite kochanek_bartels .5 .75 -.25',
hermite_interpolate(
*zip(*points), type='kochanek_bartels', c=.5, b=.75, t=-.25
)
)
xy.add(
'hermite kochanek_bartels .25 -.75 .5',
hermite_interpolate(
*zip(*points), type='kochanek_bartels', c=.25, b=-.75, t=.5
)
)
xy.render_in_browser()

1
pygal/maps/__init__.py

@ -16,5 +16,4 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Maps extensions namespace module"""

1
pygal/serie.py

@ -22,7 +22,6 @@ from pygal.util import cached_property
class Serie(object):
"""Serie class containing title, values and the graph serie index"""
def __init__(self, index, values, config, metadata=None):

2
pygal/state.py

@ -16,14 +16,12 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Class holding state during render"""
from pygal.util import merge
class State(object):
"""
Class containing config values
overriden by chart values

16
pygal/stats.py

@ -35,15 +35,18 @@ def ppf(x, n):
# http://eprints.maths.ox.ac.uk/184/1/tdist.pdf
raise ImportError(
'You must have scipy installed to use t-student '
'when sample_size is below 30')
'when sample_size is below 30'
)
return norm_ppf(x)
# According to http://sphweb.bumc.bu.edu/otlt/MPH-Modules/BS/
# BS704_Confidence_Intervals/BS704_Confidence_Intervals_print.html
def confidence_interval_continuous(
point_estimate, stddev, sample_size, confidence=.95, **kwargs):
point_estimate, stddev, sample_size, confidence=.95, **kwargs
):
"""Continuous confidence interval from sample size and standard error"""
alpha = ppf((confidence + 1) / 2, sample_size - 1)
@ -52,8 +55,13 @@ def confidence_interval_continuous(
def confidence_interval_dichotomous(
point_estimate, sample_size, confidence=.95, bias=False,
percentage=True, **kwargs):
point_estimate,
sample_size,
confidence=.95,
bias=False,
percentage=True,
**kwargs
):
"""Dichotomous confidence interval from sample size and maybe a bias"""
alpha = ppf((confidence + 1) / 2, sample_size - 1)
p = point_estimate

166
pygal/style.py

@ -27,7 +27,6 @@ from pygal.colors import darken, is_foreground_light, lighten
class Style(object):
"""Styling class containing colors for the css generation"""
plot_background = 'rgba(255, 255, 255, 1)'
@ -38,8 +37,7 @@ class Style(object):
foreground_subtle = 'rgba(0, 0, 0, .54)'
# Monospaced font is highly encouraged
font_family = (
'Consolas, "Liberation Mono", Menlo, Courier, monospace')
font_family = ('Consolas, "Liberation Mono", Menlo, Courier, monospace')
label_font_family = None
major_label_font_family = None
@ -116,33 +114,36 @@ class Style(object):
elif fn.startswith('googlefont:'):
setattr(self, name, fn.replace('googlefont:', ''))
self._google_fonts.add(
getattr(self, name).split(',')[0].strip())
getattr(self, name).split(',')[0].strip()
)
def get_colors(self, prefix, len_):
"""Get the css color list"""
def color(tupl):
"""Make a color css"""
return ((
'%s.color-{0}, %s.color-{0} a:visited {{\n'
' stroke: {1};\n'
' fill: {1};\n'
'}}\n') % (prefix, prefix)).format(*tupl)
'}}\n'
) % (prefix, prefix)).format(*tupl)
def value_color(tupl):
"""Make a value color css"""
return ((
'%s .text-overlay .color-{0} text {{\n'
' fill: {1};\n'
'}}\n') % (prefix,)).format(*tupl)
'}}\n'
) % (prefix, )).format(*tupl)
def ci_color(tupl):
"""Make a value color css"""
if not tupl[1]:
return ''
return ((
'%s .color-{0} .ci {{\n'
' stroke: {1};\n'
'}}\n') % (prefix,)).format(*tupl)
return (('%s .color-{0} .ci {{\n'
' stroke: {1};\n'
'}}\n') % (prefix, )).format(*tupl)
if len(self.colors) < len_:
missing = len_ - len(self.colors)
@ -165,13 +166,17 @@ class Style(object):
if i < len(self.value_colors) and self.value_colors[i] is not None:
value_colors.append(self.value_colors[i])
else:
value_colors.append('white' if is_foreground_light(
colors[i]) else 'black')
return '\n'.join(chain(
map(color, enumerate(colors)),
map(value_color, enumerate(value_colors)),
map(ci_color, enumerate(self.ci_colors))))
value_colors.append(
'white' if is_foreground_light(colors[i]) else 'black'
)
return '\n'.join(
chain(
map(color, enumerate(colors)),
map(value_color, enumerate(value_colors)),
map(ci_color, enumerate(self.ci_colors))
)
)
def to_dict(self):
"""Convert instance to a serializable mapping."""
@ -188,7 +193,6 @@ DefaultStyle = Style
class DarkStyle(Style):
"""A dark style (old default)"""
background = 'black'
@ -200,14 +204,13 @@ class DarkStyle(Style):
opacity_hover = '.4'
transition = '250ms'
colors = (
'#ff5995', '#b6e354', '#feed6c', '#8cedff', '#9e6ffe',
'#899ca1', '#f8f8f2', '#bf4646', '#516083', '#f92672',
'#82b414', '#fd971f', '#56c2d6', '#808384', '#8c54fe',
'#465457')
'#ff5995', '#b6e354', '#feed6c', '#8cedff', '#9e6ffe', '#899ca1',
'#f8f8f2', '#bf4646', '#516083', '#f92672', '#82b414', '#fd971f',
'#56c2d6', '#808384', '#8c54fe', '#465457'
)
class LightStyle(Style):
"""A light style"""
background = 'white'
@ -215,13 +218,13 @@ class LightStyle(Style):
foreground = 'rgba(0, 0, 0, 0.7)'
foreground_strong = 'rgba(0, 0, 0, 0.9)'
foreground_subtle = 'rgba(0, 0, 0, 0.5)'
colors = ('#242424', '#9f6767', '#92ac68',
'#d0d293', '#9aacc3', '#bb77a4',
'#77bbb5', '#777777')
colors = (
'#242424', '#9f6767', '#92ac68', '#d0d293', '#9aacc3', '#bb77a4',
'#77bbb5', '#777777'
)
class NeonStyle(DarkStyle):
"""Similar to DarkStyle but with more opacity and effects"""
opacity = '.1'
@ -230,7 +233,6 @@ class NeonStyle(DarkStyle):
class CleanStyle(Style):
"""A rather clean style"""
background = 'transparent'
@ -240,11 +242,11 @@ class CleanStyle(Style):
foreground_subtle = 'rgba(0, 0, 0, 0.5)'
colors = (
'rgb(12,55,149)', 'rgb(117,38,65)', 'rgb(228,127,0)', 'rgb(159,170,0)',
'rgb(149,12,12)')
'rgb(149,12,12)'
)
class DarkSolarizedStyle(Style):
"""Dark solarized popular theme"""
background = '#073642'
@ -256,12 +258,12 @@ class DarkSolarizedStyle(Style):
opacity_hover = '.9'
transition = '500ms ease-in'
colors = (
'#b58900', '#cb4b16', '#dc322f', '#d33682',
'#6c71c4', '#268bd2', '#2aa198', '#859900')
'#b58900', '#cb4b16', '#dc322f', '#d33682', '#6c71c4', '#268bd2',
'#2aa198', '#859900'
)
class LightSolarizedStyle(DarkSolarizedStyle):
"""Light solarized popular theme"""
background = '#fdf6e3'
@ -272,7 +274,6 @@ class LightSolarizedStyle(DarkSolarizedStyle):
class RedBlueStyle(Style):
"""A red and blue theme"""
background = lighten('#e6e7e9', 7)
@ -283,13 +284,13 @@ class RedBlueStyle(Style):
opacity = '.6'
opacity_hover = '.9'
colors = (
'#d94e4c', '#e5884f', '#39929a',
lighten('#d94e4c', 10), darken('#39929a', 15), lighten('#e5884f', 17),
darken('#d94e4c', 10), '#234547')
'#d94e4c', '#e5884f', '#39929a', lighten('#d94e4c', 10),
darken('#39929a', 15), lighten('#e5884f', 17), darken('#d94e4c', 10),
'#234547'
)
class LightColorizedStyle(Style):
"""A light colorized style"""
background = '#f8f8f8'
@ -301,13 +302,13 @@ class LightColorizedStyle(Style):
opacity_hover = '.9'
transition = '250ms ease-in'
colors = (
'#fe9592', '#534f4c', '#3ac2c0', '#a2a7a1',
darken('#fe9592', 15), lighten('#534f4c', 15), lighten('#3ac2c0', 15),
lighten('#a2a7a1', 15), lighten('#fe9592', 15), darken('#3ac2c0', 10))
'#fe9592', '#534f4c', '#3ac2c0', '#a2a7a1', darken('#fe9592', 15),
lighten('#534f4c', 15), lighten('#3ac2c0', 15), lighten('#a2a7a1', 15),
lighten('#fe9592', 15), darken('#3ac2c0', 10)
)
class DarkColorizedStyle(Style):
"""A dark colorized style"""
background = darken('#3a2d3f', 5)
@ -321,11 +322,11 @@ class DarkColorizedStyle(Style):
colors = (
'#c900fe', '#01b8fe', '#59f500', '#ff00e4', '#f9fa00',
darken('#c900fe', 20), darken('#01b8fe', 15), darken('#59f500', 20),
darken('#ff00e4', 15), lighten('#f9fa00', 20))
darken('#ff00e4', 15), lighten('#f9fa00', 20)
)
class TurquoiseStyle(Style):
"""A turquoise style"""
background = darken('#1b8088', 15)
@ -337,13 +338,12 @@ class TurquoiseStyle(Style):
opacity_hover = '.9'
transition = '250ms ease-in'
colors = (
'#93d2d9', '#ef940f', '#8C6243', '#fff',
darken('#93d2d9', 20), lighten('#ef940f', 15),
lighten('#8c6243', 15), '#1b8088')
'#93d2d9', '#ef940f', '#8C6243', '#fff', darken('#93d2d9', 20),
lighten('#ef940f', 15), lighten('#8c6243', 15), '#1b8088'
)
class LightGreenStyle(Style):
"""A light green style"""
background = lighten('#f3f3f3', 3)
@ -357,11 +357,11 @@ class LightGreenStyle(Style):
colors = (
'#7dcf30', '#247fab', lighten('#7dcf30', 10), '#ccc',
darken('#7dcf30', 15), '#ddd', lighten('#247fab', 10),
darken('#247fab', 15))
darken('#247fab', 15)
)
class DarkGreenStyle(Style):
"""A dark green style"""
background = darken('#251e01', 3)
@ -374,11 +374,11 @@ class DarkGreenStyle(Style):
transition = '250ms ease-in'
colors = (
'#adde09', '#6e8c06', '#4a5e04', '#fcd202', '#C1E34D',
lighten('#fcd202', 25))
lighten('#fcd202', 25)
)
class DarkGreenBlueStyle(Style):
"""A dark green and blue style"""
background = '#000'
@ -389,13 +389,14 @@ class DarkGreenBlueStyle(Style):
opacity = '.55'
opacity_hover = '.9'
transition = '250ms ease-in'
colors = (lighten('#34B8F7', 15), '#7dcf30', '#247fab',
darken('#7dcf30', 10), lighten('#247fab', 10),
lighten('#7dcf30', 10), darken('#247fab', 10), '#fff')
colors = (
lighten('#34B8F7', 15), '#7dcf30', '#247fab', darken('#7dcf30', 10),
lighten('#247fab', 10), lighten('#7dcf30', 10), darken('#247fab', 10),
'#fff'
)
class BlueStyle(Style):
"""A blue style"""
background = darken('#f8f8f8', 3)
@ -409,11 +410,11 @@ class BlueStyle(Style):
colors = (
'#00b2f0', '#43d9be', '#0662ab', darken('#00b2f0', 20),
lighten('#43d9be', 20), lighten('#7dcf30', 10), darken('#0662ab', 15),
'#ffd541', '#7dcf30', lighten('#00b2f0', 15), darken('#ffd541', 20))
'#ffd541', '#7dcf30', lighten('#00b2f0', 15), darken('#ffd541', 20)
)
class SolidColorStyle(Style):
"""A light style with strong colors"""
background = '#FFFFFF'
@ -425,30 +426,32 @@ class SolidColorStyle(Style):
opacity_hover = '.9'
transition = '400ms ease-in'
colors = (
'#FF9900', '#DC3912', '#4674D1', '#109618', '#990099',
'#0099C6', '#DD4477', '#74B217', '#B82E2E', '#316395', '#994499')
styles = {'default': DefaultStyle,
'dark': DarkStyle,
'light': LightStyle,
'neon': NeonStyle,
'clean': CleanStyle,
'light_red_blue': RedBlueStyle,
'dark_solarized': DarkSolarizedStyle,
'light_solarized': LightSolarizedStyle,
'dark_colorized': DarkColorizedStyle,
'light_colorized': LightColorizedStyle,
'turquoise': TurquoiseStyle,
'green': LightGreenStyle,
'dark_green': DarkGreenStyle,
'dark_green_blue': DarkGreenBlueStyle,
'blue': BlueStyle,
'solid_color': SolidColorStyle}
'#FF9900', '#DC3912', '#4674D1', '#109618', '#990099', '#0099C6',
'#DD4477', '#74B217', '#B82E2E', '#316395', '#994499'
)
class ParametricStyleBase(Style):
styles = {
'default': DefaultStyle,
'dark': DarkStyle,
'light': LightStyle,
'neon': NeonStyle,
'clean': CleanStyle,
'light_red_blue': RedBlueStyle,
'dark_solarized': DarkSolarizedStyle,
'light_solarized': LightSolarizedStyle,
'dark_colorized': DarkColorizedStyle,
'light_colorized': LightColorizedStyle,
'turquoise': TurquoiseStyle,
'green': LightGreenStyle,
'dark_green': DarkGreenStyle,
'dark_green_blue': DarkGreenBlueStyle,
'blue': BlueStyle,
'solid_color': SolidColorStyle
}
class ParametricStyleBase(Style):
"""Parametric Style base class for all the parametric operations"""
_op = None
@ -495,35 +498,30 @@ class ParametricStyleBase(Style):
class LightenStyle(ParametricStyleBase):
"""Create a style by lightening the given color"""
_op = 'lighten'
class DarkenStyle(ParametricStyleBase):
"""Create a style by darkening the given color"""
_op = 'darken'
class SaturateStyle(ParametricStyleBase):
"""Create a style by saturating the given color"""
_op = 'saturate'
class DesaturateStyle(ParametricStyleBase):
"""Create a style by desaturating the given color"""
_op = 'desaturate'
class RotateStyle(ParametricStyleBase):
"""Create a style by rotating the given color"""
_op = 'rotate'

296
pygal/svg.py

@ -32,13 +32,13 @@ from pygal._compat import quote_plus, to_str, u
from pygal.etree import etree
from pygal.util import (
coord_abs_project, coord_diff, coord_dual, coord_format, coord_project,
minify_css, template)
minify_css, template
)
nearly_2pi = 2 * pi - .00001
class Svg(object):
"""Svg related methods"""
ns = 'http://www.w3.org/2000/svg'
@ -53,16 +53,9 @@ class Svg(object):
self.id = ''
self.processing_instructions = []
if etree.lxml:
attrs = {
'nsmap': {
None: self.ns,
'xlink': self.xlink_ns
}
}
attrs = {'nsmap': {None: self.ns, 'xlink': self.xlink_ns}}
else:
attrs = {
'xmlns': self.ns
}
attrs = {'xmlns': self.ns}
if hasattr(etree, 'register_namespace'):
etree.register_namespace('xlink', self.xlink_ns)
else:
@ -73,11 +66,15 @@ class Svg(object):
if graph.classes:
self.root.attrib['class'] = ' '.join(graph.classes)
self.root.append(
etree.Comment(u(
'Generated with pygal %s (%s) ©Kozea 2012-2016 on %s' % (
__version__,
'lxml' if etree.lxml else 'etree',
date.today().isoformat()))))
etree.Comment(
u(
'Generated with pygal %s (%s) ©Kozea 2012-2016 on %s' % (
__version__, 'lxml' if etree.lxml else 'etree',
date.today().isoformat()
)
)
)
)
self.root.append(etree.Comment(u('http://pygal.org')))
self.root.append(etree.Comment(u('http://github.com/Kozea/pygal')))
self.defs = self.node(tag='defs')
@ -96,8 +93,8 @@ class Svg(object):
if self.graph.style._google_fonts:
auto_css.append(
'//fonts.googleapis.com/css?family=%s' % quote_plus(
'|'.join(self.graph.style._google_fonts))
'//fonts.googleapis.com/css?family=%s' %
quote_plus('|'.join(self.graph.style._google_fonts))
)
for css in auto_css + list(self.graph.css):
@ -108,8 +105,7 @@ class Svg(object):
css = css[len('file://'):]
if not os.path.exists(css):
css = os.path.join(
os.path.dirname(__file__), 'css', css)
css = os.path.join(os.path.dirname(__file__), 'css', css)
with io.open(css, encoding='utf-8') as f:
css_text = template(
@ -117,7 +113,8 @@ class Svg(object):
style=self.graph.style,
colors=colors,
strokes=strokes,
id=self.id)
id=self.id
)
if css_text is not None:
if not self.graph.pretty_print:
@ -127,10 +124,11 @@ class Svg(object):
if css.startswith('//') and self.graph.force_uri_protocol:
css = '%s:%s' % (self.graph.force_uri_protocol, css)
self.processing_instructions.append(
etree.PI(
u('xml-stylesheet'), u('href="%s"' % css)))
etree.PI(u('xml-stylesheet'), u('href="%s"' % css))
)
self.node(
self.defs, 'style', type='text/css').text = '\n'.join(all_css)
self.defs, 'style', type='text/css'
).text = '\n'.join(all_css)
def add_scripts(self):
"""Add the js to the svg"""
@ -140,8 +138,9 @@ class Svg(object):
return dict(
(k, getattr(self.graph.state, k))
for k in dir(self.graph.config)
if not k.startswith('_') and hasattr(self.graph.state, k) and
not hasattr(getattr(self.graph.state, k), '__call__'))
if not k.startswith('_') and hasattr(self.graph.state, k)
and not hasattr(getattr(self.graph.state, k), '__call__')
)
def json_default(o):
if isinstance(o, (datetime, date)):
@ -154,7 +153,8 @@ class Svg(object):
# Config adds
dct['legends'] = [
l.get('title') if isinstance(l, dict) else l
for l in self.graph._legends + self.graph._secondary_legends]
for l in self.graph._legends + self.graph._secondary_legends
]
common_js = 'window.pygal = window.pygal || {};'
common_js += 'window.pygal.config = window.pygal.config || {};'
@ -187,7 +187,7 @@ class Svg(object):
for pos, dim in (('x', 'width'), ('y', 'height')):
if in_attrib_and_number(dim) and attrib[dim] < 0:
attrib[dim] = - attrib[dim]
attrib[dim] = -attrib[dim]
if in_attrib_and_number(pos):
attrib[pos] = attrib[pos] - attrib[dim]
@ -200,8 +200,8 @@ class Svg(object):
attrib[key.rstrip('_')] = attrib[key]
del attrib[key]
elif key == 'href':
attrib[etree.QName(
'http://www.w3.org/1999/xlink', key)] = attrib[key]
attrib[etree.QName('http://www.w3.org/1999/xlink',
key)] = attrib[key]
del attrib[key]
return etree.SubElement(parent, tag, attrib)
@ -226,16 +226,17 @@ class Svg(object):
return dict(
plot=self.node(
self.graph.nodes['plot'],
class_='series serie-%d color-%d' % (
serie.index, serie.index)),
class_='series serie-%d color-%d' % (serie.index, serie.index)
),
overlay=self.node(
self.graph.nodes['overlay'],
class_='series serie-%d color-%d' % (
serie.index, serie.index)),
class_='series serie-%d color-%d' % (serie.index, serie.index)
),
text_overlay=self.node(
self.graph.nodes['text_overlay'],
class_='series serie-%d color-%d' % (
serie.index, serie.index)))
class_='series serie-%d color-%d' % (serie.index, serie.index)
)
)
def line(self, node, coords, close=False, **kwargs):
"""Draw a svg line"""
@ -254,47 +255,54 @@ class Svg(object):
coord_format = lambda xy: '%f %f' % xy
origin = coord_format(coords[origin_index])
line = ' '.join([coord_format(c)
for c in coords[origin_index + 1:]
if None not in c])
return self.node(
node, 'path', d=root % (origin, line), **kwargs)
line = ' '.join([
coord_format(c) for c in coords[origin_index + 1:] if None not in c
])
return self.node(node, 'path', d=root % (origin, line), **kwargs)
def slice(
self, serie_node, node, radius, small_radius,
angle, start_angle, center, val, i, metadata):
self, serie_node, node, radius, small_radius, angle, start_angle,
center, val, i, metadata
):
"""Draw a pie slice"""
if angle == 2 * pi:
angle = nearly_2pi
if angle > 0:
to = [coord_abs_project(center, radius, start_angle),
coord_abs_project(center, radius, start_angle + angle),
coord_abs_project(center, small_radius, start_angle + angle),
coord_abs_project(center, small_radius, start_angle)]
to = [
coord_abs_project(center, radius, start_angle),
coord_abs_project(center, radius, start_angle + angle),
coord_abs_project(center, small_radius, start_angle + angle),
coord_abs_project(center, small_radius, start_angle)
]
rv = self.node(
node, 'path',
node,
'path',
d='M%s A%s 0 %d 1 %s L%s A%s 0 %d 0 %s z' % (
to[0],
coord_dual(radius), int(angle > pi), to[1],
to[2],
coord_dual(small_radius), int(angle > pi), to[3]),
class_='slice reactive tooltip-trigger')
to[0], coord_dual(radius), int(angle > pi), to[1], to[2],
coord_dual(small_radius), int(angle > pi), to[3]
),
class_='slice reactive tooltip-trigger'
)
else:
rv = None
x, y = coord_diff(center, coord_project(
(radius + small_radius) / 2, start_angle + angle / 2))
x, y = coord_diff(
center,
coord_project((radius + small_radius) / 2, start_angle + angle / 2)
)
self.graph._tooltip_data(
node, val, x, y, "centered",
self.graph._x_labels and self.graph._x_labels[i][0])
node, val, x, y, "centered", self.graph._x_labels
and self.graph._x_labels[i][0]
)
if angle >= 0.3: # 0.3 radians is about 17 degrees
self.graph._static_value(serie_node, val, x, y, metadata)
return rv
def gauge_background(
self, serie_node, start_angle, center, radius, small_radius,
end_angle, half_pie, max_value):
end_angle, half_pie, max_value
):
if end_angle == 2 * pi:
end_angle = nearly_2pi
@ -303,37 +311,45 @@ class Svg(object):
coord_abs_project(center, radius, start_angle),
coord_abs_project(center, radius, end_angle),
coord_abs_project(center, small_radius, end_angle),
coord_abs_project(center, small_radius, start_angle)]
coord_abs_project(center, small_radius, start_angle)
]
self.node(
serie_node['plot'], 'path',
serie_node['plot'],
'path',
d='M%s A%s 0 1 1 %s L%s A%s 0 1 0 %s z' % (
to_shade[0],
coord_dual(radius),
to_shade[1],
to_shade[2],
coord_dual(small_radius),
to_shade[3]),
class_='gauge-background reactive')
to_shade[0], coord_dual(radius), to_shade[1], to_shade[2],
coord_dual(small_radius), to_shade[3]
),
class_='gauge-background reactive'
)
if half_pie:
begin_end = [
coord_diff(
center,
coord_project(
radius - (radius - small_radius) / 2, start_angle)),
radius - (radius - small_radius) / 2, start_angle
)
),
coord_diff(
center,
coord_project(
radius - (radius - small_radius) / 2, end_angle))]
radius - (radius - small_radius) / 2, end_angle
)
)
]
pos = 0
for i in begin_end:
self.node(
serie_node['plot'], 'text',
serie_node['plot'],
'text',
class_='y-{} bound reactive'.format(pos),
x=i[0],
y=i[1] + 10,
attrib={'text-anchor': 'middle'}
attrib={
'text-anchor': 'middle'
}
).text = '{}'.format(0 if pos == 0 else max_value)
pos += 1
else:
@ -341,22 +357,21 @@ class Svg(object):
# Correct text vertical alignment
middle_radius -= .1 * (radius - small_radius)
to_labels = [
coord_abs_project(
center, middle_radius, 0),
coord_abs_project(
center, middle_radius, nearly_2pi)
coord_abs_project(center, middle_radius, 0),
coord_abs_project(center, middle_radius, nearly_2pi)
]
self.node(
self.defs, 'path', id='valuePath-%s%s' % center,
d='M%s A%s 0 1 1 %s' % (
to_labels[0],
coord_dual(middle_radius),
to_labels[1]
))
text_ = self.node(
serie_node['text_overlay'], 'text')
self.defs,
'path',
id='valuePath-%s%s' % center,
d='M%s A%s 0 1 1 %s' %
(to_labels[0], coord_dual(middle_radius), to_labels[1])
)
text_ = self.node(serie_node['text_overlay'], 'text')
self.node(
text_, 'textPath', class_='max-value reactive',
text_,
'textPath',
class_='max-value reactive',
attrib={
'href': '#valuePath-%s%s' % center,
'startOffset': '99%',
@ -365,40 +380,42 @@ class Svg(object):
).text = max_value
def solid_gauge(
self, serie_node, node, radius, small_radius,
angle, start_angle, center, val, i, metadata, half_pie, end_angle,
max_value):
self, serie_node, node, radius, small_radius, angle, start_angle,
center, val, i, metadata, half_pie, end_angle, max_value
):
"""Draw a solid gauge slice and background slice"""
if angle == 2 * pi:
angle = nearly_2pi
if angle > 0:
to = [coord_abs_project(center, radius, start_angle),
coord_abs_project(center, radius, start_angle + angle),
coord_abs_project(center, small_radius, start_angle + angle),
coord_abs_project(center, small_radius, start_angle)]
to = [
coord_abs_project(center, radius, start_angle),
coord_abs_project(center, radius, start_angle + angle),
coord_abs_project(center, small_radius, start_angle + angle),
coord_abs_project(center, small_radius, start_angle)
]
self.node(
node, 'path',
node,
'path',
d='M%s A%s 0 %d 1 %s L%s A%s 0 %d 0 %s z' % (
to[0],
coord_dual(radius),
int(angle > pi),
to[1],
to[2],
coord_dual(small_radius),
int(angle > pi),
to[3]),
class_='slice reactive tooltip-trigger')
to[0], coord_dual(radius), int(angle > pi), to[1], to[2],
coord_dual(small_radius), int(angle > pi), to[3]
),
class_='slice reactive tooltip-trigger'
)
else:
return
x, y = coord_diff(center, coord_project(
(radius + small_radius) / 2, start_angle + angle / 2))
x, y = coord_diff(
center,
coord_project((radius + small_radius) / 2, start_angle + angle / 2)
)
self.graph._static_value(serie_node, val, x, y, metadata, 'middle')
self.graph._tooltip_data(
node, val, x, y, "centered",
self.graph._x_labels and self.graph._x_labels[i][0])
node, val, x, y, "centered", self.graph._x_labels
and self.graph._x_labels[i][0]
)
def confidence_interval(self, node, x, low, high, width=7):
if self.graph.horizontal:
@ -415,12 +432,17 @@ class Svg(object):
ci = self.node(node, class_="ci")
self.node(
ci, 'path', d="M%s L%s M%s L%s M%s L%s L%s M%s L%s" % tuple(
map(fmt, (
top, shr(top), top, shl(top), top,
bottom, shr(bottom), bottom, shl(bottom)
))
), class_='nofill reactive'
ci,
'path',
d="M%s L%s M%s L%s M%s L%s L%s M%s L%s" % tuple(
map(
fmt, (
top, shr(top), top, shl(top), top, bottom, shr(bottom),
bottom, shl(bottom)
)
)
),
class_='nofill reactive'
)
def pre_render(self):
@ -428,26 +450,28 @@ class Svg(object):
self.add_styles()
self.add_scripts()
self.root.set(
'viewBox', '0 0 %d %d' % (self.graph.width, self.graph.height))
'viewBox', '0 0 %d %d' % (self.graph.width, self.graph.height)
)
if self.graph.explicit_size:
self.root.set('width', str(self.graph.width))
self.root.set('height', str(self.graph.height))
def draw_no_data(self):
"""Write the no data text to the svg"""
no_data = self.node(self.graph.nodes['text_overlay'], 'text',
x=self.graph.view.width / 2,
y=self.graph.view.height / 2,
class_='no_data')
no_data = self.node(
self.graph.nodes['text_overlay'],
'text',
x=self.graph.view.width / 2,
y=self.graph.view.height / 2,
class_='no_data'
)
no_data.text = self.graph.no_data_text
def render(self, is_unicode=False, pretty_print=False):
"""Last thing to do before rendering"""
for f in self.graph.xml_filters:
self.root = f(self.root)
args = {
'encoding': 'utf-8'
}
args = {'encoding': 'utf-8'}
svg = b''
if etree.lxml:
@ -457,14 +481,12 @@ class Svg(object):
svg = b"<?xml version='1.0' encoding='utf-8'?>\n"
if not self.graph.disable_xml_declaration:
svg += b'\n'.join(
[etree.tostring(
pi, **args)
for pi in self.processing_instructions]
)
svg += b'\n'.join([
etree.tostring(pi, **args)
for pi in self.processing_instructions
])
svg += etree.tostring(
self.root, **args)
svg += etree.tostring(self.root, **args)
if self.graph.disable_xml_declaration or is_unicode:
svg = svg.decode('utf-8')
@ -472,16 +494,17 @@ class Svg(object):
def get_strokes(self):
"""Return a css snippet containing all stroke style options"""
def stroke_dict_to_css(stroke, i=None):
"""Return a css style for the given option"""
css = ['%s.series%s {\n' % (
self.id, '.serie-%d' % i if i is not None else '')]
for key in (
'width', 'linejoin', 'linecap',
'dasharray', 'dashoffset'):
css = [
'%s.series%s {\n' %
(self.id, '.serie-%d' % i if i is not None else '')
]
for key in ('width', 'linejoin', 'linecap', 'dasharray',
'dashoffset'):
if stroke.get(key):
css.append(' stroke-%s: %s;\n' % (
key, stroke[key]))
css.append(' stroke-%s: %s;\n' % (key, stroke[key]))
css.append('}')
return '\n'.join(css)
@ -494,6 +517,9 @@ class Svg(object):
for secondary_serie in self.graph.secondary_series:
if secondary_serie.stroke_style is not None:
css.append(stroke_dict_to_css(
secondary_serie.stroke_style, secondary_serie.index))
css.append(
stroke_dict_to_css(
secondary_serie.stroke_style, secondary_serie.index
)
)
return '\n'.join(css)

26
pygal/table.py

@ -30,7 +30,6 @@ from pygal.util import template
class HTML(object):
"""Lower case adapter of lxml builder"""
def __getattribute__(self, attr):
@ -39,7 +38,6 @@ class HTML(object):
class Table(object):
"""Table generator class"""
_dual = None
@ -135,33 +133,23 @@ class Table(object):
if thead:
parts.append(
html.thead(
*[html.tr(
*[html.th(_(col)) for col in r]
) for r in thead]
*[html.tr(*[html.th(_(col)) for col in r]) for r in thead]
)
)
if tbody:
parts.append(
html.tbody(
*[html.tr(
*[html.td(_(col)) for col in r]
) for r in tbody]
*[html.tr(*[html.td(_(col)) for col in r]) for r in tbody]
)
)
if tfoot:
parts.append(
html.tfoot(
*[html.tr(
*[html.th(_(col)) for col in r]
) for r in tfoot]
*[html.tr(*[html.th(_(col)) for col in r]) for r in tfoot]
)
)
table = tostring(
html.table(
*parts, **attrs
)
)
table = tostring(html.table(*parts, **attrs))
if style:
if style is True:
css = '''
@ -197,9 +185,9 @@ class Table(object):
'''
else:
css = style
table = tostring(html.style(
template(css, **attrs),
scoped='scoped')) + table
table = tostring(
html.style(template(css, **attrs), scoped='scoped')
) + table
table = table.decode('utf-8')
self.chart.teardown()
return table

14
pygal/test/__init__.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Pygal test package"""
import pygal
@ -27,12 +26,9 @@ from decimal import Decimal
def get_data(i):
"""Return sample test data for an index"""
return [
[(-1, 1), (2, 0), (0, 4)],
[(0, 1), (None, 2), (3, 2)],
[(-3, 3), (1, 3), (1, 1)],
[(1, 1), (Decimal('1.'), 1), (1, 1)],
[(3, 2), (2, 1), (1., 1)]][i]
return [[(-1, 1), (2, 0), (0, 4)], [(0, 1), (None, 2), (3, 2)],
[(-3, 3), (1, 3), (1, 1)], [(1, 1), (Decimal('1.'), 1),
(1, 1)], [(3, 2), (2, 1), (1., 1)]][i]
def adapt(chart, data):
@ -52,7 +48,5 @@ def adapt(chart, data):
def make_data(chart, datas):
"""Add sample data to the test chart"""
for i, data in enumerate(datas):
chart.add(data[0],
adapt(chart, data[1]),
secondary=bool(i % 2))
chart.add(data[0], adapt(chart, data[1]), secondary=bool(i % 2))
return chart

9
pygal/test/conftest.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""pytest fixtures"""
import sys
@ -54,8 +53,6 @@ def pytest_generate_tests(metafunc):
metafunc.parametrize("Chart", pygal.CHARTS)
if "datas" in metafunc.funcargnames:
metafunc.parametrize(
"datas",
[
[("Serie %d" % i, get_data(i)) for i in range(s)]
for s in (5, 1, 0)
])
"datas", [[("Serie %d" % i, get_data(i)) for i in range(s)]
for s in (5, 1, 0)]
)

1
pygal/test/test_bar.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Bar chart related tests"""
from pygal import Bar

49
pygal/test/test_box.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Box chart related tests"""
from pygal.graph.box import Box
@ -26,7 +25,8 @@ def test_quartiles():
"""Test box points for the 1.5IQR computation method"""
a = [-2.0, 3.0, 4.0, 5.0, 8.0] # odd test data
(min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(
a, mode='1.5IQR')
a, mode='1.5IQR'
)
assert q1 == 7.0 / 4.0
assert q2 == 4.0
@ -36,19 +36,22 @@ def test_quartiles():
b = [1.0, 4.0, 6.0, 8.0] # even test data
(min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(
b, mode='1.5IQR')
b, mode='1.5IQR'
)
assert q2 == 5.0
c = [2.0, None, 4.0, 6.0, None] # odd with None elements
(min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(
c, mode='1.5IQR')
c, mode='1.5IQR'
)
assert q2 == 4.0
d = [4]
(min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(
d, mode='1.5IQR')
d, mode='1.5IQR'
)
assert q0 == 4
assert q1 == 4
@ -61,7 +64,8 @@ def test_quartiles_min_extremes():
"""Test box points for the extremes computation method"""
a = [-2.0, 3.0, 4.0, 5.0, 8.0] # odd test data
(min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(
a, mode='extremes')
a, mode='extremes'
)
assert q1 == 7.0 / 4.0
assert q2 == 4.0
@ -71,19 +75,22 @@ def test_quartiles_min_extremes():
b = [1.0, 4.0, 6.0, 8.0] # even test data
(min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(
b, mode='extremes')
b, mode='extremes'
)
assert q2 == 5.0
c = [2.0, None, 4.0, 6.0, None] # odd with None elements
(min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(
c, mode='extremes')
c, mode='extremes'
)
assert q2 == 4.0
d = [4]
(min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(
d, mode='extremes')
d, mode='extremes'
)
assert q0 == 4
assert q1 == 4
@ -96,14 +103,16 @@ def test_quartiles_tukey():
"""Test box points for the tukey computation method"""
a = [] # empty data
(min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(
a, mode='tukey')
a, mode='tukey'
)
assert min_s == q0 == q1 == q2 == q3 == q4 == 0
assert outliers == []
# https://en.wikipedia.org/wiki/Quartile example 1
b = [6, 7, 15, 36, 39, 40, 41, 42, 43, 47, 49]
(min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(
b, mode='tukey')
b, mode='tukey'
)
assert min_s == q0 == 6
assert q1 == 20.25
assert q2 == 40
@ -114,7 +123,8 @@ def test_quartiles_tukey():
# previous test with added outlier 75
c = [6, 7, 15, 36, 39, 40, 41, 42, 43, 47, 49, 75]
(min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(
c, mode='tukey')
c, mode='tukey'
)
assert min_s == q0 == 6
assert q1 == 25.5
assert q2 == (40 + 41) / 2.0
@ -125,7 +135,8 @@ def test_quartiles_tukey():
# one more outlier, 77
c = [6, 7, 15, 36, 39, 40, 41, 42, 43, 47, 49, 75, 77]
(min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(
c, mode='tukey')
c, mode='tukey'
)
assert min_s == q0 == 6
assert q1 == 30.75
assert q2 == 41
@ -137,11 +148,14 @@ def test_quartiles_tukey():
def test_quartiles_stdev():
"""Test box points for the stdev computation method"""
a = [35, 42, 35, 41, 36, 6, 12, 51, 33, 27, 46, 36, 44, 53, 75, 46, 16,
51, 45, 29, 25, 26, 54, 61, 27, 40, 23, 34, 51, 37]
a = [
35, 42, 35, 41, 36, 6, 12, 51, 33, 27, 46, 36, 44, 53, 75, 46, 16, 51,
45, 29, 25, 26, 54, 61, 27, 40, 23, 34, 51, 37
]
SD = 14.67
(min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(
a, mode='stdev')
a, mode='stdev'
)
assert min_s == min(a)
assert max_s == max(a)
assert q2 == 36.5
@ -151,7 +165,8 @@ def test_quartiles_stdev():
b = [5] # test for posible zero division
(min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(
b, mode='stdev')
b, mode='stdev'
)
assert min_s == q0 == q1 == q2 == q3 == q4 == max_s == b[0]
assert outliers == []

4
pygal/test/test_colors.py

@ -16,14 +16,14 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Color utility functions tests"""
from __future__ import division
from pygal.colors import (
darken, desaturate, hsl_to_rgb, lighten, parse_color, rgb_to_hsl, rotate,
saturate, unparse_color)
saturate, unparse_color
)
def test_parse_color():

95
pygal/test/test_config.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Various config options tested on one chart type or more"""
from tempfile import NamedTemporaryFile
@ -25,7 +24,8 @@ from pygal import (
XY, Bar, Box, Config, DateLine, DateTimeLine, Dot, Funnel, Gauge,
Histogram, HorizontalBar, HorizontalLine, HorizontalStackedBar,
HorizontalStackedLine, Line, Pie, Pyramid, Radar, SolidGauge,
TimeDeltaLine, TimeLine, Treemap, formatters)
TimeDeltaLine, TimeLine, Treemap, formatters
)
from pygal._compat import _ellipsis, u
from pygal.graph.dual import Dual
from pygal.graph.horizontal import HorizontalGraph
@ -59,7 +59,8 @@ def test_config_behaviours():
fill=True,
pretty_print=True,
no_prefix=True,
x_labels=['a', 'b', 'c'])
x_labels=['a', 'b', 'c']
)
line2.add('_', [1, 2, 3])
l2 = line2.render()
assert l1 == l2
@ -99,6 +100,7 @@ def test_config_behaviours():
def test_config_alterations_class():
"""Assert a config can be changed on config class"""
class LineConfig(Config):
no_prefix = True
show_legend = False
@ -122,6 +124,7 @@ def test_config_alterations_class():
def test_config_alterations_instance():
"""Assert a config can be changed on instance"""
class LineConfig(Config):
no_prefix = True
show_legend = False
@ -146,6 +149,7 @@ def test_config_alterations_instance():
def test_config_alterations_kwargs():
"""Assert a config can be changed with keyword args"""
class LineConfig(Config):
no_prefix = True
show_legend = False
@ -181,7 +185,7 @@ def test_config_alterations_kwargs():
def test_logarithmic():
"""Test logarithmic option"""
line = Line(logarithmic=True)
line.add('_', [1, 10 ** 10, 1])
line.add('_', [1, 10**10, 1])
q = line.render_pyquery()
assert len(q(".axis.x")) == 0
assert len(q(".axis.y")) == 1
@ -227,7 +231,7 @@ def test_logarithmic_bad_interpolation():
def test_logarithmic_big_scale():
"""Test logarithmic option with a large range of value"""
line = Line(logarithmic=True)
line.add('_', [10 ** -10, 10 ** 10, 1])
line.add('_', [10**-10, 10**10, 1])
q = line.render_pyquery()
assert len(q(".y.axis .guides")) == 21
@ -235,17 +239,20 @@ def test_logarithmic_big_scale():
def test_value_formatter():
"""Test value formatter option"""
line = Line(value_formatter=lambda x: str(x) + u(''))
line.add('_', [10 ** 4, 10 ** 5, 23 * 10 ** 4])
line.add('_', [10**4, 10**5, 23 * 10**4])
q = line.render_pyquery()
assert len(q(".y.axis .guides")) == 11
assert q(".axis.y text").map(texts) == list(map(
lambda x: str(x) + u(''), map(float, range(20000, 240000, 20000))))
assert q(".axis.y text").map(texts) == list(
map(
lambda x: str(x) + u(''), map(float, range(20000, 240000, 20000))
)
)
def test_logarithmic_small_scale():
"""Test logarithmic with a small range of values"""
line = Line(logarithmic=True)
line.add('_', [1 + 10 ** 10, 3 + 10 ** 10, 2 + 10 ** 10])
line.add('_', [1 + 10**10, 3 + 10**10, 2 + 10**10])
q = line.render_pyquery()
assert len(q(".y.axis .guides")) == 11
@ -253,16 +260,18 @@ def test_logarithmic_small_scale():
def test_human_readable():
"""Test human readable option"""
line = Line()
line.add('_', [10 ** 4, 10 ** 5, 23 * 10 ** 4])
line.add('_', [10**4, 10**5, 23 * 10**4])
q = line.render_pyquery()
assert q(".axis.y text").map(texts) == list(map(
str, range(20000, 240000, 20000)))
assert q(".axis.y text").map(texts) == list(
map(str, range(20000, 240000, 20000))
)
line.value_formatter = formatters.human_readable
q = line.render_pyquery()
assert q(".axis.y text").map(texts) == list(map(
lambda x: '%dk' % x, range(20, 240, 20)))
assert q(".axis.y text").map(texts) == list(
map(lambda x: '%dk' % x, range(20, 240, 20))
)
def test_show_legend():
@ -300,9 +309,8 @@ def test_no_data():
def test_include_x_axis(Chart):
"""Test x axis inclusion option"""
chart = Chart()
if Chart in (
Pie, Treemap, Radar, Funnel, Dot, Gauge, Histogram, Box, SolidGauge
) or issubclass(Chart, BaseMap):
if Chart in (Pie, Treemap, Radar, Funnel, Dot, Gauge, Histogram, Box,
SolidGauge) or issubclass(Chart, BaseMap):
return
if not chart._dual:
data = 100, 200, 150
@ -312,7 +320,8 @@ def test_include_x_axis(Chart):
q = chart.render_pyquery()
# Ghost thing
yaxis = ".axis.%s .guides text" % (
'y' if not getattr(chart, 'horizontal', False) else 'x')
'y' if not getattr(chart, 'horizontal', False) else 'x'
)
if not isinstance(chart, Bar):
assert '0' not in q(yaxis).map(texts)
else:
@ -386,9 +395,11 @@ def test_legend_at_bottom(Chart):
def test_x_y_title(Chart):
"""Test x title and y title options"""
chart = Chart(title='I Am A Title',
x_title="I am a x title",
y_title="I am a y title")
chart = Chart(
title='I Am A Title',
x_title="I am a x title",
y_title="I am a y title"
)
chart.add('1', [4, -5, 123, 59, 38])
chart.add('2', [89, 0, 8, .12, 8])
q = chart.render_pyquery()
@ -397,9 +408,7 @@ def test_x_y_title(Chart):
def test_range(Chart):
"""Test y label major option"""
if Chart in (
Pie, Treemap, Dot, SolidGauge
) or issubclass(Chart, BaseMap):
if Chart in (Pie, Treemap, Dot, SolidGauge) or issubclass(Chart, BaseMap):
return
chart = Chart()
chart.range = (0, 100)
@ -414,11 +423,10 @@ def test_range(Chart):
def test_x_label_major(Chart):
"""Test x label major option"""
if Chart in (
Pie, Treemap, Funnel, Dot, Gauge, Histogram, Box, SolidGauge,
Pyramid, DateTimeLine, TimeLine, DateLine,
TimeDeltaLine
) or issubclass(Chart, (BaseMap, Dual, HorizontalGraph)):
if Chart in (Pie, Treemap, Funnel, Dot, Gauge, Histogram, Box, SolidGauge,
Pyramid, DateTimeLine, TimeLine, DateLine,
TimeDeltaLine) or issubclass(
Chart, (BaseMap, Dual, HorizontalGraph)):
return
chart = Chart()
chart.add('test', range(12))
@ -459,13 +467,10 @@ def test_x_label_major(Chart):
def test_y_label_major(Chart):
"""Test y label major option"""
if Chart in (
Pie, Treemap, Funnel, Dot, Gauge, Histogram, Box, SolidGauge,
HorizontalBar, HorizontalStackedBar,
HorizontalStackedLine, HorizontalLine,
Pyramid, DateTimeLine, TimeLine, DateLine,
TimeDeltaLine
) or issubclass(Chart, BaseMap):
if Chart in (Pie, Treemap, Funnel, Dot, Gauge, Histogram, Box, SolidGauge,
HorizontalBar, HorizontalStackedBar, HorizontalStackedLine,
HorizontalLine, Pyramid, DateTimeLine, TimeLine, DateLine,
TimeDeltaLine) or issubclass(Chart, BaseMap):
return
chart = Chart()
data = range(12)
@ -529,24 +534,24 @@ def test_render_data_uri(Chart):
chart = Chart(fill=True)
chart.add(u('ééé'), [1, 2, 3])
chart.add(u('èèè'), [10, 21, 5])
assert chart.render_data_uri().startswith(
'data:image/svg+xml;charset=utf-8;base64,')
assert chart.render_data_uri(
).startswith('data:image/svg+xml;charset=utf-8;base64,')
def test_formatters(Chart):
"""Test custom formatters"""
if Chart._dual or Chart == Box:
return
chart = Chart(
formatter=lambda x, chart, serie: '%s%s$' % (x, serie.title))
chart = Chart(formatter=lambda x, chart, serie: '%s%s$' % (x, serie.title))
chart.add('_a', [1, 2, {'value': 3, 'formatter': lambda x: u('%s¥') % x}])
chart.add('_b', [4, 5, 6], formatter=lambda x: u('%s') % x)
chart.x_labels = [2, 4, 6]
chart.x_labels_major = [4]
q = chart.render_pyquery()
assert set([v.text for v in q(".value")]) == set((
u('4€'), u('5€'), u('6€'), '1_a$', '2_a$', u('')) + (
('6_a$', u('15€')) if Chart in (Pie, SolidGauge) else ()))
assert set(
[v.text for v in q(".value")]
) == set((u('4€'), u('5€'), u('6€'), '1_a$', '2_a$', u('')) +
(('6_a$', u('15€')) if Chart in (Pie, SolidGauge) else ()))
def test_classes(Chart):
@ -557,10 +562,10 @@ def test_classes(Chart):
chart = Chart(classes=())
assert not chart.render_pyquery().attr('class')
chart = Chart(classes=(_ellipsis,))
chart = Chart(classes=(_ellipsis, ))
assert chart.render_pyquery().attr('class') == 'pygal-chart'
chart = Chart(classes=('graph',))
chart = Chart(classes=('graph', ))
assert chart.render_pyquery().attr('class') == 'graph'
chart = Chart(classes=('pygal-chart', 'graph'))

168
pygal/test/test_date.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Date related charts tests"""
from datetime import date, datetime, time, timedelta
@ -29,149 +28,114 @@ from pygal.test.utils import texts
def test_date():
"""Test a simple dateline"""
date_chart = DateLine(truncate_label=1000)
date_chart.add('dates', [
(date(2013, 1, 2), 300),
(date(2013, 1, 12), 412),
(date(2013, 2, 2), 823),
(date(2013, 2, 22), 672)
])
date_chart.add(
'dates', [(date(2013, 1, 2), 300), (date(2013, 1, 12), 412),
(date(2013, 2, 2), 823), (date(2013, 2, 22), 672)]
)
q = date_chart.render_pyquery()
assert list(
map(lambda t: t.split(' ')[0],
q(".axis.x text").map(texts))) == [
'2013-01-12',
'2013-01-24',
'2013-02-04',
'2013-02-16']
assert list(map(lambda t: t.split(' ')[0],
q(".axis.x text").map(texts))) == [
'2013-01-12', '2013-01-24', '2013-02-04', '2013-02-16'
]
def test_time():
"""Test a simple timeline"""
time_chart = TimeLine(truncate_label=1000)
time_chart.add('times', [
(time(1, 12, 29), 2),
(time(21, 2, 29), 10),
(time(12, 30, 59), 7)
])
time_chart.add(
'times', [(time(1, 12, 29), 2), (time(21, 2, 29), 10),
(time(12, 30, 59), 7)]
)
q = time_chart.render_pyquery()
assert list(
map(lambda t: t.split(' ')[0],
q(".axis.x text").map(texts))) == [
'02:46:40',
'05:33:20',
'08:20:00',
'11:06:40',
'13:53:20',
'16:40:00',
'19:26:40']
assert list(map(lambda t: t.split(' ')[0],
q(".axis.x text").map(texts))) == [
'02:46:40', '05:33:20', '08:20:00', '11:06:40',
'13:53:20', '16:40:00', '19:26:40'
]
def test_datetime():
"""Test a simple datetimeline"""
datetime_chart = DateTimeLine(truncate_label=1000)
datetime_chart.add('datetimes', [
(datetime(2013, 1, 2, 1, 12, 29), 300),
(datetime(2013, 1, 12, 21, 2, 29), 412),
(datetime(2013, 2, 2, 12, 30, 59), 823),
(datetime(2013, 2, 22), 672)
])
datetime_chart.add(
'datetimes',
[(datetime(2013, 1, 2, 1, 12, 29), 300),
(datetime(2013, 1, 12, 21, 2, 29), 412),
(datetime(2013, 2, 2, 12, 30, 59), 823), (datetime(2013, 2, 22), 672)]
)
q = datetime_chart.render_pyquery()
assert list(
map(lambda t: t.split(' ')[0],
q(".axis.x text").map(texts))) == [
'2013-01-12T14:13:20',
'2013-01-24T04:00:00',
'2013-02-04T17:46:40',
'2013-02-16T07:33:20']
assert list(map(lambda t: t.split(' ')[0],
q(".axis.x text").map(texts))) == [
'2013-01-12T14:13:20', '2013-01-24T04:00:00',
'2013-02-04T17:46:40', '2013-02-16T07:33:20'
]
def test_timedelta():
"""Test a simple timedeltaline"""
timedelta_chart = TimeDeltaLine(truncate_label=1000)
timedelta_chart.add('timedeltas', [
(timedelta(seconds=1), 10),
(timedelta(weeks=1), 50),
(timedelta(hours=3, seconds=30), 3),
(timedelta(microseconds=12112), .3),
])
timedelta_chart.add(
'timedeltas', [
(timedelta(seconds=1), 10),
(timedelta(weeks=1), 50),
(timedelta(hours=3, seconds=30), 3),
(timedelta(microseconds=12112), .3),
]
)
q = timedelta_chart.render_pyquery()
assert list(
t for t in q(".axis.x text").map(texts) if t != '0:00:00'
) == [
'1 day, 3:46:40',
'2 days, 7:33:20',
'3 days, 11:20:00',
'4 days, 15:06:40',
'5 days, 18:53:20',
'6 days, 22:40:00']
assert list(t for t in q(".axis.x text").map(texts) if t != '0:00:00') == [
'1 day, 3:46:40', '2 days, 7:33:20', '3 days, 11:20:00',
'4 days, 15:06:40', '5 days, 18:53:20', '6 days, 22:40:00'
]
def test_date_xrange():
"""Test dateline with xrange"""
datey = DateLine(truncate_label=1000)
datey.add('dates', [
(date(2013, 1, 2), 300),
(date(2013, 1, 12), 412),
(date(2013, 2, 2), 823),
(date(2013, 2, 22), 672)
])
datey.add(
'dates', [(date(2013, 1, 2), 300), (date(2013, 1, 12), 412),
(date(2013, 2, 2), 823), (date(2013, 2, 22), 672)]
)
datey.xrange = (date(2013, 1, 1), date(2013, 3, 1))
q = datey.render_pyquery()
assert list(
map(lambda t: t.split(' ')[0],
q(".axis.x text").map(texts))) == [
'2013-01-01',
'2013-01-12',
'2013-01-24',
'2013-02-04',
'2013-02-16',
'2013-02-27']
assert list(map(lambda t: t.split(' ')[0],
q(".axis.x text").map(texts))) == [
'2013-01-01', '2013-01-12', '2013-01-24', '2013-02-04',
'2013-02-16', '2013-02-27'
]
def test_date_labels():
"""Test dateline with xrange"""
datey = DateLine(truncate_label=1000)
datey.add('dates', [
(date(2013, 1, 2), 300),
(date(2013, 1, 12), 412),
(date(2013, 2, 2), 823),
(date(2013, 2, 22), 672)
])
datey.x_labels = [
date(2013, 1, 1),
date(2013, 2, 1),
date(2013, 3, 1)
]
datey.add(
'dates', [(date(2013, 1, 2), 300), (date(2013, 1, 12), 412),
(date(2013, 2, 2), 823), (date(2013, 2, 22), 672)]
)
datey.x_labels = [date(2013, 1, 1), date(2013, 2, 1), date(2013, 3, 1)]
q = datey.render_pyquery()
assert list(
map(lambda t: t.split(' ')[0],
q(".axis.x text").map(texts))) == [
'2013-01-01',
'2013-02-01',
'2013-03-01']
assert list(map(lambda t: t.split(' ')[0],
q(".axis.x text").map(texts))) == [
'2013-01-01', '2013-02-01', '2013-03-01'
]
def test_utc_timestamping():
assert timestamp(
datetime(2017, 7, 14, 2, 40).replace(tzinfo=utc)
) == 1500000000
for d in (
datetime.now(),
datetime.utcnow(),
datetime(1999, 12, 31, 23, 59, 59),
datetime(2000, 1, 1, 0, 0, 0)
):
assert datetime.utcfromtimestamp(
timestamp(d)) - d < timedelta(microseconds=10)
assert timestamp(datetime(2017, 7, 14, 2,
40).replace(tzinfo=utc)) == 1500000000
for d in (datetime.now(), datetime.utcnow(), datetime(
1999, 12, 31, 23, 59, 59), datetime(2000, 1, 1, 0, 0, 0)):
assert datetime.utcfromtimestamp(timestamp(d)
) - d < timedelta(microseconds=10)

1
pygal/test/test_formatters.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Test formatters"""
from pygal import formatters

202
pygal/test/test_graph.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Generate tests for different chart types with different data"""
import io
@ -82,26 +81,44 @@ def test_metadata(Chart):
"""Test metadata values"""
chart = Chart()
v = range(7)
if Chart in (pygal.Box,):
if Chart in (pygal.Box, ):
return # summary charts cannot display per-value metadata
elif Chart == pygal.XY:
v = list(map(lambda x: (x, x + 1), v))
elif issubclass(Chart, BaseMap):
v = [(k, i) for i, k in enumerate(Chart.x_labels) if k not in [
'oecd', 'nafta', 'eur']]
chart.add('Serie with metadata', [
v[0],
{'value': v[1]},
{'value': v[2], 'label': 'Three'},
{'value': v[3], 'xlink': 'http://4.example.com/'},
{'value': v[4], 'xlink': 'http://5.example.com/', 'label': 'Five'},
{'value': v[5], 'xlink': {
'href': 'http://6.example.com/'}, 'label': 'Six'},
{'value': v[6], 'xlink': {
'href': 'http://7.example.com/',
'target': '_blank'}, 'label': 'Seven'}
])
v = [(k, i) for i, k in enumerate(Chart.x_labels)
if k not in ['oecd', 'nafta', 'eur']]
chart.add(
'Serie with metadata', [
v[0], {
'value': v[1]
}, {
'value': v[2],
'label': 'Three'
}, {
'value': v[3],
'xlink': 'http://4.example.com/'
}, {
'value': v[4],
'xlink': 'http://5.example.com/',
'label': 'Five'
}, {
'value': v[5],
'xlink': {
'href': 'http://6.example.com/'
},
'label': 'Six'
}, {
'value': v[6],
'xlink': {
'href': 'http://7.example.com/',
'target': '_blank'
},
'label': 'Seven'
}
]
)
q = chart.render_pyquery()
for md in ('Three', 'Five', 'Seven'):
assert md in cut(q('desc'), 'text')
@ -289,20 +306,22 @@ def test_no_data_with_lists_of_nones(Chart):
def test_unicode_labels_decode(Chart):
"""Test unicode labels"""
chart = Chart()
chart.add(u('Série1'), [{
'value': 1,
'xlink': 'http://1/',
'label': u('{\}°ijæð©&×&<—×€¿_…\{_…')
}, {
'value': 2,
'xlink': {
'href': 'http://6.example.com/'
},
'label': u('æ°€≠|€æ°€əæ')
}, {
'value': 3,
'label': 'unicode <3'
}])
chart.add(
u('Série1'), [{
'value': 1,
'xlink': 'http://1/',
'label': u('{\}°ijæð©&×&<—×€¿_…\{_…')
}, {
'value': 2,
'xlink': {
'href': 'http://6.example.com/'
},
'label': u('æ°€≠|€æ°€əæ')
}, {
'value': 3,
'label': 'unicode <3'
}]
)
if not chart._dual:
chart.x_labels = [u(''), u('¿?'), u('††††††††'), 'unicode <3']
chart.render_pyquery()
@ -313,20 +332,22 @@ def test_unicode_labels_python2(Chart):
if sys.version_info[0] == 3:
return
chart = Chart()
chart.add(u('Série1'), [{
'value': 1,
'xlink': 'http://1/',
'label': eval("u'{\}°ijæð©&×&<—×€¿_…\{_…'")
}, {
'value': 2,
'xlink': {
'href': 'http://6.example.com/'
},
'label': eval("u'æ°€≠|€æ°€əæ'")
}, {
'value': 3,
'label': eval("'unicode <3'")
}])
chart.add(
u('Série1'), [{
'value': 1,
'xlink': 'http://1/',
'label': eval("u'{\}°ijæð©&×&<—×€¿_…\{_…'")
}, {
'value': 2,
'xlink': {
'href': 'http://6.example.com/'
},
'label': eval("u'æ°€≠|€æ°€əæ'")
}, {
'value': 3,
'label': eval("'unicode <3'")
}]
)
if not chart._dual:
chart.x_labels = eval("[u'', u'¿?', u'††††††††', 'unicode <3']")
chart.render_pyquery()
@ -337,20 +358,22 @@ def test_unicode_labels_python3(Chart):
if sys.version_info[0] == 2:
return
chart = Chart()
chart.add(u('Série1'), [{
'value': 1,
'xlink': 'http://1/',
'label': eval("'{\}°ijæð©&×&<—×€¿_…\{_…'")
}, {
'value': 2,
'xlink': {
'href': 'http://6.example.com/'
},
'label': eval("'æ°€≠|€æ°€əæ'")
}, {
'value': 3,
'label': eval("b'unicode <3'")
}])
chart.add(
u('Série1'), [{
'value': 1,
'xlink': 'http://1/',
'label': eval("'{\}°ijæð©&×&<—×€¿_…\{_…'")
}, {
'value': 2,
'xlink': {
'href': 'http://6.example.com/'
},
'label': eval("'æ°€≠|€æ°€əæ'")
}, {
'value': 3,
'label': eval("b'unicode <3'")
}]
)
if not chart._dual:
chart.x_labels = eval("['', '¿?', '††††††††', 'unicode <3']")
chart.render_pyquery()
@ -361,31 +384,49 @@ def test_labels_with_links(Chart):
chart = Chart()
# link on chart and label
chart.add({
'title': 'Red', 'xlink': {'href': 'http://en.wikipedia.org/wiki/Red'}
'title': 'Red',
'xlink': {
'href': 'http://en.wikipedia.org/wiki/Red'
}
}, [{
'value': 2,
'label': 'This is red',
'xlink': {'href': 'http://en.wikipedia.org/wiki/Red'}}])
'xlink': {
'href': 'http://en.wikipedia.org/wiki/Red'
}
}])
# link on chart only
chart.add('Green', [{
'value': 4,
'label': 'This is green',
'xlink': {
'href': 'http://en.wikipedia.org/wiki/Green',
'target': '_top'}}])
chart.add(
'Green', [{
'value': 4,
'label': 'This is green',
'xlink': {
'href': 'http://en.wikipedia.org/wiki/Green',
'target': '_top'
}
}]
)
# link on label only opens in new tab
chart.add({'title': 'Yellow', 'xlink': {
'href': 'http://en.wikipedia.org/wiki/Yellow',
'target': '_blank'}}, 7)
chart.add({
'title': 'Yellow',
'xlink': {
'href': 'http://en.wikipedia.org/wiki/Yellow',
'target': '_blank'
}
}, 7)
# link on chart only
chart.add('Blue', [{
'value': 5,
'xlink': {
'href': 'http://en.wikipedia.org/wiki/Blue',
'target': '_blank'}}])
chart.add(
'Blue', [{
'value': 5,
'xlink': {
'href': 'http://en.wikipedia.org/wiki/Blue',
'target': '_blank'
}
}]
)
# link on label and chart with diffrent behaviours
chart.add({
@ -396,7 +437,9 @@ def test_labels_with_links(Chart):
'label': 'This is violet',
'xlink': {
'href': 'http://en.wikipedia.org/wiki/Violet_(color)',
'target': '_self'}}])
'target': '_self'
}
}])
q = chart.render_pyquery()
links = q('a')
@ -416,9 +459,7 @@ def test_secondary(Chart):
chart = Chart()
rng = [83, .12, -34, 59]
chart.add('First serie', rng)
chart.add('Secondary serie',
map(lambda x: x * 2, rng),
secondary=True)
chart.add('Secondary serie', map(lambda x: x * 2, rng), secondary=True)
assert chart.render_pyquery()
@ -436,7 +477,8 @@ def test_long_title(Chart, datas):
"'the data is represented by symbols, such as bars in a bar chart, "
"lines in a line chart, or slices in a pie chart'. A chart can "
"represent tabular numeric data, functions or some kinds of "
"qualitative structure and provides different info.")
"qualitative structure and provides different info."
)
chart = make_data(chart, datas)
q = chart.render_pyquery()
assert len(q('.titles text')) == 5

9
pygal/test/test_histogram.py

@ -16,22 +16,15 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Histogram chart related tests"""
from pygal import Histogram
def test_histogram():
"""Simple histogram test"""
hist = Histogram()
hist.add('1', [
(2, 0, 1),
(4, 1, 3),
(3, 3.5, 5),
(1.5, 5, 10)
])
hist.add('1', [(2, 0, 1), (4, 1, 3), (3, 3.5, 5), (1.5, 5, 10)])
hist.add('2', [(2, 2, 8)], secondary=True)
q = hist.render_pyquery()
assert len(q('.rect')) == 5

62
pygal/test/test_interpolate.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Interpolations tests"""
from pygal.test import make_data
@ -70,44 +69,75 @@ def test_hermite(Chart, datas):
def test_hermite_finite(Chart, datas):
"""Test hermite finite difference interpolation"""
chart = Chart(interpolate='hermite',
interpolation_parameters={'type': 'finite_difference'})
chart = Chart(
interpolate='hermite',
interpolation_parameters={
'type': 'finite_difference'
}
)
chart = make_data(chart, datas)
assert chart.render()
def test_hermite_cardinal(Chart, datas):
"""Test hermite cardinal interpolation"""
chart = Chart(interpolate='hermite',
interpolation_parameters={'type': 'cardinal', 'c': .75})
chart = Chart(
interpolate='hermite',
interpolation_parameters={
'type': 'cardinal',
'c': .75
}
)
chart = make_data(chart, datas)
assert chart.render()
def test_hermite_catmull_rom(Chart, datas):
"""Test hermite catmull rom interpolation"""
chart = Chart(interpolate='hermite',
interpolation_parameters={'type': 'catmull_rom'})
chart = Chart(
interpolate='hermite',
interpolation_parameters={
'type': 'catmull_rom'
}
)
chart = make_data(chart, datas)
assert chart.render()
def test_hermite_kochanek_bartels(Chart, datas):
"""Test hermite kochanek bartels interpolation"""
chart = Chart(interpolate='hermite',
interpolation_parameters={
'type': 'kochanek_bartels', 'b': -1, 'c': 1, 't': 1})
chart = Chart(
interpolate='hermite',
interpolation_parameters={
'type': 'kochanek_bartels',
'b': -1,
'c': 1,
't': 1
}
)
chart = make_data(chart, datas)
assert chart.render()
chart = Chart(interpolate='hermite',
interpolation_parameters={
'type': 'kochanek_bartels', 'b': -1, 'c': -8, 't': 0})
chart = Chart(
interpolate='hermite',
interpolation_parameters={
'type': 'kochanek_bartels',
'b': -1,
'c': -8,
't': 0
}
)
chart = make_data(chart, datas)
assert chart.render()
chart = Chart(interpolate='hermite',
interpolation_parameters={
'type': 'kochanek_bartels', 'b': 0, 'c': 10, 't': -1})
chart = Chart(
interpolate='hermite',
interpolation_parameters={
'type': 'kochanek_bartels',
'b': 0,
'c': 10,
't': -1
}
)
chart = make_data(chart, datas)
assert chart.render()

23
pygal/test/test_line.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Line chart related tests"""
from __future__ import division
@ -45,11 +44,13 @@ def test_simple_line():
assert len(q(".y.axis .guides")) == 13
assert len(q(".dots")) == 3 * 13
assert q(".axis.x text").map(texts) == [
'-30', '-25', '-20', '-15', '-10', '-5',
'0', '5', '10', '15', '20', '25', '30']
'-30', '-25', '-20', '-15', '-10', '-5', '0', '5', '10', '15', '20',
'25', '30'
]
assert q(".axis.y text").map(texts) == [
'-1.2', '-1', '-0.8', '-0.6', '-0.4', '-0.2',
'0', '0.2', '0.4', '0.6', '0.8', '1', '1.2']
'-1.2', '-1', '-0.8', '-0.6', '-0.4', '-0.2', '0', '0.2', '0.4', '0.6',
'0.8', '1', '1.2'
]
assert q(".title").text() == 'cos sin and cos - sin'
assert q(".legend text").map(texts) == ['test1', 'test2', 'test3']
@ -103,7 +104,8 @@ def test_not_equal_x_labels():
assert len(q(".dots")) == 100
assert len(q(".axis.x")) == 1
assert q(".axis.x text").map(texts) == [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10']
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10'
]
def test_int_x_labels():
@ -116,7 +118,8 @@ def test_int_x_labels():
assert len(q(".dots")) == 100
assert len(q(".axis.x")) == 1
assert q(".axis.x text").map(texts) == [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10']
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10'
]
def test_only_major_dots_every():
@ -148,7 +151,7 @@ def test_only_major_dots_count():
def test_only_major_dots():
"""Test major dots with specified major labels"""
line = Line(show_only_major_dots=True,)
line = Line(show_only_major_dots=True, )
line.add('test', range(12))
line.x_labels = map(str, range(12))
line.x_labels_major = ['1', '5', '11']
@ -161,9 +164,7 @@ def test_line_secondary():
line = Line()
rng = [8, 12, 23, 73, 39, 57]
line.add('First serie', rng)
line.add('Secondary serie',
map(lambda x: x * 2, rng),
secondary=True)
line.add('Secondary serie', map(lambda x: x * 2, rng), secondary=True)
line.title = "One serie"
q = line.render_pyquery()
assert len(q(".axis.x")) == 0

1
pygal/test/test_maps.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Map plugins tests are imported here"""
import pkg_resources

1
pygal/test/test_pie.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Donut chart related tests"""
from pygal import Pie

1
pygal/test/test_serie_config.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Test per serie configuration"""
from pygal import Line

1
pygal/test/test_sparktext.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Test sparktext rendering"""
from pygal import Bar, Line

13
pygal/test/test_stacked.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Stacked chart related tests"""
from pygal import StackedLine
@ -29,7 +28,8 @@ def test_stacked_line():
stacked.add('ten_twelve', [10, 12])
q = stacked.render_pyquery()
assert set([v.text for v in q("desc.value")]) == set(
('1', '2', '11 (+10)', '14 (+12)'))
('1', '2', '11 (+10)', '14 (+12)')
)
def test_stacked_line_reverse():
@ -39,7 +39,8 @@ def test_stacked_line_reverse():
stacked.add('ten_twelve', [10, 12])
q = stacked.render_pyquery()
assert set([v.text for v in q("desc.value")]) == set(
('11 (+1)', '14 (+2)', '10', '12'))
('11 (+1)', '14 (+2)', '10', '12')
)
def test_stacked_line_log():
@ -49,7 +50,8 @@ def test_stacked_line_log():
stacked.add('ten_twelve', [10, 12])
q = stacked.render_pyquery()
assert set([v.text for v in q("desc.value")]) == set(
('1', '2', '11 (+10)', '14 (+12)'))
('1', '2', '11 (+10)', '14 (+12)')
)
def test_stacked_line_interpolate():
@ -59,4 +61,5 @@ def test_stacked_line_interpolate():
stacked.add('ten_twelve', [10, 12])
q = stacked.render_pyquery()
assert set([v.text for v in q("desc.value")]) == set(
('1', '2', '11 (+10)', '14 (+12)'))
('1', '2', '11 (+10)', '14 (+12)')
)

9
pygal/test/test_style.py

@ -16,13 +16,13 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Style related tests"""
from pygal import Line
from pygal.style import (
DarkenStyle, DesaturateStyle, LightenStyle, LightStyle, RotateStyle,
SaturateStyle)
SaturateStyle
)
STYLES = LightenStyle, DarkenStyle, SaturateStyle, DesaturateStyle, RotateStyle
@ -41,8 +41,9 @@ def test_parametric_styles():
def test_parametric_styles_with_parameters():
"""Test a parametric style with parameters"""
line = Line(style=RotateStyle(
'#de3804', step=12, max_=180, base_style=LightStyle))
line = Line(
style=RotateStyle('#de3804', step=12, max_=180, base_style=LightStyle)
)
line.add('_', [1, 2, 3])
line.x_labels = 'abc'
assert line.render()

1
pygal/test/test_table.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Box chart related tests"""
from pyquery import PyQuery as pq

42
pygal/test/test_util.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Utility functions tests"""
import sys
@ -26,7 +25,8 @@ from pytest import raises
from pygal._compat import _ellipsis, u
from pygal.util import (
_swap_curly, majorize, mergextend, minify_css, round_to_float,
round_to_int, template, truncate)
round_to_int, template, truncate
)
def test_round_to_int():
@ -54,11 +54,8 @@ def test_round_to_float():
def test_swap_curly():
"""Test swap curly function"""
for str in (
'foo',
u('foo foo foo bar'),
'foo béè b¡ð/ijə˘©þß®~¯æ',
u('foo béè b¡ð/ijə˘©þß®~¯æ')):
for str in ('foo', u('foo foo foo bar'), 'foo béè b¡ð/ijə˘©þß®~¯æ',
u('foo béè b¡ð/ijə˘©þß®~¯æ')):
assert _swap_curly(str) == str
assert _swap_curly('foo{bar}baz') == 'foo{{bar}}baz'
assert _swap_curly('foo{{bar}}baz') == 'foo{bar}baz'
@ -80,13 +77,12 @@ def test_format():
class Object(object):
pass
obj = Object()
obj.a = 1
obj.b = True
obj.c = '3'
assert template(
'foo {{ o.a }} {{o.b}}-{{o.c}}',
o=obj) == 'foo 1 True-3'
assert template('foo {{ o.a }} {{o.b}}-{{o.c}}', o=obj) == 'foo 1 True-3'
def test_truncate():
@ -119,28 +115,29 @@ def test_minify_css():
'''
assert minify_css(css) == (
'.title{font-family:sans;font-size:12}'
'.legends .legend text{font-family:monospace;font-size:14}')
'.legends .legend text{font-family:monospace;font-size:14}'
)
def test_majorize():
"""Test majorize function"""
assert majorize(()) == []
assert majorize((0,)) == []
assert majorize((0, )) == []
assert majorize((0, 1)) == []
assert majorize((0, 1, 2)) == []
assert majorize((-1, 0, 1, 2)) == [0]
assert majorize((0, .1, .2, .3, .4, .5, .6, .7, .8, .9, 1)) == [0, .5, 1]
assert majorize((0, .2, .4, .6, .8, 1)) == [0, 1]
assert majorize((-.4, -.2, 0, .2, .4, .6, .8, 1)) == [0, 1]
assert majorize(
(-1, -.8, -.6, -.4, -.2, 0, .2, .4, .6, .8, 1)) == [-1, 0, 1]
assert majorize((-1, -.8, -.6, -.4, -.2, 0, .2, .4, .6, .8,
1)) == [-1, 0, 1]
assert majorize((0, .2, .4, .6, .8, 1, 1.2, 1.4, 1.6)) == [0, 1]
assert majorize((0, .2, .4, .6, .8, 1, 1.2, 1.4, 1.6, 1.8, 2)) == [0, 1, 2]
assert majorize(
(0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120)) == [0, 50, 100]
assert majorize(
(0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20,
22, 24, 26, 28, 30, 32, 34, 36)) == [0, 10, 20, 30]
assert majorize((0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110,
120)) == [0, 50, 100]
assert majorize((
0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36
)) == [0, 10, 20, 30]
assert majorize((0, 1, 2, 3, 4, 5)) == [0, 5]
assert majorize((-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5)) == [-5, 0, 5]
assert majorize((-5, 5, -4, 4, 0, 1, -1, 3, -2, 2, -3)) == [-5, 0, 5]
@ -167,10 +164,11 @@ def test_mergextend():
assert mergextend([_ellipsis], ['c', 'd']) == ['c', 'd']
assert mergextend([_ellipsis, 'b'], ['c', 'd']) == ['c', 'd', 'b']
assert mergextend(['a', _ellipsis], ['c', 'd']) == ['a', 'c', 'd']
assert mergextend(['a', _ellipsis, 'b'], ['c', 'd']) == [
'a', 'c', 'd', 'b']
assert mergextend(['a', _ellipsis, 'b'],
['c', 'd']) == ['a', 'c', 'd', 'b']
if sys.version_info[0] >= 3:
# For @#! sake it's 2016 now
assert eval("mergextend(['a', ..., 'b'], ['c', 'd'])") == [
'a', 'c', 'd', 'b']
'a', 'c', 'd', 'b'
]

2
pygal/test/test_view.py

@ -16,8 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""View related tests"""
# TODO

16
pygal/test/test_xml_filters.py

@ -16,14 +16,12 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Xml filter tests"""
from pygal import Bar
class ChangeBarsXMLFilter(object):
"""xml filter that insert a subplot"""
def __init__(self, a, b):
@ -32,8 +30,9 @@ class ChangeBarsXMLFilter(object):
def __call__(self, T):
"""Apply the filter on the tree"""
subplot = Bar(legend_at_bottom=True, explicit_size=True,
width=800, height=150)
subplot = Bar(
legend_at_bottom=True, explicit_size=True, width=800, height=150
)
subplot.add("Difference", self.data)
subplot = subplot.render_tree()
subplot = subplot.findall("g")[0]
@ -55,8 +54,9 @@ def test_xml_filters_round_trip():
def test_xml_filters_change_bars():
"""Test the use a xml filter"""
plot = Bar(legend_at_bottom=True, explicit_size=True,
width=800, height=600)
plot = Bar(
legend_at_bottom=True, explicit_size=True, width=800, height=600
)
A = [60, 75, 80, 78, 83, 90]
B = [92, 87, 81, 73, 68, 55]
plot.add("A", A)
@ -64,5 +64,5 @@ def test_xml_filters_change_bars():
plot.add_xml_filter(ChangeBarsXMLFilter(A, B))
q = plot.render_tree()
assert len(q.findall("g")) == 2
assert q.findall("g")[1].attrib[
"transform"] == "translate(0,150), scale(1,0.75)"
assert q.findall("g")[1].attrib["transform"
] == "translate(0,150), scale(1,0.75)"

1
pygal/test/utils.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Tests helpers"""
from pyquery import PyQuery as pq

62
pygal/util.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Various utility functions"""
from __future__ import division
@ -42,10 +41,10 @@ def majorize(values):
return []
values_step = sorted_values[1] - sorted_values[0]
full_range = sorted_values[-1] - sorted_values[0]
step = 10 ** int(log10(full_range))
step = 10**int(log10(full_range))
if step == values_step:
step *= 10
step_factor = 10 ** (int(log10(step)) + 1)
step_factor = 10**(int(log10(step)) + 1)
if round(step * step_factor) % (round(values_step * step_factor) or 1):
# TODO: Find lower common multiple instead
step *= values_step
@ -54,7 +53,8 @@ def majorize(values):
elif full_range >= 5 * step:
step *= 5
major_values = [
value for value in values if value / step == round(value / step)]
value for value in values if value / step == round(value / step)
]
return [value for value in sorted_values if value in major_values]
@ -67,9 +67,8 @@ def round_to_int(number, precision):
def round_to_float(number, precision):
"""Round a float to a precision"""
rounded = Decimal(
str(floor((number + precision / 2) // precision))
) * Decimal(str(precision))
rounded = Decimal(str(floor((number + precision / 2) // precision))
) * Decimal(str(precision))
return float(rounded)
@ -101,15 +100,11 @@ def deg(radiants):
def _swap_curly(string):
"""Swap single and double curly brackets"""
return (string
.replace('{{ ', '{{')
.replace('{{', '\x00')
.replace('{', '{{')
.replace('\x00', '{')
.replace(' }}', '}}')
.replace('}}', '\x00')
.replace('}', '}}')
.replace('\x00', '}'))
return (
string.replace('{{ ', '{{').replace('{{', '\x00').replace('{', '{{')
.replace('\x00', '{').replace(' }}', '}}').replace('}}', '\x00')
.replace('}', '}}').replace('\x00', '}')
)
def template(string, **kwargs):
@ -138,24 +133,21 @@ def compute_logarithmic_scale(min_, max_, min_scale, max_scale):
detail /= 2
for order in range(min_order, max_order + 1):
for i in range(int(detail)):
tick = (10 * i / detail or 1) * 10 ** order
tick = (10 * i / detail or 1) * 10**order
tick = round_to_scale(tick, tick)
if min_ <= tick <= max_ and tick not in positions:
positions.append(tick)
return positions
def compute_scale(
min_, max_, logarithmic, order_min,
min_scale, max_scale):
def compute_scale(min_, max_, logarithmic, order_min, min_scale, max_scale):
"""Compute an optimal scale between min and max"""
if min_ == 0 and max_ == 0:
return [0]
if max_ - min_ == 0:
return [min_]
if logarithmic:
log_scale = compute_logarithmic_scale(
min_, max_, min_scale, max_scale)
log_scale = compute_logarithmic_scale(min_, max_, min_scale, max_scale)
if log_scale:
return log_scale
# else we fallback to normal scalling
@ -164,10 +156,10 @@ def compute_scale(
if order_min is not None and order < order_min:
order = order_min
else:
while ((max_ - min_) / (10 ** order) < min_scale and
(order_min is None or order > order_min)):
while ((max_ - min_) / (10**order) < min_scale
and (order_min is None or order > order_min)):
order -= 1
step = float(10 ** order)
step = float(10**order)
while (max_ - min_) / step > max_scale:
step *= 2.
positions = []
@ -213,24 +205,24 @@ def decorate(svg, node, metadata):
if not isinstance(xlink, dict):
xlink = {'href': xlink, 'target': '_blank'}
node = svg.node(node, 'a', **xlink)
svg.node(node, 'desc', class_='xlink').text = to_unicode(
xlink.get('href'))
svg.node(
node, 'desc', class_='xlink'
).text = to_unicode(xlink.get('href'))
if 'tooltip' in metadata:
svg.node(node, 'title').text = to_unicode(
metadata['tooltip'])
svg.node(node, 'title').text = to_unicode(metadata['tooltip'])
if 'color' in metadata:
color = metadata.pop('color')
node.attrib['style'] = 'fill: %s; stroke: %s' % (
color, color)
node.attrib['style'] = 'fill: %s; stroke: %s' % (color, color)
if 'style' in metadata:
node.attrib['style'] = metadata.pop('style')
if 'label' in metadata and metadata['label']:
svg.node(node, 'desc', class_='label').text = to_unicode(
metadata['label'])
svg.node(
node, 'desc', class_='label'
).text = to_unicode(metadata['label'])
return node
@ -238,7 +230,8 @@ def alter(node, metadata):
"""Override nodes attributes from metadata node mapping"""
if node is not None and metadata and 'node' in metadata:
node.attrib.update(
dict((k, str(v)) for k, v in metadata['node'].items()))
dict((k, str(v)) for k, v in metadata['node'].items())
)
def truncate(string, index):
@ -250,7 +243,6 @@ def truncate(string, index):
# # Stolen partly from brownie http://packages.python.org/Brownie/
class cached_property(object):
"""Memoize a property"""
def __init__(self, getter, doc=None):

78
pygal/view.py

@ -16,7 +16,6 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Projection and bounding helpers"""
from __future__ import division
@ -25,7 +24,6 @@ from math import cos, log10, pi, sin
class Margin(object):
"""Class reprensenting a margin (top, right, left, bottom)"""
def __init__(self, top, right, bottom, left):
@ -47,7 +45,6 @@ class Margin(object):
class Box(object):
"""Chart boundings"""
margin = .02
@ -147,7 +144,6 @@ class Box(object):
class View(object):
"""Projection base class"""
def __init__(self, width, height, box):
@ -167,8 +163,9 @@ class View(object):
"""Project y"""
if y is None:
return None
return (self.height - self.height *
(y - self.box.ymin) / self.box.height)
return (
self.height - self.height * (y - self.box.ymin) / self.box.height
)
def __call__(self, xy):
"""Project x and y"""
@ -177,7 +174,6 @@ class View(object):
class ReverseView(View):
"""Same as view but reversed vertically"""
def y(self, y):
@ -188,7 +184,6 @@ class ReverseView(View):
class HorizontalView(View):
"""Same as view but transposed"""
def __init__(self, width, height, box):
@ -219,7 +214,6 @@ class HorizontalView(View):
class PolarView(View):
"""Polar projection for pie like graphs"""
def __call__(self, rhotheta):
@ -227,12 +221,11 @@ class PolarView(View):
if None in rhotheta:
return None, None
rho, theta = rhotheta
return super(PolarView, self).__call__(
(rho * cos(theta), rho * sin(theta)))
return super(PolarView,
self).__call__((rho * cos(theta), rho * sin(theta)))
class PolarLogView(View):
"""Logarithmic polar projection"""
def __init__(self, width, height, box):
@ -240,7 +233,8 @@ class PolarLogView(View):
super(PolarLogView, self).__init__(width, height, box)
if not hasattr(box, '_rmin') or not hasattr(box, '_rmax'):
raise Exception(
'Box must be set with set_polar_box for polar charts')
'Box must be set with set_polar_box for polar charts'
)
self.log10_rmax = log10(self.box._rmax)
self.log10_rmin = log10(self.box._rmin)
@ -256,14 +250,13 @@ class PolarLogView(View):
if rho == 0:
return super(PolarLogView, self).__call__((0, 0))
rho = (self.box._rmax - self.box._rmin) * (
log10(rho) - self.log10_rmin) / (
self.log10_rmax - self.log10_rmin)
return super(PolarLogView, self).__call__(
(rho * cos(theta), rho * sin(theta)))
log10(rho) - self.log10_rmin
) / (self.log10_rmax - self.log10_rmin)
return super(PolarLogView,
self).__call__((rho * cos(theta), rho * sin(theta)))
class PolarThetaView(View):
"""Logarithmic polar projection"""
def __init__(self, width, height, box, aperture=pi / 3):
@ -272,7 +265,8 @@ class PolarThetaView(View):
self.aperture = aperture
if not hasattr(box, '_tmin') or not hasattr(box, '_tmax'):
raise Exception(
'Box must be set with set_polar_box for polar charts')
'Box must be set with set_polar_box for polar charts'
)
def __call__(self, rhotheta):
"""Project rho and theta"""
@ -280,15 +274,14 @@ class PolarThetaView(View):
return None, None
rho, theta = rhotheta
start = 3 * pi / 2 + self.aperture / 2
theta = start + (2 * pi - self.aperture) * (
theta - self.box._tmin) / (
self.box._tmax - self.box._tmin)
return super(PolarThetaView, self).__call__(
(rho * cos(theta), rho * sin(theta)))
theta = start + (2 * pi - self.aperture) * (theta - self.box._tmin) / (
self.box._tmax - self.box._tmin
)
return super(PolarThetaView,
self).__call__((rho * cos(theta), rho * sin(theta)))
class PolarThetaLogView(View):
"""Logarithmic polar projection"""
def __init__(self, width, height, box, aperture=pi / 3):
@ -297,7 +290,8 @@ class PolarThetaLogView(View):
self.aperture = aperture
if not hasattr(box, '_tmin') or not hasattr(box, '_tmax'):
raise Exception(
'Box must be set with set_polar_box for polar charts')
'Box must be set with set_polar_box for polar charts'
)
self.log10_tmax = log10(self.box._tmax) if self.box._tmax > 0 else 0
self.log10_tmin = log10(self.box._tmin) if self.box._tmin > 0 else 0
if self.log10_tmin == self.log10_tmax:
@ -312,20 +306,19 @@ class PolarThetaLogView(View):
if theta == 0:
return super(PolarThetaLogView, self).__call__((0, 0))
theta = self.box._tmin + (self.box._tmax - self.box._tmin) * (
log10(theta) - self.log10_tmin) / (
self.log10_tmax - self.log10_tmin)
log10(theta) - self.log10_tmin
) / (self.log10_tmax - self.log10_tmin)
start = 3 * pi / 2 + self.aperture / 2
theta = start + (2 * pi - self.aperture) * (
theta - self.box._tmin) / (
self.box._tmax - self.box._tmin)
theta = start + (2 * pi - self.aperture) * (theta - self.box._tmin) / (
self.box._tmax - self.box._tmin
)
return super(PolarThetaLogView, self).__call__(
(rho * cos(theta), rho * sin(theta)))
return super(PolarThetaLogView,
self).__call__((rho * cos(theta), rho * sin(theta)))
class LogView(View):
"""Y Logarithmic projection"""
# Do not want to call the parent here
@ -344,13 +337,13 @@ class LogView(View):
"""Project y"""
if y is None or y <= 0 or self.log10_ymax - self.log10_ymin == 0:
return 0
return (self.height - self.height *
(log10(y) - self.log10_ymin) / (
self.log10_ymax - self.log10_ymin))
return (
self.height - self.height * (log10(y) - self.log10_ymin) /
(self.log10_ymax - self.log10_ymin)
)
class XLogView(View):
"""X logarithmic projection"""
# Do not want to call the parent here
@ -367,13 +360,13 @@ class XLogView(View):
"""Project x"""
if x is None or x <= 0 or self.log10_xmax - self.log10_xmin == 0:
return None
return (self.width *
(log10(x) - self.log10_xmin) /
(self.log10_xmax - self.log10_xmin))
return (
self.width * (log10(x) - self.log10_xmin) /
(self.log10_xmax - self.log10_xmin)
)
class XYLogView(XLogView, LogView):
"""X and Y logarithmic projection"""
def __init__(self, width, height, box):
@ -389,7 +382,6 @@ class XYLogView(XLogView, LogView):
class HorizontalLogView(XLogView):
"""Transposed Logarithmic projection"""
# Do not want to call the parent here

Loading…
Cancel
Save