File: funnel.py

package info (click to toggle)
python-pygal 3.0.5-1
  • links: PTS
  • area: main
  • in suites: forky, sid, trixie
  • size: 636 kB
  • sloc: python: 7,359; makefile: 9
file content (115 lines) | stat: -rw-r--r-- 4,183 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# -*- coding: utf-8 -*-
# This file is part of pygal
#
# A python svg graph plotting library
# Copyright © 2012-2016 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""Funnel chart: Represent values as a funnel"""

from pygal.adapters import none_to_zero, positive
from pygal.graph.graph import Graph
from pygal.util import alter, cut, decorate


class Funnel(Graph):
    """Funnel graph class"""

    _adapters = [positive, none_to_zero]

    def _value_format(self, value):
        """Format value for dual value display."""
        return super(Funnel, self)._value_format(value and abs(value))

    def funnel(self, serie):
        """Draw a funnel slice"""
        serie_node = self.svg.serie(serie)
        fmt = lambda x: '%f %f' % x
        for i, poly in enumerate(serie.points):
            metadata = serie.metadata.get(i)
            val = self._format(serie, i)

            funnels = decorate(
                self.svg, self.svg.node(serie_node['plot'], class_="funnels"),
                metadata
            )

            alter(
                self.svg.node(
                    funnels,
                    'polygon',
                    points=' '.join(map(fmt, map(self.view, poly))),
                    class_='funnel reactive tooltip-trigger'
                ), metadata
            )

            # Poly center from label
            x, y = self.view((
                self._center(self._x_pos[serie.index]),
                sum([point[1] for point in poly]) / len(poly)
            ))
            self._tooltip_data(
                funnels, val, x, y, 'centered', self._get_x_label(serie.index)
            )
            self._static_value(serie_node, val, x, y, metadata)

    def _center(self, x):
        return x - 1 / (2 * self._order)

    def _compute(self):
        """Compute y min and max and y scale and set labels"""
        self._x_pos = [
            (x + 1) / self._order for x in range(self._order)
        ] if self._order != 1 else [.5]  # Center if only one value

        previous = [[self.zero, self.zero] for i in range(self._len)]
        for i, serie in enumerate(self.series):
            y_height = -sum(serie.safe_values) / 2
            all_x_pos = [0] + self._x_pos
            serie.points = []
            for j, value in enumerate(serie.values):
                poly = []
                poly.append((all_x_pos[i], previous[j][0]))
                poly.append((all_x_pos[i], previous[j][1]))
                previous[j][0] = y_height
                y_height = previous[j][1] = y_height + value
                poly.append((all_x_pos[i + 1], previous[j][1]))
                poly.append((all_x_pos[i + 1], previous[j][0]))
                serie.points.append(poly)

        val_max = max(list(map(sum, cut(self.series, 'values'))) + [self.zero])
        self._box.ymin = -val_max
        self._box.ymax = val_max

        if self.range and self.range[0] is not None:
            self._box.ymin = self.range[0]

        if self.range and self.range[1] is not None:
            self._box.ymax = self.range[1]

    def _compute_x_labels(self):
        self._x_labels = list(
            zip(
                self.x_labels and map(self._x_format, self.x_labels) or [
                    serie.title['title']
                    if isinstance(serie.title, dict) else serie.title or ''
                    for serie in self.series
                ], map(self._center, self._x_pos)
            )
        )

    def _plot(self):
        """Plot the funnel"""
        for serie in self.series:
            self.funnel(serie)