2
# -*- coding: utf-8 -*-
11
# This program is free software; you can redistribute it and/or modify
12
# it under the terms of the GNU General Public License as published by
13
# the Free Software Foundation; either version 3 of the License, or
14
# (at your option) any later version.
16
# This program is distributed in the hope that it will be useful,
17
# but WITHOUT ANY WARRANTY; without even the implied warranty of
18
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
# GNU General Public License for more details.
21
# You should have received a copy of the GNU General Public License
22
# along with this program; if not, write to the Free Software
23
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
28
# allow negative values for bar charts
29
# show values for stacked bar charts
30
# don't create a new layer for each chart, but a normal group
31
# correct bar height for stacked bars (it's only half as high as it should be, double)
32
# adjust position of heading
33
# use aliasing workaround for stacked bars (e.g. let the rectangles overlap)
35
# Example CSV file contents:
37
Month;1978;1979;1980;1981
39
February;6.5;2.4;1.2;6.1
42
May;10.9;11.7;18.7;11.1
43
June;12.6;14.2;14.7;14.7
44
July;16.5;15.5;17.5;15.1
45
August;15.9;15.4;14.6;16.6
46
September;14;14.5;13.2;15.3
47
October;11.9;13.9;11.5;9.2
48
November;6.7;8.5;7;6.6
49
December;6.4;2.2;6.3;3.5
51
# The extension creates one chart for a single value column in one go,
52
# e.g. chart all temperatures for all months of the year 1978 into one chart.
53
# (for this, select column 0 for labels and column 1 for values).
54
# "1978" etc. can be used as heading (Need not be numeric. If not used delete the heading line.)
55
# Month names can be used as labels
56
# Values can be shown, in addition to labels (doesn't work with stacked bar charts)
57
# Values can contain commas as decimal separator, as long as delimiter isn't comma
58
# Negative values are not yet supported.
66
from simplestyle import *
68
#www.sapdesignguild.org/goodies/diagram_guidelines/color_palettes.html#mss
70
"red": ["#460101", "#980101", "#d40000", "#f44800", "#fb8b00", "#eec73e", "#d9bb7a", "#fdd99b"],
71
"blue": ["#000442", "#0F1781", "#252FB7", "#3A45E1", "#656DDE", "#8A91EC"],
72
"gray": ["#222222", "#444444", "#666666", "#888888", "#aaaaaa", "#cccccc", "#eeeeee"],
73
"contrast": ["#0000FF", "#FF0000", "#00FF00", "#CF9100", "#FF00FF", "#00FFFF"],
74
"sap": ["#f8d753", "#5c9746", "#3e75a7", "#7a653e", "#e1662a", "#74796f", "#c4384f",
75
"#fff8a3", "#a9cc8f", "#b2c8d9", "#bea37a", "#f3aa79", "#b5b5a9", "#e6a5a5"]
78
def get_color_scheme(name="default"):
79
return COLOUR_TABLE.get(name.lower(), COLOUR_TABLE['red'])
82
class NiceChart(inkex.Effect):
84
Inkscape extension that can draw pie charts and bar charts
85
(stacked, single, horizontally or vertically)
86
with optional drop shadow, from a csv file or from pasted text
92
Defines the "--what" option of a script.
94
# Call the base class constructor.
95
inkex.Effect.__init__(self)
97
# Define string option "--what" with "-w" shortcut and default chart values.
98
self.OptionParser.add_option('-w', '--what', action='store',
99
type='string', dest='what', default='22,11,67',
102
# Define string option "--type" with "-t" shortcut.
103
self.OptionParser.add_option("-t", "--type", action="store",
104
type="string", dest="type", default='',
107
# Define bool option "--blur" with "-b" shortcut.
108
self.OptionParser.add_option("-b", "--blur", action="store",
109
type="inkbool", dest="blur", default='True',
112
# Define string option "--file" with "-f" shortcut.
113
self.OptionParser.add_option("-f", "--filename", action="store",
114
type="string", dest="filename", default='',
117
# Define string option "--input_type" with "-i" shortcut.
118
self.OptionParser.add_option("-i", "--input_type", action="store",
119
type="string", dest="input_type", default='file',
122
# Define string option "--delimiter" with "-d" shortcut.
123
self.OptionParser.add_option("-d", "--delimiter", action="store",
124
type="string", dest="csv_delimiter", default=';',
127
# Define string option "--colors" with "-c" shortcut.
128
self.OptionParser.add_option("-c", "--colors", action="store",
129
type="string", dest="colors", default='default',
132
# Define string option "--colors_override"
133
self.OptionParser.add_option("", "--colors_override", action="store",
134
type="string", dest="colors_override", default='',
135
help="color-scheme-override")
138
self.OptionParser.add_option("", "--reverse_colors", action="store",
139
type="inkbool", dest="reverse_colors", default='False',
140
help="reverse color-scheme")
142
self.OptionParser.add_option("-k", "--col_key", action="store",
143
type="int", dest="col_key", default='0',
144
help="column that contains the keys")
147
self.OptionParser.add_option("-v", "--col_val", action="store",
148
type="int", dest="col_val", default='1',
149
help="column that contains the values")
151
self.OptionParser.add_option("", "--encoding", action="store",
152
type="string", dest="encoding", default='utf-8',
153
help="encoding of the CSV file, e.g. utf-8")
155
self.OptionParser.add_option("", "--headings", action="store",
156
type="inkbool", dest="headings", default='False',
157
help="the first line of the CSV file consists of headings for the columns")
159
self.OptionParser.add_option("-r", "--rotate", action="store",
160
type="inkbool", dest="rotate", default='False',
161
help="Draw barchart horizontally")
163
self.OptionParser.add_option("-W", "--bar-width", action="store",
164
type="int", dest="bar_width", default='10',
165
help="width of bars")
167
self.OptionParser.add_option("-p", "--pie-radius", action="store",
168
type="int", dest="pie_radius", default='100',
169
help="radius of pie-charts")
171
self.OptionParser.add_option("-H", "--bar-height", action="store",
172
type="int", dest="bar_height", default='100',
173
help="height of bars")
175
self.OptionParser.add_option("-O", "--bar-offset", action="store",
176
type="int", dest="bar_offset", default='5',
177
help="distance between bars")
179
self.OptionParser.add_option("", "--stroke-width", action="store",
180
type="float", dest="stroke_width", default='1')
182
self.OptionParser.add_option("-o", "--text-offset", action="store",
183
type="int", dest="text_offset", default='5',
184
help="distance between bar and descriptions")
186
self.OptionParser.add_option("", "--heading-offset", action="store",
187
type="int", dest="heading_offset", default='50',
188
help="distance between chart and chart title")
190
self.OptionParser.add_option("", "--segment-overlap", action="store",
191
type="inkbool", dest="segment_overlap", default='False',
192
help="work around aliasing effects by letting pie chart segments overlap")
194
self.OptionParser.add_option("-F", "--font", action="store",
195
type="string", dest="font", default='sans-serif',
196
help="font of description")
198
self.OptionParser.add_option("-S", "--font-size", action="store",
199
type="int", dest="font_size", default='10',
200
help="font size of description")
202
self.OptionParser.add_option("-C", "--font-color", action="store",
203
type="string", dest="font_color", default='black',
204
help="font color of description")
206
self.OptionParser.add_option("","--input_sections")
208
self.OptionParser.add_option("-V", "--show_values", action="store",
209
type="inkbool", dest="show_values", default='False',
210
help="Show values in chart")
215
Overrides base class' method and inserts a nice looking chart into SVG document.
217
# Get script's "--what" option value and process the data type --- i concess the if term is a little bit of magic
218
what = self.options.what
225
csv_file_name = self.options.filename
226
csv_delimiter = self.options.csv_delimiter
227
input_type = self.options.input_type
228
col_key = self.options.col_key
229
col_val = self.options.col_val
230
show_values = self.options.show_values
231
encoding = self.options.encoding.strip() or 'utf-8'
232
headings = self.options.headings
233
heading_offset = self.options.heading_offset
235
if input_type == "\"file\"":
236
csv_file = open(csv_file_name, "r")
238
for linenum, line in enumerate(csv_file):
239
value = line.decode(encoding).split(csv_delimiter)
240
#make sure that there is at least one value (someone may want to use it as description)
242
# allow to parse headings as strings
243
if linenum == 0 and headings:
244
heading = value[col_val]
246
keys.append(value[col_key])
247
# replace comma decimal separator from file by colon,
248
# to avoid file editing for people whose programs output
250
values.append(float(value[col_val].replace(",",".")))
253
elif input_type == "\"direct_input\"":
254
what = re.findall("([A-Z|a-z|0-9]+:[0-9]+\.?[0-9]*)", what)
256
value = value.split(":")
257
keys.append(value[0])
258
values.append(float(value[1]))
260
# warn about negative values (not yet supported)
263
inkex.errormsg("Negative values are currently not supported!")
266
# Get script's "--type" option value.
267
charttype = self.options.type
269
if charttype == "pie_abs":
273
# Get access to main SVG document element and get its dimensions.
274
svg = self.document.getroot()
276
# Get the page attibutes:
277
width = self.getUnittouu(svg.get('width'))
278
height = self.getUnittouu(svg.attrib['height'])
280
# Create a new layer.
281
layer = inkex.etree.SubElement(svg, 'g')
282
layer.set(inkex.addNS('label', 'inkscape'), 'Chart-Layer: %s' % (what))
283
layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer')
285
# Check if a drop shadow should be drawn:
286
draw_blur = self.options.blur
289
# Get defs of Document
290
defs = self.xpathSingle('/svg:svg//svg:defs')
292
defs = inkex.etree.SubElement(self.document.getroot(), inkex.addNS('defs', 'svg'))
295
filt = inkex.etree.SubElement(defs,inkex.addNS('filter', 'svg'))
296
filtId = self.uniqueId('filter')
297
self.filtId = 'filter:url(#%s);' % filtId
298
for k, v in [('id', filtId), ('height', "3"),
300
('x', '-0.5'), ('y', '-0.5')]:
303
# Append Gaussian Blur to that Filter
304
fe = inkex.etree.SubElement(filt, inkex.addNS('feGaussianBlur', 'svg'))
305
fe.set('stdDeviation', "1.1")
308
self.options.colors_override.strip()
309
if len(self.options.colors_override) > 0:
310
colors = self.options.colors_override
312
colors = self.options.colors
314
if colors[0].isalpha():
315
colors = get_color_scheme(colors)
317
colors = re.findall("(#[0-9a-fA-F]{6})", colors)
318
#to be sure we create a fallback:
320
colors = get_color_scheme()
322
color_count = len(colors)
324
if self.options.reverse_colors:
327
# Those values should be self-explanatory:
328
bar_height = self.options.bar_height
329
bar_width = self.options.bar_width
330
bar_offset = self.options.bar_offset
331
# offset of the description in stacked-bar-charts:
332
# stacked_bar_text_offset=self.options.stacked_bar_text_offset
333
text_offset = self.options.text_offset
334
# prevents ugly aliasing effects between pie chart segments by overlapping
335
segment_overlap = self.options.segment_overlap
338
font = self.options.font
339
font_size = self.options.font_size
340
font_color = self.options.font_color
343
rotate = self.options.rotate
345
pie_radius = self.options.pie_radius
346
stroke_width = self.options.stroke_width
348
if charttype == "bar":
353
# iterate all values, use offset to draw the bars in different places
357
# Normalize the bars to the largest value
359
value_max = max(values)
363
for x in range(len(values)):
364
orig_values.append(values[x])
365
values[x] = (values[x]/value_max) * bar_height
367
# Draw Single bars with their shadows
370
# draw drop shadow, if necessary
372
# Create shadow element
373
shadow = inkex.etree.Element(inkex.addNS("rect", "svg"))
374
# Set chart position to center of document. Make it horizontal or vertical
376
shadow.set('x', str(width/2 + offset + 1))
377
shadow.set('y', str(height/2 - int(value) + 1))
378
shadow.set("width", str(bar_width))
379
shadow.set("height", str(int(value)))
381
shadow.set('y', str(width/2 + offset + 1))
382
shadow.set('x', str(height/2 + 1))
383
shadow.set("height", str(bar_width))
384
shadow.set("width", str(int(value)))
386
# Set shadow blur (connect to filter object in xml path)
387
shadow.set("style", "filter:url(#filter)")
389
# Create rectangle element
390
rect = inkex.etree.Element(inkex.addNS('rect', 'svg'))
392
# Set chart position to center of document.
394
rect.set('x', str(width/2 + offset))
395
rect.set('y', str(height/2 - int(value)))
396
rect.set("width", str(bar_width))
397
rect.set("height", str(int(value)))
399
rect.set('y', str(width/2 + offset))
400
rect.set('x', str(height/2))
401
rect.set("height", str(bar_width))
402
rect.set("width", str(int(value)))
404
rect.set("style", "fill:" + colors[color % color_count])
406
# If keys are given, create text elements
408
text = inkex.etree.Element(inkex.addNS('text', 'svg'))
409
if not rotate: #=vertical
410
text.set("transform", "matrix(0,-1,1,0,0,0)")
412
text.set("x", "-" + str(height/2 + text_offset))
414
text.set("y", str(width/2 + offset + bar_width/2 + font_size/3))
416
text.set("y", str(width/2 + offset + bar_width/2 + font_size/3))
417
text.set("x", str(height/2 - text_offset))
419
text.set("style", "font-size:" + str(font_size)\
420
+ "px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:"\
421
+ font + ";-inkscape-font-specification:Bitstream Charter;text-align:end;text-anchor:end;fill:"\
424
text.text = keys[cnt]
426
# Increase Offset and Color
427
#offset=offset+bar_width+bar_offset
428
color = (color + 1) % 8
429
# Connect elements together.
437
vtext = inkex.etree.Element(inkex.addNS('text', 'svg'))
438
if not rotate: #=vertical
439
vtext.set("transform", "matrix(0,-1,1,0,0,0)")
441
vtext.set("x", "-"+str(height/2+text_offset-value-text_offset-text_offset))
443
vtext.set("y", str(width/2+offset+bar_width/2+font_size/3))
445
vtext.set("y", str(width/2+offset+bar_width/2+font_size/3))
446
vtext.set("x", str(height/2-text_offset+value+text_offset+text_offset))
448
vtext.set("style", "font-size:"+str(font_size)\
449
+ "px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:"\
450
+ font + ";-inkscape-font-specification:Bitstream Charter;text-align:start;text-anchor:start;fill:"\
453
vtext.text = str(int(orig_values[cnt]))
457
offset = offset + bar_width + bar_offset
459
# set x position for heading line
461
heading_x = width/2 # TODO: adjust
463
heading_x = width/2 # TODO: adjust
466
elif charttype == "pie":
470
# Iterate all values to draw the different slices
473
# Create the shadow first (if it should be created):
475
shadow = inkex.etree.Element(inkex.addNS("circle", "svg"))
476
shadow.set('cx', str(width/2))
477
shadow.set('cy', str(height/2))
478
shadow.set('r', str(pie_radius))
479
shadow.set("style", "filter:url(#filter);fill:#000000")
483
# Add a grey background circle with a light stroke
484
background = inkex.etree.Element(inkex.addNS("circle", "svg"))
485
background.set("cx", str(width/2))
486
background.set("cy", str(height/2))
487
background.set("r", str(pie_radius))
488
background.set("style", "stroke:#ececec;fill:#f9f9f9")
489
layer.append(background)
491
#create value sum in order to divide the slices
493
valuesum = sum(values)
501
num_values = len(values)
507
for i in range(num_values):
509
# Calculate the PI-angles for start and end
510
angle = (2*3.141592) / valuesum * float(value)
516
if i != num_values-1:
517
end += 0.09 # add a 5° overlap
519
start -= 0.09 # let the first element overlap into the other direction
522
pieslice = inkex.etree.Element(inkex.addNS("path", "svg"))
523
pieslice.set(inkex.addNS('type', 'sodipodi'), 'arc')
524
pieslice.set(inkex.addNS('cx', 'sodipodi'), str(width/2))
525
pieslice.set(inkex.addNS('cy', 'sodipodi'), str(height/2))
526
pieslice.set(inkex.addNS('rx', 'sodipodi'), str(pie_radius))
527
pieslice.set(inkex.addNS('ry', 'sodipodi'), str(pie_radius))
528
pieslice.set(inkex.addNS('start', 'sodipodi'), str(start))
529
pieslice.set(inkex.addNS('end', 'sodipodi'), str(end))
530
pieslice.set("style", "fill:"+ colors[color % color_count] + ";stroke:none;fill-opacity:1")
532
#If text is given, draw short paths and add the text
534
path = inkex.etree.Element(inkex.addNS("path", "svg"))
536
+ str((width/2) + pie_radius * math.cos(angle/2 + offset)) + ","
537
+ str((height/2) + pie_radius * math.sin(angle/2 + offset)) + " "
538
+ str((text_offset - 2) * math.cos(angle/2 + offset)) + ","
539
+ str((text_offset - 2) * math.sin(angle/2 + offset)))
541
path.set("style", "fill:none;stroke:"
542
+ font_color + ";stroke-width:" + str(stroke_width)
543
+ "px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1")
545
text = inkex.etree.Element(inkex.addNS('text', 'svg'))
546
text.set("x", str((width/2) + (pie_radius + text_offset) * math.cos(angle/2 + offset)))
547
text.set("y", str((height/2) + (pie_radius + text_offset) * math.sin(angle/2 + offset) + font_size/3))
548
textstyle = "font-size:" + str(font_size) \
549
+ "px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:" \
550
+ font + ";-inkscape-font-specification:Bitstream Charter;fill:" + font_color
551
# check if it is right or left of the Pie
552
if math.cos(angle/2 + offset) > 0:
553
text.set("style", textstyle)
555
text.set("style", textstyle + ";text-align:end;text-anchor:end")
556
text.text = keys[cnt]
558
text.text = text.text + "(" + str(values[cnt])
561
text.text = text.text + " %"
563
text.text = text.text + ")"
568
# increase the rotation-offset and the colorcycle-position
569
offset = offset + angle
570
color = (color + 1) % 8
572
# append the objects to the extension-layer
573
layer.append(pieslice)
575
# set x position for heading line
576
heading_x = width/2 - pie_radius # TODO: adjust
578
elif charttype == "stbar":
582
# Iterate over all values to draw the different slices
585
#create value sum in order to divide the bars
587
valuesum = sum(values)
592
valuesum = valuesum + float(value)
598
# Create rectangle element
599
shadow = inkex.etree.Element(inkex.addNS("rect", "svg"))
600
# Set chart position to center of document.
602
shadow.set('x', str(width/2))
603
shadow.set('y', str(height/2 - bar_height/2))
605
shadow.set('x', str(width/2))
606
shadow.set('y', str(height/2))
607
# Set rectangle properties
609
shadow.set("width", str(bar_width))
610
shadow.set("height", str(bar_height/2))
612
shadow.set("width",str(bar_height/2))
613
shadow.set("height", str(bar_width))
614
# Set shadow blur (connect to filter object in xml path)
615
shadow.set("style", "filter:url(#filter)")
622
# Calculate the individual heights normalized on 100units
623
normedvalue = (bar_height / valuesum) * float(value)
625
# Create rectangle element
626
rect = inkex.etree.Element(inkex.addNS('rect', 'svg'))
628
# Set chart position to center of document.
630
rect.set('x', str(width / 2 ))
631
rect.set('y', str(height / 2 - offset - normedvalue))
633
rect.set('x', str(width / 2 + offset ))
634
rect.set('y', str(height / 2 ))
635
# Set rectangle properties
637
rect.set("width", str(bar_width))
638
rect.set("height", str(normedvalue))
640
rect.set("height", str(bar_width))
641
rect.set("width", str(normedvalue))
642
rect.set("style", "fill:" + colors[color % color_count])
644
#If text is given, draw short paths and add the text
645
# TODO: apply overlap workaround for visible gaps in between
648
path = inkex.etree.Element(inkex.addNS("path", "svg"))
649
path.set("d","m " + str((width + bar_width)/2) + ","
650
+ str(height/2 - offset - (normedvalue / 2)) + " "
651
+ str(bar_width/2 + text_offset) + ",0")
652
path.set("style", "fill:none;stroke:" + font_color
653
+ ";stroke-width:" + str(stroke_width)
654
+ "px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1")
656
text = inkex.etree.Element(inkex.addNS('text', 'svg'))
657
text.set("x", str(width/2 + bar_width + text_offset + 1))
658
text.set("y", str(height/ 2 - offset + font_size/3 - (normedvalue/2)))
659
text.set("style", "font-size:" + str(font_size)
660
+ "px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:"
661
+ font + ";-inkscape-font-specification:Bitstream Charter;fill:" + font_color)
662
text.text = keys[cnt]
666
path = inkex.etree.Element(inkex.addNS("path", "svg"))
667
path.set("d","m " + str((width)/2 + offset + normedvalue/2) + ","
668
+ str(height / 2 + bar_width/2) + " 0,"
669
+ str(bar_width/2 + (font_size * i) + text_offset)) #line
670
path.set("style", "fill:none;stroke:" + font_color
671
+ ";stroke-width:" + str(stroke_width)
672
+ "px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1")
674
text = inkex.etree.Element(inkex.addNS('text', 'svg'))
675
text.set("x", str((width)/2 + offset + normedvalue/2 - font_size/3))
676
text.set("y", str((height/2) + bar_width + (font_size * (i + 1)) + text_offset))
677
text.set("style", "font-size:" + str(font_size)
678
+ "px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:"
679
+ font + ";-inkscape-font-specification:Bitstream Charter;fill:" + font_color)
680
text.text = keys[color]
683
# Increase Offset and Color
684
offset = offset + normedvalue
685
color = (color + 1) % 8
691
# set x position for heading line
693
heading_x = width/2 + offset + normedvalue # TODO: adjust
695
heading_x = width/2 + offset + normedvalue # TODO: adjust
697
if headings and input_type == "\"file\"":
698
headingtext = inkex.etree.Element(inkex.addNS('text', 'svg'))
699
headingtext.set("y", str(height/2 + heading_offset))
700
headingtext.set("x", str(heading_x))
701
headingtext.set("style", "font-size:" + str(font_size + 4)\
702
+ "px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:"\
703
+ font + ";-inkscape-font-specification:Bitstream Charter;text-align:end;text-anchor:end;fill:"\
706
headingtext.text = heading
707
layer.append(headingtext)
709
def getUnittouu(self, param):
711
return inkex.unittouu(param)
712
except AttributeError:
713
return self.unittouu(param)
715
if __name__ == '__main__':
716
# Create effect instance and apply it.