diff --git a/demo/moulinrouge/tests.py b/demo/moulinrouge/tests.py index b77bcf7..203b870 100644 --- a/demo/moulinrouge/tests.py +++ b/demo/moulinrouge/tests.py @@ -48,14 +48,19 @@ def get_test_routes(app): @app.route('/test/bar_links') def test_bar_links(): - bar = Bar(style=styles['neon']) + bar = Bar(style=styles['default']( + font_family='googlefont:Raleway')) bar.js = ('http://l:2343/2.0.x/pygal-tooltips.js',) - bar.add('1234', [ + bar.title = 'Wow ! Such Chart !' + bar.x_title = 'Many x labels' + bar.y_title = 'Much y labels' + + bar.add('Red serie', [ {'value': 10, 'label': 'Ten', 'xlink': 'http://google.com?q=10'}, - {'value': 20, - 'tooltip': 'Twenty', + {'value': 25, + 'label': 'Twenty is a good number yada yda yda yada yada', 'xlink': 'http://google.com?q=20'}, 30, {'value': 40, @@ -63,14 +68,15 @@ def get_test_routes(app): 'xlink': 'http://google.com?q=40'} ]) - bar.add('4321', [40, { + bar.add('Blue serie', [40, { 'value': 30, 'label': 'Thirty', 'xlink': 'http://google.com?q=30' }, 20, 10]) - bar.x_labels = map(str, range(1, 5)) + bar.x_labels = ['Yesterday', 'Today or any other day', + 'Tomorrow', 'Someday'] bar.logarithmic = True - bar.zero = 1 + # bar.zero = 1 return bar.render_response() @app.route('/test/xy_links') @@ -395,8 +401,8 @@ def get_test_routes(app): js = ['http://l:2343/2.0.x/pygal-tooltips.js'] stacked = StackedBar(LolConfig()) - stacked.add('1', [1, 2, 3]) - stacked.add('2', [4, 5, 6]) + stacked.add('', [1, 2, 3]) + stacked.add('My beautiful serie of 2019', [4, 5, 6]) return stacked.render_response() @app.route('/test/dateline') @@ -628,7 +634,7 @@ def get_test_routes(app): @app.route('/test/half_pie') def test_half_pie(): pie = Pie(half_pie=True) - for i in range(100): + for i in range(20): pie.add(str(i), i, inner_radius=.1) pie.legend_at_bottom = True pie.legend_at_bottom_columns = 4 diff --git a/docs/changelog.rst b/docs/changelog.rst index a45706e..ae96f73 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -31,6 +31,9 @@ Changelog * Rename in Style foreground_light as foreground_strong * Rename in Style foreground_dark as foreground_subtle * Add a ``render_data_uri`` method (#237) +* Move ``font_size`` config to style +* Add ``font_family`` for various elements in style +* Add ``googlefont:font`` support for style fonts 1.7.0 ===== diff --git a/pygal/_compat.py b/pygal/_compat.py index d5ce470..4c318ca 100644 --- a/pygal/_compat.py +++ b/pygal/_compat.py @@ -80,3 +80,8 @@ def timestamp(x): return x.timestamp() else: return time.mktime(x.utctimetuple()) + +try: + from urllib import quote_plus +except ImportError: + from urllib.parse import quote_plus diff --git a/pygal/config.py b/pygal/config.py index f42d55e..ce56616 100644 --- a/pygal/config.py +++ b/pygal/config.py @@ -301,6 +301,10 @@ class Config(CommonConfig): tooltip_border_radius = Key(0, int, "Look", "Tooltip border radius") + tooltip_fancy_mode = Key( + True, bool, "Look", "Fancy tooltips", + "Print legend, x label in tooltip and use serie color for value.") + inner_radius = Key( 0, float, "Look", "Piechart inner radius (donut), must be <.9") @@ -441,20 +445,6 @@ class Config(CommonConfig): no_data_text = Key( "No data", str, "Text", "Text to display when no data is given") - label_font_size = Key(10, int, "Text", "Label font size") - - major_label_font_size = Key(10, int, "Text", "Major label font size") - - value_font_size = Key(8, int, "Text", "Value font size") - - tooltip_font_size = Key(16, int, "Text", "Tooltip font size") - - title_font_size = Key(16, int, "Text", "Title font size") - - legend_font_size = Key(14, int, "Text", "Legend font size") - - no_data_font_size = Key(64, int, "Text", "No data text font size") - print_values = Key( False, bool, "Text", "Print values when graph is in non interactive mode") diff --git a/pygal/css/base.css b/pygal/css/base.css index 0af06af..2623d37 100644 --- a/pygal/css/base.css +++ b/pygal/css/base.css @@ -22,41 +22,43 @@ * Font-sizes from config, override with care */ -{{ id }}.graph { +{{ id }} { -webkit-user-select: none; -webkit-font-smoothing: antialiased; + font-family: {{ style.font_family }}; } {{ id }}.title { - font-family: {{ style.font_family }}; - font-size: {{ font_sizes.title }}; + font-family: {{ style.title_font_family }}; + font-size: {{ style.title_font_size }}px; } {{ id }}.legends .legend text { - font-family: {{ style.font_family }}; - font-size: {{ font_sizes.legend }}; + font-family: {{ style.legend_font_family }}; + font-size: {{ style.legend_font_size }}px; } {{ id }}.axis text { - font-family: {{ style.font_family }}; - font-size: {{ font_sizes.label }}; + font-family: {{ style.label_font_family }}; + font-size: {{ style.label_font_size }}px; } {{ id }}.axis text.major { - font-family: {{ style.font_family }}; - font-size: {{ font_sizes.major_label }}; + font-family: {{ style.major_label_font_family }}; + font-size: {{ style.major_label_font_size }}px; } {{ id }}.series text { - font-family: {{ style.font_family }}; - font-size: {{ font_sizes.value }}; + font-family: {{ style.value_font_family }}; + font-size: {{ style.value_font_size }}px; } -{{ id }}.tooltip text { - font-family: {{ style.font_family }}; - font-size: {{ font_sizes.tooltip }}; +{{ id }}.tooltip { + font-family: {{ style.tooltip_font_family }}; + font-size: {{ style.tooltip_font_size }}px; } {{ id }}text.no_data { - font-size: {{ font_sizes.no_data }}; + font-family: {{ style.no_data_font_family }}; + font-size: {{ style.no_data_font_size }}px; } diff --git a/pygal/css/graph.css b/pygal/css/graph.css index cf91f13..d8e1297 100644 --- a/pygal/css/graph.css +++ b/pygal/css/graph.css @@ -103,8 +103,8 @@ fill: transparent; } -{{ id }} text { - stroke: none; +{{ id }} text, {{ id }} tspan { + stroke: none !important; } {{ id }}.series text.active { @@ -118,7 +118,3 @@ {{ id }}.tooltip text { fill-opacity: 1; } - -{{ id }}.tooltip text tspan.label { - fill-opacity: .8; -} diff --git a/pygal/css/style.css b/pygal/css/style.css index cf9c1e3..97ad81c 100644 --- a/pygal/css/style.css +++ b/pygal/css/style.css @@ -112,10 +112,28 @@ transition: opacity {{ style.transition }}; } -{{ id }}.tooltip text { +{{ id }}.tooltip .label { + fill: {{ style.foreground }}; +} + +{{ id }}.tooltip .label { + fill: {{ style.foreground }}; +} + +{{ id }}.tooltip .legend { + font-size: .8em; + fill: {{ style.foreground_subtle }}; +} + +{{ id }}.tooltip .x_label { + font-size: .6em; fill: {{ style.foreground_strong }}; } +{{ id }}.tooltip .value { + font-size: 2em; +} + {{ id }}.map-element { fill: {{ style.foreground }}; stroke: {{ style.foreground_subtle }} !important; diff --git a/pygal/graph/graph.py b/pygal/graph/graph.py index 971f1dc..bda4868 100644 --- a/pygal/graph/graph.py +++ b/pygal/graph/graph.py @@ -108,15 +108,12 @@ class Graph(PublicApi): style="opacity: 0", **{'class': 'tooltip'}) - a = self.svg.node(self.nodes['tooltip'], 'a') - self.svg.node(a, 'rect', + self.svg.node(self.nodes['tooltip'], 'rect', rx=self.tooltip_border_radius, ry=self.tooltip_border_radius, width=0, height=0, **{'class': 'tooltip-box'}) - text = self.svg.node(a, 'text', class_='text') - self.svg.node(text, 'tspan', class_='label') - self.svg.node(text, 'tspan', class_='value') + self.svg.node(self.nodes['tooltip'], 'g', class_='text') def _x_axis(self): """Make the x axis: labels and guides""" @@ -136,7 +133,7 @@ class Graph(PublicApi): last_label_position - first_label_position) / ( len(self._x_labels) - 1) truncation = reverse_text_len( - available_space, self.label_font_size) + available_space, self.style.label_font_size) truncation = max(truncation, 1) if 0 not in [label[1] for label in self._x_labels]: @@ -160,7 +157,7 @@ class Graph(PublicApi): 'axis ' if label == "0" else '', 'major ' if major else '', 'guide ' if position != 0 and not last_guide else '')) - y += .5 * self.label_font_size + 5 + y += .5 * self.style.label_font_size + 5 text = self.svg.node( guides, 'text', x=x, @@ -241,7 +238,7 @@ class Graph(PublicApi): text = self.svg.node( guides, 'text', x=x, - y=y + .35 * self.label_font_size, + y=y + .35 * self.style.label_font_size, class_='major' if major else '' ) @@ -267,7 +264,7 @@ class Graph(PublicApi): text = self.svg.node( guides, 'text', x=x, - y=y + .35 * self.label_font_size, + y=y + .35 * self.style.label_font_size, class_='major' if major else '' ) text.text = label @@ -292,7 +289,7 @@ class Graph(PublicApi): available_space = self.view.width / cols - ( self.legend_box_size + 5) truncation = reverse_text_len( - available_space, self.legend_font_size) + available_space, self.style.legend_font_size) else: x = self.spacing y = self.margin_box.top + self.spacing @@ -304,7 +301,7 @@ class Graph(PublicApi): self.nodes['graph'], class_='legends', transform='translate(%d, %d)' % (x, y)) - h = max(self.legend_box_size, self.legend_font_size) + h = max(self.legend_box_size, self.style.legend_font_size) x_step = self.view.width / cols if self.legend_at_bottom: # if legends at the bottom, we dont split the windows @@ -326,7 +323,7 @@ class Graph(PublicApi): x = self.margin_box.left + self.view.width + self.spacing if self._y_2nd_labels: h, w = get_texts_box( - cut(self._y_2nd_labels), self.label_font_size) + cut(self._y_2nd_labels), self.style.label_font_size) x += self.spacing + max(w * cos(rad(self.y_label_rotation)), h) y = self.margin_box.top + self.spacing @@ -348,8 +345,8 @@ class Graph(PublicApi): legend, 'rect', x=col * x_step, y=1.5 * row * h + ( - self.legend_font_size - self.legend_box_size - if self.legend_font_size > self.legend_box_size else 0 + self.style.legend_font_size - self.legend_box_size + if self.style.legend_font_size > self.legend_box_size else 0 ) / 2, width=self.legend_box_size, height=self.legend_box_size, @@ -367,7 +364,7 @@ class Graph(PublicApi): self.svg.node( node, 'text', x=col * x_step + self.legend_box_size + 5, - y=1.5 * row * h + .5 * h + .3 * self.legend_font_size + y=1.5 * row * h + .5 * h + .3 * self.style.legend_font_size ).text = truncated if truncated != title: @@ -380,7 +377,7 @@ class Graph(PublicApi): self.svg.node( self.nodes['title'], 'text', class_='title plot_title', x=self.width / 2, - y=i * (self.title_font_size + self.spacing) + y=i * (self.style.title_font_size + self.spacing) ).text = title_line def _make_x_title(self): @@ -392,7 +389,7 @@ class Graph(PublicApi): text = self.svg.node( self.nodes['title'], 'text', class_='title', x=self.margin_box.left + self.view.width / 2, - y=y + i * (self.title_font_size + self.spacing) + y=y + i * (self.style.title_font_size + self.spacing) ) text.text = title_line @@ -404,7 +401,7 @@ class Graph(PublicApi): text = self.svg.node( self.nodes['title'], 'text', class_='title', x=self._legend_at_left_width, - y=i * (self.title_font_size + self.spacing) + yc + y=i * (self.style.title_font_size + self.spacing) + yc ) text.attrib['transform'] = "rotate(%d %f %f)" % ( -90, self._legend_at_left_width, yc) @@ -455,7 +452,7 @@ class Graph(PublicApi): serie_node['text_overlay'], 'text', class_='centered', x=x, - y=y + self.value_font_size / 3 + y=y + self.style.value_font_size / 3 ).text = value if self.print_zeroes or value != '0' else '' def _get_value(self, values, i): @@ -530,7 +527,7 @@ class Graph(PublicApi): h, w = get_texts_box( map(lambda x: truncate(x, self.truncate_legend or 15), cut(series_group, 'title')), - self.legend_font_size) + self.style.legend_font_size) if self.legend_at_bottom: h_max = max(h, self.legend_box_size) cols = (self._order // self.legend_at_bottom_columns @@ -554,7 +551,7 @@ class Graph(PublicApi): h, w = get_texts_box( map(lambda x: truncate(x, self.truncate_label or 25), cut(xlabels)), - self.label_font_size) + self.style.label_font_size) self._x_labels_height = self.spacing + max( w * sin(rad(self.x_label_rotation)), h) if xlabels is self._x_labels: @@ -570,7 +567,7 @@ class Graph(PublicApi): for ylabels in (self._y_labels, self._y_2nd_labels): if ylabels: h, w = get_texts_box( - cut(ylabels), self.label_font_size) + cut(ylabels), self.style.label_font_size) if ylabels is self._y_labels: self.margin_box.left += self.spacing + max( w * cos(rad(self.y_label_rotation)), h) @@ -579,29 +576,30 @@ class Graph(PublicApi): w * cos(rad(self.y_label_rotation)), h) self._title = split_title( - self.title, self.width, self.title_font_size) + self.title, self.width, self.style.title_font_size) if self.title: - h, _ = get_text_box(self._title[0], self.title_font_size) + h, _ = get_text_box(self._title[0], self.style.title_font_size) self.margin_box.top += len(self._title) * (self.spacing + h) self._x_title = split_title( - self.x_title, self.width - self.margin_box.x, self.title_font_size) + self.x_title, self.width - self.margin_box.x, + self.style.title_font_size) self._x_title_height = 0 if self._x_title: - h, _ = get_text_box(self._x_title[0], self.title_font_size) + h, _ = get_text_box(self._x_title[0], self.style.title_font_size) height = len(self._x_title) * (self.spacing + h) self.margin_box.bottom += height self._x_title_height = height + self.spacing self._y_title = split_title( self.y_title, self.height - self.margin_box.y, - self.title_font_size) + self.style.title_font_size) self._y_title_height = 0 if self._y_title: - h, _ = get_text_box(self._y_title[0], self.title_font_size) + h, _ = get_text_box(self._y_title[0], self.style.title_font_size) height = len(self._y_title) * (self.spacing + h) self.margin_box.left += height self._y_title_height = height + self.spacing diff --git a/pygal/graph/line.py b/pygal/graph/line.py index 7343bdb..ba952de 100644 --- a/pygal/graph/line.py +++ b/pygal/graph/line.py @@ -122,8 +122,8 @@ class Line(Graph): dots, val, x, y) self._static_value( serie_node, val, - x + self.value_font_size, - y + self.value_font_size) + x + self.style.value_font_size, + y + self.style.value_font_size) if serie.stroke: if self.interpolate: diff --git a/pygal/graph/public.py b/pygal/graph/public.py index e84f163..165ecc6 100644 --- a/pygal/graph/public.py +++ b/pygal/graph/public.py @@ -80,6 +80,7 @@ class PublicApi(BaseGraph): from lxml.html import open_in_browser except ImportError: raise ImportError('You must install lxml to use render in browser') + kwargs.setdefault('force_uri_protocol', 'https') open_in_browser(self.render_tree(**kwargs), encoding='utf-8') def render_response(self, **kwargs): diff --git a/pygal/style.py b/pygal/style.py index 7966e04..2545a7f 100644 --- a/pygal/style.py +++ b/pygal/style.py @@ -36,6 +36,24 @@ class Style(object): # Monospaced font is highly encouraged font_family = 'Consolas, "Liberation Mono", Menlo, Courier, ' 'monospace' + + label_font_family = None + major_label_font_family = None + value_font_family = None + tooltip_font_family = None + title_font_family = None + legend_font_family = None + no_data_font_family = None + + label_font_size = 10 + major_label_font_size = 10 + value_font_size = 8 + tooltip_font_size = 16 + title_font_size = 16 + legend_font_size = 14 + no_data_font_size = 64 + + opacity = '.7' opacity_hover = '.8' transition = '150ms' @@ -64,6 +82,19 @@ class Style(object): def __init__(self, **kwargs): """Create the style""" self.__dict__.update(kwargs) + self._google_fonts = set() + if self.font_family.startswith('googlefont:'): + self.font_family = self.font_family.replace('googlefont:', '') + self._google_fonts.add(self.font_family.split(',')[0].strip()) + + for name in dir(self): + if name.endswith('_font_family'): + fn = getattr(self, name) + if fn is None: + setattr(self, name, self.font_family) + elif fn.startswith('googlefont:'): + setattr(self, name, fn.replace('googlefont:', '')) + self._google_fonts.add(getattr(self, name).split(',')[0].strip()) def get_colors(self, prefix, len_): """Get the css color list""" @@ -96,7 +127,7 @@ class Style(object): """Convert instance to a serializable mapping.""" config = {} for attr in dir(self): - if not attr.startswith('__'): + if not attr.startswith('_'): value = getattr(self, attr) if not hasattr(value, '__call__'): config[attr] = value diff --git a/pygal/svg.py b/pygal/svg.py index d867e28..f75d61d 100644 --- a/pygal/svg.py +++ b/pygal/svg.py @@ -20,7 +20,7 @@ """Svg helper""" from __future__ import division -from pygal._compat import to_str, u +from pygal._compat import to_str, u, quote_plus from pygal.etree import etree import io import os @@ -85,7 +85,15 @@ class Svg(object): colors = self.graph.style.get_colors(self.id, self.graph._order) strokes = self.get_strokes() all_css = [] - for css in ['file://base.css'] + list(self.graph.css): + auto_css = ['file://base.css'] + + if self.graph.style._google_fonts: + auto_css.append( + '//fonts.googleapis.com/css?family=%s' % quote_plus( + '|'.join(self.graph.style._google_fonts)) + ) + + for css in auto_css + list(self.graph.css): css_text = None if css.startswith('inline:'): css_text = css[len('inline:'):] @@ -94,25 +102,12 @@ class Svg(object): css = os.path.join( os.path.dirname(__file__), 'css', css[len('file://'):]) - class FontSizes(object): - - """Container for font sizes""" - - fs = FontSizes() - for name in dir(self.graph.state): - if name.endswith('_font_size'): - setattr( - fs, - name.replace('_font_size', ''), - ('%dpx' % getattr(self.graph, name))) - with io.open(css, encoding='utf-8') as f: css_text = template( f.read(), style=self.graph.style, colors=colors, strokes=strokes, - font_sizes=fs, id=self.id) if css_text is not None: @@ -146,9 +141,13 @@ class Svg(object): return o.to_dict() return json.JSONEncoder().default(o) + dct = get_js_dict() + # Config adds + dct['legends'] = self.graph._legends + common_script.text = " = ".join( ("window.config", json.dumps( - get_js_dict(), default=json_default))) + dct, default=json_default))) for js in self.graph.js: if js.startswith('file://'):