From 29aa0bf0e202298cd7b3bd1616b6fe629b6a02f1 Mon Sep 17 00:00:00 2001 From: Wiktor Niesiobedzki Date: Thu, 10 Jan 2013 15:26:23 +0100 Subject: [PATCH 01/14] Secondary axis support --- pygal/css/graph.css | 8 +++- pygal/ghost.py | 17 +++++--- pygal/graph/__init__.py | 3 +- pygal/graph/base.py | 46 ++++++++++++++++++--- pygal/graph/graph.py | 92 +++++++++++++++++++++++++++++++++++++---- pygal/util.py | 11 ++++- 6 files changed, 154 insertions(+), 23 deletions(-) diff --git a/pygal/css/graph.css b/pygal/css/graph.css index 40fcd28..40fb16b 100644 --- a/pygal/css/graph.css +++ b/pygal/css/graph.css @@ -49,8 +49,12 @@ text.no_data { .axis.y text { text-anchor: end; } +.axis.2y text { + text-anchor: start; +} -.axis.y .logarithmic text:not(.major) { +.axis.y .logarithmic text:not(.major) , +.axis.2y .logarithmic text:not(.major) { font-size: 50%; } @@ -66,11 +70,13 @@ text.no_data { } .horizontal .axis.y .guide.line, +.horizontal .axis.2y .guide.line, .vertical .axis.x .guide.line { opacity: 0; } .axis.y .guides:hover .guide.line, +.axis.2y .guides:hover .guide.line, .line-graph .axis.x .guides:hover .guide.line, .gauge-graph .axis.x .guides:hover .guide.line, .stackedline-graph .axis.x .guides:hover .guide.line, diff --git a/pygal/ghost.py b/pygal/ghost.py index 5dff44f..9f91094 100644 --- a/pygal/ghost.py +++ b/pygal/ghost.py @@ -55,20 +55,25 @@ class Ghost(object): config(**kwargs) self.config = config self.raw_series = [] + self.raw_series2 = [] - def add(self, title, values): + def add(self, title, values, secondary=False): """Add a serie to this graph""" if not hasattr(values, '__iter__') and not isinstance(values, dict): values = [values] - self.raw_series.append((title, values)) + if secondary: + self.raw_series2.append((title, values)) + else: + self.raw_series.append((title, values)) - def make_series(self): - return prepare_values(self.raw_series, self.config, self.cls) + def make_series(self, series): + return prepare_values(series, self.config, self.cls) def make_instance(self): self.config(**self.__dict__) - series = self.make_series() - self._last__inst = self.cls(self.config, series) + series = self.make_series(self.raw_series) + secondary_series = self.make_series(self.raw_series2) + self._last__inst = self.cls(self.config, series, secondary_series) return self._last__inst # Rendering diff --git a/pygal/graph/__init__.py b/pygal/graph/__init__.py index b255205..175a099 100644 --- a/pygal/graph/__init__.py +++ b/pygal/graph/__init__.py @@ -35,5 +35,6 @@ CHARTS_NAMES = [ 'Pyramid', 'VerticalPyramid', 'Dot', - 'Gauge' + 'Gauge', + 'DoubleYLine', ] diff --git a/pygal/graph/base.py b/pygal/graph/base.py index 0835ef6..c9bcbca 100644 --- a/pygal/graph/base.py +++ b/pygal/graph/base.py @@ -35,17 +35,20 @@ class BaseGraph(object): _adapters = [] - def __init__(self, config, series): + def __init__(self, config, series, secondary_series): """Init the graph""" self.config = config self.series = series or [] + self.secondary_series = secondary_series or [] self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self._x_labels = None self._y_labels = None + self._y_2nd_labels = None self.nodes = {} self.margin = Margin(*([20] * 4)) self._box = Box() + self._secondary_box = Box() self.view = None if self.logarithmic and self.zero == 0: # Explicit min to avoid interpolation dependency @@ -94,7 +97,7 @@ class BaseGraph(object): if self.show_legend and self.series: h, w = get_texts_box( map(lambda x: truncate(x, self.truncate_legend or 15), - cut(self.series, 'title')), + cut(self.series + self.secondary_series, 'title')), self.legend_font_size) if self.legend_at_bottom: h_max = max(h, self.legend_box_size) @@ -123,12 +126,18 @@ class BaseGraph(object): h, w = get_texts_box( cut(self._y_labels), self.label_font_size) self.margin.left += 10 + max( - w * cos(rad(self.y_label_rotation)), h) + w * cos(rad(self.y_label_rotation)), w) + @cached_property def _legends(self): """Getter for series title""" return [serie.title for serie in self.series] + + @cached_property + def _secondary_legends(self): + """Getter for series title on secondary y axis""" + return [serie.title for serie in self.secondary_series] @cached_property def _values(self): @@ -138,10 +147,24 @@ class BaseGraph(object): for val in serie.values if val is not None] + @cached_property + def _secondary_values(self): + """Getter for secondary series values (flattened)""" + return [val + for serie in self.secondary_series + for val in serie.values + if val is not None] + @cached_property def _len(self): """Getter for the maximum series size""" - return max([len(serie.values) for serie in self.series] or [0]) + return max([len(serie.values) for serie in self.series + self.secondary_series] or [0]) + + @cached_property + def _secondary_min(self): + """Getter for the minimum series value""" + return (self.range and self.range[0]) or ( + min(self._secondary_values) if self._secondary_values else None) @cached_property def _min(self): @@ -149,16 +172,29 @@ class BaseGraph(object): return (self.range and self.range[0]) or ( min(self._values) if self._values else None) + @cached_property + def _secondary_min(self): + """Getter for the secondary minimum series value""" + return (self.range and self.range[0]) or ( + min(self._secondary_values) if self._secondary_values else None) + + @cached_property def _max(self): """Getter for the maximum series value""" return (self.range and self.range[1]) or ( max(self._values) if self._values else None) + @cached_property + def _secondary_max(self): + """Getter for the maximum series value""" + return (self.range and self.range[1]) or ( + max(self._secondary_values) if self._secondary_values else None) + @cached_property def _order(self): """Getter for the maximum series value""" - return len(self.series) + return len(self.series + self.secondary_series) def _draw(self): """Draw all the things""" diff --git a/pygal/graph/graph.py b/pygal/graph/graph.py index a86fd60..a0ef9be 100644 --- a/pygal/graph/graph.py +++ b/pygal/graph/graph.py @@ -25,8 +25,9 @@ from __future__ import division from pygal.interpolate import interpolation from pygal.graph.base import BaseGraph from pygal.view import View, LogView, XYLogView -from pygal.util import is_major, truncate, reverse_text_len -from math import isnan, pi, sqrt, ceil +from pygal.util import is_major, truncate, reverse_text_len, get_texts_box, cut, rad +from math import isnan, pi, sqrt, ceil, cos +from itertools import repeat, izip, chain, count class Graph(BaseGraph): @@ -60,6 +61,8 @@ class Graph(BaseGraph): self.height - self.margin.y, self._box) + + def _make_graph(self): """Init common graph svg structure""" self.nodes['graph'] = self.svg.node( @@ -128,18 +131,21 @@ class Graph(BaseGraph): self.svg.node(axis, 'path', d='M%f %f v%f' % (0, 0, self.view.height), class_='line') + lastlabel = self._x_labels[-1][0] for label, position in self._x_labels: major = is_major(position) guides = self.svg.node(axis, class_='guides') x = self.view.x(position) y = self.view.height + 5 if draw_axes: + last_guide = (self._y_2nd_labels and label == lastlabel) self.svg.node( guides, 'path', d='M%f %f v%f' % (x, 0, self.view.height), class_='%s%sline' % ( 'major ' if major else '', - 'guide ' if position != 0 else '')) + 'guide ' if position != 0 and not last_guide + else '')) y += .5 * self.label_font_size + 5 text = self.svg.node( guides, 'text', @@ -192,6 +198,37 @@ class Graph(BaseGraph): text.attrib['transform'] = "rotate(%d %f %f)" % ( self.y_label_rotation, x, y) + # TODO: + # - shall we do separate axis 2y node, or use the above (and have an inner + # loop condition) + # - it + # 10 is a magic number around here - margin size, don't know, + # what stands for the additional 2 px + if self._y_2nd_labels: + secondary_ax = self.svg.node(self.nodes['plot'], class_="axis 2y") + #self.svg.node(secondary_ax, 'path', + # d='M%f %f v%f' % (self.view.width-12, 0, self.view.height), + # class_='major line' + #) + for label, position in self._y_2nd_labels: + major = is_major(position) + # it is needed, to have the same structure + guides = self.svg.node(secondary_ax, class_='guides') + x = self.view.width + 5 + y = self.view.y(position) + text = self.svg.node(guides, 'text', + x = x, + # XXX: plus or minus? + y = y + .35 * self.label_font_size, + class_ = 'major' if major else '' + ) + text.text = label + if self.y_label_rotation: + text.attrib['transform'] = "rotate(%d %f %f)" % ( + self.y_label_rotation, x, y) + + + def _legend(self): """Make the legend box""" if not self.show_legend: @@ -209,7 +246,15 @@ class Graph(BaseGraph): truncation = reverse_text_len( available_space, self.legend_font_size) else: - x = self.margin.left + self.view.width + 10 + # draw primary y axis on left + x = 0 + h, w = get_texts_box( + cut(self._y_labels), self.label_font_size) + #x -= 10 + max(w * cos(rad(self.y_label_rotation)), h) + x -= 10 + w + h, w = get_texts_box(self._legends + self._secondary_legends, self.label_font_size) + x -= w + y = self.margin.top + 10 cols = 1 if not truncation: @@ -219,15 +264,44 @@ class Graph(BaseGraph): self.nodes['graph'], class_='legends', transform='translate(%d, %d)' % (x, y)) + h = max(self.legend_box_size, self.legend_font_size) x_step = self.view.width / cols - for i, title in enumerate(self._legends): + if self.legend_at_bottom: + # if legends at the bottom, we dont split the windows + counter = count() + # gen structure - (i, (j, (l, tf))) + gen = enumerate(enumerate(chain( + izip(self._legends, repeat(False)), + izip(self._secondary_legends, repeat(True))))) + secondary_legends = legends + else: + gen = enumerate(chain( + enumerate(izip(self._legends, repeat(False))), + enumerate(izip(self._secondary_legends, repeat(True))))) + + # draw secondary axis on right + x = self.margin.left + self.view.width + 10 + if self._y_2nd_labels: + h, w = get_texts_box( + cut(self._y_labels), self.label_font_size) + x += 10 + max(w * cos(rad(self.y_label_rotation)), h) + + y = self.margin.top + 10 + + secondary_legends = self.svg.node( + self.nodes['graph'], class_='legends', + transform='translate(%d, %d)' % (x, y)) + + for (global_serie_number, (i, (title, is_secondary))) in gen: + col = i % cols row = i // cols legend = self.svg.node( - legends, class_='legend reactive activate-serie', - id="activate-serie-%d" % i) + secondary_legends if is_secondary else legends, + class_='legend reactive activate-serie', + id="activate-serie-%d" % global_serie_number) self.svg.node( legend, 'rect', x=col * x_step, @@ -237,7 +311,7 @@ class Graph(BaseGraph): ) / 2, width=self.legend_box_size, height=self.legend_box_size, - class_="color-%d reactive" % (i % 16) + class_="color-%d reactive" % (global_serie_number % 16) ) truncated = truncate(title, truncation) # Serious magical numbers here @@ -327,7 +401,7 @@ class Graph(BaseGraph): return self._format(values[i][1]) def _points(self, x_pos): - for serie in self.series: + for serie in self.series + self.secondary_series: serie.points = [ (x_pos[i], v) for i, v in enumerate(serie.values)] diff --git a/pygal/util.py b/pygal/util.py index 083cb18..52bd5b0 100644 --- a/pygal/util.py +++ b/pygal/util.py @@ -143,12 +143,21 @@ def compute_logarithmic_scale(min_, max_, min_scale, max_scale): def compute_scale( min_, max_, logarithmic=False, order_min=None, - min_scale=4, max_scale=20): + min_scale=4, max_scale=20, force_steps=None): """Compute an optimal scale between min and max""" if min_ == 0 and max_ == 0: return [0] if max_ - min_ == 0: return [min_] + if force_steps: + # TODO: handle logarithmic scale + step = float(max_ - min_) / (force_steps - 1) + curr = min_ + ret = [] + for i in range(force_steps): + ret.append(curr) + curr += step + return ret if logarithmic: log_scale = compute_logarithmic_scale( min_, max_, min_scale, max_scale) From 0656ea628a51de71e00cd9045d2106ddbcaa83f8 Mon Sep 17 00:00:00 2001 From: Wiktor Niesiobedzki Date: Tue, 15 Jan 2013 16:10:35 +0100 Subject: [PATCH 02/14] Added secondary axis support to Line --- pygal/graph/__init__.py | 1 - pygal/graph/line.py | 48 ++++++++++++++++++++++++++++++++++------- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/pygal/graph/__init__.py b/pygal/graph/__init__.py index 175a099..f9122fa 100644 --- a/pygal/graph/__init__.py +++ b/pygal/graph/__init__.py @@ -36,5 +36,4 @@ CHARTS_NAMES = [ 'VerticalPyramid', 'Dot', 'Gauge', - 'DoubleYLine', ] diff --git a/pygal/graph/line.py b/pygal/graph/line.py index 2ce920b..1534bf8 100644 --- a/pygal/graph/line.py +++ b/pygal/graph/line.py @@ -48,9 +48,13 @@ class Line(Graph): values + [(values[-1][0], zero)]) - def line(self, serie_node, serie): + def line(self, serie_node, serie, rescale=False): """Draw the line serie""" - view_values = map(self.view, serie.points) + if rescale and self.secondary_series: + points = list ((x, self._scale_diff+(y - self._scale_min_2nd) * self._scale) for x, y in serie.points) + else: + points = serie.points + view_values = map(self.view, points) if self.show_dots: for i, (x, y) in enumerate(view_values): if None in (x, y): @@ -87,12 +91,23 @@ class Line(Graph): class_='line reactive' + (' nofill' if not self.fill else '')) def _compute(self): + # X Labels x_pos = [ x / (self._len - 1) for x in range(self._len) ] if self._len != 1 else [.5] # Center if only one value + # XXX: we need to have rescaled serie.values to execute this self._points(x_pos) + x_labels = zip(self.x_labels, x_pos) + + if self.x_labels_num_limit and len(x_labels)>self.x_labels_num_limit: + step = (len(x_labels)-1)/(self.x_labels_num_limit-1) + x_labels = list(x_labels[int(i*step)] for i in range(self.x_labels_num_limit)) + + self._x_labels = self.x_labels and x_labels + # Y Label + if self.include_x_axis: self._box.ymin = min(self._min, 0) self._box.ymax = max(self._max, 0) @@ -104,15 +119,32 @@ class Line(Graph): self._box.ymin, self._box.ymax, self.logarithmic, self.order_min ) if not self.y_labels else map(float, self.y_labels) - x_labels = zip(self.x_labels, x_pos) + self._y_labels = zip(map(self._format, y_pos), y_pos) + # secondary y axis support + if self.secondary_series: + if self.include_x_axis: + ymin = min(self._secondary_min, 0) + ymax = max(self._secondary_max, 0) + else: + ymin = self._secondary_min + ymax = self._secondary_max + print("ymin: %f, ymax: %f" % (ymin, ymax)) + steps = len(y_pos) + left_range = abs(y_pos[-1] - y_pos[0]) + right_range = abs(ymax - ymin) + scale = right_range / (steps-1) + self._y_2nd_labels = list((self._format(ymin+i*scale), pos) for i, pos in enumerate(y_pos)) + + min_2nd = float(self._y_2nd_labels[0][0]) + self._scale = left_range / right_range + self._scale_diff = y_pos[0] + self._scale_min_2nd = min_2nd + - if self.x_labels_num_limit and len(x_labels)>self.x_labels_num_limit: - step = (len(x_labels)-1)/(self.x_labels_num_limit-1) - x_labels = list(x_labels[int(i*step)] for i in range(self.x_labels_num_limit)) - self._x_labels = self.x_labels and x_labels - self._y_labels = zip(map(self._format, y_pos), y_pos) def _plot(self): for index, serie in enumerate(self.series): self.line(self._serie(index), serie) + for index, serie in enumerate(self.secondary_series, len(self.series)): + self.line(self._serie(index), serie, True) From c379f0b6e3df17b5572f04a4e1092da149907511 Mon Sep 17 00:00:00 2001 From: Wiktor Niesiobedzki Date: Wed, 16 Jan 2013 10:58:54 +0100 Subject: [PATCH 03/14] Extend tooltips with x value. --- pygal/graph/line.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pygal/graph/line.py b/pygal/graph/line.py index 1534bf8..b179950 100644 --- a/pygal/graph/line.py +++ b/pygal/graph/line.py @@ -75,7 +75,11 @@ class Line(Graph): val = self._get_value(serie.points, i) self.svg.node(dots, 'circle', cx=x, cy=y, r=self.dots_size, class_='dot reactive tooltip-trigger') - self._tooltip_data(dots, val, x, y) + self._tooltip_data(dots, + "%s: %s" % (self.x_labels[i], val) if self.x_labels and + self.x_labels_num_limit + else val, + x, y) self._static_value( serie_node, val, x + self.value_font_size, From 5cae09dd7f118c34d1c6a032c72f078f1acb7c92 Mon Sep 17 00:00:00 2001 From: Wiktor Niesiobedzki Date: Wed, 16 Jan 2013 13:12:55 +0100 Subject: [PATCH 04/14] remove debug print --- pygal/graph/line.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pygal/graph/line.py b/pygal/graph/line.py index b179950..2b8da08 100644 --- a/pygal/graph/line.py +++ b/pygal/graph/line.py @@ -132,7 +132,6 @@ class Line(Graph): else: ymin = self._secondary_min ymax = self._secondary_max - print("ymin: %f, ymax: %f" % (ymin, ymax)) steps = len(y_pos) left_range = abs(y_pos[-1] - y_pos[0]) right_range = abs(ymax - ymin) From 118907a80bbf43f96503611870e486237c82a315 Mon Sep 17 00:00:00 2001 From: Wiktor Niesiobedzki Date: Wed, 16 Jan 2013 16:37:30 +0100 Subject: [PATCH 05/14] Fix margin settings --- pygal/graph/base.py | 17 +++++++++++++++-- pygal/graph/graph.py | 10 +--------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/pygal/graph/base.py b/pygal/graph/base.py index c9bcbca..ff38171 100644 --- a/pygal/graph/base.py +++ b/pygal/graph/base.py @@ -97,7 +97,7 @@ class BaseGraph(object): if self.show_legend and self.series: h, w = get_texts_box( map(lambda x: truncate(x, self.truncate_legend or 15), - cut(self.series + self.secondary_series, 'title')), + cut(self.series, 'title')), self.legend_font_size) if self.legend_at_bottom: h_max = max(h, self.legend_box_size) @@ -105,6 +105,19 @@ class BaseGraph(object): sqrt(self._order) - 1) * 1.5 + h_max else: self.margin.right += 10 + w + self.legend_box_size + + if self.show_legend and self.secondary_series: + h, w = get_texts_box( + map(lambda x: truncate(x, self.truncate_legend or 15), + cut(self.secondary_series, 'title')), + self.legend_font_size) + if self.legend_at_bottom: + h_max = max(h, self.legend_box_size) + self.margin.bottom += 10 + h_max * round( + sqrt(self._order) - 1) * 1.5 + h_max + else: + self.margin.left += w + self.legend_box_size + if self.title: h, _ = get_text_box(self.title[0], self.title_font_size) @@ -127,7 +140,7 @@ class BaseGraph(object): cut(self._y_labels), self.label_font_size) self.margin.left += 10 + max( w * cos(rad(self.y_label_rotation)), w) - + @cached_property def _legends(self): diff --git a/pygal/graph/graph.py b/pygal/graph/graph.py index a0ef9be..e76a47a 100644 --- a/pygal/graph/graph.py +++ b/pygal/graph/graph.py @@ -246,15 +246,7 @@ class Graph(BaseGraph): truncation = reverse_text_len( available_space, self.legend_font_size) else: - # draw primary y axis on left - x = 0 - h, w = get_texts_box( - cut(self._y_labels), self.label_font_size) - #x -= 10 + max(w * cos(rad(self.y_label_rotation)), h) - x -= 10 + w - h, w = get_texts_box(self._legends + self._secondary_legends, self.label_font_size) - x -= w - + x = 10 y = self.margin.top + 10 cols = 1 if not truncation: From ff8ff3677e00fd27bc42e62cdd25c30275742394 Mon Sep 17 00:00:00 2001 From: Wiktor Niesiobedzki Date: Mon, 21 Jan 2013 15:45:08 +0100 Subject: [PATCH 06/14] Remove surplus CSS declarations for secondary axis --- pygal/css/graph.css | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pygal/css/graph.css b/pygal/css/graph.css index 40fb16b..40fcd28 100644 --- a/pygal/css/graph.css +++ b/pygal/css/graph.css @@ -49,12 +49,8 @@ text.no_data { .axis.y text { text-anchor: end; } -.axis.2y text { - text-anchor: start; -} -.axis.y .logarithmic text:not(.major) , -.axis.2y .logarithmic text:not(.major) { +.axis.y .logarithmic text:not(.major) { font-size: 50%; } @@ -70,13 +66,11 @@ text.no_data { } .horizontal .axis.y .guide.line, -.horizontal .axis.2y .guide.line, .vertical .axis.x .guide.line { opacity: 0; } .axis.y .guides:hover .guide.line, -.axis.2y .guides:hover .guide.line, .line-graph .axis.x .guides:hover .guide.line, .gauge-graph .axis.x .guides:hover .guide.line, .stackedline-graph .axis.x .guides:hover .guide.line, From c2a0e49f9c75f2cc207284f1167571ec138a719b Mon Sep 17 00:00:00 2001 From: Wiktor Niesiobedzki Date: Mon, 21 Jan 2013 15:48:47 +0100 Subject: [PATCH 07/14] Remove surplus "," --- pygal/graph/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygal/graph/__init__.py b/pygal/graph/__init__.py index f9122fa..b255205 100644 --- a/pygal/graph/__init__.py +++ b/pygal/graph/__init__.py @@ -35,5 +35,5 @@ CHARTS_NAMES = [ 'Pyramid', 'VerticalPyramid', 'Dot', - 'Gauge', + 'Gauge' ] From a51f34ab04805a8ab58a4296ee9ec547057fa68e Mon Sep 17 00:00:00 2001 From: Wiktor Niesiobedzki Date: Mon, 21 Jan 2013 15:53:41 +0100 Subject: [PATCH 08/14] Remove secondary_box - not used in code --- pygal/graph/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pygal/graph/base.py b/pygal/graph/base.py index ff38171..38ef0b1 100644 --- a/pygal/graph/base.py +++ b/pygal/graph/base.py @@ -48,7 +48,6 @@ class BaseGraph(object): self.nodes = {} self.margin = Margin(*([20] * 4)) self._box = Box() - self._secondary_box = Box() self.view = None if self.logarithmic and self.zero == 0: # Explicit min to avoid interpolation dependency From 35723ec6cfe33f2936e9a4dc451145e737eb8d82 Mon Sep 17 00:00:00 2001 From: Wiktor Niesiobedzki Date: Mon, 21 Jan 2013 15:56:58 +0100 Subject: [PATCH 09/14] Remove duplicated _secondary_min method --- pygal/graph/base.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pygal/graph/base.py b/pygal/graph/base.py index 38ef0b1..d77e526 100644 --- a/pygal/graph/base.py +++ b/pygal/graph/base.py @@ -184,13 +184,6 @@ class BaseGraph(object): return (self.range and self.range[0]) or ( min(self._values) if self._values else None) - @cached_property - def _secondary_min(self): - """Getter for the secondary minimum series value""" - return (self.range and self.range[0]) or ( - min(self._secondary_values) if self._secondary_values else None) - - @cached_property def _max(self): """Getter for the maximum series value""" From ec51633dc1c4a76cc066e573e2af848d498c77cf Mon Sep 17 00:00:00 2001 From: Wiktor Niesiobedzki Date: Mon, 21 Jan 2013 16:05:10 +0100 Subject: [PATCH 10/14] Fixed CSS declarations - this are needed for proper alignment of values on secondary y axis --- pygal/css/graph.css | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pygal/css/graph.css b/pygal/css/graph.css index 40fcd28..cca96e2 100644 --- a/pygal/css/graph.css +++ b/pygal/css/graph.css @@ -49,8 +49,12 @@ text.no_data { .axis.y text { text-anchor: end; } +.axis.y2 text { + text-anchor: start; +} -.axis.y .logarithmic text:not(.major) { +.axis.y .logarithmic text:not(.major) , +.axis.y2 .logarithmic text:not(.major) { font-size: 50%; } @@ -66,11 +70,13 @@ text.no_data { } .horizontal .axis.y .guide.line, +.horizontal .axis.y2 .guide.line, .vertical .axis.x .guide.line { opacity: 0; } .axis.y .guides:hover .guide.line, +.axis.y2 .guides:hover .guide.line, .line-graph .axis.x .guides:hover .guide.line, .gauge-graph .axis.x .guides:hover .guide.line, .stackedline-graph .axis.x .guides:hover .guide.line, From c5096359ff9e582eb9306a91104972953e2138f8 Mon Sep 17 00:00:00 2001 From: Wiktor Niesiobedzki Date: Mon, 21 Jan 2013 16:19:31 +0100 Subject: [PATCH 11/14] Removal of force_order - not needed in final solution --- pygal/util.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/pygal/util.py b/pygal/util.py index 4ee1922..1ae9829 100644 --- a/pygal/util.py +++ b/pygal/util.py @@ -143,21 +143,12 @@ def compute_logarithmic_scale(min_, max_, min_scale, max_scale): def compute_scale( min_, max_, logarithmic=False, order_min=None, - min_scale=4, max_scale=20, force_steps=None): + min_scale=4, max_scale=20): """Compute an optimal scale between min and max""" if min_ == 0 and max_ == 0: return [0] if max_ - min_ == 0: return [min_] - if force_steps: - # TODO: handle logarithmic scale - step = float(max_ - min_) / (force_steps - 1) - curr = min_ - ret = [] - for i in range(force_steps): - ret.append(curr) - curr += step - return ret if logarithmic: log_scale = compute_logarithmic_scale( min_, max_, min_scale, max_scale) From 76b66492c7d8aa82c1f3a3210637b9cff061a172 Mon Sep 17 00:00:00 2001 From: Wiktor Niesiobedzki Date: Mon, 21 Jan 2013 16:20:07 +0100 Subject: [PATCH 12/14] fixing whitespaces and box computations --- pygal/graph/base.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pygal/graph/base.py b/pygal/graph/base.py index d77e526..7dc5fbd 100644 --- a/pygal/graph/base.py +++ b/pygal/graph/base.py @@ -104,7 +104,7 @@ class BaseGraph(object): sqrt(self._order) - 1) * 1.5 + h_max else: self.margin.right += 10 + w + self.legend_box_size - + if self.show_legend and self.secondary_series: h, w = get_texts_box( map(lambda x: truncate(x, self.truncate_legend or 15), @@ -117,7 +117,6 @@ class BaseGraph(object): else: self.margin.left += w + self.legend_box_size - if self.title: h, _ = get_text_box(self.title[0], self.title_font_size) self.margin.top += len(self.title) * (10 + h) @@ -138,8 +137,7 @@ class BaseGraph(object): h, w = get_texts_box( cut(self._y_labels), self.label_font_size) self.margin.left += 10 + max( - w * cos(rad(self.y_label_rotation)), w) - + w * cos(rad(self.y_label_rotation)), h) @cached_property def _legends(self): From 2f640560ddd6a1e6f966867ba0ce0d2202239d55 Mon Sep 17 00:00:00 2001 From: Wiktor Niesiobedzki Date: Mon, 21 Jan 2013 16:20:24 +0100 Subject: [PATCH 13/14] Whitespace cleanup and comments cleanup --- pygal/graph/graph.py | 28 +++++++++------------------- pygal/graph/line.py | 4 ---- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/pygal/graph/graph.py b/pygal/graph/graph.py index e76a47a..0b4c870 100644 --- a/pygal/graph/graph.py +++ b/pygal/graph/graph.py @@ -61,8 +61,6 @@ class Graph(BaseGraph): self.height - self.margin.y, self._box) - - def _make_graph(self): """Init common graph svg structure""" self.nodes['graph'] = self.svg.node( @@ -198,27 +196,16 @@ class Graph(BaseGraph): text.attrib['transform'] = "rotate(%d %f %f)" % ( self.y_label_rotation, x, y) - # TODO: - # - shall we do separate axis 2y node, or use the above (and have an inner - # loop condition) - # - it - # 10 is a magic number around here - margin size, don't know, - # what stands for the additional 2 px if self._y_2nd_labels: - secondary_ax = self.svg.node(self.nodes['plot'], class_="axis 2y") - #self.svg.node(secondary_ax, 'path', - # d='M%f %f v%f' % (self.view.width-12, 0, self.view.height), - # class_='major line' - #) + secondary_ax = self.svg.node(self.nodes['plot'], class_="axis y2") for label, position in self._y_2nd_labels: major = is_major(position) - # it is needed, to have the same structure + # it is needed, to have the same structure as primary axis guides = self.svg.node(secondary_ax, class_='guides') x = self.view.width + 5 y = self.view.y(position) text = self.svg.node(guides, 'text', x = x, - # XXX: plus or minus? y = y + .35 * self.label_font_size, class_ = 'major' if major else '' ) @@ -227,7 +214,6 @@ class Graph(BaseGraph): text.attrib['transform'] = "rotate(%d %f %f)" % ( self.y_label_rotation, x, y) - def _legend(self): """Make the legend box""" @@ -263,10 +249,14 @@ class Graph(BaseGraph): # if legends at the bottom, we dont split the windows counter = count() # gen structure - (i, (j, (l, tf))) + # i - global serie number - used for coloring and identification + # j - position within current legend box + # l - label + # tf - whether it is secondary label gen = enumerate(enumerate(chain( izip(self._legends, repeat(False)), izip(self._secondary_legends, repeat(True))))) - secondary_legends = legends + secondary_legends = legends # svg node is the same else: gen = enumerate(chain( enumerate(izip(self._legends, repeat(False))), @@ -284,9 +274,9 @@ class Graph(BaseGraph): secondary_legends = self.svg.node( self.nodes['graph'], class_='legends', transform='translate(%d, %d)' % (x, y)) - + for (global_serie_number, (i, (title, is_secondary))) in gen: - + col = i % cols row = i // cols diff --git a/pygal/graph/line.py b/pygal/graph/line.py index 2b8da08..9d089fd 100644 --- a/pygal/graph/line.py +++ b/pygal/graph/line.py @@ -100,7 +100,6 @@ class Line(Graph): x / (self._len - 1) for x in range(self._len) ] if self._len != 1 else [.5] # Center if only one value - # XXX: we need to have rescaled serie.values to execute this self._points(x_pos) x_labels = zip(self.x_labels, x_pos) @@ -143,9 +142,6 @@ class Line(Graph): self._scale_diff = y_pos[0] self._scale_min_2nd = min_2nd - - - def _plot(self): for index, serie in enumerate(self.series): self.line(self._serie(index), serie) From 8db55bcbb3ac5e5fae2494d4fd0316949e30cbcb Mon Sep 17 00:00:00 2001 From: Wiktor Niesiobedzki Date: Mon, 21 Jan 2013 16:35:09 +0100 Subject: [PATCH 14/14] Fixed setting of marigins - now on the left side are primary labels, and on right side - secondary labels - that needs to be taken into consideration, when computing margins. --- pygal/graph/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygal/graph/base.py b/pygal/graph/base.py index 7dc5fbd..94f7c7e 100644 --- a/pygal/graph/base.py +++ b/pygal/graph/base.py @@ -103,7 +103,7 @@ class BaseGraph(object): self.margin.bottom += 10 + h_max * round( sqrt(self._order) - 1) * 1.5 + h_max else: - self.margin.right += 10 + w + self.legend_box_size + self.margin.left += 10 + w + self.legend_box_size if self.show_legend and self.secondary_series: h, w = get_texts_box( @@ -115,7 +115,7 @@ class BaseGraph(object): self.margin.bottom += 10 + h_max * round( sqrt(self._order) - 1) * 1.5 + h_max else: - self.margin.left += w + self.legend_box_size + self.margin.right += 10 + w + self.legend_box_size if self.title: h, _ = get_text_box(self.title[0], self.title_font_size)