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
20"""Treemap chart: Visualize data using nested recangles"""
21
22from __future__ import division
23
24from pygal.adapters import none_to_zero, positive
25from pygal.graph.graph import Graph
26from pygal.util import alter, cut, decorate
27
28
29class Treemap(Graph):
30
31    """Treemap graph class"""
32
33    _adapters = [positive, none_to_zero]
34
35    def _rect(self, serie, serie_node, rects, val, x, y, w, h, i):
36        rx, ry = self.view((x, y))
37        rw, rh = self.view((x + w, y + h))
38        rw -= rx
39        rh -= ry
40
41        metadata = serie.metadata.get(i)
42
43        val = self._format(serie, i)
44
45        rect = decorate(
46            self.svg,
47            self.svg.node(rects, class_="rect"),
48            metadata)
49
50        alter(
51            self.svg.node(
52                rect, 'rect',
53                x=rx,
54                y=ry,
55                width=rw,
56                height=rh,
57                class_='rect reactive tooltip-trigger'),
58            metadata)
59
60        self._tooltip_data(
61            rect, val,
62            rx + rw / 2,
63            ry + rh / 2,
64            'centered',
65            self._get_x_label(i))
66        self._static_value(
67            serie_node, val,
68            rx + rw / 2,
69            ry + rh / 2,
70            metadata)
71
72    def _binary_tree(self, data, total, x, y, w, h, parent=None):
73        if total == 0:
74            return
75        if len(data) == 1:
76            if parent:
77                i, datum = data[0]
78                serie, serie_node, rects = parent
79                self._rect(serie, serie_node, rects, datum, x, y, w, h, i)
80            else:
81                datum = data[0]
82                serie_node = self.svg.serie(datum)
83                self._binary_tree(
84                    list(enumerate(datum.values)),
85                    total, x, y, w, h,
86                    (datum, serie_node,
87                     self.svg.node(serie_node['plot'], class_="rects")))
88            return
89
90        midpoint = total / 2
91        pivot_index = 1
92        running_sum = 0
93        for i, elt in enumerate(data):
94            if running_sum >= midpoint:
95                pivot_index = i
96                break
97
98            running_sum += elt[1] if parent else sum(elt.values)
99
100        half1 = data[:pivot_index]
101        half2 = data[pivot_index:]
102
103        if parent:
104            half1_sum = sum(cut(half1, 1))
105            half2_sum = sum(cut(half2, 1))
106        else:
107            half1_sum = sum(map(sum, map(lambda x: x.values, half1)))
108            half2_sum = sum(map(sum, map(lambda x: x.values, half2)))
109        pivot_pct = half1_sum / total
110
111        if h > w:
112            y_pivot = pivot_pct * h
113            self._binary_tree(
114                half1, half1_sum, x, y, w, y_pivot, parent)
115            self._binary_tree(
116                half2, half2_sum, x, y + y_pivot, w, h - y_pivot, parent)
117        else:
118            x_pivot = pivot_pct * w
119            self._binary_tree(
120                half1, half1_sum, x, y, x_pivot, h, parent)
121            self._binary_tree(
122                half2, half2_sum, x + x_pivot, y, w - x_pivot, h, parent)
123
124    def _compute_x_labels(self):
125        pass
126
127    def _compute_y_labels(self):
128        pass
129
130    def _plot(self):
131        total = sum(map(sum, map(lambda x: x.values, self.series)))
132        if total == 0:
133            return
134
135        gw = self.width - self.margin_box.x
136        gh = self.height - self.margin_box.y
137
138        self.view.box.xmin = self.view.box.ymin = x = y = 0
139        self.view.box.xmax = w = (total * gw / gh) ** .5
140        self.view.box.ymax = h = total / w
141        self.view.box.fix()
142
143        self._binary_tree(self.series, total, x, y, w, h)
144