1# -*- coding: utf-8 -*-
2# This file is part of pygal
3#
4# A python svg graph plotting library
5# Copyright © 2012-2016 Kozea
6#
7# This library is free software: you can redistribute it and/or modify it under
8# the terms of the GNU Lesser General Public License as published by the Free
9# Software Foundation, either version 3 of the License, or (at your option) any
10# later version.
11#
12# This library is distributed in the hope that it will be useful, but WITHOUT
13# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
14# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
15# details.
16#
17# You should have received a copy of the GNU Lesser General Public License
18# along with pygal. If not, see <http://www.gnu.org/licenses/>.
19"""Config module holding all options and their default values."""
20
21from copy import deepcopy
22
23from pygal import formatters
24from pygal.interpolate import INTERPOLATIONS
25from pygal.style import DefaultStyle, Style
26
27CONFIG_ITEMS = []
28callable = type(lambda: 1)
29
30
31class Key(object):
32
33    """
34    Represents a config parameter.
35
36    A config parameter has a name, a default value, a type,
37    a category, a documentation, an optional longer documentatation
38    and an optional subtype for list style option.
39
40    Most of these informations are used in cabaret to auto generate
41    forms representing these options.
42    """
43
44    _categories = []
45
46    def __init__(
47            self, default_value, type_, category, doc,
48            subdoc="", subtype=None):
49        """Create a configuration key"""
50        self.value = default_value
51        self.type = type_
52        self.doc = doc
53        self.category = category
54        self.subdoc = subdoc
55        self.subtype = subtype
56        self.name = "Unbound"
57        if category not in self._categories:
58            self._categories.append(category)
59
60        CONFIG_ITEMS.append(self)
61
62    def __repr__(self):
63        """
64        Make a documentation repr.
65        This is a hack to generate doc from inner doc
66        """
67        return """
68        Type: %s%s     
69        Default: %r     
70        %s%s
71        """ % (
72            self.type.__name__,
73            (' of %s' % self.subtype.__name__) if self.subtype else '',
74            self.value,
75            self.doc,
76            (' %s' % self.subdoc) if self.subdoc else ''
77        )
78
79    @property
80    def is_boolean(self):
81        """Return `True` if this parameter is a boolean"""
82        return self.type == bool
83
84    @property
85    def is_numeric(self):
86        """Return `True` if this parameter is numeric (int or float)"""
87        return self.type in (int, float)
88
89    @property
90    def is_string(self):
91        """Return `True` if this parameter is a string"""
92        return self.type == str
93
94    @property
95    def is_dict(self):
96        """Return `True` if this parameter is a mapping"""
97        return self.type == dict
98
99    @property
100    def is_list(self):
101        """Return `True` if this parameter is a list"""
102        return self.type == list
103
104    def coerce(self, value):
105        """Cast a string into this key type"""
106        if self.type == Style:
107            return value
108        elif self.type == list:
109            return self.type(
110                map(
111                    self.subtype, map(
112                        lambda x: x.strip(), value.split(','))))
113        elif self.type == dict:
114            rv = {}
115            for pair in value.split(','):
116                key, val = pair.split(':')
117                key = key.strip()
118                val = val.strip()
119                try:
120                    rv[key] = self.subtype(val)
121                except:
122                    rv[key] = val
123            return rv
124        return self.type(value)
125
126
127class MetaConfig(type):
128
129    """Config metaclass. Used to get the key name and set it on the value."""
130
131    def __new__(mcs, classname, bases, classdict):
132        """Get the name of the key and set it on the key"""
133        for k, v in classdict.items():
134            if isinstance(v, Key):
135                v.name = k
136
137        return type.__new__(mcs, classname, bases, classdict)
138
139
140class BaseConfig(MetaConfig('ConfigBase', (object,), {})):
141
142    """
143    This class holds the common method for configs.
144
145    A config object can be instanciated with keyword arguments and
146    updated on call with keyword arguments.
147    """
148
149    def __init__(self, **kwargs):
150        """Can be instanciated with config kwargs"""
151        for k in dir(self):
152            v = getattr(self, k)
153            if (k not in self.__dict__ and not
154                    k.startswith('_') and not
155                    hasattr(v, '__call__')):
156                if isinstance(v, Key):
157                    if v.is_list and v.value is not None:
158                        v = list(v.value)
159                    else:
160                        v = v.value
161                setattr(self, k, v)
162        self._update(kwargs)
163
164    def __call__(self, **kwargs):
165        """Can be updated with kwargs"""
166        self._update(kwargs)
167
168    def _update(self, kwargs):
169        """Update the config with the given dictionary"""
170        from pygal.util import merge
171        dir_self_set = set(dir(self))
172        merge(
173            self.__dict__, dict([
174                (k, v) for (k, v) in kwargs.items()
175                if not k.startswith('_') and k in dir_self_set]))
176
177    def to_dict(self):
178        """Export a JSON serializable dictionary of the config"""
179        config = {}
180        for attr in dir(self):
181            if not attr.startswith('__'):
182                value = getattr(self, attr)
183                if hasattr(value, 'to_dict'):
184                    config[attr] = value.to_dict()
185                elif not hasattr(value, '__call__'):
186                    config[attr] = value
187        return config
188
189    def copy(self):
190        """Copy this config object into another"""
191        return deepcopy(self)
192
193
194class CommonConfig(BaseConfig):
195
196    """Class holding options used in both chart and serie configuration"""
197
198    stroke = Key(
199        True, bool, "Look",
200        "Line dots (set it to false to get a scatter plot)")
201
202    show_dots = Key(True, bool, "Look", "Set to false to remove dots")
203
204    show_only_major_dots = Key(
205        False, bool, "Look",
206        "Set to true to show only major dots according to their majored label")
207
208    dots_size = Key(2.5, float, "Look", "Radius of the dots")
209
210    fill = Key(
211        False, bool, "Look", "Fill areas under lines")
212
213    stroke_style = Key(None, dict, "Look", "Stroke style of serie element.",
214                       "This is a dict which can contain a "
215                       "'width', 'linejoin', 'linecap', 'dasharray' "
216                       "and 'dashoffset'")
217
218    rounded_bars = Key(
219        None, int, "Look",
220        "Set this to the desired radius in px (for Bar-like charts)")
221
222    inner_radius = Key(
223        0, float, "Look", "Piechart inner radius (donut), must be <.9")
224
225    allow_interruptions = Key(
226        False, bool, "Look", "Break lines on None values")
227
228    formatter = Key(
229        None, callable, "Value",
230        "A function to convert raw value to strings for this chart or serie",
231        "Default to value_formatter in most charts, it depends on dual charts."
232        "(Can be overriden by value with the formatter metadata.)")
233
234
235class Config(CommonConfig):
236
237    """Class holding config values"""
238
239    style = Key(
240        DefaultStyle, Style, "Style", "Style holding values injected in css")
241
242    css = Key(
243        ('file://style.css', 'file://graph.css'), list, "Style",
244        "List of css file",
245        "It can be any uri from file:///tmp/style.css to //domain/style.css",
246        str)
247
248    classes = Key(
249        ('pygal-chart',),
250        list, "Style", "Classes of the root svg node",
251        str)
252
253    defs = Key(
254        [],
255        list, "Misc", "Extraneous defs to be inserted in svg",
256        "Useful for adding gradients / patterns…",
257        str)
258
259    # Look #
260    title = Key(
261        None, str, "Look",
262        "Graph title.", "Leave it to None to disable title.")
263
264    x_title = Key(
265        None, str, "Look",
266        "Graph X-Axis title.", "Leave it to None to disable X-Axis title.")
267
268    y_title = Key(
269        None, str, "Look",
270        "Graph Y-Axis title.", "Leave it to None to disable Y-Axis title.")
271
272    width = Key(
273        800, int, "Look", "Graph width")
274
275    height = Key(
276        600, int, "Look", "Graph height")
277
278    show_x_guides = Key(False, bool, "Look",
279                        "Set to true to always show x guide lines")
280
281    show_y_guides = Key(True, bool, "Look",
282                        "Set to false to hide y guide lines")
283
284    show_legend = Key(
285        True, bool, "Look", "Set to false to remove legend")
286
287    legend_at_bottom = Key(
288        False, bool, "Look", "Set to true to position legend at bottom")
289
290    legend_at_bottom_columns = Key(
291        None, int, "Look", "Set to true to position legend at bottom")
292
293    legend_box_size = Key(
294        12, int, "Look", "Size of legend boxes")
295
296    rounded_bars = Key(
297        None, int, "Look", "Set this to the desired radius in px")
298
299    stack_from_top = Key(
300        False, bool, "Look", "Stack from top to zero, this makes the stacked "
301        "data match the legend order")
302
303    spacing = Key(
304        10, int, "Look",
305        "Space between titles/legend/axes")
306
307    margin = Key(
308        20, int, "Look",
309        "Margin around chart")
310
311    margin_top = Key(
312        None, int, "Look",
313        "Margin around top of chart")
314
315    margin_right = Key(
316        None, int, "Look",
317        "Margin around right of chart")
318
319    margin_bottom = Key(
320        None, int, "Look",
321        "Margin around bottom of chart")
322
323    margin_left = Key(
324        None, int, "Look",
325        "Margin around left of chart")
326
327    tooltip_border_radius = Key(0, int, "Look", "Tooltip border radius")
328
329    tooltip_fancy_mode = Key(
330        True, bool, "Look", "Fancy tooltips",
331        "Print legend, x label in tooltip and use serie color for value.")
332
333    inner_radius = Key(
334        0, float, "Look", "Piechart inner radius (donut), must be <.9")
335
336    half_pie = Key(
337        False, bool, "Look", "Create a half-pie chart")
338
339    x_labels = Key(
340        None, list, "Label",
341        "X labels, must have same len than data.",
342        "Leave it to None to disable x labels display.",
343        str)
344
345    x_labels_major = Key(
346        None, list, "Label",
347        "X labels that will be marked major.",
348        subtype=str)
349
350    x_labels_major_every = Key(
351        None, int, "Label",
352        "Mark every n-th x label as major.")
353
354    x_labels_major_count = Key(
355        None, int, "Label",
356        "Mark n evenly distributed labels as major.")
357
358    show_x_labels = Key(
359        True, bool, "Label", "Set to false to hide x-labels")
360
361    show_minor_x_labels = Key(
362        True, bool, "Label", "Set to false to hide x-labels not marked major")
363
364    y_labels = Key(
365        None, list, "Label",
366        "You can specify explicit y labels",
367        "Must be a list of numbers", float)
368
369    y_labels_major = Key(
370        None, list, "Label",
371        "Y labels that will be marked major. Default: auto",
372        subtype=str)
373
374    y_labels_major_every = Key(
375        None, int, "Label",
376        "Mark every n-th y label as major.")
377
378    y_labels_major_count = Key(
379        None, int, "Label",
380        "Mark n evenly distributed y labels as major.")
381
382    show_minor_y_labels = Key(
383        True, bool, "Label", "Set to false to hide y-labels not marked major")
384
385    show_y_labels = Key(
386        True, bool, "Label", "Set to false to hide y-labels")
387
388    x_label_rotation = Key(
389        0, int, "Label", "Specify x labels rotation angles", "in degrees")
390
391    y_label_rotation = Key(
392        0, int, "Label", "Specify y labels rotation angles", "in degrees")
393
394    missing_value_fill_truncation = Key(
395        "x", str, "Look",
396        "Filled series with missing x and/or y values at the end of a series "
397        "are closed at the first value with a missing "
398        "'x' (default), 'y' or 'either'")
399
400    # Value #
401    x_value_formatter = Key(
402        formatters.default, callable, "Value",
403        "A function to convert abscissa numeric value to strings "
404        "(used in XY and Date charts)")
405
406    value_formatter = Key(
407        formatters.default, callable, "Value",
408        "A function to convert ordinate numeric value to strings")
409
410    logarithmic = Key(
411        False, bool, "Value", "Display values in logarithmic scale")
412
413    interpolate = Key(
414        None, str, "Value", "Interpolation",
415        "May be %s" % ' or '.join(INTERPOLATIONS))
416
417    interpolation_precision = Key(
418        250, int, "Value", "Number of interpolated points between two values")
419
420    interpolation_parameters = Key(
421        {}, dict, "Value", "Various parameters for parametric interpolations",
422        "ie: For hermite interpolation, you can set the cardinal tension with"
423        "{'type': 'cardinal', 'c': .5}", int)
424
425    box_mode = Key(
426        'extremes', str, "Value", "Sets the mode to be used. "
427        "(Currently only supported on box plot)",
428        "May be %s" % ' or '.join([
429            "1.5IQR", "extremes", "tukey", "stdev", "pstdev"]))
430
431    order_min = Key(
432        None, int, "Value",
433        "Minimum order of scale, defaults to None")
434
435    min_scale = Key(
436        4, int, "Value",
437        "Minimum number of scale graduation for auto scaling")
438
439    max_scale = Key(
440        16, int, "Value",
441        "Maximum number of scale graduation for auto scaling")
442
443    range = Key(
444        None, list, "Value", "Explicitly specify min and max of values",
445        "(ie: (0, 100))", int)
446
447    secondary_range = Key(
448        None, list, "Value",
449        "Explicitly specify min and max of secondary values",
450        "(ie: (0, 100))", int)
451
452    xrange = Key(
453        None, list, "Value", "Explicitly specify min and max of x values "
454        "(used in XY and Date charts)",
455        "(ie: (0, 100))", int)
456
457    include_x_axis = Key(
458        False, bool, "Value", "Always include x axis")
459
460    zero = Key(
461        0, int, "Value",
462        "Set the ordinate zero value",
463        "Useful for filling to another base than abscissa")
464
465    # Text #
466    no_data_text = Key(
467        "No data", str, "Text", "Text to display when no data is given")
468
469    print_values = Key(
470        False, bool,
471        "Text", "Display values as text over plot")
472
473    dynamic_print_values = Key(
474        False, bool,
475        "Text", "Show values only on hover")
476
477    print_values_position = Key(
478        'center', str,
479        "Text", "Customize position of `print_values`. "
480        "(For bars: `top`, `center` or `bottom`)")
481
482    print_zeroes = Key(
483        True, bool,
484        "Text", "Display zero values as well")
485
486    print_labels = Key(
487        False, bool,
488        "Text", "Display value labels")
489
490    truncate_legend = Key(
491        None, int, "Text",
492        "Legend string length truncation threshold",
493        "None = auto, Negative for none")
494
495    truncate_label = Key(
496        None, int, "Text",
497        "Label string length truncation threshold",
498        "None = auto, Negative for none")
499
500    # Misc #
501    js = Key(
502        ('//kozea.github.io/pygal.js/2.0.x/pygal-tooltips.min.js',),
503        list, "Misc", "List of js file",
504        "It can be any uri from file:///tmp/ext.js to //domain/ext.js",
505        str)
506
507    disable_xml_declaration = Key(
508        False, bool, "Misc",
509        "Don't write xml declaration and return str instead of string",
510        "useful for writing output directly in html")
511
512    force_uri_protocol = Key(
513        'https', str, "Misc",
514        "Default uri protocol",
515        "Default protocol for external files. "
516        "Can be set to None to use a // uri")
517
518    explicit_size = Key(
519        False, bool, "Misc", "Write width and height attributes")
520
521    pretty_print = Key(
522        False, bool, "Misc", "Pretty print the svg")
523
524    strict = Key(
525        False, bool, "Misc",
526        "If True don't try to adapt / filter wrong values")
527
528    no_prefix = Key(
529        False, bool, "Misc",
530        "Don't prefix css")
531
532    inverse_y_axis = Key(False, bool, "Misc", "Inverse Y axis direction")
533
534
535class SerieConfig(CommonConfig):
536
537    """Class holding serie config values"""
538
539    title = Key(
540        None, str, "Look",
541        "Serie title.", "Leave it to None to disable title.")
542
543    secondary = Key(
544        False, bool, "Misc",
545        "Set it to put the serie in a second axis")
546