From b7f2ec91607da253afe22d9751843f8275097086 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Fri, 13 Jun 2014 16:46:02 +0200 Subject: [PATCH] First almost working first level only treemap --- demo/moulinrouge/tests.py | 12 +++- pygal/graph/__init__.py | 3 +- pygal/graph/treemap.py | 116 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 pygal/graph/treemap.py diff --git a/demo/moulinrouge/tests.py b/demo/moulinrouge/tests.py index 9526ac1..2a3545a 100644 --- a/demo/moulinrouge/tests.py +++ b/demo/moulinrouge/tests.py @@ -3,7 +3,7 @@ from pygal import ( Bar, Gauge, Pyramid, Funnel, Dot, StackedBar, StackedLine, XY, CHARTS_BY_NAME, Config, Line, DateY, Worldmap, Histogram, Box, - FrenchMap_Departments, FrenchMap_Regions, Pie) + FrenchMap_Departments, FrenchMap_Regions, Pie, Treemap) from pygal.style import styles, Style from pygal.colors import rotate from pygal.graph.frenchmap import DEPARTMENTS, REGIONS @@ -118,6 +118,14 @@ def get_test_routes(app): bar.add('Lol', [2, None, 12]) return bar.render_response() + @app.route('/test/treemap') + def test_treemap(): + treemap = Treemap() + treemap.add('A', [2]) + treemap.add('B', [4]) + treemap.add('C', [3]) + return treemap.render_response() + @app.route('/test/gauge') def test_gauge(): gauge = Gauge() @@ -505,4 +513,4 @@ def get_test_routes(app): graph.legend_at_bottom = True return graph.render_response() - return list(filter(lambda x: x.startswith('test'), locals())) + return list(sorted(filter(lambda x: x.startswith('test'), locals()))) diff --git a/pygal/graph/__init__.py b/pygal/graph/__init__.py index 77ae278..250ee08 100644 --- a/pygal/graph/__init__.py +++ b/pygal/graph/__init__.py @@ -41,5 +41,6 @@ CHARTS_NAMES = [ 'SupranationalWorldmap', 'Histogram', 'Box', - 'FrenchMap' + 'FrenchMap', + 'Treemap' ] diff --git a/pygal/graph/treemap.py b/pygal/graph/treemap.py new file mode 100644 index 0000000..07403f0 --- /dev/null +++ b/pygal/graph/treemap.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# This file is part of pygal +# +# A python svg graph plotting library +# Copyright © 2012-2014 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 . +""" +Treemap chart + +""" + +from __future__ import division +from pygal.util import decorate +from pygal.graph.graph import Graph +from pygal.adapters import positive, none_to_zero + + +class Treemap(Graph): + """Treemap graph""" + + _adapters = [positive, none_to_zero] + + def _rect(self, serie, x, y, w, h): + rx, ry = self.view((x, y)) + rw, rh = self.view((x + w, y + h)) + rw -= rx + rh -= ry + + serie_node = self._serie(serie._index) + rects = self.svg.node(serie_node['plot'], class_="rects") + # metadata = serie.metadata.get(i) + # value = self._format(serie.values[i]) + + # rect = decorate( + # self.svg, + # self.svg.node(rects, class_="rect"), + # metadata) + + self.svg.node(rects, 'rect', + x=rx, + y=ry, + width=rw, + height=rh, + class_='rect reactive tooltip-trigger') + + # self._tooltip_data(rect, value, + # self.view.x(acc + w / 2), + # self.view.y(.5), + # classes='centered') + # self._static_value(serie_node, value, + # self.view.x(acc + w / 2), + # self.view.y(.5)) + + def _binary_tree(self, series, total, x, y, w, h): + if len(series) == 1: + self._rect(series[0], x, y, w, h) + return + + midpoint = total / 2 + pivot_index = 1 + running_sum = 0 + for serie in series: + if running_sum >= midpoint: + pivot_index = serie._index + break + + running_sum += sum(serie.values) + + half1 = series[:pivot_index] + half2 = series[pivot_index:] + + half1_sum = sum(map(sum, map(lambda x: x.values, half1))) + half2_sum = sum(map(sum, map(lambda x: x.values, half2))) + pivot_pct = half1_sum / total + if h > w: + y_pivot = pivot_pct * h + self._binary_tree( + half1, half1_sum, x, y, w, y_pivot) + self._binary_tree( + half2, half2_sum, x, y + y_pivot, w, h - y_pivot) + else: + x_pivot = pivot_pct * w + self._binary_tree( + half1, half1_sum, x, y, x_pivot, h) + self._binary_tree( + half2, half2_sum, x + x_pivot, y, w - x_pivot, h) + + def _plot(self): + total = sum(map(sum, map(lambda x: x.values, self.series))) + if total == 0: + return + + gw = self.width - self.margin.x + gh = self.height - self.margin.y + + self.view.box.xmin = self.view.box.ymin = x = y = 0 + self.view.box.xmax = w = (total * gw / gh) ** .5 + self.view.box.ymax = h = total / w + self.view.box.fix() + + for index, serie in enumerate(self.series): + serie._index = index + + self._binary_tree(self.series, total, x, y, w, h)