Browse Source

Fix some bugs + add generated tests + support external style sheets

pull/8/head
Florian Mounier 13 years ago
parent
commit
d7277f1f51
  1. 7
      demo/simple_test.py
  2. 10
      pygal/config.py
  3. 48
      pygal/css/base.css
  4. 68
      pygal/css/graph.css
  5. 112
      pygal/css/style.css
  6. 4
      pygal/graph/bar.py
  7. 14
      pygal/graph/base.py
  8. 4
      pygal/graph/graph.py
  9. 2
      pygal/serie.py
  10. 57
      pygal/svg.py
  11. 31
      pygal/test/__init__.py
  12. 3
      pygal/test/test_config.py
  13. 61
      pygal/test/test_graph.py
  14. 4
      pygal/util.py

7
demo/simple_test.py

@ -28,7 +28,7 @@ dot = Dot()
dot.x_labels = map(str, range(4)) dot.x_labels = map(str, range(4))
dot.add('a', [1, lnk(3, 'Foo'), 5, 3]) dot.add('a', [1, lnk(3, 'Foo'), 5, 3])
dot.add('b', [2, -2, 0, 2]) dot.add('b', [2, 2, 0, 2])
dot.add('c', [5, 1, 5, lnk(3, 'Bar')]) dot.add('c', [5, 1, 5, lnk(3, 'Bar')])
dot.add('d', [5, 5, lnk(0, 'Babar'), 3]) dot.add('d', [5, 5, lnk(0, 'Babar'), 3])
@ -45,7 +45,8 @@ bar.add('1234', [
bar.add('4321', [40, {'value': 30, 'label': 'Thirty', 'xlink': 'http://google.com?q=30'}, 20, 10]) bar.add('4321', [40, {'value': 30, 'label': 'Thirty', 'xlink': 'http://google.com?q=30'}, 20, 10])
bar.x_labels = map(str, range(1, 5)) bar.x_labels = map(str, range(1, 5))
bar.logarithmic = True
bar.zero = 1
# bar.included_js = [] # bar.included_js = []
# bar.external_js = [ # bar.external_js = [
# 'http://localhost:7575/svg.jquery.js', # 'http://localhost:7575/svg.jquery.js',
@ -163,7 +164,7 @@ config.x_labels = (
'black', 'red', 'blue', 'yellow', 'orange', 'green', 'white') 'black', 'red', 'blue', 'yellow', 'orange', 'green', 'white')
# config.interpolate = 'nearest' # config.interpolate = 'nearest'
radar = Radar(config) radar = Radar(config)
radar.add('test', [1, 4, lnk(10), 5, None, -2, 5]) radar.add('test', [1, 4, lnk(10), 5, None, 2, 5])
radar.add('test2', [10, 2, 0, 5, 1, 9, 4]) radar.add('test2', [10, 2, 0, 5, 1, 9, 4])
radar.title = "Radar test" radar.title = "Radar test"

10
pygal/config.py

@ -21,7 +21,6 @@
Config module with all options Config module with all options
""" """
import os
from pygal.style import DefaultStyle from pygal.style import DefaultStyle
@ -40,11 +39,10 @@ class Config(object):
human_readable = False human_readable = False
#: Display values in logarithmic scale #: Display values in logarithmic scale
logarithmic = False logarithmic = False
#: If set to a filename, this will replace the default css #: List of css file, can be an absolute file path or an external link
base_css = None css = ['style.css', 'graph.css'] # Relative path to pygal css
#: or default js #: List of js file, can be a filepath or an external link
included_js = [] js = [
external_js = [
'https://raw.github.com/Kozea/pygal.js/master/svg.jquery.js', 'https://raw.github.com/Kozea/pygal.js/master/svg.jquery.js',
'https://raw.github.com/Kozea/pygal.js/master/pygal-tooltips.js' 'https://raw.github.com/Kozea/pygal.js/master/pygal-tooltips.js'
] ]

48
pygal/css/base.css

@ -0,0 +1,48 @@
/*
* This file is part of pygal
*
* A python svg graph plotting library
* Copyright © 2012 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/>.
*/
/*
* Font-sizes from config, override with care
*/
.title {
font-family: monospace;
font-size: {{ font_sizes.title }};
}
.legends .legend text {
font-family: monospace;
font-size: {{ font_sizes.legend }};
}
.axis text {
font-family: monospace;
font-size: {{ font_sizes.label }};
}
.series text {
font-family: monospace;
font-size: {{ font_sizes.value }};
}
#tooltip text {
font-family: monospace;
font-size: {{ font_sizes.tooltip }};
}

68
pygal/css/graph.css

@ -18,30 +18,7 @@
* along with pygal. If not, see <http://www.gnu.org/licenses/>. * along with pygal. If not, see <http://www.gnu.org/licenses/>.
*/ */
svg {
background-color: {{ style.background }};
}
svg * {
-webkit-transition: {{ style.transition }};
-moz-transition: {{ style.transition }};
transition: {{ style.transition }};
}
.graph > .background {
fill: {{ style.background }};
}
.plot > .background {
fill: {{ style.plot_background }};
}
.graph {
fill: {{ style.foreground }};
}
text.no_data { text.no_data {
fill: {{ style.foreground_light }};
text-anchor: middle; text-anchor: middle;
} }
@ -54,29 +31,14 @@ text.no_data {
} }
.title { .title {
fill: {{ style.foreground_light }};
font-size: {{ font_sizes.title }};
text-anchor: middle; text-anchor: middle;
} }
.legends .legend text { .legends .legend text {
font-family: monospace;
font-size: {{ font_sizes.legend }};
fill: {{ style.foreground }};
fill-opacity: 1; fill-opacity: 1;
} }
.legends .legend:hover text {
fill: {{ style.foreground_light }};
}
.axis text {
font-size: {{ font_sizes.label }};
font-family: sans;
}
.axis.x text { .axis.x text {
font-family: monospace;
text-anchor: middle; text-anchor: middle;
} }
@ -85,7 +47,6 @@ text.no_data {
} }
.axis.y text { .axis.y text {
font-family: monospace;
text-anchor: end; text-anchor: end;
} }
@ -93,26 +54,19 @@ text.no_data {
font-size: 50%; font-size: 50%;
} }
.axis .line {
stroke: {{ style.foreground_light }};
}
.axis .guide.line { .axis .guide.line {
stroke: {{ style.foreground_dark }};
stroke-dasharray: 4,4; stroke-dasharray: 4,4;
} }
.axis .major.line { .axis .major.line {
stroke: {{ style.foreground }};
stroke-dasharray: 6,6; stroke-dasharray: 6,6;
} }
.axis text.major { .axis text.major {
stroke-width: 0.5px; stroke-width: 0.5px;
stroke: {{ style.foreground_light }};
fill: {{ style.foreground_light }};
} }
.axis.{{ hidden }} .guide.line { .horizontal .axis.y .guide.line,
.vertical .axis.x .guide.line {
opacity: 0; opacity: 0;
} }
@ -120,12 +74,10 @@ text.no_data {
.line-graph .axis.x .guides:hover .guide.line, .line-graph .axis.x .guides:hover .guide.line,
.stackedline-graph .axis.x .guides:hover .guide.line, .stackedline-graph .axis.x .guides:hover .guide.line,
.xy-graph .axis.x .guides:hover .guide.line { .xy-graph .axis.x .guides:hover .guide.line {
stroke: {{ style.foreground_light }};
opacity: 1; opacity: 1;
} }
.axis .guides:hover text { .axis .guides:hover text {
fill: {{ style.foreground_light }};
opacity: 1; opacity: 1;
} }
@ -133,14 +85,6 @@ text.no_data {
fill: none; fill: none;
} }
.reactive {
fill-opacity: {{ style.opacity }};
}
.reactive.active, .active .reactive {
fill-opacity: {{ style.opacity_hover }};
}
.dot { .dot {
stroke-width: 1px; stroke-width: 1px;
fill-opacity: 1; fill-opacity: 1;
@ -151,9 +95,7 @@ text.no_data {
} }
.series text { .series text {
font-size: {{ font_sizes.value }};
stroke: none; stroke: none;
fill: {{ style.foreground_light }};
} }
.series text.active { .series text.active {
@ -162,14 +104,10 @@ text.no_data {
#tooltip rect { #tooltip rect {
fill-opacity: 0.8; fill-opacity: 0.8;
fill: {{ style.plot_background }};
stroke: {{ style.foreground_light }};
} }
#tooltip text { #tooltip text {
fill-opacity: 1; fill-opacity: 1;
fill: {{ style.foreground_light }};
font-size: {{ font_sizes.tooltip }};
} }
#tooltip text tspan.label { #tooltip text tspan.label {
@ -179,5 +117,3 @@ text.no_data {
a:visited { a:visited {
fill: none; fill: none;
} }
{{ style.colors }}

112
pygal/css/style.css

@ -0,0 +1,112 @@
/*
* This file is part of pygal
*
* A python svg graph plotting library
* Copyright © 2012 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/>.
*/
/*
* Styles from config
*/
svg {
background-color: {{ style.background }};
}
svg * {
-webkit-transition: {{ style.transition }};
-moz-transition: {{ style.transition }};
transition: {{ style.transition }};
}
.graph > .background {
fill: {{ style.background }};
}
.plot > .background {
fill: {{ style.plot_background }};
}
.graph {
fill: {{ style.foreground }};
}
text.no_data {
fill: {{ style.foreground_light }};
}
.title {
fill: {{ style.foreground_light }};
}
.legends .legend text {
fill: {{ style.foreground }};
}
.legends .legend:hover text {
fill: {{ style.foreground_light }};
}
.axis .line {
stroke: {{ style.foreground_light }};
}
.axis .guide.line {
stroke: {{ style.foreground_dark }};
}
.axis .major.line {
stroke: {{ style.foreground }};
}
.axis text.major {
stroke: {{ style.foreground_light }};
fill: {{ style.foreground_light }};
}
.axis.y .guides:hover .guide.line,
.line-graph .axis.x .guides:hover .guide.line,
.stackedline-graph .axis.x .guides:hover .guide.line,
.xy-graph .axis.x .guides:hover .guide.line {
stroke: {{ style.foreground_light }};
}
.axis .guides:hover text {
fill: {{ style.foreground_light }};
}
.reactive {
fill-opacity: {{ style.opacity }};
}
.reactive.active, .active .reactive {
fill-opacity: {{ style.opacity_hover }};
}
.series text {
fill: {{ style.foreground_light }};
}
#tooltip rect {
fill: {{ style.plot_background }};
stroke: {{ style.foreground_light }};
}
#tooltip text {
fill: {{ style.foreground_light }};
}
{{ style.colors }}

4
pygal/graph/bar.py

@ -68,9 +68,9 @@ class Bar(Graph):
padding = .1 * width padding = .1 * width
inner_width = width - 2 * padding inner_width = width - 2 * padding
if self.horizontal: if self.horizontal:
height = self.view.x(0) - y height = self.view.x(self.zero) - y
else: else:
height = self.view.y(0) - y height = self.view.y(self.zero) - y
if stack_vals == None: if stack_vals == None:
bar_width = inner_width / len(self.series) bar_width = inner_width / len(self.series)
bar_padding = .1 * bar_width bar_padding = .1 * bar_width

14
pygal/graph/base.py

@ -23,7 +23,7 @@ Base for pygal charts
from __future__ import division from __future__ import division
import io import io
from pygal.serie import Serie, Value from pygal.serie import Serie, Value, PositiveValue
from pygal.view import Margin, Box from pygal.view import Margin, Box
from pygal.util import get_text_box, get_texts_box, cut, rad, humanize from pygal.util import get_text_box, get_texts_box, cut, rad, humanize
from pygal.svg import Svg from pygal.svg import Svg
@ -60,6 +60,9 @@ class BaseGraph(object):
"""(Re-)Init the graph""" """(Re-)Init the graph"""
self.margin = Margin(*([20] * 4)) self.margin = Margin(*([20] * 4))
self._box = Box() self._box = Box()
if self.logarithmic and self.zero == 0:
# If logarithmic, default zero to 1
self.zero = 1
def __getattr__(self, attr): def __getattr__(self, attr):
"""Search in config, then in self""" """Search in config, then in self"""
@ -129,6 +132,7 @@ class BaseGraph(object):
def _draw(self): def _draw(self):
"""Draw all the things""" """Draw all the things"""
self._prepare_data()
self._compute() self._compute()
self._compute_margin() self._compute_margin()
self._decorate() self._decorate()
@ -142,6 +146,14 @@ class BaseGraph(object):
return False return False
return True return True
def _prepare_data(self):
"""Remove aberrant values"""
if self.logarithmic:
for serie in self.series:
for metadata in serie.metadata:
if metadata.value <= 0:
metadata.value = None
def _uniformize_data(self): def _uniformize_data(self):
"""Make all series to max len""" """Make all series to max len"""
for serie in self.series: for serie in self.series:

4
pygal/graph/graph.py

@ -55,7 +55,9 @@ class Graph(BaseGraph):
def _make_graph(self): def _make_graph(self):
"""Init common graph svg structure""" """Init common graph svg structure"""
self.nodes['graph'] = self.svg.node( self.nodes['graph'] = self.svg.node(
class_='graph %s-graph' % self.__class__.__name__.lower()) class_='graph %s-graph %s' % (
self.__class__.__name__.lower(),
'horizontal' if self.horizontal else 'vertical'))
self.svg.node(self.nodes['graph'], 'rect', self.svg.node(self.nodes['graph'], 'rect',
class_='background', class_='background',
x=0, y=0, x=0, y=0,

2
pygal/serie.py

@ -50,7 +50,7 @@ class PositiveValue(Value):
"""Positive or zero value container""" """Positive or zero value container"""
def __init__(self, value): def __init__(self, value):
super(PositiveValue, self).__init__(max(0, value)) super(PositiveValue, self).__init__(max(value, 0))
class Label(object): class Label(object):

57
pygal/svg.py

@ -27,6 +27,7 @@ import os
import json import json
from lxml import etree from lxml import etree
from math import cos, sin, pi from math import cos, sin, pi
from urlparse import urlparse
from pygal.util import template, coord_format from pygal.util import template, coord_format
from pygal import __version__ from pygal import __version__
@ -37,6 +38,8 @@ class Svg(object):
def __init__(self, graph): def __init__(self, graph):
self.graph = graph self.graph = graph
self.processing_instructions = [
etree.PI('xml', "version='1.0' encoding='utf-8'")]
self.root = None self.root = None
self.defs = None self.defs = None
@ -47,24 +50,30 @@ class Svg(object):
nsmap={ nsmap={
None: self.ns, None: self.ns,
'xlink': 'http://www.w3.org/1999/xlink', 'xlink': 'http://www.w3.org/1999/xlink',
}, })
# onload="svg_load(evt);"
)
self.root.append(etree.Comment( self.root.append(etree.Comment(
u'Generated with pygal %s ©Kozea 2012' % __version__)) u'Generated with pygal %s ©Kozea 2012' % __version__))
self.root.append(etree.Comment(u'http://github.com/Kozea/pygal')) self.root.append(etree.Comment(u'http://github.com/Kozea/pygal'))
self.defs = self.node(tag='defs') self.defs = self.node(tag='defs')
def add_style(self, css): def add_styles(self):
"""Add the css to the svg""" """Add the css to the svg"""
style = self.node(self.defs, 'style', type='text/css') for css in ['base.css'] + list(self.graph.css):
with io.open(css, encoding='utf-8') as f: if urlparse(css).scheme:
templ = template( self.processing_instructions.append(
f.read(), etree.PI(
style=self.graph.style, 'xml-stylesheet', 'href="%s"' % css))
font_sizes=self.graph.font_sizes(), else:
hidden='y' if self.graph.horizontal else 'x') if not os.path.exists(css):
style.text = templ css = os.path.join(
os.path.dirname(__file__), 'css', css)
with io.open(css, encoding='utf-8') as f:
templ = template(
f.read(),
style=self.graph.style,
font_sizes=self.graph.font_sizes())
self.node(
self.defs, 'style', type='text/css').text = templ
def add_scripts(self): def add_scripts(self):
"""Add the js to the svg""" """Add the js to the svg"""
@ -72,13 +81,14 @@ class Svg(object):
common_script.text = " = ".join( common_script.text = " = ".join(
("window.config", json.dumps(self.graph.config.to_dict()))) ("window.config", json.dumps(self.graph.config.to_dict())))
for external_js in self.graph.external_js: for js in self.graph.js:
self.node( if urlparse(js).scheme:
self.defs, 'script', type='text/javascript', href=external_js) self.node(
for included_js in self.graph.included_js: self.defs, 'script', type='text/javascript', href=js)
script = self.node(self.defs, 'script', type='text/javascript') else:
with io.open(included_js, encoding='utf-8') as f: script = self.node(self.defs, 'script', type='text/javascript')
script.text = f.read() with io.open(js, encoding='utf-8') as f:
script.text = f.read()
def node(self, parent=None, tag='g', attrib=None, **extras): def node(self, parent=None, tag='g', attrib=None, **extras):
"""Make a new svg node""" """Make a new svg node"""
@ -160,8 +170,7 @@ class Svg(object):
def pre_render(self, no_data=False): def pre_render(self, no_data=False):
"""Last things to do before rendering""" """Last things to do before rendering"""
self.add_style(self.graph.base_css or os.path.join( self.add_styles()
os.path.dirname(__file__), 'css', 'graph.css'))
self.add_scripts() self.add_scripts()
self.root.set( self.root.set(
'viewBox', '0 0 %d %d' % (self.graph.width, self.graph.height)) 'viewBox', '0 0 %d %d' % (self.graph.width, self.graph.height))
@ -179,8 +188,12 @@ class Svg(object):
"""Last thing to do before rendering""" """Last thing to do before rendering"""
svg = etree.tostring( svg = etree.tostring(
self.root, pretty_print=True, self.root, pretty_print=True,
xml_declaration=not self.graph.disable_xml_declaration, xml_declaration=False,
encoding='utf-8') encoding='utf-8')
if not self.graph.disable_xml_declaration:
svg = '\n'.join(
[etree.tostring(pi) for pi in self.processing_instructions]
) + '\n' + svg
if self.graph.disable_xml_declaration or is_unicode: if self.graph.disable_xml_declaration or is_unicode:
svg = svg.decode('utf-8') svg = svg.decode('utf-8')
return svg return svg

31
pygal/test/__init__.py

@ -16,3 +16,34 @@
# #
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
import pygal
from pygal.util import cut
def get_data(i):
return [
[(-1, 1), (2, 0), (0, 4)],
[(0, 1), (None, 2), (3, 2)],
[(-3, 3), (1, 3), (1, 1)],
[(1, 1), (1, 1), (1, 1)],
[(3, 2), (2, 1), (1, 1)]][i]
def pytest_generate_tests(metafunc):
if "Chart" in metafunc.funcargnames:
metafunc.parametrize("Chart", pygal.CHARTS)
if "datas" in metafunc.funcargnames:
metafunc.parametrize(
"datas",
[
[("Serie %d" % i, get_data(i)) for i in range(s)]
for s in (5, 1, 0)
])
def make_data(chart, datas):
for data in datas:
chart.add(data[0],
data[1] if chart.__class__ == pygal.XY else cut(data[1]))
return chart

3
pygal/test/test_config.py

@ -16,8 +16,9 @@
# #
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
from pygal import Line from pygal import Line, Dot, Pie, Radar
from pygal.test.utils import texts from pygal.test.utils import texts
from pygal.test import pytest_generate_tests, make_data
def test_logarithmic(): def test_logarithmic():

61
pygal/test/test_graph.py

@ -17,38 +17,34 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
import os import os
from pygal import Line
import pygal import pygal
import uuid import uuid
from pygal.util import cut
from pygal.test import pytest_generate_tests, make_data
def test_multi_render(): def test_multi_render(Chart, datas):
for Chart in pygal.CHARTS: chart = Chart()
chart = Chart() chart = make_data(chart, datas)
rng = range(20) svg = chart.render()
if Chart == pygal.XY: for i in range(2):
rng = zip(rng, rng) assert svg == chart.render()
chart.add('Serie', rng)
chart.add('Serie 2', list(reversed(rng)))
svg = chart.render()
for i in range(2):
assert svg == chart.render()
def test_render_to_file(): def test_render_to_file(Chart, datas):
file_name = '/tmp/test_graph-%s.svg' % uuid.uuid4() file_name = '/tmp/test_graph-%s.svg' % uuid.uuid4()
if os.path.exists(file_name): if os.path.exists(file_name):
os.remove(file_name) os.remove(file_name)
line = Line() chart = Chart()
line.add('Serie 1', [1]) chart = make_data(chart, datas)
line.render_to_file(file_name) chart.render_to_file(file_name)
with open(file_name) as f: with open(file_name) as f:
assert 'pygal' in f.read() assert 'pygal' in f.read()
os.remove(file_name) os.remove(file_name)
def test_render_to_png(): def test_render_to_png(Chart, datas):
try: try:
import cairosvg import cairosvg
except ImportError: except ImportError:
@ -58,9 +54,34 @@ def test_render_to_png():
if os.path.exists(file_name): if os.path.exists(file_name):
os.remove(file_name) os.remove(file_name)
line = Line() chart = Chart()
line.add('Serie 1', [1]) chart = make_data(chart, datas)
line.render_to_png(file_name) chart.render_to_png(file_name)
with open(file_name, 'rb') as f: with open(file_name, 'rb') as f:
assert f.read() assert f.read()
os.remove(file_name) os.remove(file_name)
def test_metadata(Chart):
chart = Chart()
v = range(7)
if Chart == pygal.XY:
v = map(lambda x: (x, x + 1), v)
chart.add('Serie with metadata', [
v[0],
{'value': v[1]},
{'value': v[2], 'label': 'Three'},
{'value': v[3], 'xlink': 'http://4.example.com/'},
{'value': v[4], 'xlink': 'http://5.example.com/', 'label': 'Five'},
{'value': v[5], 'xlink': {
'href': 'http://6.example.com/'}, 'label': 'Six'},
{'value': v[6], 'xlink': {
'href': 'http://7.example.com/',
'target': '_blank'}, 'label': 'Seven'}
])
q = chart.render_pyquery()
for md in (
'Three', 'http://4.example.com/',
'Five', 'http://7.example.com/', 'Seven'):
assert md in cut(q('desc'), 'text')

4
pygal/util.py

@ -123,6 +123,8 @@ ident = lambda x: x
def compute_logarithmic_scale(min_, max_): def compute_logarithmic_scale(min_, max_):
"""Compute an optimal scale for logarithmic""" """Compute an optimal scale for logarithmic"""
if max_ <= 0 or min_ <= 0:
return []
min_order = int(floor(log10(min_))) min_order = int(floor(log10(min_)))
max_order = int(ceil(log10(max_))) max_order = int(ceil(log10(max_)))
positions = [] positions = []
@ -199,6 +201,8 @@ def decorate(svg, node, metadata):
for key in dir(metadata): for key in dir(metadata):
if key not in ('value') and not key.startswith('_'): if key not in ('value') and not key.startswith('_'):
value = getattr(metadata, key) value = getattr(metadata, key)
if key == 'xlink' and isinstance(value, dict):
value = value.get('href', value)
if value: if value:
svg.node(node, 'desc', class_=key).text = str(value) svg.node(node, 'desc', class_=key).text = str(value)
return node return node

Loading…
Cancel
Save