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

10
demo/cabaret/__init__.py

@ -34,10 +34,14 @@ def create_app():
@app.route("/") @app.route("/")
def index(): def index():
return render_template( return render_template(
'index.jinja2', charts_names=CHARTS_NAMES, configs=CONFIG_ITEMS, 'index.jinja2',
interpolations=INTERPOLATIONS, styles_names=styles.keys()) 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(): def svg():
values = request.values values = request.values
config = loads(values['opts']) 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.etree import etree
from pygal.style import styles, parametric_styles from pygal.style import styles, parametric_styles
from base64 import ( from base64 import (
urlsafe_b64encode as b64encode, urlsafe_b64encode as b64encode, urlsafe_b64decode as b64decode
urlsafe_b64decode as b64decode) )
import string import string
import random import random
import pickle import pickle
@ -39,9 +39,9 @@ def get(type):
def random_label(): def random_label():
chars = string.ascii_letters + string.digits + u' àéèçêâäëï' chars = string.ascii_letters + string.digits + u' àéèçêâäëï'
return ''.join( return ''.join([
[random.choice(chars) random.choice(chars) for i in range(random.randrange(4, 30))
for i in range(random.randrange(4, 30))]) ])
def random_value(min=0, max=15): def random_value(min=0, max=15):
@ -61,8 +61,8 @@ def create_app():
etree.to_lxml() etree.to_lxml()
def _random(data, order): def _random(data, order):
max = 10 ** order max = 10**order
min = 10 ** random.randrange(0, order) min = 10**random.randrange(0, order)
series = [] series = []
for i in range(random.randrange(1, 10)): for i in range(random.randrange(1, 10)):
@ -74,8 +74,8 @@ def create_app():
return series return series
def _random_series(type, data, order): def _random_series(type, data, order):
max = 10 ** order max = 10**order
min = 10 ** random.randrange(0, order) min = 10**random.randrange(0, order)
with_secondary = bool(random.randint(0, 1)) with_secondary = bool(random.randint(0, 1))
series = [] series = []
for i in range(random.randrange(1, 10)): for i in range(random.randrange(1, 10)):
@ -84,11 +84,13 @@ def create_app():
elif type == 'XY': elif type == 'XY':
values = [( values = [(
random_value((-max, min)[random.randrange(0, 2)], max), random_value((-max, min)[random.randrange(0, 2)], max),
random_value((-max, min)[random.randrange(0, 2)], max)) random_value((-max, min)[random.randrange(0, 2)], max)
for i in range(data)] ) for i in range(data)]
else: else:
values = [random_value((-max, min)[random.randrange(1, 2)], values = [
max) for i in range(data)] random_value((-max, min)[random.randrange(1, 2)], max)
for i in range(data)
]
config = { config = {
'secondary': with_secondary and bool(random.randint(0, 1)) 'secondary': with_secondary and bool(random.randint(0, 1))
} }
@ -101,25 +103,29 @@ def create_app():
@app.route("/") @app.route("/")
def index(): def index():
return render_template( return render_template(
'index.jinja2', styles=styles, parametric_styles=parametric_styles, 'index.jinja2',
styles=styles,
parametric_styles=parametric_styles,
parametric_colors=( parametric_colors=(
'#ff5995', '#b6e354', '#feed6c', '#8cedff', '#9e6ffe'), '#ff5995', '#b6e354', '#feed6c', '#8cedff', '#9e6ffe'
links=links, charts_name=pygal.CHARTS_NAMES) ),
links=links,
charts_name=pygal.CHARTS_NAMES
)
@app.route("/svg/<type>/<series>/<config>") @app.route("/svg/<type>/<series>/<config>")
def svg(type, series, config): def svg(type, series, config):
graph = get(type)( graph = get(type)(pickle.loads(b64decode(str(config))))
pickle.loads(b64decode(str(config)))) for title, values, serie_config in pickle.loads(b64decode(
for title, values, serie_config in pickle.loads( str(series))):
b64decode(str(series))):
graph.add(title, values, **serie_config) graph.add(title, values, **serie_config)
return graph.render_response() return graph.render_response()
@app.route("/table/<type>/<series>/<config>") @app.route("/table/<type>/<series>/<config>")
def table(type, series, config): def table(type, series, config):
graph = get(type)(pickle.loads(b64decode(str(config)))) graph = get(type)(pickle.loads(b64decode(str(config))))
for title, values, serie_config in pickle.loads( for title, values, serie_config in pickle.loads(b64decode(
b64decode(str(series))): str(series))):
graph.add(title, values, **serie_config) graph.add(title, values, **serie_config)
return graph.render_table() return graph.render_table()
@ -134,17 +140,19 @@ def create_app():
line = pygal.Line(style=style, pretty_print=True) line = pygal.Line(style=style, pretty_print=True)
line.add('_', [random.randrange(0, 10) for _ in range(25)]) line.add('_', [random.randrange(0, 10) for _ in range(25)])
return Response( 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>") @app.route("/with/table/<type>")
def with_table(type): def with_table(type):
chart = pygal.StackedBar( chart = pygal.StackedBar(
disable_xml_declaration=True, disable_xml_declaration=True, x_label_rotation=35
x_label_rotation=35) )
chart.title = ( chart.title = (
'What Linux distro do you primarily use' 'What Linux distro do you primarily use'
' on your server computers? (Desktop' ' on your server computers? (Desktop'
' users vs Server Users)') ' users vs Server Users)'
)
if type == 'series': if type == 'series':
chart.add('Debian', [1775, 82]) chart.add('Debian', [1775, 82])
@ -170,14 +178,18 @@ def create_app():
'Red Hat Enterprise Linux', 'Gentoo', 'Fedora', 'Amazon Linux', 'Red Hat Enterprise Linux', 'Gentoo', 'Fedora', 'Amazon Linux',
'OpenSUSE', 'Slackware', 'Xubuntu', 'Rasbian', 'OpenSUSE', 'Slackware', 'Xubuntu', 'Rasbian',
'SUSE Linux Enterprise Server', 'Linux Mint', 'SUSE Linux Enterprise Server', 'Linux Mint',
'Scientific Linux', 'Other'] 'Scientific Linux', 'Other'
chart.add('Desktop Users', [ ]
1775, 1515, 807, 549, 247, 129, 91, 60, 58, 50, 38, 33, 33, chart.add(
30, 32, 187 'Desktop Users', [
]) 1775, 1515, 807, 549, 247, 129, 91, 60, 58, 50, 38, 33, 33,
chart.add('Server Users', [ 30, 32, 187
82, 80, 60, 12, 10, 7, 6, 0, 0, 3, 1, 4, 1, 4, 0, 5 ]
]) )
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) return render_template('table.jinja2', chart=chart)
@ -194,13 +206,13 @@ def create_app():
style = styles[style] style = styles[style]
else: else:
style = parametric_styles[style]( 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) xy_series = _random(data, order)
other_series = [] other_series = []
for title, values, config in xy_series: for title, values, config in xy_series:
other_series.append( other_series.append((title, cut(values, 1), config))
(title, cut(values, 1), config))
xy_series = b64encode(pickle.dumps(xy_series)) xy_series = b64encode(pickle.dumps(xy_series))
other_series = b64encode(pickle.dumps(other_series)) other_series = b64encode(pickle.dumps(other_series))
config = Config() config = Config()
@ -216,14 +228,15 @@ def create_app():
config.x_labels = None config.x_labels = None
else: else:
config.x_labels = [random_label() for i in range(data)] config.x_labels = [random_label() for i in range(data)]
svgs.append({'type': type, svgs.append({
'series': xy_series if chart._dual else other_series, 'type': type,
'config': b64encode(pickle.dumps(config))}) 'series': xy_series if chart._dual else other_series,
'config': b64encode(pickle.dumps(config))
})
return render_template('svgs.jinja2', return render_template(
svgs=svgs, 'svgs.jinja2', svgs=svgs, width=width, height=height
width=width, )
height=height)
@app.route("/rotation") @app.route("/rotation")
def rotation(): def rotation():
@ -244,14 +257,15 @@ def create_app():
config.x_labels = labels config.x_labels = labels
config.x_label_rotation = angle config.x_label_rotation = angle
config.y_label_rotation = angle config.y_label_rotation = angle
svgs.append({'type': 'pygal.Bar', svgs.append({
'series': series, 'type': 'pygal.Bar',
'config': b64encode(pickle.dumps(config))}) 'series': series,
'config': b64encode(pickle.dumps(config))
})
return render_template('svgs.jinja2', return render_template(
svgs=svgs, 'svgs.jinja2', svgs=svgs, width=width, height=height
width=width, )
height=height)
@app.route("/interpolation") @app.route("/interpolation")
def interpolation(): def interpolation():
@ -268,51 +282,56 @@ def create_app():
for interpolation in 'quadratic', 'cubic', 'lagrange', 'trigonometric': for interpolation in 'quadratic', 'cubic', 'lagrange', 'trigonometric':
config.title = "%s interpolation" % interpolation config.title = "%s interpolation" % interpolation
config.interpolate = interpolation config.interpolate = interpolation
svgs.append({'type': 'pygal.StackedLine', svgs.append({
'series': series, 'type': 'pygal.StackedLine',
'config': b64encode(pickle.dumps(config))}) 'series': series,
'config': b64encode(pickle.dumps(config))
for params in [ })
{'type': 'catmull_rom'},
{'type': 'finite_difference'}, for params in [{'type': 'catmull_rom'}, {'type': 'finite_difference'},
{'type': 'cardinal', 'c': .25}, {'type': 'cardinal',
{'type': 'cardinal', 'c': .5}, 'c': .25}, {'type': 'cardinal',
{'type': 'cardinal', 'c': .75}, 'c': .5}, {'type': 'cardinal', 'c': .75},
{'type': 'cardinal', 'c': 1.5}, {'type': 'cardinal',
{'type': 'cardinal', 'c': 2}, 'c': 1.5}, {'type': 'cardinal',
{'type': 'cardinal', 'c': 5}, 'c': 2}, {'type': 'cardinal', 'c': 5},
{'type': 'kochanek_bartels', 'b': 1, 'c': 1, 't': 1}, {'type': 'kochanek_bartels', 'b': 1, 'c': 1,
{'type': 'kochanek_bartels', 'b': -1, 'c': 1, 't': 1}, 't': 1}, {'type': 'kochanek_bartels', 'b': -1, 'c': 1,
{'type': 'kochanek_bartels', 'b': 1, 'c': -1, 't': 1}, 't': 1}, {'type': 'kochanek_bartels', 'b': 1,
{'type': 'kochanek_bartels', 'b': 1, 'c': 1, 't': -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}, {'type': 'kochanek_bartels', 'b': -1,
'c': -1, 't': -1}]:
config.title = "Hermite interpolation with params %r" % params config.title = "Hermite interpolation with params %r" % params
config.interpolate = 'hermite' config.interpolate = 'hermite'
config.interpolation_parameters = params config.interpolation_parameters = params
svgs.append({'type': 'pygal.StackedLine', svgs.append({
'series': series, 'type': 'pygal.StackedLine',
'config': b64encode(pickle.dumps(config))}) 'series': series,
'config': b64encode(pickle.dumps(config))
})
return render_template('svgs.jinja2', return render_template(
svgs=svgs, 'svgs.jinja2', svgs=svgs, width=width, height=height
width=width, )
height=height)
@app.route("/raw_svgs/") @app.route("/raw_svgs/")
def raw_svgs(): def raw_svgs():
svgs = [] svgs = []
for color in styles['neon'].colors: for color in styles['neon'].colors:
chart = pygal.Pie(style=parametric_styles['rotate'](color), chart = pygal.Pie(
width=400, height=300) style=parametric_styles['rotate'](color),
width=400,
height=300
)
chart.title = color chart.title = color
chart.disable_xml_declaration = True chart.disable_xml_declaration = True
chart.explicit_size = True chart.explicit_size = True
chart.js = ['http://l:2343/2.0.x/pygal-tooltips.js'] chart.js = ['http://l:2343/2.0.x/pygal-tooltips.js']
for i in range(6): for i in range(6):
chart.add(str(i), 2 ** i) chart.add(str(i), 2**i)
svgs.append(chart.render()) svgs.append(chart.render())
return render_template('raw_svgs.jinja2', svgs=svgs) 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
labels = ['AURSAUTRAUIA', labels = [
'dpvluiqhu enuie', 'AURSAUTRAUIA', 'dpvluiqhu enuie', 'su sru a nanan a',
'su sru a nanan a', '09_28_3023_98120398', u'éàé瀮ð{æə|&'
'09_28_3023_98120398', ]
u'éàé瀮ð{æə|&'] series = {'Female': [4, 2, 3, 0, 2], 'Male': [5, 1, 1, 3, 2]}
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 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = [ extensions = [
'sphinx.ext.autodoc', 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx',
'sphinx.ext.doctest', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'pygal_sphinx_directives'
'sphinx.ext.intersphinx',
'sphinx.ext.coverage',
'sphinx.ext.viewcode',
'pygal_sphinx_directives'
] ]
# Add any paths that contain templates here, relative to this directory. # 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. # If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False todo_include_todos = False
# on_rtd is whether we are on readthedocs.org # on_rtd is whether we are on readthedocs.org
on_rtd = os.environ.get('READTHEDOCS', None) == 'True' on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
@ -223,25 +218,27 @@ htmlhelp_basename = 'pygaldoc'
# -- Options for LaTeX output --------------------------------------------- # -- Options for LaTeX output ---------------------------------------------
latex_elements = { latex_elements = {
# The paper size ('letterpaper' or 'a4paper'). # The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper', #'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt'). # The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt', #'pointsize': '10pt',
# Additional stuff for the LaTeX preamble. # Additional stuff for the LaTeX preamble.
#'preamble': '', #'preamble': '',
# Latex figure (float) alignment # Latex figure (float) alignment
#'figure_align': 'htbp', #'figure_align': 'htbp',
} }
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, # (source start file, target name, title,
# author, documentclass [howto, manual, or own class]). # author, documentclass [howto, manual, or own class]).
latex_documents = [ 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 # 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. # If false, no module index is generated.
#latex_domain_indices = True #latex_domain_indices = True
# -- Options for manual page output --------------------------------------- # -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [(master_doc, 'pygal', 'pygal Documentation', [author], 1)]
(master_doc, 'pygal', 'pygal Documentation',
[author], 1)
]
# If true, show URL addresses after external links. # If true, show URL addresses after external links.
#man_show_urls = False #man_show_urls = False
# -- Options for Texinfo output ------------------------------------------- # -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples # Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
(master_doc, 'pygal', 'pygal Documentation', (
author, 'pygal', 'One line description of project.', master_doc, 'pygal', 'pygal Documentation', author, 'pygal',
'Miscellaneous'), 'One line description of project.', 'Miscellaneous'
),
] ]
# Documents to append as an appendix to all manuals. # 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. # If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False #texinfo_no_detailmenu = False
# Example configuration for intersphinx: refer to the Python standard library. # Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'https://docs.python.org/': None} 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
from traceback import format_exc, print_exc from traceback import format_exc, print_exc
import docutils.core import docutils.core
@ -37,7 +36,8 @@ pygal.config.Config.style.value = pygal.style.RotateStyle(
foreground_subtle='#909090', foreground_subtle='#909090',
opacity='.8', opacity='.8',
opacity_hover='.9', opacity_hover='.9',
transition='400ms ease-in') transition='400ms ease-in'
)
class PygalDirective(Directive): class PygalDirective(Directive):
@ -48,8 +48,8 @@ class PygalDirective(Directive):
has_content = True has_content = True
def run(self): def run(self):
width, height = map(int, self.arguments[:2]) if len( width, height = map(int, self.arguments[:2]
self.arguments) >= 2 else (600, 400) ) if len(self.arguments) >= 2 else (600, 400)
if len(self.arguments) == 1: if len(self.arguments) == 1:
self.render_fix = bool(self.arguments[0]) self.render_fix = bool(self.arguments[0])
elif len(self.arguments) == 3: elif len(self.arguments) == 3:
@ -67,10 +67,15 @@ class PygalDirective(Directive):
except Exception: except Exception:
print(code) print(code)
print_exc() print_exc()
return [docutils.nodes.system_message( return [
'An exception as occured during code parsing:' docutils.nodes.system_message(
' \n %s' % format_exc(), type='ERROR', source='/', 'An exception as occured during code parsing:'
level=3)] ' \n %s' % format_exc(),
type='ERROR',
source='/',
level=3
)
]
if self.render_fix: if self.render_fix:
rv = scope['rv'] rv = scope['rv']
else: else:
@ -81,9 +86,14 @@ class PygalDirective(Directive):
self.content.append(key + '.render()') self.content.append(key + '.render()')
break break
if chart is None: if chart is None:
return [docutils.nodes.system_message( return [
'No instance of graph found', level=3, docutils.nodes.system_message(
type='ERROR', source='/')] 'No instance of graph found',
level=3,
type='ERROR',
source='/'
)
]
chart.config.width = width chart.config.width = width
chart.config.height = height chart.config.height = height
chart.explicit_size = True chart.explicit_size = True
@ -91,27 +101,28 @@ class PygalDirective(Directive):
try: try:
svg = '<embed src="%s" />' % chart.render_data_uri() svg = '<embed src="%s" />' % chart.render_data_uri()
except Exception: except Exception:
return [docutils.nodes.system_message( return [
'An exception as occured during graph generation:' docutils.nodes.system_message(
' \n %s' % format_exc(), type='ERROR', source='/', 'An exception as occured during graph generation:'
level=3)] ' \n %s' % format_exc(),
type='ERROR',
source='/',
level=3
)
]
return [docutils.nodes.raw('', svg, format='html')] return [docutils.nodes.raw('', svg, format='html')]
class PygalWithCode(PygalDirective): class PygalWithCode(PygalDirective):
def run(self): def run(self):
node_list = super(PygalWithCode, self).run() node_list = super(PygalWithCode, self).run()
node_list.extend(CodeBlock( node_list.extend(
self.name, CodeBlock(
['python'], self.name, ['python'], self.options, self.content, self.lineno,
self.options, self.content_offset, self.block_text, self.state,
self.content, self.state_machine
self.lineno, ).run()
self.content_offset, )
self.block_text,
self.state,
self.state_machine).run())
return [docutils.nodes.compound('', *node_list)] return [docutils.nodes.compound('', *node_list)]
@ -134,29 +145,30 @@ class PygalTable(Directive):
exec(code, scope) exec(code, scope)
except Exception: except Exception:
print_exc() print_exc()
return [docutils.nodes.system_message( return [
'An exception as occured during code parsing:' docutils.nodes.system_message(
' \n %s' % format_exc(), type='ERROR', source='/', 'An exception as occured during code parsing:'
level=3)] ' \n %s' % format_exc(),
type='ERROR',
source='/',
level=3
)
]
rv = scope['rv'] rv = scope['rv']
return [docutils.nodes.raw('', rv, format='html')] return [docutils.nodes.raw('', rv, format='html')]
class PygalTableWithCode(PygalTable): class PygalTableWithCode(PygalTable):
def run(self): def run(self):
node_list = super(PygalTableWithCode, self).run() node_list = super(PygalTableWithCode, self).run()
node_list.extend(CodeBlock( node_list.extend(
self.name, CodeBlock(
['python'], self.name, ['python'], self.options, self.content, self.lineno,
self.options, self.content_offset, self.block_text, self.state,
self.content, self.state_machine
self.lineno, ).run()
self.content_offset, )
self.block_text,
self.state,
self.state_machine).run())
return [docutils.nodes.compound('', *node_list)] 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.config import Config
from pygal import maps from pygal import maps
CHARTS_BY_NAME = dict([
CHARTS_BY_NAME = dict( (k, v) for k, v in locals().items()
[(k, v) for k, v in locals().items() if isinstance(v, type) and issubclass(v, Graph) and v != Graph
if isinstance(v, type) and issubclass(v, Graph) and v != Graph]) ])
from pygal.graph.map import BaseMap from pygal.graph.map import BaseMap
for entry in pkg_resources.iter_entry_points('pygal.maps'): for entry in pkg_resources.iter_entry_points('pygal.maps'):
try: try:
module = entry.load() module = entry.load()
except Exception: except Exception:
warnings.warn('Unable to load %s pygal plugin \n\n%s' % ( warnings.warn(
entry, traceback.format_exc()), Warning) 'Unable to load %s pygal plugin \n\n%s' %
(entry, traceback.format_exc()), Warning
)
continue continue
setattr(maps, entry.name, module) setattr(maps, entry.name, module)
for k, v in module.__dict__.items(): for k, v in module.__dict__.items():
@ -78,7 +79,6 @@ CHARTS = list(CHARTS_BY_NAME.values())
class PluginImportFixer(object): class PluginImportFixer(object):
""" """
Allow external map plugins to be imported from pygal.maps package. Allow external map plugins to be imported from pygal.maps package.

2
pygal/_compat.py

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

18
pygal/colors.py

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

5
pygal/etree.py

@ -25,7 +25,6 @@ import os
class Etree(object): class Etree(object):
"""Etree wrapper using lxml.etree or standard xml.etree""" """Etree wrapper using lxml.etree or standard xml.etree"""
def __init__(self): def __init__(self):
@ -46,8 +45,8 @@ class Etree(object):
def __getattribute__(self, attr): def __getattribute__(self, attr):
"""Retrieve attr from current active etree implementation""" """Retrieve attr from current active etree implementation"""
if (attr not in object.__getattribute__(self, '__dict__') and if (attr not in object.__getattribute__(self, '__dict__')
attr not in Etree.__dict__): and attr not in Etree.__dict__):
return object.__getattribute__(self._etree, attr) return object.__getattribute__(self._etree, attr)
return object.__getattribute__(self, 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))) order = val and int(floor(log(abs(val)) / log(1000)))
orders = self.ORDERS.split(" ")[int(order > 0)] orders = self.ORDERS.split(" ")[int(order > 0)]
if order == 0 or order > len(orders): if order == 0 or order > len(orders):
return float_format(val / (1000 ** int(order))) return float_format(val / (1000**int(order)))
return ( return (
float_format(val / (1000 ** int(order))) + float_format(val / (1000**int(order))) +
orders[int(order) - int(order > 0)]) orders[int(order) - int(order > 0)]
)
class Significant(Formatter): class Significant(Formatter):
"""Show precision significant digit of float""" """Show precision significant digit of float"""
def __init__(self, precision=10): def __init__(self, precision=10):
self.format = '%%.%dg' % precision 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Graph package containing all builtin charts""" """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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
""" """
Bar chart that presents grouped data with rectangular bars with lengths Bar chart that presents grouped data with rectangular bars with lengths
proportional to the values that they represent. proportional to the values that they represent.
@ -29,7 +28,6 @@ from pygal.util import alter, decorate, ident, swap
class Bar(Graph): class Bar(Graph):
"""Bar graph class""" """Bar graph class"""
_series_margin = .06 _series_margin = .06
@ -54,15 +52,25 @@ class Bar(Graph):
width -= 2 * serie_margin width -= 2 * serie_margin
height = self.view.y(zero) - y height = self.view.y(zero) - y
r = serie.rounded_bars * 1 if serie.rounded_bars else 0 r = serie.rounded_bars * 1 if serie.rounded_bars else 0
alter(self.svg.transposable_node( alter(
parent, 'rect', self.svg.transposable_node(
x=x, y=y, rx=r, ry=r, width=width, height=height, parent,
class_='rect reactive tooltip-trigger'), serie.metadata.get(i)) '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 return x, y, width, height
def _tooltip_and_print_values( def _tooltip_and_print_values(
self, serie_node, serie, parent, i, val, metadata, self, serie_node, serie, parent, i, val, metadata, x, y, width,
x, y, width, height): height
):
transpose = swap if self.horizontal else ident transpose = swap if self.horizontal else ident
x_center, y_center = transpose((x + width / 2, y + height / 2)) x_center, y_center = transpose((x + width / 2, y + height / 2))
x_top, y_top = transpose((x + width, y + height)) x_top, y_top = transpose((x + width, y + height))
@ -73,8 +81,8 @@ class Bar(Graph):
v = serie.values[i] v = serie.values[i]
sign = -1 if v < self.zero else 1 sign = -1 if v < self.zero else 1
self._tooltip_data( self._tooltip_data(
parent, val, x_center, y_center, "centered", parent, val, x_center, y_center, "centered", self._get_x_label(i)
self._get_x_label(i)) )
if self.print_values_position == 'top': if self.print_values_position == 'top':
if self.horizontal: if self.horizontal:
@ -111,20 +119,21 @@ class Bar(Graph):
val = self._format(serie, i) val = self._format(serie, i)
bar = decorate( bar = decorate(
self.svg, self.svg, self.svg.node(bars, class_='bar'), metadata
self.svg.node(bars, class_='bar'), )
metadata)
x_, y_, width, height = self._bar( 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( self._confidence_interval(
serie_node['overlay'], x_ + width / 2, y_, serie.values[i], serie_node['overlay'], x_ + width / 2, y_, serie.values[i],
metadata) metadata
)
self._tooltip_and_print_values( self._tooltip_and_print_values(
serie_node, serie, bar, i, val, metadata, serie_node, serie, bar, i, val, metadata, x_, y_, width, height
x_, y_, width, height) )
def _compute(self): def _compute(self):
"""Compute y min and max and y scale and set labels""" """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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Base for pygal charts""" """Base for pygal charts"""
from __future__ import division from __future__ import division
@ -36,7 +35,6 @@ from pygal.view import Box, Margin
class BaseGraph(object): class BaseGraph(object):
"""Chart internal behaviour related functions""" """Chart internal behaviour related functions"""
_adapters = [] _adapters = []
@ -93,7 +91,7 @@ class BaseGraph(object):
if not raw: if not raw:
return return
adapters = list(self._adapters) or [lambda x:x] adapters = list(self._adapters) or [lambda x: x]
if self.logarithmic: if self.logarithmic:
for fun in not_zero, positive: for fun in not_zero, positive:
if fun in adapters: if fun in adapters:
@ -103,19 +101,18 @@ class BaseGraph(object):
self._adapt = reduce(compose, adapters) if not self.strict else ident self._adapt = reduce(compose, adapters) if not self.strict else ident
self._x_adapt = reduce( self._x_adapt = reduce(
compose, self._x_adapters) if not self.strict and getattr( compose, self._x_adapters
self, '_x_adapters', None) else ident ) if not self.strict and getattr(self, '_x_adapters', None) else ident
series = [] series = []
raw = [( raw = [(
list(raw_values) if not isinstance( list(raw_values) if not isinstance(raw_values, dict) else
raw_values, dict) else raw_values, raw_values, serie_config_kwargs
serie_config_kwargs
) for raw_values, serie_config_kwargs in raw] ) for raw_values, serie_config_kwargs in raw]
width = max([len(values) for values, _ in raw] + width = max([len(values)
[len(self.x_labels or [])]) for values, _ in raw] + [len(self.x_labels or [])])
for raw_values, serie_config_kwargs in raw: for raw_values, serie_config_kwargs in raw:
metadata = {} metadata = {}
@ -130,10 +127,9 @@ class BaseGraph(object):
value_list[self.x_labels.index(k)] = v value_list[self.x_labels.index(k)] = v
raw_values = value_list raw_values = value_list
for index, raw_value in enumerate( for index, raw_value in enumerate(raw_values + (
raw_values + ( (width - len(raw_values)) * [None] # aligning values
(width - len(raw_values)) * [None] # aligning values if len(raw_values) < width else [])):
if len(raw_values) < width else [])):
if isinstance(raw_value, dict): if isinstance(raw_value, dict):
raw_value = dict(raw_value) raw_value = dict(raw_value)
value = raw_value.pop('value', None) value = raw_value.pop('value', None)
@ -157,8 +153,8 @@ class BaseGraph(object):
value = (value, self.zero) value = (value, self.zero)
if self._x_adapt: if self._x_adapt:
value = ( value = (
self._x_adapt(value[0]), self._x_adapt(value[0]), self._adapt(value[1])
self._adapt(value[1])) )
if isinstance(self, BaseMap): if isinstance(self, BaseMap):
value = (self._adapt(value[0]), value[1]) value = (self._adapt(value[0]), value[1])
else: else:
@ -168,11 +164,14 @@ class BaseGraph(object):
values.append(value) values.append(value)
serie_config = SerieConfig() serie_config = SerieConfig()
serie_config(**dict((k, v) for k, v in self.state.__dict__.items() serie_config(
if k in dir(serie_config))) **dict((k, v) for k, v in self.state.__dict__.items()
if k in dir(serie_config))
)
serie_config(**serie_config_kwargs) serie_config(**serie_config_kwargs)
series.append( series.append(
Serie(offset + len(series), values, serie_config, metadata)) Serie(offset + len(series), values, serie_config, metadata)
)
return series return series
def setup(self, **kwargs): def setup(self, **kwargs):
@ -185,11 +184,13 @@ class BaseGraph(object):
self.state = State(self, **kwargs) self.state = State(self, **kwargs)
if isinstance(self.style, type): if isinstance(self.style, type):
self.style = self.style() self.style = self.style()
self.series = self.prepare_values( self.series = self.prepare_values([
[rs for rs in self.raw_series if not rs[1].get('secondary')]) or [] rs for rs in self.raw_series if not rs[1].get('secondary')
]) or []
self.secondary_series = self.prepare_values( self.secondary_series = self.prepare_values(
[rs for rs in self.raw_series if rs[1].get('secondary')], [rs for rs in self.raw_series
len(self.series)) or [] if rs[1].get('secondary')], len(self.series)
) or []
self.horizontal = getattr(self, 'horizontal', False) self.horizontal = getattr(self, 'horizontal', False)
self.svg = Svg(self) self.svg = Svg(self)
self._x_labels = None self._x_labels = None
@ -198,20 +199,23 @@ class BaseGraph(object):
self._y_2nd_labels = None self._y_2nd_labels = None
self.nodes = {} self.nodes = {}
self.margin_box = Margin( self.margin_box = Margin(
self.margin_top or self.margin, self.margin_top or self.margin, self.margin_right or self.margin,
self.margin_right or self.margin, self.margin_bottom or self.margin, self.margin_left or self.margin
self.margin_bottom or self.margin, )
self.margin_left or self.margin)
self._box = Box() self._box = Box()
self.view = None self.view = None
if self.logarithmic and self.zero == 0: if self.logarithmic and self.zero == 0:
# Explicit min to avoid interpolation dependency # Explicit min to avoid interpolation dependency
positive_values = list(filter( positive_values = list(
lambda x: x > 0, filter(
[val[1] or 1 if self._dual else val lambda x: x > 0, [
for serie in self.series for val in serie.safe_values])) 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 ]
)
)
self.zero = min(positive_values or (1, )) or 1
if self._len < 3: if self._len < 3:
self.interpolate = None self.interpolate = None
self._draw() self._draw()

134
pygal/graph/box.py

@ -16,7 +16,6 @@
# #
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # 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 Box plot: a convenient way to display series as box with whiskers and outliers
Different types are available throught the box_mode option Different types are available throught the box_mode option
@ -31,7 +30,6 @@ from pygal.util import alter, decorate
class Box(Graph): class Box(Graph):
""" """
Box plot Box plot
For each series, shows the median value, the 25th and 75th percentiles, For each series, shows the median value, the 25th and 75th percentiles,
@ -49,17 +47,20 @@ class Box(Graph):
""" """
if self.box_mode == "extremes": if self.box_mode == "extremes":
return ( return (
'Min: %s\nQ1 : %s\nQ2 : %s\nQ3 : %s\nMax: %s' % tuple( 'Min: %s\nQ1 : %s\nQ2 : %s\nQ3 : %s\nMax: %s' %
map(self._y_format, serie.points[1:6]))) tuple(map(self._y_format, serie.points[1:6]))
)
elif self.box_mode in ["tukey", "stdev", "pstdev"]: elif self.box_mode in ["tukey", "stdev", "pstdev"]:
return ( return (
'Min: %s\nLower Whisker: %s\nQ1: %s\nQ2: %s\nQ3: %s\n' 'Min: %s\nLower Whisker: %s\nQ1: %s\nQ2: %s\nQ3: %s\n'
'Upper Whisker: %s\nMax: %s' % tuple(map( 'Upper Whisker: %s\nMax: %s' %
self._y_format, serie.points))) tuple(map(self._y_format, serie.points))
)
elif self.box_mode == '1.5IQR': elif self.box_mode == '1.5IQR':
# 1.5IQR mode # 1.5IQR mode
return 'Q1: %s\nQ2: %s\nQ3: %s' % tuple(map( return 'Q1: %s\nQ2: %s\nQ3: %s' % tuple(
self._y_format, serie.points[2:5])) map(self._y_format, serie.points[2:5])
)
else: else:
return self._y_format(serie.points) return self._y_format(serie.points)
@ -72,8 +73,7 @@ class Box(Graph):
serie.points, serie.outliers = \ serie.points, serie.outliers = \
self._box_points(serie.values, self.box_mode) self._box_points(serie.values, self.box_mode)
self._x_pos = [ self._x_pos = [(i + .5) / self._order for i in range(self._order)]
(i + .5) / self._order for i in range(self._order)]
if self._min: if self._min:
self._box.ymin = min(self._min, self.zero) self._box.ymin = min(self._min, self.zero)
@ -100,17 +100,17 @@ class Box(Graph):
metadata = serie.metadata.get(0) metadata = serie.metadata.get(0)
box = decorate( box = decorate(self.svg, self.svg.node(boxes, class_='box'), metadata)
self.svg,
self.svg.node(boxes, class_='box'),
metadata)
val = self._format(serie, 0) val = self._format(serie, 0)
x_center, y_center = self._draw_box( x_center, y_center = self._draw_box(
box, serie.points[1:6], serie.outliers, serie.index, metadata) 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._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) self._static_value(serie_node, val, x_center, y_center, metadata)
def _draw_box(self, parent_node, quartiles, outliers, box_index, metadata): def _draw_box(self, parent_node, quartiles, outliers, box_index, metadata):
@ -124,55 +124,78 @@ class Box(Graph):
width -= 2 * series_margin width -= 2 * series_margin
# draw lines for whiskers - bottom, median, and top # draw lines for whiskers - bottom, median, and top
for i, whisker in enumerate( for i, whisker in enumerate((quartiles[0], quartiles[2],
(quartiles[0], quartiles[2], quartiles[4])): quartiles[4])):
whisker_width = width if i == 1 else width / 2 whisker_width = width if i == 1 else width / 2
shift = (width - whisker_width) / 2 shift = (width - whisker_width) / 2
xs = left_edge + shift xs = left_edge + shift
xe = left_edge + width - shift xe = left_edge + width - shift
alter(self.svg.line( alter(
parent_node, self.svg.line(
coords=[(xs, self.view.y(whisker)), parent_node,
(xe, self.view.y(whisker))], coords=[(xs, self.view.y(whisker)),
class_='reactive tooltip-trigger', (xe, self.view.y(whisker))],
attrib={'stroke-width': 3}), metadata) class_='reactive tooltip-trigger',
attrib={
'stroke-width': 3
}
), metadata
)
# draw lines connecting whiskers to box (Q1 and Q3) # draw lines connecting whiskers to box (Q1 and Q3)
alter(self.svg.line( alter(
parent_node, self.svg.line(
coords=[(left_edge + width / 2, self.view.y(quartiles[0])), parent_node,
(left_edge + width / 2, self.view.y(quartiles[1]))], coords=[(left_edge + width / 2, self.view.y(quartiles[0])),
class_='reactive tooltip-trigger', (left_edge + width / 2, self.view.y(quartiles[1]))],
attrib={'stroke-width': 2}), metadata) class_='reactive tooltip-trigger',
alter(self.svg.line( attrib={
parent_node, 'stroke-width': 2
coords=[(left_edge + width / 2, self.view.y(quartiles[4])), }
(left_edge + width / 2, self.view.y(quartiles[3]))], ), metadata
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 # box, bounded by Q1 and Q3
alter(self.svg.node( alter(
parent_node, self.svg.node(
tag='rect', parent_node,
x=left_edge, tag='rect',
y=self.view.y(quartiles[1]), x=left_edge,
height=self.view.y(quartiles[3]) - self.view.y(quartiles[1]), y=self.view.y(quartiles[1]),
width=width, height=self.view.y(quartiles[3]) - self.view.y(quartiles[1]),
class_='subtle-fill reactive tooltip-trigger'), metadata) width=width,
class_='subtle-fill reactive tooltip-trigger'
), metadata
)
# draw outliers # draw outliers
for o in outliers: for o in outliers:
alter(self.svg.node( alter(
parent_node, self.svg.node(
tag='circle', parent_node,
cx=left_edge + width / 2, tag='circle',
cy=self.view.y(o), cx=left_edge + width / 2,
r=3, cy=self.view.y(o),
class_='subtle-fill reactive tooltip-trigger'), metadata) r=3,
class_='subtle-fill reactive tooltip-trigger'
return (left_edge + width / 2, self.view.y( ), metadata
sum(quartiles) / len(quartiles))) )
return (
left_edge + width / 2,
self.view.y(sum(quartiles) / len(quartiles))
)
@staticmethod @staticmethod
def _box_points(values, mode='extremes'): def _box_points(values, mode='extremes'):
@ -199,6 +222,7 @@ class Box(Graph):
Sincich, T. L. Statistics for Engineering and the Sincich, T. L. Statistics for Engineering and the
Sciences, 4th ed. Prentice-Hall, 1995. Sciences, 4th ed. Prentice-Hall, 1995.
""" """
def median(seq): def median(seq):
n = len(seq) n = len(seq)
if n % 2 == 0: # seq has an even length 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
""" """
Dot chart displaying values as a grid of dots, the bigger the value Dot chart displaying values as a grid of dots, the bigger the value
the bigger the dot the bigger the dot
@ -33,7 +32,6 @@ from pygal.view import ReverseView, View
class Dot(Graph): class Dot(Graph):
"""Dot graph class""" """Dot graph class"""
def dot(self, serie, r_max): def dot(self, serie, r_max):
@ -48,10 +46,8 @@ class Dot(Graph):
log10max = log10(self._max or 1) log10max = log10(self._max or 1)
if value != 0: if value != 0:
size = r_max * ( size = r_max * ((log10(abs(value)) - log10min) /
(log10(abs(value)) - log10min) / (log10max - log10min))
(log10max - log10min)
)
else: else:
size = 0 size = 0
else: else:
@ -59,19 +55,25 @@ class Dot(Graph):
metadata = serie.metadata.get(i) metadata = serie.metadata.get(i)
dots = decorate( dots = decorate(
self.svg, self.svg, self.svg.node(serie_node['plot'], class_="dots"),
self.svg.node(serie_node['plot'], class_="dots"), metadata
metadata) )
alter(self.svg.node( alter(
dots, 'circle', self.svg.node(
cx=x, cy=y, r=size, dots,
class_='dot reactive tooltip-trigger' + ( 'circle',
' negative' if value < 0 else '')), metadata) cx=x,
cy=y,
r=size,
class_='dot reactive tooltip-trigger' +
(' negative' if value < 0 else '')
), metadata
)
val = self._format(serie, i) val = self._format(serie, i)
self._tooltip_data( self._tooltip_data(
dots, val, x, y, 'centered', dots, val, x, y, 'centered', self._get_x_label(i)
self._get_x_label(i)) )
self._static_value(serie_node, val, x, y, metadata) self._static_value(serie_node, val, x, y, metadata)
def _compute(self): 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))] self._y_pos = [n / 2 for n in reversed(range(1, 2 * y_len, 2))]
for j, serie in enumerate(self.series): for j, serie in enumerate(self.series):
serie.points = [ serie.points = [(self._x_pos[i], self._y_pos[j])
(self._x_pos[i], self._y_pos[j]) for i in range(x_len)]
for i in range(x_len)]
def _compute_y_labels(self): def _compute_y_labels(self):
self._y_labels = list(zip( self._y_labels = list(
self.y_labels and map(to_str, self.y_labels) or [ zip(
serie.title['title'] self.y_labels and map(to_str, self.y_labels) or [
if isinstance(serie.title, dict) serie.title['title']
else serie.title or '' for serie in self.series], if isinstance(serie.title, dict) else serie.title or ''
self._y_pos)) for serie in self.series
], self._y_pos
)
)
def _set_view(self): def _set_view(self):
"""Assign a view to current graph""" """Assign a view to current graph"""
view_class = ReverseView if self.inverse_y_axis else View view_class = ReverseView if self.inverse_y_axis else View
self.view = view_class( self.view = view_class(
self.width - self.margin_box.x, self.width - self.margin_box.x, self.height - self.margin_box.y,
self.height - self.margin_box.y, self._box
self._box) )
@cached_property @cached_property
def _values(self): def _values(self):
@ -114,14 +118,16 @@ class Dot(Graph):
@cached_property @cached_property
def _max(self): def _max(self):
"""Getter for the maximum series value""" """Getter for the maximum series value"""
return (self.range[1] if (self.range and self.range[1] is not None) return (
else (max(map(abs, self._values)) if self._values else None)) 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): def _plot(self):
"""Plot all dots for series""" """Plot all dots for series"""
r_max = min( r_max = min(
self.view.x(1) - self.view.x(0), self.view.x(1) - self.view.x(0),
(self.view.y(0) or 0) - self.view.y(1)) / ( (self.view.y(0) or 0) - self.view.y(1)
2 * 1.05) ) / (2 * 1.05)
for serie in self.series: for serie in self.series:
self.dot(serie, r_max) 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Dual chart base. Dual means a chart with 2 scaled axis like xy""" """Dual chart base. Dual means a chart with 2 scaled axis like xy"""
from pygal._compat import is_str from pygal._compat import is_str
@ -31,14 +30,12 @@ class Dual(Graph):
""" """
Format value for dual value display. Format value for dual value display.
""" """
return '%s: %s' % ( return '%s: %s' % (self._x_format(value[0]), self._y_format(value[1]))
self._x_format(value[0]),
self._y_format(value[1]))
def _compute_x_labels(self): def _compute_x_labels(self):
x_pos = compute_scale( x_pos = compute_scale(
self._box.xmin, self._box.xmax, self.logarithmic, self._box.xmin, self._box.xmax, self.logarithmic, self.order_min,
self.order_min, self.min_scale, self.max_scale self.min_scale, self.max_scale
) )
if self.x_labels: if self.x_labels:
self._x_labels = [] self._x_labels = []
@ -63,7 +60,8 @@ class Dual(Graph):
def _compute_x_labels_major(self): def _compute_x_labels_major(self):
# In case of dual, x labels must adapters and so majors too # In case of dual, x labels must adapters and so majors too
self.x_labels_major = self.x_labels_major and list( 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() super(Dual, self)._compute_x_labels_major()
def _get_x_label(self, i): 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): class Funnel(Graph):
"""Funnel graph class""" """Funnel graph class"""
_adapters = [positive, none_to_zero] _adapters = [positive, none_to_zero]
@ -44,22 +43,27 @@ class Funnel(Graph):
val = self._format(serie, i) val = self._format(serie, i)
funnels = decorate( funnels = decorate(
self.svg, self.svg, self.svg.node(serie_node['plot'], class_="funnels"),
self.svg.node(serie_node['plot'], class_="funnels"), metadata
metadata) )
alter(self.svg.node( alter(
funnels, 'polygon', self.svg.node(
points=' '.join(map(fmt, map(self.view, poly))), funnels,
class_='funnel reactive tooltip-trigger'), metadata) 'polygon',
points=' '.join(map(fmt, map(self.view, poly))),
class_='funnel reactive tooltip-trigger'
), metadata
)
# Poly center from label # Poly center from label
x, y = self.view(( x, y = self.view((
self._center(self._x_pos[serie.index]), 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( self._tooltip_data(
funnels, val, x, y, 'centered', funnels, val, x, y, 'centered', self._get_x_label(serie.index)
self._get_x_label(serie.index)) )
self._static_value(serie_node, val, x, y, metadata) self._static_value(serie_node, val, x, y, metadata)
def _center(self, x): def _center(self, x):
@ -73,7 +77,7 @@ class Funnel(Graph):
previous = [[self.zero, self.zero] for i in range(self._len)] previous = [[self.zero, self.zero] for i in range(self._len)]
for i, serie in enumerate(self.series): 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 all_x_pos = [0] + self._x_pos
serie.points = [] serie.points = []
for j, value in enumerate(serie.values): for j, value in enumerate(serie.values):
@ -98,12 +102,14 @@ class Funnel(Graph):
def _compute_x_labels(self): def _compute_x_labels(self):
self._x_labels = list( self._x_labels = list(
zip(self.x_labels and zip(
map(self._x_format, self.x_labels) or [ self.x_labels and map(self._x_format, self.x_labels) or [
serie.title['title'] serie.title['title']
if isinstance(serie.title, dict) if isinstance(serie.title, dict) else serie.title or ''
else serie.title or '' for serie in self.series], for serie in self.series
map(self._center, self._x_pos))) ], map(self._center, self._x_pos)
)
)
def _plot(self): def _plot(self):
"""Plot the funnel""" """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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Gauge chart representing values as needles on a polar scale""" """Gauge chart representing values as needles on a polar scale"""
from __future__ import division from __future__ import division
@ -28,7 +27,6 @@ from pygal.view import PolarThetaLogView, PolarThetaView
class Gauge(Graph): class Gauge(Graph):
"""Gauge graph class""" """Gauge graph class"""
needle_width = 1 / 20 needle_width = 1 / 20
@ -41,9 +39,9 @@ class Gauge(Graph):
view_class = PolarThetaView view_class = PolarThetaView
self.view = view_class( self.view = view_class(
self.width - self.margin_box.x, self.width - self.margin_box.x, self.height - self.margin_box.y,
self.height - self.margin_box.y, self._box
self._box) )
def needle(self, serie): def needle(self, serie):
"""Draw a needle for each value""" """Draw a needle for each value"""
@ -58,9 +56,9 @@ class Gauge(Graph):
val = self._format(serie, i) val = self._format(serie, i)
metadata = serie.metadata.get(i) metadata = serie.metadata.get(i)
gauges = decorate( gauges = decorate(
self.svg, self.svg, self.svg.node(serie_node['plot'], class_="dots"),
self.svg.node(serie_node['plot'], class_="dots"), metadata
metadata) )
tolerance = 1.15 tolerance = 1.15
@ -73,23 +71,24 @@ class Gauge(Graph):
w = (self._box._tmax - self._box._tmin + self.view.aperture) / 4 w = (self._box._tmax - self._box._tmin + self.view.aperture) / 4
if self.logarithmic: if self.logarithmic:
w = min(w, self._min - self._min * 10 ** -10) w = min(w, self._min - self._min * 10**-10)
alter( alter(
self.svg.node( 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(.85, theta),
point(self.needle_width, theta - w), point(self.needle_width, theta - w),
'%f %f' % (self.needle_width, self.needle_width), '%f %f' % (self.needle_width, self.needle_width),
point(self.needle_width, theta + w), point(self.needle_width, theta + w),
), ),
class_='line reactive tooltip-trigger'), class_='line reactive tooltip-trigger'
metadata) ), metadata
)
x, y = self.view((.75, theta)) x, y = self.view((.75, theta))
self._tooltip_data( self._tooltip_data(gauges, val, x, y, xlabel=self._get_x_label(i))
gauges, val, x, y,
xlabel=self._get_x_label(i))
self._static_value(serie_node, val, x, y, metadata) self._static_value(serie_node, val, x, y, metadata)
def _y_axis(self, draw_axes=True): def _y_axis(self, draw_axes=True):
@ -100,26 +99,26 @@ class Gauge(Graph):
guides = self.svg.node(axis, class_='guides') guides = self.svg.node(axis, class_='guides')
self.svg.line( self.svg.line(
guides, [self.view((.95, theta)), self.view((1, theta))], guides, [self.view((.95, theta)),
self.view((1, theta))],
close=True, close=True,
class_='line') class_='line'
)
self.svg.line( self.svg.line(
guides, [self.view((0, theta)), self.view((.95, theta))], guides, [self.view((0, theta)),
self.view((.95, theta))],
close=True, close=True,
class_='guide line %s' % ( class_='guide line %s' %
'major' if i in (0, len(self._y_labels) - 1) ('major' if i in (0, len(self._y_labels) - 1) else '')
else '')) )
x, y = self.view((.9, theta)) x, y = self.view((.9, theta))
self.svg.node( self.svg.node(guides, 'text', x=x, y=y).text = label
guides, 'text',
x=x,
y=y
).text = label
self.svg.node( self.svg.node(
guides, 'title', guides,
'title',
).text = self._y_format(theta) ).text = self._y_format(theta)
def _x_axis(self, draw_axes=True): def _x_axis(self, draw_axes=True):
@ -136,18 +135,15 @@ class Gauge(Graph):
self.min_ -= 1 self.min_ -= 1
self.max_ += 1 self.max_ += 1
self._box.set_polar_box( self._box.set_polar_box(0, 1, self.min_, self.max_)
0, 1,
self.min_,
self.max_)
def _compute_x_labels(self): def _compute_x_labels(self):
pass pass
def _compute_y_labels(self): def _compute_y_labels(self):
y_pos = compute_scale( y_pos = compute_scale(
self.min_, self.max_, self.logarithmic, self.min_, self.max_, self.logarithmic, self.order_min,
self.order_min, self.min_scale, self.max_scale self.min_scale, self.max_scale
) )
if self.y_labels: if self.y_labels:
self._y_labels = [] self._y_labels = []
@ -164,10 +160,7 @@ class Gauge(Graph):
self._y_labels.append((title, pos)) self._y_labels.append((title, pos))
self.min_ = min(self.min_, min(cut(self._y_labels, 1))) self.min_ = min(self.min_, min(cut(self._y_labels, 1)))
self.max_ = max(self.max_, max(cut(self._y_labels, 1))) self.max_ = max(self.max_, max(cut(self._y_labels, 1)))
self._box.set_polar_box( self._box.set_polar_box(0, 1, self.min_, self.max_)
0, 1,
self.min_,
self.max_)
else: else:
self._y_labels = list(zip(map(self._y_format, y_pos), y_pos)) 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.interpolate import INTERPOLATIONS
from pygal.util import ( from pygal.util import (
cached_property, compute_scale, cut, decorate, filter_kwargs, get_text_box, 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 from pygal.view import LogView, ReverseView, View, XYLogView
class Graph(PublicApi): class Graph(PublicApi):
"""Graph super class containing generic common functions""" """Graph super class containing generic common functions"""
_dual = False _dual = False
@ -64,65 +64,93 @@ class Graph(PublicApi):
view_class = ReverseView if self.inverse_y_axis else View view_class = ReverseView if self.inverse_y_axis else View
self.view = view_class( self.view = view_class(
self.width - self.margin_box.x, self.width - self.margin_box.x, self.height - self.margin_box.y,
self.height - self.margin_box.y, self._box
self._box) )
def _make_graph(self): def _make_graph(self):
"""Init common graph svg structure""" """Init common graph svg structure"""
self.nodes['graph'] = self.svg.node( self.nodes['graph'] = self.svg.node(
class_='graph %s-graph %s' % ( class_='graph %s-graph %s' % (
self.__class__.__name__.lower(), self.__class__.__name__.lower(),
'horizontal' if self.horizontal else 'vertical')) 'horizontal' if self.horizontal else 'vertical'
self.svg.node(self.nodes['graph'], 'rect', )
class_='background', )
x=0, y=0, self.svg.node(
width=self.width, self.nodes['graph'],
height=self.height) 'rect',
class_='background',
x=0,
y=0,
width=self.width,
height=self.height
)
self.nodes['plot'] = self.svg.node( 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'], 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['overlay'] = self.svg.node(
self.nodes['graph'], class_="plot overlay", self.nodes['graph'],
transform="translate(%d, %d)" % ( class_="plot overlay",
self.margin_box.left, self.margin_box.top)) transform="translate(%d, %d)" %
(self.margin_box.left, self.margin_box.top)
)
self.nodes['text_overlay'] = self.svg.node( self.nodes['text_overlay'] = self.svg.node(
self.nodes['graph'], class_="plot text-overlay", self.nodes['graph'],
transform="translate(%d, %d)" % ( class_="plot text-overlay",
self.margin_box.left, self.margin_box.top)) transform="translate(%d, %d)" %
(self.margin_box.left, self.margin_box.top)
)
self.nodes['tooltip_overlay'] = self.svg.node( self.nodes['tooltip_overlay'] = self.svg.node(
self.nodes['graph'], class_="plot tooltip-overlay", self.nodes['graph'],
transform="translate(%d, %d)" % ( class_="plot tooltip-overlay",
self.margin_box.left, self.margin_box.top)) transform="translate(%d, %d)" %
(self.margin_box.left, self.margin_box.top)
)
self.nodes['tooltip'] = self.svg.node( self.nodes['tooltip'] = self.svg.node(
self.nodes['tooltip_overlay'], self.nodes['tooltip_overlay'],
transform='translate(0 0)', transform='translate(0 0)',
style="opacity: 0", style="opacity: 0",
**{'class': 'tooltip'}) **{
'class': 'tooltip'
}
)
self.svg.node(self.nodes['tooltip'], 'rect', self.svg.node(
rx=self.tooltip_border_radius, self.nodes['tooltip'],
ry=self.tooltip_border_radius, 'rect',
width=0, height=0, rx=self.tooltip_border_radius,
**{'class': 'tooltip-box'}) ry=self.tooltip_border_radius,
width=0,
height=0,
**{
'class': 'tooltip-box'
}
)
self.svg.node(self.nodes['tooltip'], 'g', class_='text') self.svg.node(self.nodes['tooltip'], 'g', class_='text')
def _x_axis(self): def _x_axis(self):
"""Make the x axis: labels and guides""" """Make the x axis: labels and guides"""
if not self._x_labels or not self.show_x_labels: if not self._x_labels or not self.show_x_labels:
return return
axis = self.svg.node(self.nodes['plot'], class_="axis x%s" % ( axis = self.svg.node(
' always_show' if self.show_x_guides else '' self.nodes['plot'],
)) class_="axis x%s" % (' always_show' if self.show_x_guides else '')
)
truncation = self.truncate_label truncation = self.truncate_label
if not truncation: if not truncation:
if self.x_label_rotation or len(self._x_labels) <= 1: if self.x_label_rotation or len(self._x_labels) <= 1:
@ -130,18 +158,21 @@ class Graph(PublicApi):
else: else:
first_label_position = self.view.x(self._x_labels[0][1]) or 0 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 last_label_position = self.view.x(self._x_labels[-1][1]) or 0
available_space = ( available_space = (last_label_position - first_label_position
last_label_position - first_label_position) / ( ) / (len(self._x_labels) - 1)
len(self._x_labels) - 1)
truncation = reverse_text_len( truncation = reverse_text_len(
available_space, self.style.label_font_size) available_space, self.style.label_font_size
)
truncation = max(truncation, 1) truncation = max(truncation, 1)
lastlabel = self._x_labels[-1][0] lastlabel = self._x_labels[-1][0]
if 0 not in [label[1] for label in self._x_labels]: if 0 not in [label[1] for label in self._x_labels]:
self.svg.node(axis, 'path', self.svg.node(
d='M%f %f v%f' % (0, 0, self.view.height), axis,
class_='line') 'path',
d='M%f %f v%f' % (0, 0, self.view.height),
class_='line'
)
lastlabel = None lastlabel = None
for label, position in self._x_labels: for label, position in self._x_labels:
@ -158,18 +189,18 @@ class Graph(PublicApi):
y = self.view.height + 5 y = self.view.height + 5
last_guide = (self._y_2nd_labels and label == lastlabel) last_guide = (self._y_2nd_labels and label == lastlabel)
self.svg.node( self.svg.node(
guides, 'path', guides,
'path',
d='M%f %f v%f' % (x or 0, 0, self.view.height), d='M%f %f v%f' % (x or 0, 0, self.view.height),
class_='%s%s%sline' % ( class_='%s%s%sline' % (
'axis ' if label == "0" else '', 'axis ' if label == "0" else '', 'major '
'major ' if major else '', if major else '', 'guide '
'guide ' if position != 0 and not last_guide else '')) if position != 0 and not last_guide else ''
)
)
y += .5 * self.style.label_font_size + 5 y += .5 * self.style.label_font_size + 5
text = self.svg.node( text = self.svg.node(
guides, 'text', guides, 'text', x=x, y=y, class_='major' if major else ''
x=x,
y=y,
class_='major' if major else ''
) )
text.text = truncate(label, truncation) text.text = truncate(label, truncation)
@ -177,29 +208,35 @@ class Graph(PublicApi):
self.svg.node(guides, 'title').text = label self.svg.node(guides, 'title').text = label
elif self._dual: elif self._dual:
self.svg.node( self.svg.node(
guides, 'title', guides,
'title',
).text = self._x_format(position) ).text = self._x_format(position)
if self.x_label_rotation: if self.x_label_rotation:
text.attrib['transform'] = "rotate(%d %f %f)" % ( 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: if self.x_label_rotation >= 180:
text.attrib['class'] = ' '.join( text.attrib['class'] = ' '.join((
(text.attrib['class'] and text.attrib['class'].split( text.attrib['class']
' ') or []) + ['backwards']) and text.attrib['class'].split(' ') or []
) + ['backwards'])
if self._y_2nd_labels and 0 not in [ if self._y_2nd_labels and 0 not in [label[1]
label[1] for label in self._x_labels]: for label in self._x_labels]:
self.svg.node(axis, 'path', self.svg.node(
d='M%f %f v%f' % ( axis,
self.view.width, 0, self.view.height), 'path',
class_='line') d='M%f %f v%f' % (self.view.width, 0, self.view.height),
class_='line'
)
if self._x_2nd_labels: if self._x_2nd_labels:
secondary_ax = self.svg.node( secondary_ax = self.svg.node(
self.nodes['plot'], class_="axis x x2%s" % ( self.nodes['plot'],
' always_show' if self.show_x_guides else '' class_="axis x x2%s" %
)) (' always_show' if self.show_x_guides else '')
)
for label, position in self._x_2nd_labels: for label, position in self._x_2nd_labels:
major = label in self._x_labels_major major = label in self._x_labels_major
if not (self.show_minor_x_labels or major): if not (self.show_minor_x_labels or major):
@ -209,37 +246,38 @@ class Graph(PublicApi):
x = self.view.x(position) x = self.view.x(position)
y = -5 y = -5
text = self.svg.node( text = self.svg.node(
guides, 'text', guides, 'text', x=x, y=y, class_='major' if major else ''
x=x,
y=y,
class_='major' if major else ''
) )
text.text = label text.text = label
if self.x_label_rotation: if self.x_label_rotation:
text.attrib['transform'] = "rotate(%d %f %f)" % ( 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: if self.x_label_rotation >= 180:
text.attrib['class'] = ' '.join(( text.attrib['class'] = ' '.join((
text.attrib['class'] and text.attrib['class']
text.attrib['class'].split( and text.attrib['class'].split(' ') or []
' ') or []) + ['backwards']) ) + ['backwards'])
def _y_axis(self): def _y_axis(self):
"""Make the y axis: labels and guides""" """Make the y axis: labels and guides"""
if not self._y_labels or not self.show_y_labels: if not self._y_labels or not self.show_y_labels:
return return
axis = self.svg.node(self.nodes['plot'], class_="axis y%s" % ( axis = self.svg.node(
' always_show' if self.show_y_guides else '' 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 if (0 not in [label[1] for label in self._y_labels]
self.show_y_guides): and self.show_y_guides):
self.svg.node( self.svg.node(
axis, 'path', axis,
'path',
d='M%f %f h%f' % ( d='M%f %f h%f' % (
0, 0 if self.inverse_y_axis else self.view.height, 0, 0 if self.inverse_y_axis else self.view.height,
self.view.width), self.view.width
),
class_='line' class_='line'
) )
@ -251,23 +289,28 @@ class Graph(PublicApi):
if not (self.show_minor_y_labels or major): if not (self.show_minor_y_labels or major):
continue continue
guides = self.svg.node(axis, class_='%sguides' % ( guides = self.svg.node(
'logarithmic ' if self.logarithmic else '' axis,
)) class_='%sguides' %
('logarithmic ' if self.logarithmic else '')
)
x = -5 x = -5
y = self.view.y(position) y = self.view.y(position)
if not y: if not y:
continue continue
if self.show_y_guides: if self.show_y_guides:
self.svg.node( self.svg.node(
guides, 'path', guides,
'path',
d='M%f %f h%f' % (0, y, self.view.width), d='M%f %f h%f' % (0, y, self.view.width),
class_='%s%s%sline' % ( class_='%s%s%sline' % (
'axis ' if label == "0" else '', 'axis ' if label == "0" else '', 'major '
'major ' if major else '', if major else '', 'guide ' if position != 0 else ''
'guide ' if position != 0 else '')) )
)
text = self.svg.node( text = self.svg.node(
guides, 'text', guides,
'text',
x=x, x=x,
y=y + .35 * self.style.label_font_size, y=y + .35 * self.style.label_font_size,
class_='major' if major else '' class_='major' if major else ''
@ -277,18 +320,20 @@ class Graph(PublicApi):
if self.y_label_rotation: if self.y_label_rotation:
text.attrib['transform'] = "rotate(%d %f %f)" % ( 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: if 90 < self.y_label_rotation < 270:
text.attrib['class'] = ' '.join( text.attrib['class'] = ' '.join((
(text.attrib['class'] and text.attrib['class'].split( text.attrib['class']
' ') or []) + ['backwards']) and text.attrib['class'].split(' ') or []
) + ['backwards'])
self.svg.node( self.svg.node(
guides, 'title', guides,
'title',
).text = self._y_format(position) ).text = self._y_format(position)
if self._y_2nd_labels: if self._y_2nd_labels:
secondary_ax = self.svg.node( secondary_ax = self.svg.node(self.nodes['plot'], class_="axis y2")
self.nodes['plot'], class_="axis y2")
for label, position in self._y_2nd_labels: for label, position in self._y_2nd_labels:
major = position in self._y_labels_major major = position in self._y_labels_major
if not (self.show_minor_y_labels or major): if not (self.show_minor_y_labels or major):
@ -298,7 +343,8 @@ class Graph(PublicApi):
x = self.view.width + 5 x = self.view.width + 5
y = self.view.y(position) y = self.view.y(position)
text = self.svg.node( text = self.svg.node(
guides, 'text', guides,
'text',
x=x, x=x,
y=y + .35 * self.style.label_font_size, y=y + .35 * self.style.label_font_size,
class_='major' if major else '' class_='major' if major else ''
@ -306,12 +352,13 @@ class Graph(PublicApi):
text.text = label text.text = label
if self.y_label_rotation: if self.y_label_rotation:
text.attrib['transform'] = "rotate(%d %f %f)" % ( 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: if 90 < self.y_label_rotation < 270:
text.attrib['class'] = ' '.join( text.attrib['class'] = ' '.join((
(text.attrib['class'] and text.attrib['class']
text.attrib['class'].split( and text.attrib['class'].split(' ') or []
' ') or []) + ['backwards']) ) + ['backwards'])
def _legend(self): def _legend(self):
"""Make the legend box""" """Make the legend box"""
@ -320,17 +367,20 @@ class Graph(PublicApi):
truncation = self.truncate_legend truncation = self.truncate_legend
if self.legend_at_bottom: if self.legend_at_bottom:
x = self.margin_box.left + self.spacing x = self.margin_box.left + self.spacing
y = (self.margin_box.top + self.view.height + y = (
self._x_title_height + self.margin_box.top + self.view.height + self._x_title_height +
self._x_labels_height + self.spacing) self._x_labels_height + self.spacing
cols = self.legend_at_bottom_columns or ceil( )
sqrt(self._order)) or 1 cols = self.legend_at_bottom_columns or ceil(sqrt(self._order)
) or 1
if not truncation: if not truncation:
available_space = self.view.width / cols - ( available_space = self.view.width / cols - (
self.legend_box_size + 5) self.legend_box_size + 5
)
truncation = reverse_text_len( truncation = reverse_text_len(
available_space, self.style.legend_font_size) available_space, self.style.legend_font_size
)
else: else:
x = self.spacing x = self.spacing
y = self.margin_box.top + self.spacing y = self.margin_box.top + self.spacing
@ -339,8 +389,10 @@ class Graph(PublicApi):
truncation = 15 truncation = 15
legends = self.svg.node( legends = self.svg.node(
self.nodes['graph'], class_='legends', self.nodes['graph'],
transform='translate(%d, %d)' % (x, y)) class_='legends',
transform='translate(%d, %d)' % (x, y)
)
h = max(self.legend_box_size, self.style.legend_font_size) h = max(self.legend_box_size, self.style.legend_font_size)
x_step = self.view.width / cols x_step = self.view.width / cols
@ -352,22 +404,25 @@ class Graph(PublicApi):
x = self.margin_box.left + self.view.width + self.spacing x = self.margin_box.left + self.view.width + self.spacing
if self._y_2nd_labels: if self._y_2nd_labels:
h, w = get_texts_box( h, w = get_texts_box(
cut(self._y_2nd_labels), self.style.label_font_size) cut(self._y_2nd_labels), self.style.label_font_size
x += self.spacing + max(w * abs(cos(rad( )
self.y_label_rotation))), h) x += self.spacing + max(
w * abs(cos(rad(self.y_label_rotation))), h
)
y = self.margin_box.top + self.spacing y = self.margin_box.top + self.spacing
secondary_legends = self.svg.node( secondary_legends = self.svg.node(
self.nodes['graph'], class_='legends', self.nodes['graph'],
transform='translate(%d, %d)' % (x, y)) class_='legends',
transform='translate(%d, %d)' % (x, y)
)
serie_number = -1 serie_number = -1
i = 0 i = 0
for titles, is_secondary in ( for titles, is_secondary in ((self._legends, False),
(self._legends, False), (self._secondary_legends, True)):
(self._secondary_legends, True)):
if not self.legend_at_bottom and is_secondary: if not self.legend_at_bottom and is_secondary:
i = 0 i = 0
@ -381,9 +436,11 @@ class Graph(PublicApi):
legend = self.svg.node( legend = self.svg.node(
secondary_legends if is_secondary else legends, secondary_legends if is_secondary else legends,
class_='legend reactive activate-serie', class_='legend reactive activate-serie',
id="activate-serie-%d" % serie_number) id="activate-serie-%d" % serie_number
)
self.svg.node( self.svg.node(
legend, 'rect', legend,
'rect',
x=col * x_step, x=col * x_step,
y=1.5 * row * h + ( y=1.5 * row * h + (
self.style.legend_font_size - self.legend_box_size self.style.legend_font_size - self.legend_box_size
@ -403,7 +460,8 @@ class Graph(PublicApi):
truncated = truncate(title, truncation) truncated = truncate(title, truncation)
self.svg.node( self.svg.node(
node, 'text', node,
'text',
x=col * x_step + self.legend_box_size + 5, x=col * x_step + self.legend_box_size + 5,
y=1.5 * row * h + .5 * h + .3 * self.style.legend_font_size y=1.5 * row * h + .5 * h + .3 * self.style.legend_font_size
).text = truncated ).text = truncated
@ -418,19 +476,22 @@ class Graph(PublicApi):
if self._title: if self._title:
for i, title_line in enumerate(self._title, 1): for i, title_line in enumerate(self._title, 1):
self.svg.node( self.svg.node(
self.nodes['title'], 'text', class_='title plot_title', self.nodes['title'],
'text',
class_='title plot_title',
x=self.width / 2, x=self.width / 2,
y=i * (self.style.title_font_size + self.spacing) y=i * (self.style.title_font_size + self.spacing)
).text = title_line ).text = title_line
def _make_x_title(self): def _make_x_title(self):
"""Make the X-Axis title""" """Make the X-Axis title"""
y = (self.height - self.margin_box.bottom + y = (self.height - self.margin_box.bottom + self._x_labels_height)
self._x_labels_height)
if self._x_title: if self._x_title:
for i, title_line in enumerate(self._x_title, 1): for i, title_line in enumerate(self._x_title, 1):
text = self.svg.node( 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, x=self.margin_box.left + self.view.width / 2,
y=y + i * (self.style.title_font_size + self.spacing) 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 yc = self.margin_box.top + self.view.height / 2
for i, title_line in enumerate(self._y_title, 1): for i, title_line in enumerate(self._y_title, 1):
text = self.svg.node( text = self.svg.node(
self.nodes['title'], 'text', class_='title', self.nodes['title'],
'text',
class_='title',
x=self._legend_at_left_width, x=self._legend_at_left_width,
y=i * (self.style.title_font_size + self.spacing) + yc y=i * (self.style.title_font_size + self.spacing) + yc
) )
text.attrib['transform'] = "rotate(%d %f %f)" % ( 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 text.text = title_line
def _interpolate(self, xs, ys): def _interpolate(self, xs, ys):
@ -461,16 +525,19 @@ class Graph(PublicApi):
interpolate = INTERPOLATIONS[self.interpolate] interpolate = INTERPOLATIONS[self.interpolate]
return list(interpolate( return list(
x, y, self.interpolation_precision, interpolate(
**self.interpolation_parameters)) x, y, self.interpolation_precision,
**self.interpolation_parameters
)
)
def _rescale(self, points): def _rescale(self, points):
"""Scale for secondary""" """Scale for secondary"""
return [ return [(
(x, self._scale_diff + (y - self._scale_min_2nd) * self._scale x, self._scale_diff + (y - self._scale_min_2nd) * self._scale
if y is not None else None) if y is not None else None
for x, y in points] ) for x, y in points]
def _tooltip_data(self, node, value, x, y, classes=None, xlabel=None): def _tooltip_data(self, node, value, x, y, classes=None, xlabel=None):
"""Insert in desc tags informations for the javascript tooltip""" """Insert in desc tags informations for the javascript tooltip"""
@ -483,16 +550,21 @@ class Graph(PublicApi):
classes.append('top') classes.append('top')
classes = ' '.join(classes) classes = ' '.join(classes)
self.svg.node(node, 'desc', self.svg.node(node, 'desc', class_="x " + classes).text = to_str(x)
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_="y " + classes).text = to_str(y)
if xlabel: if xlabel:
self.svg.node(node, 'desc', self.svg.node(node, 'desc', class_="x_label").text = to_str(xlabel)
class_="x_label").text = to_str(xlabel)
def _static_value(
def _static_value(self, serie_node, value, x, y, metadata, self,
align_text='left', classes=None): serie_node,
value,
x,
y,
metadata,
align_text='left',
classes=None
):
"""Write the print value""" """Write the print value"""
label = metadata and metadata.get('label') label = metadata and metadata.get('label')
classes = classes and [classes] or [] classes = classes and [classes] or []
@ -502,7 +574,8 @@ class Graph(PublicApi):
if self.print_values: if self.print_values:
y -= self.style.value_font_size / 2 y -= self.style.value_font_size / 2
self.svg.node( self.svg.node(
serie_node['text_overlay'], 'text', serie_node['text_overlay'],
'text',
class_=' '.join(label_cls), class_=' '.join(label_cls),
x=x, x=x,
y=y + self.style.value_font_size / 3 y=y + self.style.value_font_size / 3
@ -515,11 +588,14 @@ class Graph(PublicApi):
val_cls.append('showable') val_cls.append('showable')
self.svg.node( self.svg.node(
serie_node['text_overlay'], 'text', serie_node['text_overlay'],
'text',
class_=' '.join(val_cls), class_=' '.join(val_cls),
x=x, x=x,
y=y + self.style.value_font_size / 3, 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 '' ).text = value if self.print_zeroes or value != '0' else ''
def _points(self, x_pos): def _points(self, x_pos):
@ -528,9 +604,7 @@ class Graph(PublicApi):
and interpolated points if interpolate option is specified and interpolated points if interpolate option is specified
""" """
for serie in self.all_series: for serie in self.all_series:
serie.points = [ serie.points = [(x_pos[i], v) for i, v in enumerate(serie.values)]
(x_pos[i], v)
for i, v in enumerate(serie.values)]
if serie.points and self.interpolate: if serie.points and self.interpolate:
serie.interpolated = self._interpolate(x_pos, serie.values) serie.interpolated = self._interpolate(x_pos, serie.values)
else: else:
@ -600,33 +674,18 @@ class Graph(PublicApi):
value = serie.values[i] value = serie.values[i]
metadata = serie.metadata.get(i) metadata = serie.metadata.get(i)
kwargs = { kwargs = {'chart': self, 'serie': serie, 'index': i}
'chart': self, formatter = ((metadata and metadata.get('formatter'))
'serie': serie, or serie.formatter or self.formatter
'index': i or self._value_format)
}
formatter = (
(metadata and metadata.get('formatter')) or
serie.formatter or
self.formatter or
self._value_format
)
kwargs = filter_kwargs(formatter, kwargs) kwargs = filter_kwargs(formatter, kwargs)
return formatter(value, **kwargs) return formatter(value, **kwargs)
def _serie_format(self, serie, value): def _serie_format(self, serie, value):
"""Format an independent value for the serie""" """Format an independent value for the serie"""
kwargs = { kwargs = {'chart': self, 'serie': serie, 'index': None}
'chart': self, formatter = (serie.formatter or self.formatter or self._value_format)
'serie': serie,
'index': None
}
formatter = (
serie.formatter or
self.formatter or
self._value_format
)
kwargs = filter_kwargs(formatter, kwargs) kwargs = filter_kwargs(formatter, kwargs)
return formatter(value, **kwargs) return formatter(value, **kwargs)
@ -639,18 +698,24 @@ class Graph(PublicApi):
for series_group in (self.series, self.secondary_series): for series_group in (self.series, self.secondary_series):
if self.show_legend and series_group: if self.show_legend and series_group:
h, w = get_texts_box( h, w = get_texts_box(
map(lambda x: truncate(x, self.truncate_legend or 15), map(
[serie.title['title'] lambda x: truncate(x, self.truncate_legend or 15), [
if isinstance(serie.title, dict) serie.title['title']
else serie.title or '' for serie in series_group]), if isinstance(serie.title, dict) else serie.title
self.style.legend_font_size) or '' for serie in series_group
]
), self.style.legend_font_size
)
if self.legend_at_bottom: if self.legend_at_bottom:
h_max = max(h, self.legend_box_size) h_max = max(h, self.legend_box_size)
cols = (self._order // self.legend_at_bottom_columns cols = (
if self.legend_at_bottom_columns self._order // self.legend_at_bottom_columns
else ceil(sqrt(self._order)) or 1) if self.legend_at_bottom_columns else
ceil(sqrt(self._order)) or 1
)
self.margin_box.bottom += self.spacing + h_max * round( self.margin_box.bottom += self.spacing + h_max * round(
cols - 1) * 1.5 + h_max cols - 1
) * 1.5 + h_max
else: else:
if series_group is self.series: if series_group is self.series:
legend_width = self.spacing + w + self.legend_box_size legend_width = self.spacing + w + self.legend_box_size
@ -658,18 +723,22 @@ class Graph(PublicApi):
self._legend_at_left_width += legend_width self._legend_at_left_width += legend_width
else: else:
self.margin_box.right += ( self.margin_box.right += (
self.spacing + w + self.legend_box_size) self.spacing + w + self.legend_box_size
)
self._x_labels_height = 0 self._x_labels_height = 0
if (self._x_labels or self._x_2nd_labels) and self.show_x_labels: if (self._x_labels or self._x_2nd_labels) and self.show_x_labels:
for xlabels in (self._x_labels, self._x_2nd_labels): for xlabels in (self._x_labels, self._x_2nd_labels):
if xlabels: if xlabels:
h, w = get_texts_box( h, w = get_texts_box(
map(lambda x: truncate(x, self.truncate_label or 25), map(
cut(xlabels)), lambda x: truncate(x, self.truncate_label or 25),
self.style.label_font_size) cut(xlabels)
), self.style.label_font_size
)
self._x_labels_height = self.spacing + max( 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: if xlabels is self._x_labels:
self.margin_box.bottom += self._x_labels_height self.margin_box.bottom += self._x_labels_height
else: else:
@ -678,26 +747,32 @@ class Graph(PublicApi):
if self.x_label_rotation % 180 < 90: if self.x_label_rotation % 180 < 90:
self.margin_box.right = max( self.margin_box.right = max(
w * abs(cos(rad(self.x_label_rotation))), w * abs(cos(rad(self.x_label_rotation))),
self.margin_box.right) self.margin_box.right
)
else: else:
self.margin_box.left = max( self.margin_box.left = max(
w * abs(cos(rad(self.x_label_rotation))), w * abs(cos(rad(self.x_label_rotation))),
self.margin_box.left) self.margin_box.left
)
if self.show_y_labels: if self.show_y_labels:
for ylabels in (self._y_labels, self._y_2nd_labels): for ylabels in (self._y_labels, self._y_2nd_labels):
if ylabels: if ylabels:
h, w = get_texts_box( 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: if ylabels is self._y_labels:
self.margin_box.left += self.spacing + max( 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: else:
self.margin_box.right += self.spacing + max( 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 = split_title(
self.title, self.width, self.style.title_font_size) self.title, self.width, self.style.title_font_size
)
if self.title: if self.title:
h, _ = get_text_box(self._title[0], self.style.title_font_size) 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 = split_title(
self.x_title, self.width - self.margin_box.x, self.x_title, self.width - self.margin_box.x,
self.style.title_font_size) self.style.title_font_size
)
self._x_title_height = 0 self._x_title_height = 0
if self._x_title: if self._x_title:
@ -716,7 +792,8 @@ class Graph(PublicApi):
self._y_title = split_title( self._y_title = split_title(
self.y_title, self.height - self.margin_box.y, self.y_title, self.height - self.margin_box.y,
self.style.title_font_size) self.style.title_font_size
)
self._y_title_height = 0 self._y_title_height = 0
if self._y_title: if self._y_title:
@ -741,15 +818,16 @@ class Graph(PublicApi):
ci['point_estimate'] = value ci['point_estimate'] = value
low, high = getattr( low, high = getattr(
stats, stats, 'confidence_interval_%s' % ci.get('type', 'manual')
'confidence_interval_%s' % ci.get('type', 'manual')
)(**ci) )(**ci)
self.svg.confidence_interval( self.svg.confidence_interval(
node, x, node,
x,
# Respect some charts y modifications (pyramid, stackbar) # Respect some charts y modifications (pyramid, stackbar)
y + (self.view.y(low) - self.view.y(value)), 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 @cached_property
def _legends(self): def _legends(self):
@ -764,54 +842,59 @@ class Graph(PublicApi):
@cached_property @cached_property
def _values(self): def _values(self):
"""Getter for series values (flattened)""" """Getter for series values (flattened)"""
return [val return [
for serie in self.series val for serie in self.series for val in serie.values
for val in serie.values if val is not None
if val is not None] ]
@cached_property @cached_property
def _secondary_values(self): def _secondary_values(self):
"""Getter for secondary series values (flattened)""" """Getter for secondary series values (flattened)"""
return [val return [
for serie in self.secondary_series val for serie in self.secondary_series for val in serie.values
for val in serie.values if val is not None
if val is not None] ]
@cached_property @cached_property
def _len(self): def _len(self):
"""Getter for the maximum series size""" """Getter for the maximum series size"""
return max([ return max([len(serie.values) for serie in self.all_series] or [0])
len(serie.values)
for serie in self.all_series] or [0])
@cached_property @cached_property
def _secondary_min(self): def _secondary_min(self):
"""Getter for the minimum series value""" """Getter for the minimum series value"""
return (self.secondary_range[0] if ( return (
self.secondary_range and self.secondary_range[0] is not None) self.secondary_range[0]
else (min(self._secondary_values) if (self.secondary_range
if self._secondary_values else None)) and self.secondary_range[0] is not None) else
(min(self._secondary_values) if self._secondary_values else None)
)
@cached_property @cached_property
def _min(self): def _min(self):
"""Getter for the minimum series value""" """Getter for the minimum series value"""
return (self.range[0] if (self.range and self.range[0] is not None) return (
else (min(self._values) self.range[0] if (self.range and self.range[0] is not None) else
if self._values else None)) (min(self._values) if self._values else None)
)
@cached_property @cached_property
def _max(self): def _max(self):
"""Getter for the maximum series value""" """Getter for the maximum series value"""
return (self.range[1] if (self.range and self.range[1] is not None) return (
else (max(self._values) if self._values else None)) self.range[1] if (self.range and self.range[1] is not None) else
(max(self._values) if self._values else None)
)
@cached_property @cached_property
def _secondary_max(self): def _secondary_max(self):
"""Getter for the maximum series value""" """Getter for the maximum series value"""
return (self.secondary_range[1] if ( return (
self.secondary_range and self.secondary_range[1] is not None) self.secondary_range[1]
else (max(self._secondary_values) if (self.secondary_range
if self._secondary_values else None)) and self.secondary_range[1] is not None) else
(max(self._secondary_values) if self._secondary_values else None)
)
@cached_property @cached_property
def _order(self): def _order(self):
@ -825,13 +908,18 @@ class Graph(PublicApi):
def _compute_x_labels(self): def _compute_x_labels(self):
self._x_labels = self.x_labels and list( self._x_labels = self.x_labels and list(
zip(map(self._x_label_format_if_value, self.x_labels), zip(
self._x_pos)) map(self._x_label_format_if_value, self.x_labels), self._x_pos
)
)
def _compute_x_labels_major(self): def _compute_x_labels_major(self):
if self.x_labels_major_every: if self.x_labels_major_every:
self._x_labels_major = [self._x_labels[i][0] for i in range( self._x_labels_major = [
0, len(self._x_labels), self.x_labels_major_every)] 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: elif self.x_labels_major_count:
label_count = len(self._x_labels) 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] self._x_labels_major = [label[0] for label in self._x_labels]
else: else:
self._x_labels_major = [self._x_labels[ self._x_labels_major = [
int(i * (label_count - 1) / (major_count - 1))][0] self._x_labels[int(
for i in range(major_count)] i * (label_count - 1) / (major_count - 1)
)][0] for i in range(major_count)
]
else: else:
self._x_labels_major = self.x_labels_major and list( 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): def _compute_y_labels(self):
y_pos = compute_scale( y_pos = compute_scale(
self._box.ymin, self._box.ymax, self.logarithmic, self._box.ymin, self._box.ymax, self.logarithmic, self.order_min,
self.order_min, self.min_scale, self.max_scale self.min_scale, self.max_scale
) )
if self.y_labels: if self.y_labels:
self._y_labels = [] self._y_labels = []
@ -872,8 +963,11 @@ class Graph(PublicApi):
def _compute_y_labels_major(self): def _compute_y_labels_major(self):
if self.y_labels_major_every: if self.y_labels_major_every:
self._y_labels_major = [self._y_labels[i][1] for i in range( self._y_labels_major = [
0, len(self._y_labels), self.y_labels_major_every)] 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: elif self.y_labels_major_count:
label_count = len(self._y_labels) label_count = len(self._y_labels)
@ -881,9 +975,11 @@ class Graph(PublicApi):
if (major_count >= label_count): if (major_count >= label_count):
self._y_labels_major = [label[1] for label in self._y_labels] self._y_labels_major = [label[1] for label in self._y_labels]
else: else:
self._y_labels_major = [self._y_labels[ self._y_labels_major = [
int(i * (label_count - 1) / (major_count - 1))][1] self._y_labels[int(
for i in range(major_count)] i * (label_count - 1) / (major_count - 1)
)][1] for i in range(major_count)
]
elif self.y_labels_major: elif self.y_labels_major:
self._y_labels_major = list(map(self._adapt, 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): for line in range(x_lines):
_current_x += (self.width - self.margin_box.x) / squares[0] _current_x += (self.width - self.margin_box.x) / squares[0]
self.svg.node( self.svg.node(
self.nodes['plot'], 'path', self.nodes['plot'],
'path',
class_='bg-lines', class_='bg-lines',
d='M%s %s L%s %s' % ( d='M%s %s L%s %s' %
_current_x, 0, _current_x, (_current_x, 0, _current_x, self.height - self.margin_box.y)
self.height - self.margin_box.y)) )
for line in range(y_lines): for line in range(y_lines):
_current_y += (self.height - self.margin_box.y) / squares[1] _current_y += (self.height - self.margin_box.y) / squares[1]
self.svg.node( self.svg.node(
self.nodes['plot'], 'path', self.nodes['plot'],
'path',
class_='bg-lines', class_='bg-lines',
d='M%s %s L%s %s' % ( d='M%s %s L%s %s' %
0, _current_y, self.width - self.margin_box.x, _current_y)) (0, _current_y, self.width - self.margin_box.x, _current_y)
)
return ((self.width - self.margin_box.x) / squares[0], return ((self.width - self.margin_box.x) / squares[0],
(self.height - self.margin_box.y) / squares[1]) (self.height - self.margin_box.y) / squares[1])
@ -938,8 +1037,8 @@ class Graph(PublicApi):
"""Check if there is any data""" """Check if there is any data"""
return any([ return any([
len([ len([
v for a in (s[0] if is_list_like(s) else [s]) v
for v in (a if is_list_like(a) else [a]) for a in (s[0] if is_list_like(s) else [s])
if v is not None]) for v in (a if is_list_like(a) else [a]) if v is not None
for s in self.raw_series ]) 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): class Histogram(Dual, Bar):
"""Histogram chart class""" """Histogram chart class"""
_series_margin = 0 _series_margin = 0
@ -41,27 +40,27 @@ class Histogram(Dual, Bar):
@cached_property @cached_property
def _secondary_values(self): def _secondary_values(self):
"""Getter for secondary series values (flattened)""" """Getter for secondary series values (flattened)"""
return [val[0] return [
for serie in self.secondary_series val[0] for serie in self.secondary_series for val in serie.values
for val in serie.values if val[0] is not None
if val[0] is not None] ]
@cached_property @cached_property
def xvals(self): def xvals(self):
"""All x values""" """All x values"""
return [val return [
for serie in self.all_series val
for dval in serie.values for serie in self.all_series for dval in serie.values
for val in dval[1:3] for val in dval[1:3] if val is not None
if val is not None] ]
@cached_property @cached_property
def yvals(self): def yvals(self):
"""All y values""" """All y values"""
return [val[0] return [
for serie in self.series val[0] for serie in self.series for val in serie.values
for val in serie.values if val[0] is not None
if val[0] is not None] ]
def _bar(self, serie, parent, x0, x1, y, i, zero, secondary=False): def _bar(self, serie, parent, x0, x1, y, i, zero, secondary=False):
"""Internal bar drawing function""" """Internal bar drawing function"""
@ -74,10 +73,19 @@ class Histogram(Dual, Bar):
width -= 2 * series_margin width -= 2 * series_margin
r = serie.rounded_bars * 1 if serie.rounded_bars else 0 r = serie.rounded_bars * 1 if serie.rounded_bars else 0
alter(self.svg.transposable_node( alter(
parent, 'rect', self.svg.transposable_node(
x=x, y=y, rx=r, ry=r, width=width, height=height, parent,
class_='rect reactive tooltip-trigger'), serie.metadata.get(i)) '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 return x, y, width, height
def bar(self, serie, rescale=False): def bar(self, serie, rescale=False):
@ -92,15 +100,16 @@ class Histogram(Dual, Bar):
metadata = serie.metadata.get(i) metadata = serie.metadata.get(i)
bar = decorate( bar = decorate(
self.svg, self.svg, self.svg.node(bars, class_='histbar'), metadata
self.svg.node(bars, class_='histbar'), )
metadata)
val = self._format(serie, i) val = self._format(serie, i)
bounds = self._bar( 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( self._tooltip_and_print_values(
serie_node, serie, bar, i, val, metadata, *bounds) serie_node, serie, bar, i, val, metadata, *bounds
)
def _compute(self): def _compute(self):
"""Compute x/y min and max and x/y scale and set labels""" """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): class HorizontalGraph(Graph):
"""Horizontal graph mixin""" """Horizontal graph mixin"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -35,11 +34,14 @@ class HorizontalGraph(Graph):
"""After computations transpose labels""" """After computations transpose labels"""
self._x_labels, self._y_labels = self._y_labels, self._x_labels self._x_labels, self._y_labels = self._y_labels, self._x_labels
self._x_labels_major, self._y_labels_major = ( 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._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_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): def _axes(self):
"""Set the _force_vertical flag when rendering axes""" """Set the _force_vertical flag when rendering axes"""
@ -55,9 +57,9 @@ class HorizontalGraph(Graph):
view_class = HorizontalView view_class = HorizontalView
self.view = view_class( self.view = view_class(
self.width - self.margin_box.x, self.width - self.margin_box.x, self.height - self.margin_box.y,
self.height - self.margin_box.y, self._box
self._box) )
def _get_x_label(self, i): def _get_x_label(self, i):
"""Convenience function to get the x_label of a value index""" """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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Horizontal bar graph""" """Horizontal bar graph"""
from pygal.graph.bar import Bar from pygal.graph.bar import Bar
@ -24,7 +23,6 @@ from pygal.graph.horizontal import HorizontalGraph
class HorizontalBar(HorizontalGraph, Bar): class HorizontalBar(HorizontalGraph, Bar):
"""Horizontal Bar graph""" """Horizontal Bar graph"""
def _plot(self): 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Horizontal line graph""" """Horizontal line graph"""
from pygal.graph.horizontal import HorizontalGraph from pygal.graph.horizontal import HorizontalGraph
@ -24,7 +23,6 @@ from pygal.graph.line import Line
class HorizontalLine(HorizontalGraph, Line): class HorizontalLine(HorizontalGraph, Line):
"""Horizontal Line graph""" """Horizontal Line graph"""
def _plot(self): 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Horizontal stacked graph""" """Horizontal stacked graph"""
from pygal.graph.horizontal import HorizontalGraph from pygal.graph.horizontal import HorizontalGraph
@ -24,5 +23,4 @@ from pygal.graph.stackedbar import StackedBar
class HorizontalStackedBar(HorizontalGraph, StackedBar): class HorizontalStackedBar(HorizontalGraph, StackedBar):
"""Horizontal Stacked Bar graph""" """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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Horizontal Stacked Line graph""" """Horizontal Stacked Line graph"""
from pygal.graph.horizontal import HorizontalGraph from pygal.graph.horizontal import HorizontalGraph
@ -24,7 +23,6 @@ from pygal.graph.stackedline import StackedLine
class HorizontalStackedLine(HorizontalGraph, StackedLine): class HorizontalStackedLine(HorizontalGraph, StackedLine):
"""Horizontal Stacked Line graph""" """Horizontal Stacked Line graph"""
def _plot(self): 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
""" """
Line chart: Display series of data as markers (dots) Line chart: Display series of data as markers (dots)
connected by straight segments connected by straight segments
@ -29,7 +28,6 @@ from pygal.util import alter, cached_property, decorate
class Line(Graph): class Line(Graph):
"""Line graph class""" """Line graph class"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -42,20 +40,20 @@ class Line(Graph):
"""Getter for series values (flattened)""" """Getter for series values (flattened)"""
return [ return [
val[1] val[1]
for serie in self.series for serie in self.series for val in
for val in (serie.interpolated (serie.interpolated if self.interpolate else serie.points)
if self.interpolate else serie.points) if val[1] is not None and (not self.logarithmic or val[1] > 0)
if val[1] is not None and (not self.logarithmic or val[1] > 0)] ]
@cached_property @cached_property
def _secondary_values(self): def _secondary_values(self):
"""Getter for secondary series values (flattened)""" """Getter for secondary series values (flattened)"""
return [ return [
val[1] val[1]
for serie in self.secondary_series for serie in self.secondary_series for val in
for val in (serie.interpolated (serie.interpolated if self.interpolate else serie.points)
if self.interpolate else serie.points) if val[1] is not None and (not self.logarithmic or val[1] > 0)
if val[1] is not None and (not self.logarithmic or val[1] > 0)] ]
def _fill(self, values): def _fill(self, values):
"""Add extra values to fill the line""" """Add extra values to fill the line"""
@ -80,12 +78,12 @@ class Line(Graph):
"Invalid value ({}) for config key " "Invalid value ({}) for config key "
"'missing_value_fill_truncation';" "'missing_value_fill_truncation';"
" Use 'x', 'y' or 'either'".format( " Use 'x', 'y' or 'either'".format(
self.missing_value_fill_truncation)) self.missing_value_fill_truncation
)
)
end -= 1 end -= 1
return ([(values[0][0], zero)] + return ([(values[0][0], zero)] + values + [(values[end][0], zero)])
values +
[(values[end][0], zero)])
def line(self, serie, rescale=False): def line(self, serie, rescale=False):
"""Draw the line serie""" """Draw the line serie"""
@ -102,9 +100,9 @@ class Line(Graph):
if self.logarithmic: if self.logarithmic:
if points[i][1] is None or points[i][1] <= 0: if points[i][1] is None or points[i][1] <= 0:
continue continue
if (serie.show_only_major_dots and if (serie.show_only_major_dots and self.x_labels
self.x_labels and i < len(self.x_labels) and and i < len(self.x_labels)
self.x_labels[i] not in self._x_labels_major): and self.x_labels[i] not in self._x_labels_major):
continue continue
metadata = serie.metadata.get(i) metadata = serie.metadata.get(i)
@ -116,25 +114,33 @@ class Line(Graph):
classes = ' '.join(classes) classes = ' '.join(classes)
self._confidence_interval( self._confidence_interval(
serie_node['overlay'], x, y, serie.values[i], metadata) serie_node['overlay'], x, y, serie.values[i], metadata
)
dots = decorate( dots = decorate(
self.svg, self.svg,
self.svg.node(serie_node['overlay'], class_="dots"), self.svg.node(serie_node['overlay'], class_="dots"),
metadata) metadata
)
val = self._format(serie, i) val = self._format(serie, i)
alter(self.svg.transposable_node( alter(
dots, 'circle', cx=x, cy=y, r=serie.dots_size, self.svg.transposable_node(
class_='dot reactive tooltip-trigger'), metadata) dots,
'circle',
cx=x,
cy=y,
r=serie.dots_size,
class_='dot reactive tooltip-trigger'
), metadata
)
self._tooltip_data( self._tooltip_data(
dots, val, x, y, dots, val, x, y, xlabel=self._get_x_label(i)
xlabel=self._get_x_label(i)) )
self._static_value( self._static_value(
serie_node, val, serie_node, val, x + self.style.value_font_size,
x + self.style.value_font_size, y + self.style.value_font_size, metadata
y + self.style.value_font_size, )
metadata)
if serie.stroke: if serie.stroke:
if self.interpolate: if self.interpolate:
@ -157,12 +163,12 @@ class Line(Graph):
# emit current subsequence # emit current subsequence
sequences.append(cur_sequence) sequences.append(cur_sequence)
cur_sequence = [] cur_sequence = []
elif y is None: # just discard elif y is None: # just discard
continue continue
else: 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) sequences.append(cur_sequence)
else: else:
# plain vanilla rendering # plain vanilla rendering
@ -175,9 +181,12 @@ class Line(Graph):
del seq[seq.index(ele)] del seq[seq.index(ele)]
for seq in sequences: for seq in sequences:
self.svg.line( self.svg.line(
serie_node['plot'], seq, close=self._self_close, serie_node['plot'],
seq,
close=self._self_close,
class_='line reactive' + class_='line reactive' +
(' nofill' if not serie.fill else '')) (' nofill' if not serie.fill else '')
)
def _compute(self): def _compute(self):
"""Compute y min and max and y scale and set labels""" """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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
""" """
pygal contains no map but a base class to create extension pygal contains no map but a base class to create extension
see the pygal_maps_world package to get an exemple. 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): class BaseMap(Graph):
"""Base class for maps""" """Base class for maps"""
_dual = True _dual = True
@ -39,10 +37,10 @@ class BaseMap(Graph):
@cached_property @cached_property
def _values(self): def _values(self):
"""Getter for series values (flattened)""" """Getter for series values (flattened)"""
return [val[1] return [
for serie in self.series val[1] for serie in self.series for val in serie.values
for val in serie.values if val[1] is not None
if val[1] is not None] ]
def enumerate_values(self, serie): def enumerate_values(self, serie):
"""Hook to replace default enumeration on values""" """Hook to replace default enumeration on values"""
@ -58,7 +56,8 @@ class BaseMap(Graph):
""" """
return '%s: %s' % ( return '%s: %s' % (
self.area_names.get(self.adapt_code(value[0]), '?'), self.area_names.get(self.adapt_code(value[0]), '?'),
self._y_format(value[1])) self._y_format(value[1])
)
def _plot(self): def _plot(self):
"""Insert a map in the chart and apply data on it""" """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)) map.set('height', str(self.view.height))
for i, serie in enumerate(self.series): for i, serie in enumerate(self.series):
safe_vals = list(filter( safe_vals = list(
lambda x: x is not None, cut(serie.values, 1))) filter(lambda x: x is not None, cut(serie.values, 1))
)
if not safe_vals: if not safe_vals:
continue continue
min_ = min(safe_vals) min_ = min(safe_vals)
@ -83,9 +83,9 @@ class BaseMap(Graph):
ratio = .3 + .7 * (value - min_) / (max_ - min_) ratio = .3 + .7 * (value - min_) / (max_ - min_)
areae = map.findall( areae = map.findall(
".//*[@class='%s%s %s map-element']" % ( ".//*[@class='%s%s %s map-element']" %
self.area_prefix, area_code, (self.area_prefix, area_code, self.kind)
self.kind)) )
if not areae: if not areae:
continue continue

24
pygal/graph/pie.py

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

20
pygal/graph/public.py

@ -26,7 +26,6 @@ from pygal.graph.base import BaseGraph
class PublicApi(BaseGraph): class PublicApi(BaseGraph):
"""Chart public functions""" """Chart public functions"""
def add(self, title, values, **kwargs): def add(self, title, values, **kwargs):
@ -51,7 +50,8 @@ class PublicApi(BaseGraph):
"""Render the graph, and return the svg string""" """Render the graph, and return the svg string"""
self.setup(**kwargs) self.setup(**kwargs)
svg = self.svg.render( svg = self.svg.render(
is_unicode=is_unicode, pretty_print=self.pretty_print) is_unicode=is_unicode, pretty_print=self.pretty_print
)
self.teardown() self.teardown()
return svg return svg
@ -96,16 +96,16 @@ class PublicApi(BaseGraph):
"""Render the graph, and return a Django response""" """Render the graph, and return a Django response"""
from django.http import HttpResponse from django.http import HttpResponse
return 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): def render_data_uri(self, **kwargs):
"""Output a base 64 encoded data uri""" """Output a base 64 encoded data uri"""
# Force protocol as data uri have none # Force protocol as data uri have none
kwargs.setdefault('force_uri_protocol', 'https') kwargs.setdefault('force_uri_protocol', 'https')
return "data:image/svg+xml;charset=utf-8;base64,%s" % ( return "data:image/svg+xml;charset=utf-8;base64,%s" % (
base64.b64encode( base64.b64encode(self.render(**kwargs)
self.render(**kwargs) ).decode('utf-8').replace('\n', '')
).decode('utf-8').replace('\n', '')
) )
def render_to_file(self, filename, **kwargs): 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""" """Render the graph, convert it to png and write it to filename"""
import cairosvg import cairosvg
return cairosvg.svg2png( 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): def render_sparktext(self, relative_to=None):
"""Make a mini text sparkline from chart""" """Make a mini text sparkline from chart"""
@ -141,8 +142,9 @@ class PublicApi(BaseGraph):
divisions = len(bars) - 1 divisions = len(bars) - 1
for value in values: for value in values:
chart += bars[int(divisions * chart += bars[int(
(value - relative_to) / (vmax - relative_to))] divisions * (value - relative_to) / (vmax - relative_to)
)]
return chart return chart
def render_sparkline(self, **kwargs): 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
""" """
Pyramid chart: Stacked bar chart containing only positive values divided by two Pyramid chart: Stacked bar chart containing only positive values divided by two
axes, generally gender for age pyramid. axes, generally gender for age pyramid.
@ -30,7 +29,6 @@ from pygal.graph.stackedbar import StackedBar
class VerticalPyramid(StackedBar): class VerticalPyramid(StackedBar):
"""Vertical Pyramid graph class""" """Vertical Pyramid graph class"""
_adapters = [positive] _adapters = [positive]
@ -42,26 +40,37 @@ class VerticalPyramid(StackedBar):
def _get_separated_values(self, secondary=False): def _get_separated_values(self, secondary=False):
"""Separate values between odd and even series stacked""" """Separate values between odd and even series stacked"""
series = self.secondary_series if secondary else self.series series = self.secondary_series if secondary else self.series
positive_vals = map(sum, zip( positive_vals = map(
*[serie.safe_values sum,
for index, serie in enumerate(series) zip(
if index % 2])) *[
negative_vals = map(sum, zip( serie.safe_values for index, serie in enumerate(series)
*[serie.safe_values if index % 2
for index, serie in enumerate(series) ]
if not 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) return list(positive_vals), list(negative_vals)
def _compute_box(self, positive_vals, negative_vals): def _compute_box(self, positive_vals, negative_vals):
"""Compute Y min and max""" """Compute Y min and max"""
max_ = max( max_ = max(
max(positive_vals or [self.zero]), 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: if self.range and self.range[0] is not None:
self._box.ymin = self.range[0] self._box.ymin = self.range[0]
else: else:
self._box.ymin = - max_ self._box.ymin = -max_
if self.range and self.range[1] is not None: if self.range and self.range[1] is not None:
self._box.ymax = self.range[1] self._box.ymax = self.range[1]
@ -71,16 +80,15 @@ class VerticalPyramid(StackedBar):
def _pre_compute_secondary(self, positive_vals, negative_vals): def _pre_compute_secondary(self, positive_vals, negative_vals):
"""Compute secondary y min and max""" """Compute secondary y min and max"""
self._secondary_max = max(max(positive_vals), max(negative_vals)) 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): def _bar(self, serie, parent, x, y, i, zero, secondary=False):
"""Internal stacking bar drawing function""" """Internal stacking bar drawing function"""
if serie.index % 2: if serie.index % 2:
y = -y y = -y
return super(VerticalPyramid, self)._bar( return super(VerticalPyramid,
serie, parent, x, y, i, zero, secondary) self)._bar(serie, parent, x, y, i, zero, secondary)
class Pyramid(HorizontalGraph, VerticalPyramid): class Pyramid(HorizontalGraph, VerticalPyramid):
"""Horizontal Pyramid graph class like the one used by age pyramid""" """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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # 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 Radar chart: As known as kiviat chart or spider chart is a polar line chart
useful for multivariate observation. useful for multivariate observation.
@ -34,7 +33,6 @@ from pygal.view import PolarLogView, PolarView
class Radar(Line): class Radar(Line):
"""Rada graph class""" """Rada graph class"""
_adapters = [positive, none_to_zero] _adapters = [positive, none_to_zero]
@ -52,8 +50,9 @@ class Radar(Line):
def _values(self): def _values(self):
"""Getter for series values (flattened)""" """Getter for series values (flattened)"""
if self.interpolate: if self.interpolate:
return [val[0] for serie in self.series return [
for val in serie.interpolated] val[0] for serie in self.series for val in serie.interpolated
]
else: else:
return super(Line, self)._values return super(Line, self)._values
@ -65,18 +64,20 @@ class Radar(Line):
view_class = PolarView view_class = PolarView
self.view = view_class( self.view = view_class(
self.width - self.margin_box.x, self.width - self.margin_box.x, self.height - self.margin_box.y,
self.height - self.margin_box.y, self._box
self._box) )
def _x_axis(self, draw_axes=True): def _x_axis(self, draw_axes=True):
"""Override x axis to make it polar""" """Override x axis to make it polar"""
if not self._x_labels or not self.show_x_labels: if not self._x_labels or not self.show_x_labels:
return return
axis = self.svg.node(self.nodes['plot'], class_="axis x web%s" % ( axis = self.svg.node(
' always_show' if self.show_x_guides else '' self.nodes['plot'],
)) class_="axis x web%s" %
(' always_show' if self.show_x_guides else '')
)
format_ = lambda x: '%f %f' % x format_ = lambda x: '%f %f' % x
center = self.view((0, 0)) center = self.view((0, 0))
r = self._rmax r = self._rmax
@ -92,32 +93,37 @@ class Radar(Line):
end = self.view((r, theta)) end = self.view((r, theta))
self.svg.node( self.svg.node(
guides, 'path', guides,
'path',
d='M%s L%s' % (format_(center), format_(end)), d='M%s L%s' % (format_(center), format_(end)),
class_='%s%sline' % ( class_='%s%sline' %
'axis ' if label == "0" else '', ('axis ' if label == "0" else '', 'major ' if major else '')
'major ' if major else '')) )
r_txt = (1 - self._box.__class__.margin) * self._box.ymax r_txt = (1 - self._box.__class__.margin) * self._box.ymax
pos_text = self.view((r_txt, theta)) pos_text = self.view((r_txt, theta))
text = self.svg.node( text = self.svg.node(
guides, 'text', guides,
'text',
x=pos_text[0], x=pos_text[0],
y=pos_text[1], y=pos_text[1],
class_='major' if major else '') class_='major' if major else ''
)
text.text = truncate(label, truncation) text.text = truncate(label, truncation)
if text.text != label: if text.text != label:
self.svg.node(guides, 'title').text = label self.svg.node(guides, 'title').text = label
else: else:
self.svg.node( self.svg.node(
guides, 'title', guides,
'title',
).text = self._x_format(theta) ).text = self._x_format(theta)
angle = - theta + pi / 2 angle = -theta + pi / 2
if cos(angle) < 0: if cos(angle) < 0:
angle -= pi angle -= pi
text.attrib['transform'] = 'rotate(%f %s)' % ( 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): def _y_axis(self, draw_axes=True):
"""Override y axis to make it polar""" """Override y axis to make it polar"""
@ -130,31 +136,32 @@ class Radar(Line):
major = r in self._y_labels_major major = r in self._y_labels_major
if not (self.show_minor_y_labels or major): if not (self.show_minor_y_labels or major):
continue continue
guides = self.svg.node(axis, class_='%sguides' % ( guides = self.svg.node(
'logarithmic ' if self.logarithmic else '' axis,
)) class_='%sguides' %
('logarithmic ' if self.logarithmic else '')
)
if self.show_y_guides: if self.show_y_guides:
self.svg.line( self.svg.line(
guides, [self.view((r, theta)) for theta in self._x_pos], guides, [self.view((r, theta)) for theta in self._x_pos],
close=True, close=True,
class_='%sguide line' % ( class_='%sguide line' % ('major ' if major else '')
'major ' if major else '')) )
x, y = self.view((r, self._x_pos[0])) x, y = self.view((r, self._x_pos[0]))
x -= 5 x -= 5
text = self.svg.node( text = self.svg.node(
guides, 'text', guides, 'text', x=x, y=y, class_='major' if major else ''
x=x,
y=y,
class_='major' if major else ''
) )
text.text = label text.text = label
if self.y_label_rotation: if self.y_label_rotation:
text.attrib['transform'] = "rotate(%d %f %f)" % ( text.attrib['transform'] = "rotate(%d %f %f)" % (
self.y_label_rotation, x, y) self.y_label_rotation, x, y
)
self.svg.node( self.svg.node(
guides, 'title', guides,
'title',
).text = self._y_format(r) ).text = self._y_format(r)
def _compute(self): def _compute(self):
@ -162,19 +169,20 @@ class Radar(Line):
delta = 2 * pi / self._len if self._len else 0 delta = 2 * pi / self._len if self._len else 0
self._x_pos = [.5 * pi + i * delta for i in range(self._len + 1)] self._x_pos = [.5 * pi + i * delta for i in range(self._len + 1)]
for serie in self.all_series: for serie in self.all_series:
serie.points = [ serie.points = [(v, self._x_pos[i])
(v, self._x_pos[i]) for i, v in enumerate(serie.values)]
for i, v in enumerate(serie.values)]
if self.interpolate: if self.interpolate:
extended_x_pos = ( extended_x_pos = ([.5 * pi - delta] + self._x_pos)
[.5 * pi - delta] + self._x_pos) extended_vals = (serie.values[-1:] + serie.values)
extended_vals = (serie.values[-1:] +
serie.values)
serie.interpolated = list( serie.interpolated = list(
map(tuple, map(
map(reversed, tuple,
self._interpolate( map(
extended_x_pos, extended_vals)))) reversed,
self._interpolate(extended_x_pos, extended_vals)
)
)
)
# x labels space # x labels space
self._box.margin *= 2 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
""" """
Solid Guage Solid Guage
For each series a solid guage is shown on the plot area. 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): class SolidGauge(Graph):
def gaugify(self, serie, squares, sq_dimensions, current_square): def gaugify(self, serie, squares, sq_dimensions, current_square):
serie_node = self.svg.serie(serie) serie_node = self.svg.serie(serie)
if self.half_pie: if self.half_pie:
start_angle = 3 * pi / 2 start_angle = 3 * pi / 2
center = ( center = ((current_square[1] * sq_dimensions[0]) -
(current_square[1] * sq_dimensions[0]) - ( (sq_dimensions[0] / 2.),
sq_dimensions[0] / 2.), (current_square[0] * sq_dimensions[1]) -
(current_square[0] * sq_dimensions[1]) - ( (sq_dimensions[1] / 4))
sq_dimensions[1] / 4))
end_angle = pi / 2 end_angle = pi / 2
else: else:
start_angle = 0 start_angle = 0
center = ( center = ((current_square[1] * sq_dimensions[0]) -
(current_square[1] * sq_dimensions[0]) - ( (sq_dimensions[0] / 2.),
sq_dimensions[0] / 2.), (current_square[0] * sq_dimensions[1]) -
(current_square[0] * sq_dimensions[1]) - ( (sq_dimensions[1] / 2.))
sq_dimensions[1] / 2.))
end_angle = 2 * pi end_angle = 2 * pi
max_value = serie.metadata.get(0, {}).get('max_value', 100) max_value = serie.metadata.get(0, {}).get('max_value', 100)
@ -57,7 +52,8 @@ class SolidGauge(Graph):
self.svg.gauge_background( self.svg.gauge_background(
serie_node, start_angle, center, radius, small_radius, end_angle, 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 sum_ = 0
for i, value in enumerate(serie.values): for i, value in enumerate(serie.values):
@ -73,27 +69,30 @@ class SolidGauge(Graph):
metadata = serie.metadata.get(i) metadata = serie.metadata.get(i)
gauge_ = decorate( gauge_ = decorate(
self.svg, self.svg, self.svg.node(serie_node['plot'], class_="gauge"),
self.svg.node(serie_node['plot'], class_="gauge"), metadata
metadata) )
alter( alter(
self.svg.solid_gauge( self.svg.solid_gauge(
serie_node, gauge_, radius, small_radius, serie_node, gauge_, radius, small_radius, angle,
angle, start_angle, center, val, i, metadata, start_angle, center, val, i, metadata, self.half_pie,
self.half_pie, end_angle, end_angle, self._serie_format(serie, max_value)
self._serie_format(serie, max_value)), ), metadata
metadata) )
start_angle += angle start_angle += angle
sum_ += value sum_ += value
x, y = center x, y = center
self.svg.node( self.svg.node(
serie_node['text_overlay'], 'text', serie_node['text_overlay'],
'text',
class_='value gauge-sum', class_='value gauge-sum',
x=x, x=x,
y=y + self.style.value_font_size / 3, y=y + self.style.value_font_size / 3,
attrib={'text-anchor': 'middle'} attrib={
'text-anchor': 'middle'
}
).text = self._serie_format(serie, sum_) ).text = self._serie_format(serie, sum_)
def _compute_x_labels(self): def _compute_x_labels(self):
@ -109,8 +108,7 @@ class SolidGauge(Graph):
for index, serie in enumerate(self.series): for index, serie in enumerate(self.series):
current_square = self._current_square(squares, index) current_square = self._current_square(squares, index)
self.gaugify( self.gaugify(serie, squares, sq_dimensions, current_square)
serie, squares, sq_dimensions, current_square)
def _squares(self): def _squares(self):
@ -150,4 +148,5 @@ class SolidGauge(Graph):
else: else:
return tuple(current_square) return tuple(current_square)
raise Exception( 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): class StackedBar(Bar):
"""Stacked Bar graph class""" """Stacked Bar graph class"""
_adapters = [none_to_zero] _adapters = [none_to_zero]
@ -37,15 +36,14 @@ class StackedBar(Bar):
"""Separate values between positives and negatives stacked""" """Separate values between positives and negatives stacked"""
series = self.secondary_series if secondary else self.series series = self.secondary_series if secondary else self.series
transposed = list(zip(*[serie.values for serie in series])) transposed = list(zip(*[serie.values for serie in series]))
positive_vals = [sum([ positive_vals = [
val for val in vals sum([val for val in vals if val is not None and val >= self.zero])
if val is not None and val >= self.zero]) for vals in transposed
for vals in transposed] ]
negative_vals = [sum([ negative_vals = [
val sum([val for val in vals if val is not None and val < self.zero])
for val in vals for vals in transposed
if val is not None and val < self.zero]) ]
for vals in transposed]
return positive_vals, negative_vals return positive_vals, negative_vals
def _compute_box(self, 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] self._box.ymin = self.range[0]
else: else:
self._box.ymin = negative_vals and min( 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: if self.range and self.range[1] is not None:
self._box.ymax = self.range[1] self._box.ymax = self.range[1]
else: else:
self._box.ymax = positive_vals and max( 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): def _compute(self):
"""Compute y min and max and y scale and set labels""" """Compute y min and max and y scale and set labels"""
positive_vals, negative_vals = self._get_separated_values() positive_vals, negative_vals = self._get_separated_values()
if self.logarithmic: if self.logarithmic:
positive_vals = list(filter( positive_vals = list(
lambda x: x > self.zero, positive_vals)) filter(lambda x: x > self.zero, positive_vals)
negative_vals = list(filter( )
lambda x: x > self.zero, negative_vals)) negative_vals = list(
filter(lambda x: x > self.zero, negative_vals)
)
self._compute_box(positive_vals, negative_vals) self._compute_box(positive_vals, negative_vals)
positive_vals = positive_vals or [self.zero] positive_vals = positive_vals or [self.zero]
@ -96,21 +98,25 @@ class StackedBar(Bar):
def _pre_compute_secondary(self, positive_vals, negative_vals): def _pre_compute_secondary(self, positive_vals, negative_vals):
"""Compute secondary y min and max""" """Compute secondary y min and max"""
self._secondary_min = (negative_vals and min( self._secondary_min = (
min(negative_vals), self.zero)) or self.zero negative_vals and min(min(negative_vals), self.zero)
self._secondary_max = (positive_vals and max( ) or self.zero
max(positive_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): def _bar(self, serie, parent, x, y, i, zero, secondary=False):
"""Internal stacking bar drawing function""" """Internal stacking bar drawing function"""
if secondary: if secondary:
cumulation = (self.secondary_negative_cumulation cumulation = (
if y < self.zero else self.secondary_negative_cumulation
self.secondary_positive_cumulation) if y < self.zero else self.secondary_positive_cumulation
)
else: else:
cumulation = (self.negative_cumulation cumulation = (
if y < self.zero else self.negative_cumulation
self.positive_cumulation) if y < self.zero else self.positive_cumulation
)
zero = cumulation[i] zero = cumulation[i]
cumulation[i] = zero + y cumulation[i] = zero + y
if zero == 0: if zero == 0:
@ -133,9 +139,16 @@ class StackedBar(Bar):
height = self.view.y(zero) - y height = self.view.y(zero) - y
r = serie.rounded_bars * 1 if serie.rounded_bars else 0 r = serie.rounded_bars * 1 if serie.rounded_bars else 0
self.svg.transposable_node( self.svg.transposable_node(
parent, 'rect', parent,
x=x, y=y, rx=r, ry=r, width=width, height=height, 'rect',
class_='rect reactive tooltip-trigger') x=x,
y=y,
rx=r,
ry=r,
width=width,
height=height,
class_='rect reactive tooltip-trigger'
)
return x, y, width, height return x, y, width, height
def _plot(self): 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
""" """
Stacked Line chart: Like a line chart but with all lines stacking Stacked Line chart: Like a line chart but with all lines stacking
on top of the others. Used along fill=True option. on top of the others. Used along fill=True option.
@ -29,7 +28,6 @@ from pygal.graph.line import Line
class StackedLine(Line): class StackedLine(Line):
"""Stacked Line graph class""" """Stacked Line graph class"""
_adapters = [none_to_zero] _adapters = [none_to_zero]
@ -45,15 +43,11 @@ class StackedLine(Line):
""" """
sum_ = serie.points[index][1] sum_ = serie.points[index][1]
if serie in self.series and ( if serie in self.series and (
self.stack_from_top and self.stack_from_top
self.series.index(serie) == self._order - 1 or and self.series.index(serie) == self._order - 1
not self.stack_from_top and or not self.stack_from_top and self.series.index(serie) == 0):
self.series.index(serie) == 0):
return super(StackedLine, self)._value_format(value) return super(StackedLine, self)._value_format(value)
return '%s (+%s)' % ( return '%s (+%s)' % (self._y_format(sum_), self._y_format(value))
self._y_format(sum_),
self._y_format(value)
)
def _fill(self, values): def _fill(self, values):
"""Add extra values to fill the line""" """Add extra values to fill the line"""
@ -73,9 +67,8 @@ class StackedLine(Line):
accumulation = [0] * self._len accumulation = [0] * self._len
for serie in series_group[::-1 if self.stack_from_top else 1]: for serie in series_group[::-1 if self.stack_from_top else 1]:
accumulation = list(map(sum, zip(accumulation, serie.values))) accumulation = list(map(sum, zip(accumulation, serie.values)))
serie.points = [ serie.points = [(x_pos[i], v)
(x_pos[i], v) for i, v in enumerate(accumulation)]
for i, v in enumerate(accumulation)]
if serie.points and self.interpolate: if serie.points and self.interpolate:
serie.interpolated = self._interpolate(x_pos, accumulation) serie.interpolated = self._interpolate(x_pos, accumulation)
else: else:

25
pygal/graph/time.py

@ -16,7 +16,6 @@
# #
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
""" """
XY time extensions: handle convertion of date, time, datetime, timedelta XY time extensions: handle convertion of date, time, datetime, timedelta
into float for xy plot and back to their type for display 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): def time_to_seconds(x):
"""Convert a time in a seconds sum""" """Convert a time in a seconds sum"""
if isinstance(x, time): if isinstance(x, time):
return (( return ((((x.hour * 60) + x.minute) * 60 + x.second) * 10**6 +
((x.hour * 60) + x.minute) * 60 + x.second x.microsecond) / 10**6
) * 10 ** 6 + x.microsecond) / 10 ** 6
if is_str(x): if is_str(x):
return x return x
# Clamp to valid time # 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): def seconds_to_time(x):
"""Convert a number of second into a time""" """Convert a number of second into a time"""
t = int(x * 10 ** 6) t = int(x * 10**6)
ms = t % 10 ** 6 ms = t % 10**6
t = t // 10 ** 6 t = t // 10**6
s = t % 60 s = t % 60
t = t // 60 t = t // 60
m = t % 60 m = t % 60
@ -91,7 +89,6 @@ def seconds_to_time(x):
class DateTimeLine(XY): class DateTimeLine(XY):
"""DateTime abscissa xy graph class""" """DateTime abscissa xy graph class"""
_x_adapters = [datetime_to_timestamp, date_to_datetime] _x_adapters = [datetime_to_timestamp, date_to_datetime]
@ -99,27 +96,29 @@ class DateTimeLine(XY):
@property @property
def _x_format(self): def _x_format(self):
"""Return the value formatter for this graph""" """Return the value formatter for this graph"""
def datetime_to_str(x): def datetime_to_str(x):
dt = datetime.utcfromtimestamp(x) dt = datetime.utcfromtimestamp(x)
return self.x_value_formatter(dt) return self.x_value_formatter(dt)
return datetime_to_str return datetime_to_str
class DateLine(DateTimeLine): class DateLine(DateTimeLine):
"""Date abscissa xy graph class""" """Date abscissa xy graph class"""
@property @property
def _x_format(self): def _x_format(self):
"""Return the value formatter for this graph""" """Return the value formatter for this graph"""
def date_to_str(x): def date_to_str(x):
d = datetime.utcfromtimestamp(x).date() d = datetime.utcfromtimestamp(x).date()
return self.x_value_formatter(d) return self.x_value_formatter(d)
return date_to_str return date_to_str
class TimeLine(DateTimeLine): class TimeLine(DateTimeLine):
"""Time abscissa xy graph class""" """Time abscissa xy graph class"""
_x_adapters = [positive, time_to_seconds, datetime_to_time] _x_adapters = [positive, time_to_seconds, datetime_to_time]
@ -127,14 +126,15 @@ class TimeLine(DateTimeLine):
@property @property
def _x_format(self): def _x_format(self):
"""Return the value formatter for this graph""" """Return the value formatter for this graph"""
def date_to_str(x): def date_to_str(x):
t = seconds_to_time(x) t = seconds_to_time(x)
return self.x_value_formatter(t) return self.x_value_formatter(t)
return date_to_str return date_to_str
class TimeDeltaLine(XY): class TimeDeltaLine(XY):
"""TimeDelta abscissa xy graph class""" """TimeDelta abscissa xy graph class"""
_x_adapters = [timedelta_to_seconds] _x_adapters = [timedelta_to_seconds]
@ -142,6 +142,7 @@ class TimeDeltaLine(XY):
@property @property
def _x_format(self): def _x_format(self):
"""Return the value formatter for this graph""" """Return the value formatter for this graph"""
def timedelta_to_str(x): def timedelta_to_str(x):
td = timedelta(seconds=x) td = timedelta(seconds=x)
return self.x_value_formatter(td) 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Treemap chart: Visualize data using nested recangles""" """Treemap chart: Visualize data using nested recangles"""
from __future__ import division from __future__ import division
@ -27,7 +26,6 @@ from pygal.util import alter, cut, decorate
class Treemap(Graph): class Treemap(Graph):
"""Treemap graph class""" """Treemap graph class"""
_adapters = [positive, none_to_zero] _adapters = [positive, none_to_zero]
@ -43,31 +41,26 @@ class Treemap(Graph):
val = self._format(serie, i) val = self._format(serie, i)
rect = decorate( rect = decorate(
self.svg, self.svg, self.svg.node(rects, class_="rect"), metadata
self.svg.node(rects, class_="rect"), )
metadata)
alter( alter(
self.svg.node( self.svg.node(
rect, 'rect', rect,
'rect',
x=rx, x=rx,
y=ry, y=ry,
width=rw, width=rw,
height=rh, height=rh,
class_='rect reactive tooltip-trigger'), class_='rect reactive tooltip-trigger'
metadata) ), metadata
)
self._tooltip_data( self._tooltip_data(
rect, val, rect, val, rx + rw / 2, ry + rh / 2, 'centered',
rx + rw / 2, self._get_x_label(i)
ry + rh / 2, )
'centered', self._static_value(serie_node, val, rx + rw / 2, ry + rh / 2, metadata)
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): def _binary_tree(self, data, total, x, y, w, h, parent=None):
if total == 0: if total == 0:
@ -81,10 +74,11 @@ class Treemap(Graph):
datum = data[0] datum = data[0]
serie_node = self.svg.serie(datum) serie_node = self.svg.serie(datum)
self._binary_tree( self._binary_tree(
list(enumerate(datum.values)), list(enumerate(datum.values)), total, x, y, w, h, (
total, x, y, w, h, datum, serie_node,
(datum, serie_node, self.svg.node(serie_node['plot'], class_="rects")
self.svg.node(serie_node['plot'], class_="rects"))) )
)
return return
midpoint = total / 2 midpoint = total / 2
@ -110,16 +104,16 @@ class Treemap(Graph):
if h > w: if h > w:
y_pivot = pivot_pct * h y_pivot = pivot_pct * h
self._binary_tree(half1, half1_sum, x, y, w, y_pivot, parent)
self._binary_tree( self._binary_tree(
half1, half1_sum, x, y, w, y_pivot, parent) half2, half2_sum, x, y + y_pivot, w, h - y_pivot, parent
self._binary_tree( )
half2, half2_sum, x, y + y_pivot, w, h - y_pivot, parent)
else: else:
x_pivot = pivot_pct * w x_pivot = pivot_pct * w
self._binary_tree(half1, half1_sum, x, y, x_pivot, h, parent)
self._binary_tree( self._binary_tree(
half1, half1_sum, x, y, x_pivot, h, parent) half2, half2_sum, x + x_pivot, y, w - x_pivot, h, parent
self._binary_tree( )
half2, half2_sum, x + x_pivot, y, w - x_pivot, h, parent)
def _compute_x_labels(self): def _compute_x_labels(self):
pass pass
@ -136,7 +130,7 @@ class Treemap(Graph):
gh = self.height - self.margin_box.y gh = self.height - self.margin_box.y
self.view.box.xmin = self.view.box.ymin = x = y = 0 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.ymax = h = total / w
self.view.box.fix() 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # 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 XY Line graph: Plot a set of couple data points (x, y) connected by
straight segments. straight segments.
@ -32,7 +31,6 @@ from pygal.util import cached_property, compose, ident
class XY(Line, Dual): class XY(Line, Dual):
"""XY Line graph class""" """XY Line graph class"""
_x_adapters = [] _x_adapters = []
@ -40,38 +38,42 @@ class XY(Line, Dual):
@cached_property @cached_property
def xvals(self): def xvals(self):
"""All x values""" """All x values"""
return [val[0] return [
for serie in self.all_series val[0] for serie in self.all_series for val in serie.values
for val in serie.values if val[0] is not None
if val[0] is not None] ]
@cached_property @cached_property
def yvals(self): def yvals(self):
"""All y values""" """All y values"""
return [val[1] return [
for serie in self.series val[1] for serie in self.series for val in serie.values
for val in serie.values if val[1] is not None
if val[1] is not None] ]
@cached_property @cached_property
def _min(self): def _min(self):
"""Getter for the minimum series value""" """Getter for the minimum series value"""
return (self.range[0] if (self.range and self.range[0] is not None) return (
else (min(self.yvals) if self.yvals else None)) self.range[0] if (self.range and self.range[0] is not None) else
(min(self.yvals) if self.yvals else None)
)
@cached_property @cached_property
def _max(self): def _max(self):
"""Getter for the maximum series value""" """Getter for the maximum series value"""
return (self.range[1] if (self.range and self.range[1] is not None) return (
else (max(self.yvals) if self.yvals else None)) 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): def _compute(self):
"""Compute x/y min and max and x/y scale and set labels""" """Compute x/y min and max and x/y scale and set labels"""
if self.xvals: if self.xvals:
if self.xrange: if self.xrange:
x_adapter = reduce( x_adapter = reduce(compose, self._x_adapters) if getattr(
compose, self._x_adapters) if getattr( self, '_x_adapters', None
self, '_x_adapters', None) else ident ) else ident
xmin = x_adapter(self.xrange[0]) xmin = x_adapter(self.xrange[0])
xmax = x_adapter(self.xrange[1]) xmax = x_adapter(self.xrange[1])
@ -98,18 +100,24 @@ class XY(Line, Dual):
for serie in self.all_series: for serie in self.all_series:
serie.points = serie.values serie.points = serie.values
if self.interpolate: if self.interpolate:
vals = list(zip(*sorted( vals = list(
filter(lambda t: None not in t, zip(
serie.points), key=lambda x: x[0]))) *sorted(
filter(lambda t: None not in t, serie.points),
key=lambda x: x[0]
)
)
)
serie.interpolated = self._interpolate(vals[0], vals[1]) serie.interpolated = self._interpolate(vals[0], vals[1])
if self.interpolate: if self.interpolate:
self.xvals = [val[0] self.xvals = [
for serie in self.all_series val[0] for serie in self.all_series
for val in serie.interpolated] for val in serie.interpolated
self.yvals = [val[1] ]
for serie in self.series self.yvals = [
for val in serie.interpolated] val[1] for serie in self.series for val in serie.interpolated
]
if self.xvals: if self.xvals:
xmin = min(self.xvals) xmin = min(self.xvals)
xmax = max(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 yield x[i] + X, a[i] + b[i] * X + c[i] * X2 + d[i] * X3
def hermite_interpolate(x, y, precision=250, def hermite_interpolate(
type='cardinal', c=None, b=None, t=None): x, y, precision=250, type='cardinal', c=None, b=None, t=None
):
""" """
Interpolate x, y using the hermite method. Interpolate x, y using the hermite method.
See https://en.wikipedia.org/wiki/Cubic_Hermite_spline See https://en.wikipedia.org/wiki/Cubic_Hermite_spline
@ -132,11 +133,9 @@ def hermite_interpolate(x, y, precision=250,
c = 0 c = 0
if type == 'finite_difference': if type == 'finite_difference':
for i in range(1, n): for i in range(1, n):
m[i] = w[i] = .5 * ( m[i] = w[i] = .5 * ((y[i + 1] - y[i]) / (x[i + 1] - x[i]) +
(y[i + 1] - y[i]) / (x[i + 1] - x[i]) + (y[i] - y[i - 1]) / (x[i] - x[i - 1])
(y[i] - y[i - 1]) / ( ) if x[i + 1] - x[i] and x[i] - x[i - 1] else 0
x[i] - x[i - 1])
) if x[i + 1] - x[i] and x[i] - x[i - 1] else 0
elif type == 'kochanek_bartels': elif type == 'kochanek_bartels':
c = c or 0 c = c or 0
@ -151,9 +150,9 @@ def hermite_interpolate(x, y, precision=250,
if type == 'cardinal': if type == 'cardinal':
c = c or 0 c = c or 0
for i in range(1, n): for i in range(1, n):
m[i] = w[i] = (1 - c) * ( m[i] = w[i] = (1 - c) * (y[i + 1] - y[i - 1]) / (
y[i + 1] - y[i - 1]) / ( x[i + 1] - x[i - 1]
x[i + 1] - x[i - 1]) if x[i + 1] - x[i - 1] else 0 ) if x[i + 1] - x[i - 1] else 0
def p(i, x_): def p(i, x_):
t = (x_ - x[i]) / delta_x[i] t = (x_ - x[i]) / delta_x[i]
@ -162,13 +161,13 @@ def hermite_interpolate(x, y, precision=250,
h00 = 2 * t3 - 3 * t2 + 1 h00 = 2 * t3 - 3 * t2 + 1
h10 = t3 - 2 * t2 + t h10 = t3 - 2 * t2 + t
h01 = - 2 * t3 + 3 * t2 h01 = -2 * t3 + 3 * t2
h11 = t3 - t2 h11 = t3 - t2
return (h00 * y[i] + return (
h10 * m[i] * delta_x[i] + h00 * y[i] + h10 * m[i] * delta_x[i] + h01 * y[i + 1] +
h01 * y[i + 1] + h11 * w[i + 1] * delta_x[i]
h11 * w[i + 1] * delta_x[i]) )
for i in range(n + 1): for i in range(n + 1):
yield x[i], y[i] yield x[i], y[i]
@ -239,7 +238,6 @@ INTERPOLATIONS = {
'trigonometric': trigonometric_interpolate 'trigonometric': trigonometric_interpolate
} }
if __name__ == '__main__': if __name__ == '__main__':
from pygal import XY from pygal import XY
points = [(.1, 7), (.3, -4), (.6, 10), (.9, 8), (1.4, 3), (1.7, 1)] 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('cubic', cubic_interpolate(*zip(*points)))
xy.add('lagrange', lagrange_interpolate(*zip(*points))) xy.add('lagrange', lagrange_interpolate(*zip(*points)))
xy.add('trigonometric', trigonometric_interpolate(*zip(*points))) xy.add('trigonometric', trigonometric_interpolate(*zip(*points)))
xy.add('hermite catmul_rom', hermite_interpolate( xy.add(
*zip(*points), type='catmul_rom')) 'hermite catmul_rom',
xy.add('hermite finite_difference', hermite_interpolate( hermite_interpolate(*zip(*points), type='catmul_rom')
*zip(*points), type='finite_difference')) )
xy.add('hermite cardinal -.5', hermite_interpolate( xy.add(
*zip(*points), type='cardinal', c=-.5)) 'hermite finite_difference',
xy.add('hermite cardinal .5', hermite_interpolate( hermite_interpolate(*zip(*points), type='finite_difference')
*zip(*points), type='cardinal', c=.5)) )
xy.add('hermite kochanek_bartels .5 .75 -.25', hermite_interpolate( xy.add(
*zip(*points), type='kochanek_bartels', c=.5, b=.75, t=-.25)) 'hermite cardinal -.5',
xy.add('hermite kochanek_bartels .25 -.75 .5', hermite_interpolate( hermite_interpolate(*zip(*points), type='cardinal', c=-.5)
*zip(*points), type='kochanek_bartels', c=.25, b=-.75, t=.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() 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Maps extensions namespace module""" """Maps extensions namespace module"""

1
pygal/serie.py

@ -22,7 +22,6 @@ from pygal.util import cached_property
class Serie(object): class Serie(object):
"""Serie class containing title, values and the graph serie index""" """Serie class containing title, values and the graph serie index"""
def __init__(self, index, values, config, metadata=None): 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Class holding state during render""" """Class holding state during render"""
from pygal.util import merge from pygal.util import merge
class State(object): class State(object):
""" """
Class containing config values Class containing config values
overriden by chart 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 # http://eprints.maths.ox.ac.uk/184/1/tdist.pdf
raise ImportError( raise ImportError(
'You must have scipy installed to use t-student ' '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) return norm_ppf(x)
# According to http://sphweb.bumc.bu.edu/otlt/MPH-Modules/BS/ # According to http://sphweb.bumc.bu.edu/otlt/MPH-Modules/BS/
# BS704_Confidence_Intervals/BS704_Confidence_Intervals_print.html # BS704_Confidence_Intervals/BS704_Confidence_Intervals_print.html
def confidence_interval_continuous( 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""" """Continuous confidence interval from sample size and standard error"""
alpha = ppf((confidence + 1) / 2, sample_size - 1) alpha = ppf((confidence + 1) / 2, sample_size - 1)
@ -52,8 +55,13 @@ def confidence_interval_continuous(
def confidence_interval_dichotomous( def confidence_interval_dichotomous(
point_estimate, sample_size, confidence=.95, bias=False, point_estimate,
percentage=True, **kwargs): sample_size,
confidence=.95,
bias=False,
percentage=True,
**kwargs
):
"""Dichotomous confidence interval from sample size and maybe a bias""" """Dichotomous confidence interval from sample size and maybe a bias"""
alpha = ppf((confidence + 1) / 2, sample_size - 1) alpha = ppf((confidence + 1) / 2, sample_size - 1)
p = point_estimate p = point_estimate

166
pygal/style.py

@ -27,7 +27,6 @@ from pygal.colors import darken, is_foreground_light, lighten
class Style(object): class Style(object):
"""Styling class containing colors for the css generation""" """Styling class containing colors for the css generation"""
plot_background = 'rgba(255, 255, 255, 1)' plot_background = 'rgba(255, 255, 255, 1)'
@ -38,8 +37,7 @@ class Style(object):
foreground_subtle = 'rgba(0, 0, 0, .54)' foreground_subtle = 'rgba(0, 0, 0, .54)'
# Monospaced font is highly encouraged # Monospaced font is highly encouraged
font_family = ( font_family = ('Consolas, "Liberation Mono", Menlo, Courier, monospace')
'Consolas, "Liberation Mono", Menlo, Courier, monospace')
label_font_family = None label_font_family = None
major_label_font_family = None major_label_font_family = None
@ -116,33 +114,36 @@ class Style(object):
elif fn.startswith('googlefont:'): elif fn.startswith('googlefont:'):
setattr(self, name, fn.replace('googlefont:', '')) setattr(self, name, fn.replace('googlefont:', ''))
self._google_fonts.add( self._google_fonts.add(
getattr(self, name).split(',')[0].strip()) getattr(self, name).split(',')[0].strip()
)
def get_colors(self, prefix, len_): def get_colors(self, prefix, len_):
"""Get the css color list""" """Get the css color list"""
def color(tupl): def color(tupl):
"""Make a color css""" """Make a color css"""
return (( return ((
'%s.color-{0}, %s.color-{0} a:visited {{\n' '%s.color-{0}, %s.color-{0} a:visited {{\n'
' stroke: {1};\n' ' stroke: {1};\n'
' fill: {1};\n' ' fill: {1};\n'
'}}\n') % (prefix, prefix)).format(*tupl) '}}\n'
) % (prefix, prefix)).format(*tupl)
def value_color(tupl): def value_color(tupl):
"""Make a value color css""" """Make a value color css"""
return (( return ((
'%s .text-overlay .color-{0} text {{\n' '%s .text-overlay .color-{0} text {{\n'
' fill: {1};\n' ' fill: {1};\n'
'}}\n') % (prefix,)).format(*tupl) '}}\n'
) % (prefix, )).format(*tupl)
def ci_color(tupl): def ci_color(tupl):
"""Make a value color css""" """Make a value color css"""
if not tupl[1]: if not tupl[1]:
return '' return ''
return (( return (('%s .color-{0} .ci {{\n'
'%s .color-{0} .ci {{\n' ' stroke: {1};\n'
' stroke: {1};\n' '}}\n') % (prefix, )).format(*tupl)
'}}\n') % (prefix,)).format(*tupl)
if len(self.colors) < len_: if len(self.colors) < len_:
missing = len_ - len(self.colors) 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: if i < len(self.value_colors) and self.value_colors[i] is not None:
value_colors.append(self.value_colors[i]) value_colors.append(self.value_colors[i])
else: else:
value_colors.append('white' if is_foreground_light( value_colors.append(
colors[i]) else 'black') 'white' if is_foreground_light(colors[i]) else 'black'
)
return '\n'.join(chain(
map(color, enumerate(colors)), return '\n'.join(
map(value_color, enumerate(value_colors)), chain(
map(ci_color, enumerate(self.ci_colors)))) map(color, enumerate(colors)),
map(value_color, enumerate(value_colors)),
map(ci_color, enumerate(self.ci_colors))
)
)
def to_dict(self): def to_dict(self):
"""Convert instance to a serializable mapping.""" """Convert instance to a serializable mapping."""
@ -188,7 +193,6 @@ DefaultStyle = Style
class DarkStyle(Style): class DarkStyle(Style):
"""A dark style (old default)""" """A dark style (old default)"""
background = 'black' background = 'black'
@ -200,14 +204,13 @@ class DarkStyle(Style):
opacity_hover = '.4' opacity_hover = '.4'
transition = '250ms' transition = '250ms'
colors = ( colors = (
'#ff5995', '#b6e354', '#feed6c', '#8cedff', '#9e6ffe', '#ff5995', '#b6e354', '#feed6c', '#8cedff', '#9e6ffe', '#899ca1',
'#899ca1', '#f8f8f2', '#bf4646', '#516083', '#f92672', '#f8f8f2', '#bf4646', '#516083', '#f92672', '#82b414', '#fd971f',
'#82b414', '#fd971f', '#56c2d6', '#808384', '#8c54fe', '#56c2d6', '#808384', '#8c54fe', '#465457'
'#465457') )
class LightStyle(Style): class LightStyle(Style):
"""A light style""" """A light style"""
background = 'white' background = 'white'
@ -215,13 +218,13 @@ class LightStyle(Style):
foreground = 'rgba(0, 0, 0, 0.7)' foreground = 'rgba(0, 0, 0, 0.7)'
foreground_strong = 'rgba(0, 0, 0, 0.9)' foreground_strong = 'rgba(0, 0, 0, 0.9)'
foreground_subtle = 'rgba(0, 0, 0, 0.5)' foreground_subtle = 'rgba(0, 0, 0, 0.5)'
colors = ('#242424', '#9f6767', '#92ac68', colors = (
'#d0d293', '#9aacc3', '#bb77a4', '#242424', '#9f6767', '#92ac68', '#d0d293', '#9aacc3', '#bb77a4',
'#77bbb5', '#777777') '#77bbb5', '#777777'
)
class NeonStyle(DarkStyle): class NeonStyle(DarkStyle):
"""Similar to DarkStyle but with more opacity and effects""" """Similar to DarkStyle but with more opacity and effects"""
opacity = '.1' opacity = '.1'
@ -230,7 +233,6 @@ class NeonStyle(DarkStyle):
class CleanStyle(Style): class CleanStyle(Style):
"""A rather clean style""" """A rather clean style"""
background = 'transparent' background = 'transparent'
@ -240,11 +242,11 @@ class CleanStyle(Style):
foreground_subtle = 'rgba(0, 0, 0, 0.5)' foreground_subtle = 'rgba(0, 0, 0, 0.5)'
colors = ( colors = (
'rgb(12,55,149)', 'rgb(117,38,65)', 'rgb(228,127,0)', 'rgb(159,170,0)', '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): class DarkSolarizedStyle(Style):
"""Dark solarized popular theme""" """Dark solarized popular theme"""
background = '#073642' background = '#073642'
@ -256,12 +258,12 @@ class DarkSolarizedStyle(Style):
opacity_hover = '.9' opacity_hover = '.9'
transition = '500ms ease-in' transition = '500ms ease-in'
colors = ( colors = (
'#b58900', '#cb4b16', '#dc322f', '#d33682', '#b58900', '#cb4b16', '#dc322f', '#d33682', '#6c71c4', '#268bd2',
'#6c71c4', '#268bd2', '#2aa198', '#859900') '#2aa198', '#859900'
)
class LightSolarizedStyle(DarkSolarizedStyle): class LightSolarizedStyle(DarkSolarizedStyle):
"""Light solarized popular theme""" """Light solarized popular theme"""
background = '#fdf6e3' background = '#fdf6e3'
@ -272,7 +274,6 @@ class LightSolarizedStyle(DarkSolarizedStyle):
class RedBlueStyle(Style): class RedBlueStyle(Style):
"""A red and blue theme""" """A red and blue theme"""
background = lighten('#e6e7e9', 7) background = lighten('#e6e7e9', 7)
@ -283,13 +284,13 @@ class RedBlueStyle(Style):
opacity = '.6' opacity = '.6'
opacity_hover = '.9' opacity_hover = '.9'
colors = ( colors = (
'#d94e4c', '#e5884f', '#39929a', '#d94e4c', '#e5884f', '#39929a', lighten('#d94e4c', 10),
lighten('#d94e4c', 10), darken('#39929a', 15), lighten('#e5884f', 17), darken('#39929a', 15), lighten('#e5884f', 17), darken('#d94e4c', 10),
darken('#d94e4c', 10), '#234547') '#234547'
)
class LightColorizedStyle(Style): class LightColorizedStyle(Style):
"""A light colorized style""" """A light colorized style"""
background = '#f8f8f8' background = '#f8f8f8'
@ -301,13 +302,13 @@ class LightColorizedStyle(Style):
opacity_hover = '.9' opacity_hover = '.9'
transition = '250ms ease-in' transition = '250ms ease-in'
colors = ( colors = (
'#fe9592', '#534f4c', '#3ac2c0', '#a2a7a1', '#fe9592', '#534f4c', '#3ac2c0', '#a2a7a1', darken('#fe9592', 15),
darken('#fe9592', 15), lighten('#534f4c', 15), lighten('#3ac2c0', 15), lighten('#534f4c', 15), lighten('#3ac2c0', 15), lighten('#a2a7a1', 15),
lighten('#a2a7a1', 15), lighten('#fe9592', 15), darken('#3ac2c0', 10)) lighten('#fe9592', 15), darken('#3ac2c0', 10)
)
class DarkColorizedStyle(Style): class DarkColorizedStyle(Style):
"""A dark colorized style""" """A dark colorized style"""
background = darken('#3a2d3f', 5) background = darken('#3a2d3f', 5)
@ -321,11 +322,11 @@ class DarkColorizedStyle(Style):
colors = ( colors = (
'#c900fe', '#01b8fe', '#59f500', '#ff00e4', '#f9fa00', '#c900fe', '#01b8fe', '#59f500', '#ff00e4', '#f9fa00',
darken('#c900fe', 20), darken('#01b8fe', 15), darken('#59f500', 20), darken('#c900fe', 20), darken('#01b8fe', 15), darken('#59f500', 20),
darken('#ff00e4', 15), lighten('#f9fa00', 20)) darken('#ff00e4', 15), lighten('#f9fa00', 20)
)
class TurquoiseStyle(Style): class TurquoiseStyle(Style):
"""A turquoise style""" """A turquoise style"""
background = darken('#1b8088', 15) background = darken('#1b8088', 15)
@ -337,13 +338,12 @@ class TurquoiseStyle(Style):
opacity_hover = '.9' opacity_hover = '.9'
transition = '250ms ease-in' transition = '250ms ease-in'
colors = ( colors = (
'#93d2d9', '#ef940f', '#8C6243', '#fff', '#93d2d9', '#ef940f', '#8C6243', '#fff', darken('#93d2d9', 20),
darken('#93d2d9', 20), lighten('#ef940f', 15), lighten('#ef940f', 15), lighten('#8c6243', 15), '#1b8088'
lighten('#8c6243', 15), '#1b8088') )
class LightGreenStyle(Style): class LightGreenStyle(Style):
"""A light green style""" """A light green style"""
background = lighten('#f3f3f3', 3) background = lighten('#f3f3f3', 3)
@ -357,11 +357,11 @@ class LightGreenStyle(Style):
colors = ( colors = (
'#7dcf30', '#247fab', lighten('#7dcf30', 10), '#ccc', '#7dcf30', '#247fab', lighten('#7dcf30', 10), '#ccc',
darken('#7dcf30', 15), '#ddd', lighten('#247fab', 10), darken('#7dcf30', 15), '#ddd', lighten('#247fab', 10),
darken('#247fab', 15)) darken('#247fab', 15)
)
class DarkGreenStyle(Style): class DarkGreenStyle(Style):
"""A dark green style""" """A dark green style"""
background = darken('#251e01', 3) background = darken('#251e01', 3)
@ -374,11 +374,11 @@ class DarkGreenStyle(Style):
transition = '250ms ease-in' transition = '250ms ease-in'
colors = ( colors = (
'#adde09', '#6e8c06', '#4a5e04', '#fcd202', '#C1E34D', '#adde09', '#6e8c06', '#4a5e04', '#fcd202', '#C1E34D',
lighten('#fcd202', 25)) lighten('#fcd202', 25)
)
class DarkGreenBlueStyle(Style): class DarkGreenBlueStyle(Style):
"""A dark green and blue style""" """A dark green and blue style"""
background = '#000' background = '#000'
@ -389,13 +389,14 @@ class DarkGreenBlueStyle(Style):
opacity = '.55' opacity = '.55'
opacity_hover = '.9' opacity_hover = '.9'
transition = '250ms ease-in' transition = '250ms ease-in'
colors = (lighten('#34B8F7', 15), '#7dcf30', '#247fab', colors = (
darken('#7dcf30', 10), lighten('#247fab', 10), lighten('#34B8F7', 15), '#7dcf30', '#247fab', darken('#7dcf30', 10),
lighten('#7dcf30', 10), darken('#247fab', 10), '#fff') lighten('#247fab', 10), lighten('#7dcf30', 10), darken('#247fab', 10),
'#fff'
)
class BlueStyle(Style): class BlueStyle(Style):
"""A blue style""" """A blue style"""
background = darken('#f8f8f8', 3) background = darken('#f8f8f8', 3)
@ -409,11 +410,11 @@ class BlueStyle(Style):
colors = ( colors = (
'#00b2f0', '#43d9be', '#0662ab', darken('#00b2f0', 20), '#00b2f0', '#43d9be', '#0662ab', darken('#00b2f0', 20),
lighten('#43d9be', 20), lighten('#7dcf30', 10), darken('#0662ab', 15), 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): class SolidColorStyle(Style):
"""A light style with strong colors""" """A light style with strong colors"""
background = '#FFFFFF' background = '#FFFFFF'
@ -425,30 +426,32 @@ class SolidColorStyle(Style):
opacity_hover = '.9' opacity_hover = '.9'
transition = '400ms ease-in' transition = '400ms ease-in'
colors = ( colors = (
'#FF9900', '#DC3912', '#4674D1', '#109618', '#990099', '#FF9900', '#DC3912', '#4674D1', '#109618', '#990099', '#0099C6',
'#0099C6', '#DD4477', '#74B217', '#B82E2E', '#316395', '#994499') '#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}
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""" """Parametric Style base class for all the parametric operations"""
_op = None _op = None
@ -495,35 +498,30 @@ class ParametricStyleBase(Style):
class LightenStyle(ParametricStyleBase): class LightenStyle(ParametricStyleBase):
"""Create a style by lightening the given color""" """Create a style by lightening the given color"""
_op = 'lighten' _op = 'lighten'
class DarkenStyle(ParametricStyleBase): class DarkenStyle(ParametricStyleBase):
"""Create a style by darkening the given color""" """Create a style by darkening the given color"""
_op = 'darken' _op = 'darken'
class SaturateStyle(ParametricStyleBase): class SaturateStyle(ParametricStyleBase):
"""Create a style by saturating the given color""" """Create a style by saturating the given color"""
_op = 'saturate' _op = 'saturate'
class DesaturateStyle(ParametricStyleBase): class DesaturateStyle(ParametricStyleBase):
"""Create a style by desaturating the given color""" """Create a style by desaturating the given color"""
_op = 'desaturate' _op = 'desaturate'
class RotateStyle(ParametricStyleBase): class RotateStyle(ParametricStyleBase):
"""Create a style by rotating the given color""" """Create a style by rotating the given color"""
_op = 'rotate' _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.etree import etree
from pygal.util import ( from pygal.util import (
coord_abs_project, coord_diff, coord_dual, coord_format, coord_project, coord_abs_project, coord_diff, coord_dual, coord_format, coord_project,
minify_css, template) minify_css, template
)
nearly_2pi = 2 * pi - .00001 nearly_2pi = 2 * pi - .00001
class Svg(object): class Svg(object):
"""Svg related methods""" """Svg related methods"""
ns = 'http://www.w3.org/2000/svg' ns = 'http://www.w3.org/2000/svg'
@ -53,16 +53,9 @@ class Svg(object):
self.id = '' self.id = ''
self.processing_instructions = [] self.processing_instructions = []
if etree.lxml: if etree.lxml:
attrs = { attrs = {'nsmap': {None: self.ns, 'xlink': self.xlink_ns}}
'nsmap': {
None: self.ns,
'xlink': self.xlink_ns
}
}
else: else:
attrs = { attrs = {'xmlns': self.ns}
'xmlns': self.ns
}
if hasattr(etree, 'register_namespace'): if hasattr(etree, 'register_namespace'):
etree.register_namespace('xlink', self.xlink_ns) etree.register_namespace('xlink', self.xlink_ns)
else: else:
@ -73,11 +66,15 @@ class Svg(object):
if graph.classes: if graph.classes:
self.root.attrib['class'] = ' '.join(graph.classes) self.root.attrib['class'] = ' '.join(graph.classes)
self.root.append( self.root.append(
etree.Comment(u( etree.Comment(
'Generated with pygal %s (%s) ©Kozea 2012-2016 on %s' % ( u(
__version__, 'Generated with pygal %s (%s) ©Kozea 2012-2016 on %s' % (
'lxml' if etree.lxml else 'etree', __version__, 'lxml' if etree.lxml else 'etree',
date.today().isoformat())))) date.today().isoformat()
)
)
)
)
self.root.append(etree.Comment(u('http://pygal.org'))) self.root.append(etree.Comment(u('http://pygal.org')))
self.root.append(etree.Comment(u('http://github.com/Kozea/pygal'))) self.root.append(etree.Comment(u('http://github.com/Kozea/pygal')))
self.defs = self.node(tag='defs') self.defs = self.node(tag='defs')
@ -96,8 +93,8 @@ class Svg(object):
if self.graph.style._google_fonts: if self.graph.style._google_fonts:
auto_css.append( auto_css.append(
'//fonts.googleapis.com/css?family=%s' % quote_plus( '//fonts.googleapis.com/css?family=%s' %
'|'.join(self.graph.style._google_fonts)) quote_plus('|'.join(self.graph.style._google_fonts))
) )
for css in auto_css + list(self.graph.css): for css in auto_css + list(self.graph.css):
@ -108,8 +105,7 @@ class Svg(object):
css = css[len('file://'):] css = css[len('file://'):]
if not os.path.exists(css): if not os.path.exists(css):
css = os.path.join( css = os.path.join(os.path.dirname(__file__), 'css', css)
os.path.dirname(__file__), 'css', css)
with io.open(css, encoding='utf-8') as f: with io.open(css, encoding='utf-8') as f:
css_text = template( css_text = template(
@ -117,7 +113,8 @@ class Svg(object):
style=self.graph.style, style=self.graph.style,
colors=colors, colors=colors,
strokes=strokes, strokes=strokes,
id=self.id) id=self.id
)
if css_text is not None: if css_text is not None:
if not self.graph.pretty_print: if not self.graph.pretty_print:
@ -127,10 +124,11 @@ class Svg(object):
if css.startswith('//') and self.graph.force_uri_protocol: if css.startswith('//') and self.graph.force_uri_protocol:
css = '%s:%s' % (self.graph.force_uri_protocol, css) css = '%s:%s' % (self.graph.force_uri_protocol, css)
self.processing_instructions.append( self.processing_instructions.append(
etree.PI( etree.PI(u('xml-stylesheet'), u('href="%s"' % css))
u('xml-stylesheet'), u('href="%s"' % css))) )
self.node( 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): def add_scripts(self):
"""Add the js to the svg""" """Add the js to the svg"""
@ -140,8 +138,9 @@ class Svg(object):
return dict( return dict(
(k, getattr(self.graph.state, k)) (k, getattr(self.graph.state, k))
for k in dir(self.graph.config) for k in dir(self.graph.config)
if not k.startswith('_') and hasattr(self.graph.state, k) and if not k.startswith('_') and hasattr(self.graph.state, k)
not hasattr(getattr(self.graph.state, k), '__call__')) and not hasattr(getattr(self.graph.state, k), '__call__')
)
def json_default(o): def json_default(o):
if isinstance(o, (datetime, date)): if isinstance(o, (datetime, date)):
@ -154,7 +153,8 @@ class Svg(object):
# Config adds # Config adds
dct['legends'] = [ dct['legends'] = [
l.get('title') if isinstance(l, dict) else l 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 = window.pygal || {};'
common_js += 'window.pygal.config = window.pygal.config || {};' common_js += 'window.pygal.config = window.pygal.config || {};'
@ -187,7 +187,7 @@ class Svg(object):
for pos, dim in (('x', 'width'), ('y', 'height')): for pos, dim in (('x', 'width'), ('y', 'height')):
if in_attrib_and_number(dim) and attrib[dim] < 0: if in_attrib_and_number(dim) and attrib[dim] < 0:
attrib[dim] = - attrib[dim] attrib[dim] = -attrib[dim]
if in_attrib_and_number(pos): if in_attrib_and_number(pos):
attrib[pos] = attrib[pos] - attrib[dim] attrib[pos] = attrib[pos] - attrib[dim]
@ -200,8 +200,8 @@ class Svg(object):
attrib[key.rstrip('_')] = attrib[key] attrib[key.rstrip('_')] = attrib[key]
del attrib[key] del attrib[key]
elif key == 'href': elif key == 'href':
attrib[etree.QName( attrib[etree.QName('http://www.w3.org/1999/xlink',
'http://www.w3.org/1999/xlink', key)] = attrib[key] key)] = attrib[key]
del attrib[key] del attrib[key]
return etree.SubElement(parent, tag, attrib) return etree.SubElement(parent, tag, attrib)
@ -226,16 +226,17 @@ class Svg(object):
return dict( return dict(
plot=self.node( plot=self.node(
self.graph.nodes['plot'], self.graph.nodes['plot'],
class_='series serie-%d color-%d' % ( class_='series serie-%d color-%d' % (serie.index, serie.index)
serie.index, serie.index)), ),
overlay=self.node( overlay=self.node(
self.graph.nodes['overlay'], self.graph.nodes['overlay'],
class_='series serie-%d color-%d' % ( class_='series serie-%d color-%d' % (serie.index, serie.index)
serie.index, serie.index)), ),
text_overlay=self.node( text_overlay=self.node(
self.graph.nodes['text_overlay'], self.graph.nodes['text_overlay'],
class_='series serie-%d color-%d' % ( class_='series serie-%d color-%d' % (serie.index, serie.index)
serie.index, serie.index))) )
)
def line(self, node, coords, close=False, **kwargs): def line(self, node, coords, close=False, **kwargs):
"""Draw a svg line""" """Draw a svg line"""
@ -254,47 +255,54 @@ class Svg(object):
coord_format = lambda xy: '%f %f' % xy coord_format = lambda xy: '%f %f' % xy
origin = coord_format(coords[origin_index]) origin = coord_format(coords[origin_index])
line = ' '.join([coord_format(c) line = ' '.join([
for c in coords[origin_index + 1:] coord_format(c) for c in coords[origin_index + 1:] if None not in c
if None not in c]) ])
return self.node( return self.node(node, 'path', d=root % (origin, line), **kwargs)
node, 'path', d=root % (origin, line), **kwargs)
def slice( def slice(
self, serie_node, node, radius, small_radius, self, serie_node, node, radius, small_radius, angle, start_angle,
angle, start_angle, center, val, i, metadata): center, val, i, metadata
):
"""Draw a pie slice""" """Draw a pie slice"""
if angle == 2 * pi: if angle == 2 * pi:
angle = nearly_2pi angle = nearly_2pi
if angle > 0: if angle > 0:
to = [coord_abs_project(center, radius, start_angle), to = [
coord_abs_project(center, radius, start_angle + angle), coord_abs_project(center, radius, start_angle),
coord_abs_project(center, small_radius, start_angle + angle), coord_abs_project(center, radius, start_angle + angle),
coord_abs_project(center, small_radius, start_angle)] coord_abs_project(center, small_radius, start_angle + angle),
coord_abs_project(center, small_radius, start_angle)
]
rv = self.node( 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' % ( d='M%s A%s 0 %d 1 %s L%s A%s 0 %d 0 %s z' % (
to[0], to[0], coord_dual(radius), int(angle > pi), to[1], to[2],
coord_dual(radius), int(angle > pi), to[1], coord_dual(small_radius), int(angle > pi), to[3]
to[2], ),
coord_dual(small_radius), int(angle > pi), to[3]), class_='slice reactive tooltip-trigger'
class_='slice reactive tooltip-trigger') )
else: else:
rv = None rv = None
x, y = coord_diff(center, coord_project( x, y = coord_diff(
(radius + small_radius) / 2, start_angle + angle / 2)) center,
coord_project((radius + small_radius) / 2, start_angle + angle / 2)
)
self.graph._tooltip_data( self.graph._tooltip_data(
node, val, x, y, "centered", node, val, x, y, "centered", self.graph._x_labels
self.graph._x_labels and self.graph._x_labels[i][0]) and self.graph._x_labels[i][0]
)
if angle >= 0.3: # 0.3 radians is about 17 degrees if angle >= 0.3: # 0.3 radians is about 17 degrees
self.graph._static_value(serie_node, val, x, y, metadata) self.graph._static_value(serie_node, val, x, y, metadata)
return rv return rv
def gauge_background( def gauge_background(
self, serie_node, start_angle, center, radius, small_radius, 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: if end_angle == 2 * pi:
end_angle = nearly_2pi end_angle = nearly_2pi
@ -303,37 +311,45 @@ class Svg(object):
coord_abs_project(center, radius, start_angle), coord_abs_project(center, radius, start_angle),
coord_abs_project(center, radius, end_angle), coord_abs_project(center, radius, end_angle),
coord_abs_project(center, small_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( 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' % ( d='M%s A%s 0 1 1 %s L%s A%s 0 1 0 %s z' % (
to_shade[0], to_shade[0], coord_dual(radius), to_shade[1], to_shade[2],
coord_dual(radius), coord_dual(small_radius), to_shade[3]
to_shade[1], ),
to_shade[2], class_='gauge-background reactive'
coord_dual(small_radius), )
to_shade[3]),
class_='gauge-background reactive')
if half_pie: if half_pie:
begin_end = [ begin_end = [
coord_diff( coord_diff(
center, center,
coord_project( coord_project(
radius - (radius - small_radius) / 2, start_angle)), radius - (radius - small_radius) / 2, start_angle
)
),
coord_diff( coord_diff(
center, center,
coord_project( coord_project(
radius - (radius - small_radius) / 2, end_angle))] radius - (radius - small_radius) / 2, end_angle
)
)
]
pos = 0 pos = 0
for i in begin_end: for i in begin_end:
self.node( self.node(
serie_node['plot'], 'text', serie_node['plot'],
'text',
class_='y-{} bound reactive'.format(pos), class_='y-{} bound reactive'.format(pos),
x=i[0], x=i[0],
y=i[1] + 10, y=i[1] + 10,
attrib={'text-anchor': 'middle'} attrib={
'text-anchor': 'middle'
}
).text = '{}'.format(0 if pos == 0 else max_value) ).text = '{}'.format(0 if pos == 0 else max_value)
pos += 1 pos += 1
else: else:
@ -341,22 +357,21 @@ class Svg(object):
# Correct text vertical alignment # Correct text vertical alignment
middle_radius -= .1 * (radius - small_radius) middle_radius -= .1 * (radius - small_radius)
to_labels = [ to_labels = [
coord_abs_project( coord_abs_project(center, middle_radius, 0),
center, middle_radius, 0), coord_abs_project(center, middle_radius, nearly_2pi)
coord_abs_project(
center, middle_radius, nearly_2pi)
] ]
self.node( self.node(
self.defs, 'path', id='valuePath-%s%s' % center, self.defs,
d='M%s A%s 0 1 1 %s' % ( 'path',
to_labels[0], id='valuePath-%s%s' % center,
coord_dual(middle_radius), d='M%s A%s 0 1 1 %s' %
to_labels[1] (to_labels[0], coord_dual(middle_radius), to_labels[1])
)) )
text_ = self.node( text_ = self.node(serie_node['text_overlay'], 'text')
serie_node['text_overlay'], 'text')
self.node( self.node(
text_, 'textPath', class_='max-value reactive', text_,
'textPath',
class_='max-value reactive',
attrib={ attrib={
'href': '#valuePath-%s%s' % center, 'href': '#valuePath-%s%s' % center,
'startOffset': '99%', 'startOffset': '99%',
@ -365,40 +380,42 @@ class Svg(object):
).text = max_value ).text = max_value
def solid_gauge( def solid_gauge(
self, serie_node, node, radius, small_radius, self, serie_node, node, radius, small_radius, angle, start_angle,
angle, start_angle, center, val, i, metadata, half_pie, end_angle, center, val, i, metadata, half_pie, end_angle, max_value
max_value): ):
"""Draw a solid gauge slice and background slice""" """Draw a solid gauge slice and background slice"""
if angle == 2 * pi: if angle == 2 * pi:
angle = nearly_2pi angle = nearly_2pi
if angle > 0: if angle > 0:
to = [coord_abs_project(center, radius, start_angle), to = [
coord_abs_project(center, radius, start_angle + angle), coord_abs_project(center, radius, start_angle),
coord_abs_project(center, small_radius, start_angle + angle), coord_abs_project(center, radius, start_angle + angle),
coord_abs_project(center, small_radius, start_angle)] coord_abs_project(center, small_radius, start_angle + angle),
coord_abs_project(center, small_radius, start_angle)
]
self.node( self.node(
node, 'path', node,
'path',
d='M%s A%s 0 %d 1 %s L%s A%s 0 %d 0 %s z' % ( d='M%s A%s 0 %d 1 %s L%s A%s 0 %d 0 %s z' % (
to[0], to[0], coord_dual(radius), int(angle > pi), to[1], to[2],
coord_dual(radius), coord_dual(small_radius), int(angle > pi), to[3]
int(angle > pi), ),
to[1], class_='slice reactive tooltip-trigger'
to[2], )
coord_dual(small_radius),
int(angle > pi),
to[3]),
class_='slice reactive tooltip-trigger')
else: else:
return return
x, y = coord_diff(center, coord_project( x, y = coord_diff(
(radius + small_radius) / 2, start_angle + angle / 2)) center,
coord_project((radius + small_radius) / 2, start_angle + angle / 2)
)
self.graph._static_value(serie_node, val, x, y, metadata, 'middle') self.graph._static_value(serie_node, val, x, y, metadata, 'middle')
self.graph._tooltip_data( self.graph._tooltip_data(
node, val, x, y, "centered", node, val, x, y, "centered", self.graph._x_labels
self.graph._x_labels and self.graph._x_labels[i][0]) and self.graph._x_labels[i][0]
)
def confidence_interval(self, node, x, low, high, width=7): def confidence_interval(self, node, x, low, high, width=7):
if self.graph.horizontal: if self.graph.horizontal:
@ -415,12 +432,17 @@ class Svg(object):
ci = self.node(node, class_="ci") ci = self.node(node, class_="ci")
self.node( self.node(
ci, 'path', d="M%s L%s M%s L%s M%s L%s L%s M%s L%s" % tuple( ci,
map(fmt, ( 'path',
top, shr(top), top, shl(top), top, d="M%s L%s M%s L%s M%s L%s L%s M%s L%s" % tuple(
bottom, shr(bottom), bottom, shl(bottom) map(
)) fmt, (
), class_='nofill reactive' top, shr(top), top, shl(top), top, bottom, shr(bottom),
bottom, shl(bottom)
)
)
),
class_='nofill reactive'
) )
def pre_render(self): def pre_render(self):
@ -428,26 +450,28 @@ class Svg(object):
self.add_styles() self.add_styles()
self.add_scripts() self.add_scripts()
self.root.set( 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: if self.graph.explicit_size:
self.root.set('width', str(self.graph.width)) self.root.set('width', str(self.graph.width))
self.root.set('height', str(self.graph.height)) self.root.set('height', str(self.graph.height))
def draw_no_data(self): def draw_no_data(self):
"""Write the no data text to the svg""" """Write the no data text to the svg"""
no_data = self.node(self.graph.nodes['text_overlay'], 'text', no_data = self.node(
x=self.graph.view.width / 2, self.graph.nodes['text_overlay'],
y=self.graph.view.height / 2, 'text',
class_='no_data') x=self.graph.view.width / 2,
y=self.graph.view.height / 2,
class_='no_data'
)
no_data.text = self.graph.no_data_text no_data.text = self.graph.no_data_text
def render(self, is_unicode=False, pretty_print=False): def render(self, is_unicode=False, pretty_print=False):
"""Last thing to do before rendering""" """Last thing to do before rendering"""
for f in self.graph.xml_filters: for f in self.graph.xml_filters:
self.root = f(self.root) self.root = f(self.root)
args = { args = {'encoding': 'utf-8'}
'encoding': 'utf-8'
}
svg = b'' svg = b''
if etree.lxml: if etree.lxml:
@ -457,14 +481,12 @@ class Svg(object):
svg = b"<?xml version='1.0' encoding='utf-8'?>\n" svg = b"<?xml version='1.0' encoding='utf-8'?>\n"
if not self.graph.disable_xml_declaration: if not self.graph.disable_xml_declaration:
svg += b'\n'.join( svg += b'\n'.join([
[etree.tostring( etree.tostring(pi, **args)
pi, **args) for pi in self.processing_instructions
for pi in self.processing_instructions] ])
)
svg += etree.tostring( svg += etree.tostring(self.root, **args)
self.root, **args)
if self.graph.disable_xml_declaration or is_unicode: if self.graph.disable_xml_declaration or is_unicode:
svg = svg.decode('utf-8') svg = svg.decode('utf-8')
@ -472,16 +494,17 @@ class Svg(object):
def get_strokes(self): def get_strokes(self):
"""Return a css snippet containing all stroke style options""" """Return a css snippet containing all stroke style options"""
def stroke_dict_to_css(stroke, i=None): def stroke_dict_to_css(stroke, i=None):
"""Return a css style for the given option""" """Return a css style for the given option"""
css = ['%s.series%s {\n' % ( css = [
self.id, '.serie-%d' % i if i is not None else '')] '%s.series%s {\n' %
for key in ( (self.id, '.serie-%d' % i if i is not None else '')
'width', 'linejoin', 'linecap', ]
'dasharray', 'dashoffset'): for key in ('width', 'linejoin', 'linecap', 'dasharray',
'dashoffset'):
if stroke.get(key): if stroke.get(key):
css.append(' stroke-%s: %s;\n' % ( css.append(' stroke-%s: %s;\n' % (key, stroke[key]))
key, stroke[key]))
css.append('}') css.append('}')
return '\n'.join(css) return '\n'.join(css)
@ -494,6 +517,9 @@ class Svg(object):
for secondary_serie in self.graph.secondary_series: for secondary_serie in self.graph.secondary_series:
if secondary_serie.stroke_style is not None: if secondary_serie.stroke_style is not None:
css.append(stroke_dict_to_css( css.append(
secondary_serie.stroke_style, secondary_serie.index)) stroke_dict_to_css(
secondary_serie.stroke_style, secondary_serie.index
)
)
return '\n'.join(css) return '\n'.join(css)

26
pygal/table.py

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

14
pygal/test/__init__.py

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

9
pygal/test/conftest.py

@ -16,7 +16,6 @@
# #
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""pytest fixtures""" """pytest fixtures"""
import sys import sys
@ -54,8 +53,6 @@ def pytest_generate_tests(metafunc):
metafunc.parametrize("Chart", pygal.CHARTS) metafunc.parametrize("Chart", pygal.CHARTS)
if "datas" in metafunc.funcargnames: if "datas" in metafunc.funcargnames:
metafunc.parametrize( metafunc.parametrize(
"datas", "datas", [[("Serie %d" % i, get_data(i)) for i in range(s)]
[ for s in (5, 1, 0)]
[("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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Bar chart related tests""" """Bar chart related tests"""
from pygal import Bar 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Box chart related tests""" """Box chart related tests"""
from pygal.graph.box import Box from pygal.graph.box import Box
@ -26,7 +25,8 @@ def test_quartiles():
"""Test box points for the 1.5IQR computation method""" """Test box points for the 1.5IQR computation method"""
a = [-2.0, 3.0, 4.0, 5.0, 8.0] # odd test data 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( (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 q1 == 7.0 / 4.0
assert q2 == 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 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( (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 assert q2 == 5.0
c = [2.0, None, 4.0, 6.0, None] # odd with None elements 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( (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 assert q2 == 4.0
d = [4] d = [4]
(min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( (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 q0 == 4
assert q1 == 4 assert q1 == 4
@ -61,7 +64,8 @@ def test_quartiles_min_extremes():
"""Test box points for the extremes computation method""" """Test box points for the extremes computation method"""
a = [-2.0, 3.0, 4.0, 5.0, 8.0] # odd test data 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( (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 q1 == 7.0 / 4.0
assert q2 == 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 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( (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(
b, mode='extremes') b, mode='extremes'
)
assert q2 == 5.0 assert q2 == 5.0
c = [2.0, None, 4.0, 6.0, None] # odd with None elements 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( (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(
c, mode='extremes') c, mode='extremes'
)
assert q2 == 4.0 assert q2 == 4.0
d = [4] d = [4]
(min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(
d, mode='extremes') d, mode='extremes'
)
assert q0 == 4 assert q0 == 4
assert q1 == 4 assert q1 == 4
@ -96,14 +103,16 @@ def test_quartiles_tukey():
"""Test box points for the tukey computation method""" """Test box points for the tukey computation method"""
a = [] # empty data a = [] # empty data
(min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( (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 min_s == q0 == q1 == q2 == q3 == q4 == 0
assert outliers == [] assert outliers == []
# https://en.wikipedia.org/wiki/Quartile example 1 # https://en.wikipedia.org/wiki/Quartile example 1
b = [6, 7, 15, 36, 39, 40, 41, 42, 43, 47, 49] 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( (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 min_s == q0 == 6
assert q1 == 20.25 assert q1 == 20.25
assert q2 == 40 assert q2 == 40
@ -114,7 +123,8 @@ def test_quartiles_tukey():
# previous test with added outlier 75 # previous test with added outlier 75
c = [6, 7, 15, 36, 39, 40, 41, 42, 43, 47, 49, 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( (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 min_s == q0 == 6
assert q1 == 25.5 assert q1 == 25.5
assert q2 == (40 + 41) / 2.0 assert q2 == (40 + 41) / 2.0
@ -125,7 +135,8 @@ def test_quartiles_tukey():
# one more outlier, 77 # one more outlier, 77
c = [6, 7, 15, 36, 39, 40, 41, 42, 43, 47, 49, 75, 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( (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 min_s == q0 == 6
assert q1 == 30.75 assert q1 == 30.75
assert q2 == 41 assert q2 == 41
@ -137,11 +148,14 @@ def test_quartiles_tukey():
def test_quartiles_stdev(): def test_quartiles_stdev():
"""Test box points for the stdev computation method""" """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, a = [
51, 45, 29, 25, 26, 54, 61, 27, 40, 23, 34, 51, 37] 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 SD = 14.67
(min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( (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 min_s == min(a)
assert max_s == max(a) assert max_s == max(a)
assert q2 == 36.5 assert q2 == 36.5
@ -151,7 +165,8 @@ def test_quartiles_stdev():
b = [5] # test for posible zero division b = [5] # test for posible zero division
(min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( (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 min_s == q0 == q1 == q2 == q3 == q4 == max_s == b[0]
assert outliers == [] 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Color utility functions tests""" """Color utility functions tests"""
from __future__ import division from __future__ import division
from pygal.colors import ( from pygal.colors import (
darken, desaturate, hsl_to_rgb, lighten, parse_color, rgb_to_hsl, rotate, darken, desaturate, hsl_to_rgb, lighten, parse_color, rgb_to_hsl, rotate,
saturate, unparse_color) saturate, unparse_color
)
def test_parse_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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Various config options tested on one chart type or more""" """Various config options tested on one chart type or more"""
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
@ -25,7 +24,8 @@ from pygal import (
XY, Bar, Box, Config, DateLine, DateTimeLine, Dot, Funnel, Gauge, XY, Bar, Box, Config, DateLine, DateTimeLine, Dot, Funnel, Gauge,
Histogram, HorizontalBar, HorizontalLine, HorizontalStackedBar, Histogram, HorizontalBar, HorizontalLine, HorizontalStackedBar,
HorizontalStackedLine, Line, Pie, Pyramid, Radar, SolidGauge, HorizontalStackedLine, Line, Pie, Pyramid, Radar, SolidGauge,
TimeDeltaLine, TimeLine, Treemap, formatters) TimeDeltaLine, TimeLine, Treemap, formatters
)
from pygal._compat import _ellipsis, u from pygal._compat import _ellipsis, u
from pygal.graph.dual import Dual from pygal.graph.dual import Dual
from pygal.graph.horizontal import HorizontalGraph from pygal.graph.horizontal import HorizontalGraph
@ -59,7 +59,8 @@ def test_config_behaviours():
fill=True, fill=True,
pretty_print=True, pretty_print=True,
no_prefix=True, no_prefix=True,
x_labels=['a', 'b', 'c']) x_labels=['a', 'b', 'c']
)
line2.add('_', [1, 2, 3]) line2.add('_', [1, 2, 3])
l2 = line2.render() l2 = line2.render()
assert l1 == l2 assert l1 == l2
@ -99,6 +100,7 @@ def test_config_behaviours():
def test_config_alterations_class(): def test_config_alterations_class():
"""Assert a config can be changed on config class""" """Assert a config can be changed on config class"""
class LineConfig(Config): class LineConfig(Config):
no_prefix = True no_prefix = True
show_legend = False show_legend = False
@ -122,6 +124,7 @@ def test_config_alterations_class():
def test_config_alterations_instance(): def test_config_alterations_instance():
"""Assert a config can be changed on instance""" """Assert a config can be changed on instance"""
class LineConfig(Config): class LineConfig(Config):
no_prefix = True no_prefix = True
show_legend = False show_legend = False
@ -146,6 +149,7 @@ def test_config_alterations_instance():
def test_config_alterations_kwargs(): def test_config_alterations_kwargs():
"""Assert a config can be changed with keyword args""" """Assert a config can be changed with keyword args"""
class LineConfig(Config): class LineConfig(Config):
no_prefix = True no_prefix = True
show_legend = False show_legend = False
@ -181,7 +185,7 @@ def test_config_alterations_kwargs():
def test_logarithmic(): def test_logarithmic():
"""Test logarithmic option""" """Test logarithmic option"""
line = Line(logarithmic=True) line = Line(logarithmic=True)
line.add('_', [1, 10 ** 10, 1]) line.add('_', [1, 10**10, 1])
q = line.render_pyquery() q = line.render_pyquery()
assert len(q(".axis.x")) == 0 assert len(q(".axis.x")) == 0
assert len(q(".axis.y")) == 1 assert len(q(".axis.y")) == 1
@ -227,7 +231,7 @@ def test_logarithmic_bad_interpolation():
def test_logarithmic_big_scale(): def test_logarithmic_big_scale():
"""Test logarithmic option with a large range of value""" """Test logarithmic option with a large range of value"""
line = Line(logarithmic=True) line = Line(logarithmic=True)
line.add('_', [10 ** -10, 10 ** 10, 1]) line.add('_', [10**-10, 10**10, 1])
q = line.render_pyquery() q = line.render_pyquery()
assert len(q(".y.axis .guides")) == 21 assert len(q(".y.axis .guides")) == 21
@ -235,17 +239,20 @@ def test_logarithmic_big_scale():
def test_value_formatter(): def test_value_formatter():
"""Test value formatter option""" """Test value formatter option"""
line = Line(value_formatter=lambda x: str(x) + u('')) 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() q = line.render_pyquery()
assert len(q(".y.axis .guides")) == 11 assert len(q(".y.axis .guides")) == 11
assert q(".axis.y text").map(texts) == list(map( assert q(".axis.y text").map(texts) == list(
lambda x: str(x) + u(''), map(float, range(20000, 240000, 20000)))) map(
lambda x: str(x) + u(''), map(float, range(20000, 240000, 20000))
)
)
def test_logarithmic_small_scale(): def test_logarithmic_small_scale():
"""Test logarithmic with a small range of values""" """Test logarithmic with a small range of values"""
line = Line(logarithmic=True) 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() q = line.render_pyquery()
assert len(q(".y.axis .guides")) == 11 assert len(q(".y.axis .guides")) == 11
@ -253,16 +260,18 @@ def test_logarithmic_small_scale():
def test_human_readable(): def test_human_readable():
"""Test human readable option""" """Test human readable option"""
line = Line() line = Line()
line.add('_', [10 ** 4, 10 ** 5, 23 * 10 ** 4]) line.add('_', [10**4, 10**5, 23 * 10**4])
q = line.render_pyquery() q = line.render_pyquery()
assert q(".axis.y text").map(texts) == list(map( assert q(".axis.y text").map(texts) == list(
str, range(20000, 240000, 20000))) map(str, range(20000, 240000, 20000))
)
line.value_formatter = formatters.human_readable line.value_formatter = formatters.human_readable
q = line.render_pyquery() q = line.render_pyquery()
assert q(".axis.y text").map(texts) == list(map( assert q(".axis.y text").map(texts) == list(
lambda x: '%dk' % x, range(20, 240, 20))) map(lambda x: '%dk' % x, range(20, 240, 20))
)
def test_show_legend(): def test_show_legend():
@ -300,9 +309,8 @@ def test_no_data():
def test_include_x_axis(Chart): def test_include_x_axis(Chart):
"""Test x axis inclusion option""" """Test x axis inclusion option"""
chart = Chart() chart = Chart()
if Chart in ( if Chart in (Pie, Treemap, Radar, Funnel, Dot, Gauge, Histogram, Box,
Pie, Treemap, Radar, Funnel, Dot, Gauge, Histogram, Box, SolidGauge SolidGauge) or issubclass(Chart, BaseMap):
) or issubclass(Chart, BaseMap):
return return
if not chart._dual: if not chart._dual:
data = 100, 200, 150 data = 100, 200, 150
@ -312,7 +320,8 @@ def test_include_x_axis(Chart):
q = chart.render_pyquery() q = chart.render_pyquery()
# Ghost thing # Ghost thing
yaxis = ".axis.%s .guides text" % ( 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): if not isinstance(chart, Bar):
assert '0' not in q(yaxis).map(texts) assert '0' not in q(yaxis).map(texts)
else: else:
@ -386,9 +395,11 @@ def test_legend_at_bottom(Chart):
def test_x_y_title(Chart): def test_x_y_title(Chart):
"""Test x title and y title options""" """Test x title and y title options"""
chart = Chart(title='I Am A Title', chart = Chart(
x_title="I am a x title", title='I Am A Title',
y_title="I am a y 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('1', [4, -5, 123, 59, 38])
chart.add('2', [89, 0, 8, .12, 8]) chart.add('2', [89, 0, 8, .12, 8])
q = chart.render_pyquery() q = chart.render_pyquery()
@ -397,9 +408,7 @@ def test_x_y_title(Chart):
def test_range(Chart): def test_range(Chart):
"""Test y label major option""" """Test y label major option"""
if Chart in ( if Chart in (Pie, Treemap, Dot, SolidGauge) or issubclass(Chart, BaseMap):
Pie, Treemap, Dot, SolidGauge
) or issubclass(Chart, BaseMap):
return return
chart = Chart() chart = Chart()
chart.range = (0, 100) chart.range = (0, 100)
@ -414,11 +423,10 @@ def test_range(Chart):
def test_x_label_major(Chart): def test_x_label_major(Chart):
"""Test x label major option""" """Test x label major option"""
if Chart in ( if Chart in (Pie, Treemap, Funnel, Dot, Gauge, Histogram, Box, SolidGauge,
Pie, Treemap, Funnel, Dot, Gauge, Histogram, Box, SolidGauge, Pyramid, DateTimeLine, TimeLine, DateLine,
Pyramid, DateTimeLine, TimeLine, DateLine, TimeDeltaLine) or issubclass(
TimeDeltaLine Chart, (BaseMap, Dual, HorizontalGraph)):
) or issubclass(Chart, (BaseMap, Dual, HorizontalGraph)):
return return
chart = Chart() chart = Chart()
chart.add('test', range(12)) chart.add('test', range(12))
@ -459,13 +467,10 @@ def test_x_label_major(Chart):
def test_y_label_major(Chart): def test_y_label_major(Chart):
"""Test y label major option""" """Test y label major option"""
if Chart in ( if Chart in (Pie, Treemap, Funnel, Dot, Gauge, Histogram, Box, SolidGauge,
Pie, Treemap, Funnel, Dot, Gauge, Histogram, Box, SolidGauge, HorizontalBar, HorizontalStackedBar, HorizontalStackedLine,
HorizontalBar, HorizontalStackedBar, HorizontalLine, Pyramid, DateTimeLine, TimeLine, DateLine,
HorizontalStackedLine, HorizontalLine, TimeDeltaLine) or issubclass(Chart, BaseMap):
Pyramid, DateTimeLine, TimeLine, DateLine,
TimeDeltaLine
) or issubclass(Chart, BaseMap):
return return
chart = Chart() chart = Chart()
data = range(12) data = range(12)
@ -529,24 +534,24 @@ def test_render_data_uri(Chart):
chart = Chart(fill=True) chart = Chart(fill=True)
chart.add(u('ééé'), [1, 2, 3]) chart.add(u('ééé'), [1, 2, 3])
chart.add(u('èèè'), [10, 21, 5]) chart.add(u('èèè'), [10, 21, 5])
assert chart.render_data_uri().startswith( assert chart.render_data_uri(
'data:image/svg+xml;charset=utf-8;base64,') ).startswith('data:image/svg+xml;charset=utf-8;base64,')
def test_formatters(Chart): def test_formatters(Chart):
"""Test custom formatters""" """Test custom formatters"""
if Chart._dual or Chart == Box: if Chart._dual or Chart == Box:
return return
chart = Chart( chart = Chart(formatter=lambda x, chart, serie: '%s%s$' % (x, serie.title))
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('_a', [1, 2, {'value': 3, 'formatter': lambda x: u('%s¥') % x}])
chart.add('_b', [4, 5, 6], 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 = [2, 4, 6]
chart.x_labels_major = [4] chart.x_labels_major = [4]
q = chart.render_pyquery() q = chart.render_pyquery()
assert set([v.text for v in q(".value")]) == set(( assert set(
u('4€'), u('5€'), u('6€'), '1_a$', '2_a$', u('')) + ( [v.text for v in q(".value")]
('6_a$', u('15€')) if Chart in (Pie, SolidGauge) else ())) ) == 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): def test_classes(Chart):
@ -557,10 +562,10 @@ def test_classes(Chart):
chart = Chart(classes=()) chart = Chart(classes=())
assert not chart.render_pyquery().attr('class') assert not chart.render_pyquery().attr('class')
chart = Chart(classes=(_ellipsis,)) chart = Chart(classes=(_ellipsis, ))
assert chart.render_pyquery().attr('class') == 'pygal-chart' assert chart.render_pyquery().attr('class') == 'pygal-chart'
chart = Chart(classes=('graph',)) chart = Chart(classes=('graph', ))
assert chart.render_pyquery().attr('class') == 'graph' assert chart.render_pyquery().attr('class') == 'graph'
chart = Chart(classes=('pygal-chart', '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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Date related charts tests""" """Date related charts tests"""
from datetime import date, datetime, time, timedelta from datetime import date, datetime, time, timedelta
@ -29,149 +28,114 @@ from pygal.test.utils import texts
def test_date(): def test_date():
"""Test a simple dateline""" """Test a simple dateline"""
date_chart = DateLine(truncate_label=1000) date_chart = DateLine(truncate_label=1000)
date_chart.add('dates', [ date_chart.add(
(date(2013, 1, 2), 300), 'dates', [(date(2013, 1, 2), 300), (date(2013, 1, 12), 412),
(date(2013, 1, 12), 412), (date(2013, 2, 2), 823), (date(2013, 2, 22), 672)]
(date(2013, 2, 2), 823), )
(date(2013, 2, 22), 672)
])
q = date_chart.render_pyquery() q = date_chart.render_pyquery()
assert list( assert list(map(lambda t: t.split(' ')[0],
map(lambda t: t.split(' ')[0], q(".axis.x text").map(texts))) == [
q(".axis.x text").map(texts))) == [ '2013-01-12', '2013-01-24', '2013-02-04', '2013-02-16'
'2013-01-12', ]
'2013-01-24',
'2013-02-04',
'2013-02-16']
def test_time(): def test_time():
"""Test a simple timeline""" """Test a simple timeline"""
time_chart = TimeLine(truncate_label=1000) time_chart = TimeLine(truncate_label=1000)
time_chart.add('times', [ time_chart.add(
(time(1, 12, 29), 2), 'times', [(time(1, 12, 29), 2), (time(21, 2, 29), 10),
(time(21, 2, 29), 10), (time(12, 30, 59), 7)]
(time(12, 30, 59), 7) )
])
q = time_chart.render_pyquery() q = time_chart.render_pyquery()
assert list( assert list(map(lambda t: t.split(' ')[0],
map(lambda t: t.split(' ')[0], q(".axis.x text").map(texts))) == [
q(".axis.x text").map(texts))) == [ '02:46:40', '05:33:20', '08:20:00', '11:06:40',
'02:46:40', '13:53:20', '16:40:00', '19:26:40'
'05:33:20', ]
'08:20:00',
'11:06:40',
'13:53:20',
'16:40:00',
'19:26:40']
def test_datetime(): def test_datetime():
"""Test a simple datetimeline""" """Test a simple datetimeline"""
datetime_chart = DateTimeLine(truncate_label=1000) datetime_chart = DateTimeLine(truncate_label=1000)
datetime_chart.add('datetimes', [ datetime_chart.add(
(datetime(2013, 1, 2, 1, 12, 29), 300), 'datetimes',
(datetime(2013, 1, 12, 21, 2, 29), 412), [(datetime(2013, 1, 2, 1, 12, 29), 300),
(datetime(2013, 2, 2, 12, 30, 59), 823), (datetime(2013, 1, 12, 21, 2, 29), 412),
(datetime(2013, 2, 22), 672) (datetime(2013, 2, 2, 12, 30, 59), 823), (datetime(2013, 2, 22), 672)]
]) )
q = datetime_chart.render_pyquery() q = datetime_chart.render_pyquery()
assert list( assert list(map(lambda t: t.split(' ')[0],
map(lambda t: t.split(' ')[0], q(".axis.x text").map(texts))) == [
q(".axis.x text").map(texts))) == [ '2013-01-12T14:13:20', '2013-01-24T04:00:00',
'2013-01-12T14:13:20', '2013-02-04T17:46:40', '2013-02-16T07:33:20'
'2013-01-24T04:00:00', ]
'2013-02-04T17:46:40',
'2013-02-16T07:33:20']
def test_timedelta(): def test_timedelta():
"""Test a simple timedeltaline""" """Test a simple timedeltaline"""
timedelta_chart = TimeDeltaLine(truncate_label=1000) timedelta_chart = TimeDeltaLine(truncate_label=1000)
timedelta_chart.add('timedeltas', [ timedelta_chart.add(
(timedelta(seconds=1), 10), 'timedeltas', [
(timedelta(weeks=1), 50), (timedelta(seconds=1), 10),
(timedelta(hours=3, seconds=30), 3), (timedelta(weeks=1), 50),
(timedelta(microseconds=12112), .3), (timedelta(hours=3, seconds=30), 3),
]) (timedelta(microseconds=12112), .3),
]
)
q = timedelta_chart.render_pyquery() q = timedelta_chart.render_pyquery()
assert list( assert list(t for t in q(".axis.x text").map(texts) if t != '0:00:00') == [
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'
'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(): def test_date_xrange():
"""Test dateline with xrange""" """Test dateline with xrange"""
datey = DateLine(truncate_label=1000) datey = DateLine(truncate_label=1000)
datey.add('dates', [ datey.add(
(date(2013, 1, 2), 300), 'dates', [(date(2013, 1, 2), 300), (date(2013, 1, 12), 412),
(date(2013, 1, 12), 412), (date(2013, 2, 2), 823), (date(2013, 2, 22), 672)]
(date(2013, 2, 2), 823), )
(date(2013, 2, 22), 672)
])
datey.xrange = (date(2013, 1, 1), date(2013, 3, 1)) datey.xrange = (date(2013, 1, 1), date(2013, 3, 1))
q = datey.render_pyquery() q = datey.render_pyquery()
assert list( assert list(map(lambda t: t.split(' ')[0],
map(lambda t: t.split(' ')[0], q(".axis.x text").map(texts))) == [
q(".axis.x text").map(texts))) == [ '2013-01-01', '2013-01-12', '2013-01-24', '2013-02-04',
'2013-01-01', '2013-02-16', '2013-02-27'
'2013-01-12', ]
'2013-01-24',
'2013-02-04',
'2013-02-16',
'2013-02-27']
def test_date_labels(): def test_date_labels():
"""Test dateline with xrange""" """Test dateline with xrange"""
datey = DateLine(truncate_label=1000) datey = DateLine(truncate_label=1000)
datey.add('dates', [ datey.add(
(date(2013, 1, 2), 300), 'dates', [(date(2013, 1, 2), 300), (date(2013, 1, 12), 412),
(date(2013, 1, 12), 412), (date(2013, 2, 2), 823), (date(2013, 2, 22), 672)]
(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.x_labels = [
date(2013, 1, 1),
date(2013, 2, 1),
date(2013, 3, 1)
]
q = datey.render_pyquery() q = datey.render_pyquery()
assert list( assert list(map(lambda t: t.split(' ')[0],
map(lambda t: t.split(' ')[0], q(".axis.x text").map(texts))) == [
q(".axis.x text").map(texts))) == [ '2013-01-01', '2013-02-01', '2013-03-01'
'2013-01-01', ]
'2013-02-01',
'2013-03-01']
def test_utc_timestamping(): def test_utc_timestamping():
assert timestamp( assert timestamp(datetime(2017, 7, 14, 2,
datetime(2017, 7, 14, 2, 40).replace(tzinfo=utc) 40).replace(tzinfo=utc)) == 1500000000
) == 1500000000
for d in (datetime.now(), datetime.utcnow(), datetime(
for d in ( 1999, 12, 31, 23, 59, 59), datetime(2000, 1, 1, 0, 0, 0)):
datetime.now(), assert datetime.utcfromtimestamp(timestamp(d)
datetime.utcnow(), ) - d < timedelta(microseconds=10)
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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Test formatters""" """Test formatters"""
from pygal import 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Generate tests for different chart types with different data""" """Generate tests for different chart types with different data"""
import io import io
@ -82,26 +81,44 @@ def test_metadata(Chart):
"""Test metadata values""" """Test metadata values"""
chart = Chart() chart = Chart()
v = range(7) v = range(7)
if Chart in (pygal.Box,): if Chart in (pygal.Box, ):
return # summary charts cannot display per-value metadata return # summary charts cannot display per-value metadata
elif Chart == pygal.XY: elif Chart == pygal.XY:
v = list(map(lambda x: (x, x + 1), v)) v = list(map(lambda x: (x, x + 1), v))
elif issubclass(Chart, BaseMap): elif issubclass(Chart, BaseMap):
v = [(k, i) for i, k in enumerate(Chart.x_labels) if k not in [ v = [(k, i) for i, k in enumerate(Chart.x_labels)
'oecd', 'nafta', 'eur']] if k not in ['oecd', 'nafta', 'eur']]
chart.add('Serie with metadata', [ chart.add(
v[0], 'Serie with metadata', [
{'value': v[1]}, v[0], {
{'value': v[2], 'label': 'Three'}, 'value': v[1]
{'value': v[3], 'xlink': 'http://4.example.com/'}, }, {
{'value': v[4], 'xlink': 'http://5.example.com/', 'label': 'Five'}, 'value': v[2],
{'value': v[5], 'xlink': { 'label': 'Three'
'href': 'http://6.example.com/'}, 'label': 'Six'}, }, {
{'value': v[6], 'xlink': { 'value': v[3],
'href': 'http://7.example.com/', 'xlink': 'http://4.example.com/'
'target': '_blank'}, 'label': 'Seven'} }, {
]) '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() q = chart.render_pyquery()
for md in ('Three', 'Five', 'Seven'): for md in ('Three', 'Five', 'Seven'):
assert md in cut(q('desc'), 'text') 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): def test_unicode_labels_decode(Chart):
"""Test unicode labels""" """Test unicode labels"""
chart = Chart() chart = Chart()
chart.add(u('Série1'), [{ chart.add(
'value': 1, u('Série1'), [{
'xlink': 'http://1/', 'value': 1,
'label': u('{\}°ijæð©&×&<—×€¿_…\{_…') 'xlink': 'http://1/',
}, { 'label': u('{\}°ijæð©&×&<—×€¿_…\{_…')
'value': 2, }, {
'xlink': { 'value': 2,
'href': 'http://6.example.com/' 'xlink': {
}, 'href': 'http://6.example.com/'
'label': u('æ°€≠|€æ°€əæ') },
}, { 'label': u('æ°€≠|€æ°€əæ')
'value': 3, }, {
'label': 'unicode <3' 'value': 3,
}]) 'label': 'unicode <3'
}]
)
if not chart._dual: if not chart._dual:
chart.x_labels = [u(''), u('¿?'), u('††††††††'), 'unicode <3'] chart.x_labels = [u(''), u('¿?'), u('††††††††'), 'unicode <3']
chart.render_pyquery() chart.render_pyquery()
@ -313,20 +332,22 @@ def test_unicode_labels_python2(Chart):
if sys.version_info[0] == 3: if sys.version_info[0] == 3:
return return
chart = Chart() chart = Chart()
chart.add(u('Série1'), [{ chart.add(
'value': 1, u('Série1'), [{
'xlink': 'http://1/', 'value': 1,
'label': eval("u'{\}°ijæð©&×&<—×€¿_…\{_…'") 'xlink': 'http://1/',
}, { 'label': eval("u'{\}°ijæð©&×&<—×€¿_…\{_…'")
'value': 2, }, {
'xlink': { 'value': 2,
'href': 'http://6.example.com/' 'xlink': {
}, 'href': 'http://6.example.com/'
'label': eval("u'æ°€≠|€æ°€əæ'") },
}, { 'label': eval("u'æ°€≠|€æ°€əæ'")
'value': 3, }, {
'label': eval("'unicode <3'") 'value': 3,
}]) 'label': eval("'unicode <3'")
}]
)
if not chart._dual: if not chart._dual:
chart.x_labels = eval("[u'', u'¿?', u'††††††††', 'unicode <3']") chart.x_labels = eval("[u'', u'¿?', u'††††††††', 'unicode <3']")
chart.render_pyquery() chart.render_pyquery()
@ -337,20 +358,22 @@ def test_unicode_labels_python3(Chart):
if sys.version_info[0] == 2: if sys.version_info[0] == 2:
return return
chart = Chart() chart = Chart()
chart.add(u('Série1'), [{ chart.add(
'value': 1, u('Série1'), [{
'xlink': 'http://1/', 'value': 1,
'label': eval("'{\}°ijæð©&×&<—×€¿_…\{_…'") 'xlink': 'http://1/',
}, { 'label': eval("'{\}°ijæð©&×&<—×€¿_…\{_…'")
'value': 2, }, {
'xlink': { 'value': 2,
'href': 'http://6.example.com/' 'xlink': {
}, 'href': 'http://6.example.com/'
'label': eval("'æ°€≠|€æ°€əæ'") },
}, { 'label': eval("'æ°€≠|€æ°€əæ'")
'value': 3, }, {
'label': eval("b'unicode <3'") 'value': 3,
}]) 'label': eval("b'unicode <3'")
}]
)
if not chart._dual: if not chart._dual:
chart.x_labels = eval("['', '¿?', '††††††††', 'unicode <3']") chart.x_labels = eval("['', '¿?', '††††††††', 'unicode <3']")
chart.render_pyquery() chart.render_pyquery()
@ -361,31 +384,49 @@ def test_labels_with_links(Chart):
chart = Chart() chart = Chart()
# link on chart and label # link on chart and label
chart.add({ chart.add({
'title': 'Red', 'xlink': {'href': 'http://en.wikipedia.org/wiki/Red'} 'title': 'Red',
'xlink': {
'href': 'http://en.wikipedia.org/wiki/Red'
}
}, [{ }, [{
'value': 2, 'value': 2,
'label': 'This is red', 'label': 'This is red',
'xlink': {'href': 'http://en.wikipedia.org/wiki/Red'}}]) 'xlink': {
'href': 'http://en.wikipedia.org/wiki/Red'
}
}])
# link on chart only # link on chart only
chart.add('Green', [{ chart.add(
'value': 4, 'Green', [{
'label': 'This is green', 'value': 4,
'xlink': { 'label': 'This is green',
'href': 'http://en.wikipedia.org/wiki/Green', 'xlink': {
'target': '_top'}}]) 'href': 'http://en.wikipedia.org/wiki/Green',
'target': '_top'
}
}]
)
# link on label only opens in new tab # link on label only opens in new tab
chart.add({'title': 'Yellow', 'xlink': { chart.add({
'href': 'http://en.wikipedia.org/wiki/Yellow', 'title': 'Yellow',
'target': '_blank'}}, 7) 'xlink': {
'href': 'http://en.wikipedia.org/wiki/Yellow',
'target': '_blank'
}
}, 7)
# link on chart only # link on chart only
chart.add('Blue', [{ chart.add(
'value': 5, 'Blue', [{
'xlink': { 'value': 5,
'href': 'http://en.wikipedia.org/wiki/Blue', 'xlink': {
'target': '_blank'}}]) 'href': 'http://en.wikipedia.org/wiki/Blue',
'target': '_blank'
}
}]
)
# link on label and chart with diffrent behaviours # link on label and chart with diffrent behaviours
chart.add({ chart.add({
@ -396,7 +437,9 @@ def test_labels_with_links(Chart):
'label': 'This is violet', 'label': 'This is violet',
'xlink': { 'xlink': {
'href': 'http://en.wikipedia.org/wiki/Violet_(color)', 'href': 'http://en.wikipedia.org/wiki/Violet_(color)',
'target': '_self'}}]) 'target': '_self'
}
}])
q = chart.render_pyquery() q = chart.render_pyquery()
links = q('a') links = q('a')
@ -416,9 +459,7 @@ def test_secondary(Chart):
chart = Chart() chart = Chart()
rng = [83, .12, -34, 59] rng = [83, .12, -34, 59]
chart.add('First serie', rng) chart.add('First serie', rng)
chart.add('Secondary serie', chart.add('Secondary serie', map(lambda x: x * 2, rng), secondary=True)
map(lambda x: x * 2, rng),
secondary=True)
assert chart.render_pyquery() 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, " "'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 " "lines in a line chart, or slices in a pie chart'. A chart can "
"represent tabular numeric data, functions or some kinds of " "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) chart = make_data(chart, datas)
q = chart.render_pyquery() q = chart.render_pyquery()
assert len(q('.titles text')) == 5 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Histogram chart related tests""" """Histogram chart related tests"""
from pygal import Histogram from pygal import Histogram
def test_histogram(): def test_histogram():
"""Simple histogram test""" """Simple histogram test"""
hist = Histogram() hist = Histogram()
hist.add('1', [ hist.add('1', [(2, 0, 1), (4, 1, 3), (3, 3.5, 5), (1.5, 5, 10)])
(2, 0, 1),
(4, 1, 3),
(3, 3.5, 5),
(1.5, 5, 10)
])
hist.add('2', [(2, 2, 8)], secondary=True) hist.add('2', [(2, 2, 8)], secondary=True)
q = hist.render_pyquery() q = hist.render_pyquery()
assert len(q('.rect')) == 5 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Interpolations tests""" """Interpolations tests"""
from pygal.test import make_data from pygal.test import make_data
@ -70,44 +69,75 @@ def test_hermite(Chart, datas):
def test_hermite_finite(Chart, datas): def test_hermite_finite(Chart, datas):
"""Test hermite finite difference interpolation""" """Test hermite finite difference interpolation"""
chart = Chart(interpolate='hermite', chart = Chart(
interpolation_parameters={'type': 'finite_difference'}) interpolate='hermite',
interpolation_parameters={
'type': 'finite_difference'
}
)
chart = make_data(chart, datas) chart = make_data(chart, datas)
assert chart.render() assert chart.render()
def test_hermite_cardinal(Chart, datas): def test_hermite_cardinal(Chart, datas):
"""Test hermite cardinal interpolation""" """Test hermite cardinal interpolation"""
chart = Chart(interpolate='hermite', chart = Chart(
interpolation_parameters={'type': 'cardinal', 'c': .75}) interpolate='hermite',
interpolation_parameters={
'type': 'cardinal',
'c': .75
}
)
chart = make_data(chart, datas) chart = make_data(chart, datas)
assert chart.render() assert chart.render()
def test_hermite_catmull_rom(Chart, datas): def test_hermite_catmull_rom(Chart, datas):
"""Test hermite catmull rom interpolation""" """Test hermite catmull rom interpolation"""
chart = Chart(interpolate='hermite', chart = Chart(
interpolation_parameters={'type': 'catmull_rom'}) interpolate='hermite',
interpolation_parameters={
'type': 'catmull_rom'
}
)
chart = make_data(chart, datas) chart = make_data(chart, datas)
assert chart.render() assert chart.render()
def test_hermite_kochanek_bartels(Chart, datas): def test_hermite_kochanek_bartels(Chart, datas):
"""Test hermite kochanek bartels interpolation""" """Test hermite kochanek bartels interpolation"""
chart = Chart(interpolate='hermite', chart = Chart(
interpolation_parameters={ interpolate='hermite',
'type': 'kochanek_bartels', 'b': -1, 'c': 1, 't': 1}) interpolation_parameters={
'type': 'kochanek_bartels',
'b': -1,
'c': 1,
't': 1
}
)
chart = make_data(chart, datas) chart = make_data(chart, datas)
assert chart.render() assert chart.render()
chart = Chart(interpolate='hermite', chart = Chart(
interpolation_parameters={ interpolate='hermite',
'type': 'kochanek_bartels', 'b': -1, 'c': -8, 't': 0}) interpolation_parameters={
'type': 'kochanek_bartels',
'b': -1,
'c': -8,
't': 0
}
)
chart = make_data(chart, datas) chart = make_data(chart, datas)
assert chart.render() assert chart.render()
chart = Chart(interpolate='hermite', chart = Chart(
interpolation_parameters={ interpolate='hermite',
'type': 'kochanek_bartels', 'b': 0, 'c': 10, 't': -1}) interpolation_parameters={
'type': 'kochanek_bartels',
'b': 0,
'c': 10,
't': -1
}
)
chart = make_data(chart, datas) chart = make_data(chart, datas)
assert chart.render() 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Line chart related tests""" """Line chart related tests"""
from __future__ import division from __future__ import division
@ -45,11 +44,13 @@ def test_simple_line():
assert len(q(".y.axis .guides")) == 13 assert len(q(".y.axis .guides")) == 13
assert len(q(".dots")) == 3 * 13 assert len(q(".dots")) == 3 * 13
assert q(".axis.x text").map(texts) == [ assert q(".axis.x text").map(texts) == [
'-30', '-25', '-20', '-15', '-10', '-5', '-30', '-25', '-20', '-15', '-10', '-5', '0', '5', '10', '15', '20',
'0', '5', '10', '15', '20', '25', '30'] '25', '30'
]
assert q(".axis.y text").map(texts) == [ assert q(".axis.y text").map(texts) == [
'-1.2', '-1', '-0.8', '-0.6', '-0.4', '-0.2', '-1.2', '-1', '-0.8', '-0.6', '-0.4', '-0.2', '0', '0.2', '0.4', '0.6',
'0', '0.2', '0.4', '0.6', '0.8', '1', '1.2'] '0.8', '1', '1.2'
]
assert q(".title").text() == 'cos sin and cos - sin' assert q(".title").text() == 'cos sin and cos - sin'
assert q(".legend text").map(texts) == ['test1', 'test2', 'test3'] 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(".dots")) == 100
assert len(q(".axis.x")) == 1 assert len(q(".axis.x")) == 1
assert q(".axis.x text").map(texts) == [ 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(): def test_int_x_labels():
@ -116,7 +118,8 @@ def test_int_x_labels():
assert len(q(".dots")) == 100 assert len(q(".dots")) == 100
assert len(q(".axis.x")) == 1 assert len(q(".axis.x")) == 1
assert q(".axis.x text").map(texts) == [ 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(): def test_only_major_dots_every():
@ -148,7 +151,7 @@ def test_only_major_dots_count():
def test_only_major_dots(): def test_only_major_dots():
"""Test major dots with specified major labels""" """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.add('test', range(12))
line.x_labels = map(str, range(12)) line.x_labels = map(str, range(12))
line.x_labels_major = ['1', '5', '11'] line.x_labels_major = ['1', '5', '11']
@ -161,9 +164,7 @@ def test_line_secondary():
line = Line() line = Line()
rng = [8, 12, 23, 73, 39, 57] rng = [8, 12, 23, 73, 39, 57]
line.add('First serie', rng) line.add('First serie', rng)
line.add('Secondary serie', line.add('Secondary serie', map(lambda x: x * 2, rng), secondary=True)
map(lambda x: x * 2, rng),
secondary=True)
line.title = "One serie" line.title = "One serie"
q = line.render_pyquery() q = line.render_pyquery()
assert len(q(".axis.x")) == 0 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Map plugins tests are imported here""" """Map plugins tests are imported here"""
import pkg_resources 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Donut chart related tests""" """Donut chart related tests"""
from pygal import Pie 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Test per serie configuration""" """Test per serie configuration"""
from pygal import Line 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Test sparktext rendering""" """Test sparktext rendering"""
from pygal import Bar, Line 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Stacked chart related tests""" """Stacked chart related tests"""
from pygal import StackedLine from pygal import StackedLine
@ -29,7 +28,8 @@ def test_stacked_line():
stacked.add('ten_twelve', [10, 12]) stacked.add('ten_twelve', [10, 12])
q = stacked.render_pyquery() q = stacked.render_pyquery()
assert set([v.text for v in q("desc.value")]) == set( 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(): def test_stacked_line_reverse():
@ -39,7 +39,8 @@ def test_stacked_line_reverse():
stacked.add('ten_twelve', [10, 12]) stacked.add('ten_twelve', [10, 12])
q = stacked.render_pyquery() q = stacked.render_pyquery()
assert set([v.text for v in q("desc.value")]) == set( 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(): def test_stacked_line_log():
@ -49,7 +50,8 @@ def test_stacked_line_log():
stacked.add('ten_twelve', [10, 12]) stacked.add('ten_twelve', [10, 12])
q = stacked.render_pyquery() q = stacked.render_pyquery()
assert set([v.text for v in q("desc.value")]) == set( 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(): def test_stacked_line_interpolate():
@ -59,4 +61,5 @@ def test_stacked_line_interpolate():
stacked.add('ten_twelve', [10, 12]) stacked.add('ten_twelve', [10, 12])
q = stacked.render_pyquery() q = stacked.render_pyquery()
assert set([v.text for v in q("desc.value")]) == set( 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Style related tests""" """Style related tests"""
from pygal import Line from pygal import Line
from pygal.style import ( from pygal.style import (
DarkenStyle, DesaturateStyle, LightenStyle, LightStyle, RotateStyle, DarkenStyle, DesaturateStyle, LightenStyle, LightStyle, RotateStyle,
SaturateStyle) SaturateStyle
)
STYLES = LightenStyle, DarkenStyle, SaturateStyle, DesaturateStyle, RotateStyle STYLES = LightenStyle, DarkenStyle, SaturateStyle, DesaturateStyle, RotateStyle
@ -41,8 +41,9 @@ def test_parametric_styles():
def test_parametric_styles_with_parameters(): def test_parametric_styles_with_parameters():
"""Test a parametric style with parameters""" """Test a parametric style with parameters"""
line = Line(style=RotateStyle( line = Line(
'#de3804', step=12, max_=180, base_style=LightStyle)) style=RotateStyle('#de3804', step=12, max_=180, base_style=LightStyle)
)
line.add('_', [1, 2, 3]) line.add('_', [1, 2, 3])
line.x_labels = 'abc' line.x_labels = 'abc'
assert line.render() 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Box chart related tests""" """Box chart related tests"""
from pyquery import PyQuery as pq 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Utility functions tests""" """Utility functions tests"""
import sys import sys
@ -26,7 +25,8 @@ from pytest import raises
from pygal._compat import _ellipsis, u from pygal._compat import _ellipsis, u
from pygal.util import ( from pygal.util import (
_swap_curly, majorize, mergextend, minify_css, round_to_float, _swap_curly, majorize, mergextend, minify_css, round_to_float,
round_to_int, template, truncate) round_to_int, template, truncate
)
def test_round_to_int(): def test_round_to_int():
@ -54,11 +54,8 @@ def test_round_to_float():
def test_swap_curly(): def test_swap_curly():
"""Test swap curly function""" """Test swap curly function"""
for str in ( for str in ('foo', u('foo foo foo bar'), 'foo béè b¡ð/ijə˘©þß®~¯æ',
'foo', u('foo béè b¡ð/ijə˘©þß®~¯æ')):
u('foo foo foo bar'),
'foo béè b¡ð/ijə˘©þß®~¯æ',
u('foo béè b¡ð/ijə˘©þß®~¯æ')):
assert _swap_curly(str) == str assert _swap_curly(str) == str
assert _swap_curly('foo{bar}baz') == 'foo{{bar}}baz' assert _swap_curly('foo{bar}baz') == 'foo{{bar}}baz'
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): class Object(object):
pass pass
obj = Object() obj = Object()
obj.a = 1 obj.a = 1
obj.b = True obj.b = True
obj.c = '3' obj.c = '3'
assert template( assert template('foo {{ o.a }} {{o.b}}-{{o.c}}', o=obj) == 'foo 1 True-3'
'foo {{ o.a }} {{o.b}}-{{o.c}}',
o=obj) == 'foo 1 True-3'
def test_truncate(): def test_truncate():
@ -119,28 +115,29 @@ def test_minify_css():
''' '''
assert minify_css(css) == ( assert minify_css(css) == (
'.title{font-family:sans;font-size:12}' '.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(): def test_majorize():
"""Test majorize function""" """Test majorize function"""
assert majorize(()) == [] assert majorize(()) == []
assert majorize((0,)) == [] assert majorize((0, )) == []
assert majorize((0, 1)) == [] assert majorize((0, 1)) == []
assert majorize((0, 1, 2)) == [] assert majorize((0, 1, 2)) == []
assert majorize((-1, 0, 1, 2)) == [0] 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, .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((0, .2, .4, .6, .8, 1)) == [0, 1]
assert majorize((-.4, -.2, 0, .2, .4, .6, .8, 1)) == [0, 1] assert majorize((-.4, -.2, 0, .2, .4, .6, .8, 1)) == [0, 1]
assert majorize( assert majorize((-1, -.8, -.6, -.4, -.2, 0, .2, .4, .6, .8,
(-1, -.8, -.6, -.4, -.2, 0, .2, .4, .6, .8, 1)) == [-1, 0, 1] 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)) == [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, .2, .4, .6, .8, 1, 1.2, 1.4, 1.6, 1.8, 2)) == [0, 1, 2]
assert majorize( assert majorize((0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110,
(0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120)) == [0, 50, 100] 120)) == [0, 50, 100]
assert majorize( assert majorize((
(0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36
22, 24, 26, 28, 30, 32, 34, 36)) == [0, 10, 20, 30] )) == [0, 10, 20, 30]
assert majorize((0, 1, 2, 3, 4, 5)) == [0, 5] 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, -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] 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], ['c', 'd']) == ['c', 'd']
assert mergextend([_ellipsis, 'b'], ['c', 'd']) == ['c', 'd', 'b'] assert mergextend([_ellipsis, 'b'], ['c', 'd']) == ['c', 'd', 'b']
assert mergextend(['a', _ellipsis], ['c', 'd']) == ['a', 'c', 'd'] assert mergextend(['a', _ellipsis], ['c', 'd']) == ['a', 'c', 'd']
assert mergextend(['a', _ellipsis, 'b'], ['c', 'd']) == [ assert mergextend(['a', _ellipsis, 'b'],
'a', 'c', 'd', 'b'] ['c', 'd']) == ['a', 'c', 'd', 'b']
if sys.version_info[0] >= 3: if sys.version_info[0] >= 3:
# For @#! sake it's 2016 now # For @#! sake it's 2016 now
assert eval("mergextend(['a', ..., 'b'], ['c', 'd'])") == [ 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""View related tests""" """View related tests"""
# TODO # 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Xml filter tests""" """Xml filter tests"""
from pygal import Bar from pygal import Bar
class ChangeBarsXMLFilter(object): class ChangeBarsXMLFilter(object):
"""xml filter that insert a subplot""" """xml filter that insert a subplot"""
def __init__(self, a, b): def __init__(self, a, b):
@ -32,8 +30,9 @@ class ChangeBarsXMLFilter(object):
def __call__(self, T): def __call__(self, T):
"""Apply the filter on the tree""" """Apply the filter on the tree"""
subplot = Bar(legend_at_bottom=True, explicit_size=True, subplot = Bar(
width=800, height=150) legend_at_bottom=True, explicit_size=True, width=800, height=150
)
subplot.add("Difference", self.data) subplot.add("Difference", self.data)
subplot = subplot.render_tree() subplot = subplot.render_tree()
subplot = subplot.findall("g")[0] subplot = subplot.findall("g")[0]
@ -55,8 +54,9 @@ def test_xml_filters_round_trip():
def test_xml_filters_change_bars(): def test_xml_filters_change_bars():
"""Test the use a xml filter""" """Test the use a xml filter"""
plot = Bar(legend_at_bottom=True, explicit_size=True, plot = Bar(
width=800, height=600) legend_at_bottom=True, explicit_size=True, width=800, height=600
)
A = [60, 75, 80, 78, 83, 90] A = [60, 75, 80, 78, 83, 90]
B = [92, 87, 81, 73, 68, 55] B = [92, 87, 81, 73, 68, 55]
plot.add("A", A) plot.add("A", A)
@ -64,5 +64,5 @@ def test_xml_filters_change_bars():
plot.add_xml_filter(ChangeBarsXMLFilter(A, B)) plot.add_xml_filter(ChangeBarsXMLFilter(A, B))
q = plot.render_tree() q = plot.render_tree()
assert len(q.findall("g")) == 2 assert len(q.findall("g")) == 2
assert q.findall("g")[1].attrib[ assert q.findall("g")[1].attrib["transform"
"transform"] == "translate(0,150), scale(1,0.75)" ] == "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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Tests helpers""" """Tests helpers"""
from pyquery import PyQuery as pq 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Various utility functions""" """Various utility functions"""
from __future__ import division from __future__ import division
@ -42,10 +41,10 @@ def majorize(values):
return [] return []
values_step = sorted_values[1] - sorted_values[0] values_step = sorted_values[1] - sorted_values[0]
full_range = 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: if step == values_step:
step *= 10 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): if round(step * step_factor) % (round(values_step * step_factor) or 1):
# TODO: Find lower common multiple instead # TODO: Find lower common multiple instead
step *= values_step step *= values_step
@ -54,7 +53,8 @@ def majorize(values):
elif full_range >= 5 * step: elif full_range >= 5 * step:
step *= 5 step *= 5
major_values = [ 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] 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): def round_to_float(number, precision):
"""Round a float to a precision""" """Round a float to a precision"""
rounded = Decimal( rounded = Decimal(str(floor((number + precision / 2) // precision))
str(floor((number + precision / 2) // precision)) ) * Decimal(str(precision))
) * Decimal(str(precision))
return float(rounded) return float(rounded)
@ -101,15 +100,11 @@ def deg(radiants):
def _swap_curly(string): def _swap_curly(string):
"""Swap single and double curly brackets""" """Swap single and double curly brackets"""
return (string return (
.replace('{{ ', '{{') string.replace('{{ ', '{{').replace('{{', '\x00').replace('{', '{{')
.replace('{{', '\x00') .replace('\x00', '{').replace(' }}', '}}').replace('}}', '\x00')
.replace('{', '{{') .replace('}', '}}').replace('\x00', '}')
.replace('\x00', '{') )
.replace(' }}', '}}')
.replace('}}', '\x00')
.replace('}', '}}')
.replace('\x00', '}'))
def template(string, **kwargs): def template(string, **kwargs):
@ -138,24 +133,21 @@ def compute_logarithmic_scale(min_, max_, min_scale, max_scale):
detail /= 2 detail /= 2
for order in range(min_order, max_order + 1): for order in range(min_order, max_order + 1):
for i in range(int(detail)): 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) tick = round_to_scale(tick, tick)
if min_ <= tick <= max_ and tick not in positions: if min_ <= tick <= max_ and tick not in positions:
positions.append(tick) positions.append(tick)
return positions return positions
def compute_scale( def compute_scale(min_, max_, logarithmic, order_min, min_scale, max_scale):
min_, max_, logarithmic, order_min,
min_scale, max_scale):
"""Compute an optimal scale between min and max""" """Compute an optimal scale between min and max"""
if min_ == 0 and max_ == 0: if min_ == 0 and max_ == 0:
return [0] return [0]
if max_ - min_ == 0: if max_ - min_ == 0:
return [min_] return [min_]
if logarithmic: if logarithmic:
log_scale = compute_logarithmic_scale( log_scale = compute_logarithmic_scale(min_, max_, min_scale, max_scale)
min_, max_, min_scale, max_scale)
if log_scale: if log_scale:
return log_scale return log_scale
# else we fallback to normal scalling # else we fallback to normal scalling
@ -164,10 +156,10 @@ def compute_scale(
if order_min is not None and order < order_min: if order_min is not None and order < order_min:
order = order_min order = order_min
else: else:
while ((max_ - min_) / (10 ** order) < min_scale and while ((max_ - min_) / (10**order) < min_scale
(order_min is None or order > order_min)): and (order_min is None or order > order_min)):
order -= 1 order -= 1
step = float(10 ** order) step = float(10**order)
while (max_ - min_) / step > max_scale: while (max_ - min_) / step > max_scale:
step *= 2. step *= 2.
positions = [] positions = []
@ -213,24 +205,24 @@ def decorate(svg, node, metadata):
if not isinstance(xlink, dict): if not isinstance(xlink, dict):
xlink = {'href': xlink, 'target': '_blank'} xlink = {'href': xlink, 'target': '_blank'}
node = svg.node(node, 'a', **xlink) node = svg.node(node, 'a', **xlink)
svg.node(node, 'desc', class_='xlink').text = to_unicode( svg.node(
xlink.get('href')) node, 'desc', class_='xlink'
).text = to_unicode(xlink.get('href'))
if 'tooltip' in metadata: if 'tooltip' in metadata:
svg.node(node, 'title').text = to_unicode( svg.node(node, 'title').text = to_unicode(metadata['tooltip'])
metadata['tooltip'])
if 'color' in metadata: if 'color' in metadata:
color = metadata.pop('color') color = metadata.pop('color')
node.attrib['style'] = 'fill: %s; stroke: %s' % ( node.attrib['style'] = 'fill: %s; stroke: %s' % (color, color)
color, color)
if 'style' in metadata: if 'style' in metadata:
node.attrib['style'] = metadata.pop('style') node.attrib['style'] = metadata.pop('style')
if 'label' in metadata and metadata['label']: if 'label' in metadata and metadata['label']:
svg.node(node, 'desc', class_='label').text = to_unicode( svg.node(
metadata['label']) node, 'desc', class_='label'
).text = to_unicode(metadata['label'])
return node return node
@ -238,7 +230,8 @@ def alter(node, metadata):
"""Override nodes attributes from metadata node mapping""" """Override nodes attributes from metadata node mapping"""
if node is not None and metadata and 'node' in metadata: if node is not None and metadata and 'node' in metadata:
node.attrib.update( 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): def truncate(string, index):
@ -250,7 +243,6 @@ def truncate(string, index):
# # Stolen partly from brownie http://packages.python.org/Brownie/ # # Stolen partly from brownie http://packages.python.org/Brownie/
class cached_property(object): class cached_property(object):
"""Memoize a property""" """Memoize a property"""
def __init__(self, getter, doc=None): 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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Projection and bounding helpers""" """Projection and bounding helpers"""
from __future__ import division from __future__ import division
@ -25,7 +24,6 @@ from math import cos, log10, pi, sin
class Margin(object): class Margin(object):
"""Class reprensenting a margin (top, right, left, bottom)""" """Class reprensenting a margin (top, right, left, bottom)"""
def __init__(self, top, right, bottom, left): def __init__(self, top, right, bottom, left):
@ -47,7 +45,6 @@ class Margin(object):
class Box(object): class Box(object):
"""Chart boundings""" """Chart boundings"""
margin = .02 margin = .02
@ -147,7 +144,6 @@ class Box(object):
class View(object): class View(object):
"""Projection base class""" """Projection base class"""
def __init__(self, width, height, box): def __init__(self, width, height, box):
@ -167,8 +163,9 @@ class View(object):
"""Project y""" """Project y"""
if y is None: if y is None:
return None return None
return (self.height - self.height * return (
(y - self.box.ymin) / self.box.height) self.height - self.height * (y - self.box.ymin) / self.box.height
)
def __call__(self, xy): def __call__(self, xy):
"""Project x and y""" """Project x and y"""
@ -177,7 +174,6 @@ class View(object):
class ReverseView(View): class ReverseView(View):
"""Same as view but reversed vertically""" """Same as view but reversed vertically"""
def y(self, y): def y(self, y):
@ -188,7 +184,6 @@ class ReverseView(View):
class HorizontalView(View): class HorizontalView(View):
"""Same as view but transposed""" """Same as view but transposed"""
def __init__(self, width, height, box): def __init__(self, width, height, box):
@ -219,7 +214,6 @@ class HorizontalView(View):
class PolarView(View): class PolarView(View):
"""Polar projection for pie like graphs""" """Polar projection for pie like graphs"""
def __call__(self, rhotheta): def __call__(self, rhotheta):
@ -227,12 +221,11 @@ class PolarView(View):
if None in rhotheta: if None in rhotheta:
return None, None return None, None
rho, theta = rhotheta rho, theta = rhotheta
return super(PolarView, self).__call__( return super(PolarView,
(rho * cos(theta), rho * sin(theta))) self).__call__((rho * cos(theta), rho * sin(theta)))
class PolarLogView(View): class PolarLogView(View):
"""Logarithmic polar projection""" """Logarithmic polar projection"""
def __init__(self, width, height, box): def __init__(self, width, height, box):
@ -240,7 +233,8 @@ class PolarLogView(View):
super(PolarLogView, self).__init__(width, height, box) super(PolarLogView, self).__init__(width, height, box)
if not hasattr(box, '_rmin') or not hasattr(box, '_rmax'): if not hasattr(box, '_rmin') or not hasattr(box, '_rmax'):
raise Exception( 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_rmax = log10(self.box._rmax)
self.log10_rmin = log10(self.box._rmin) self.log10_rmin = log10(self.box._rmin)
@ -256,14 +250,13 @@ class PolarLogView(View):
if rho == 0: if rho == 0:
return super(PolarLogView, self).__call__((0, 0)) return super(PolarLogView, self).__call__((0, 0))
rho = (self.box._rmax - self.box._rmin) * ( rho = (self.box._rmax - self.box._rmin) * (
log10(rho) - self.log10_rmin) / ( log10(rho) - self.log10_rmin
self.log10_rmax - self.log10_rmin) ) / (self.log10_rmax - self.log10_rmin)
return super(PolarLogView, self).__call__( return super(PolarLogView,
(rho * cos(theta), rho * sin(theta))) self).__call__((rho * cos(theta), rho * sin(theta)))
class PolarThetaView(View): class PolarThetaView(View):
"""Logarithmic polar projection""" """Logarithmic polar projection"""
def __init__(self, width, height, box, aperture=pi / 3): def __init__(self, width, height, box, aperture=pi / 3):
@ -272,7 +265,8 @@ class PolarThetaView(View):
self.aperture = aperture self.aperture = aperture
if not hasattr(box, '_tmin') or not hasattr(box, '_tmax'): if not hasattr(box, '_tmin') or not hasattr(box, '_tmax'):
raise Exception( 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): def __call__(self, rhotheta):
"""Project rho and theta""" """Project rho and theta"""
@ -280,15 +274,14 @@ class PolarThetaView(View):
return None, None return None, None
rho, theta = rhotheta rho, theta = rhotheta
start = 3 * pi / 2 + self.aperture / 2 start = 3 * pi / 2 + self.aperture / 2
theta = start + (2 * pi - self.aperture) * ( theta = start + (2 * pi - self.aperture) * (theta - self.box._tmin) / (
theta - self.box._tmin) / ( self.box._tmax - self.box._tmin
self.box._tmax - self.box._tmin) )
return super(PolarThetaView, self).__call__( return super(PolarThetaView,
(rho * cos(theta), rho * sin(theta))) self).__call__((rho * cos(theta), rho * sin(theta)))
class PolarThetaLogView(View): class PolarThetaLogView(View):
"""Logarithmic polar projection""" """Logarithmic polar projection"""
def __init__(self, width, height, box, aperture=pi / 3): def __init__(self, width, height, box, aperture=pi / 3):
@ -297,7 +290,8 @@ class PolarThetaLogView(View):
self.aperture = aperture self.aperture = aperture
if not hasattr(box, '_tmin') or not hasattr(box, '_tmax'): if not hasattr(box, '_tmin') or not hasattr(box, '_tmax'):
raise Exception( 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_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 self.log10_tmin = log10(self.box._tmin) if self.box._tmin > 0 else 0
if self.log10_tmin == self.log10_tmax: if self.log10_tmin == self.log10_tmax:
@ -312,20 +306,19 @@ class PolarThetaLogView(View):
if theta == 0: if theta == 0:
return super(PolarThetaLogView, self).__call__((0, 0)) return super(PolarThetaLogView, self).__call__((0, 0))
theta = self.box._tmin + (self.box._tmax - self.box._tmin) * ( theta = self.box._tmin + (self.box._tmax - self.box._tmin) * (
log10(theta) - self.log10_tmin) / ( log10(theta) - self.log10_tmin
self.log10_tmax - self.log10_tmin) ) / (self.log10_tmax - self.log10_tmin)
start = 3 * pi / 2 + self.aperture / 2 start = 3 * pi / 2 + self.aperture / 2
theta = start + (2 * pi - self.aperture) * ( theta = start + (2 * pi - self.aperture) * (theta - self.box._tmin) / (
theta - self.box._tmin) / ( self.box._tmax - self.box._tmin
self.box._tmax - self.box._tmin) )
return super(PolarThetaLogView, self).__call__( return super(PolarThetaLogView,
(rho * cos(theta), rho * sin(theta))) self).__call__((rho * cos(theta), rho * sin(theta)))
class LogView(View): class LogView(View):
"""Y Logarithmic projection""" """Y Logarithmic projection"""
# Do not want to call the parent here # Do not want to call the parent here
@ -344,13 +337,13 @@ class LogView(View):
"""Project y""" """Project y"""
if y is None or y <= 0 or self.log10_ymax - self.log10_ymin == 0: if y is None or y <= 0 or self.log10_ymax - self.log10_ymin == 0:
return 0 return 0
return (self.height - self.height * return (
(log10(y) - self.log10_ymin) / ( self.height - self.height * (log10(y) - self.log10_ymin) /
self.log10_ymax - self.log10_ymin)) (self.log10_ymax - self.log10_ymin)
)
class XLogView(View): class XLogView(View):
"""X logarithmic projection""" """X logarithmic projection"""
# Do not want to call the parent here # Do not want to call the parent here
@ -367,13 +360,13 @@ class XLogView(View):
"""Project x""" """Project x"""
if x is None or x <= 0 or self.log10_xmax - self.log10_xmin == 0: if x is None or x <= 0 or self.log10_xmax - self.log10_xmin == 0:
return None return None
return (self.width * return (
(log10(x) - self.log10_xmin) / self.width * (log10(x) - self.log10_xmin) /
(self.log10_xmax - self.log10_xmin)) (self.log10_xmax - self.log10_xmin)
)
class XYLogView(XLogView, LogView): class XYLogView(XLogView, LogView):
"""X and Y logarithmic projection""" """X and Y logarithmic projection"""
def __init__(self, width, height, box): def __init__(self, width, height, box):
@ -389,7 +382,6 @@ class XYLogView(XLogView, LogView):
class HorizontalLogView(XLogView): class HorizontalLogView(XLogView):
"""Transposed Logarithmic projection""" """Transposed Logarithmic projection"""
# Do not want to call the parent here # Do not want to call the parent here

Loading…
Cancel
Save