Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env python
- # -*- coding: cp1252 -*-
- # nicechart.py
- #
- # Copyright 2011
- #
- # Christoph Sterz
- # Florian Weber
- #
- # This program is free software; you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation; either version 3 of the License, or
- # (at your option) any later version.
- #
- # This program 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 General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program; if not, write to the Free Software
- # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
- # MA 02110-1301, USA.
- #
- #
- # These two lines are only needed if you don't put the script directly into
- # the installation directory (and only works for linux :()
- #import sys
- #sys.path.append('/usr/share/inkscape/extensions')
- # We will use the inkex module with the predefined Effect base class.
- import inkex
- # The simplestyle module provides functions for style parsing.
- from simplestyle import *
- import math, re, nicechart_colors as nc_colors
- import sys # for debugging
- csv_file_name=""
- logf = open('C:\temp\log.txt', 'w')
- class NiceChart(inkex.Effect):
- """ Create a bar, pie, or stacked chart from list of named data """
- def __init__(self):
- # Call the base class constructor.
- inkex.Effect.__init__(self)
- # Define string option "--what" with "-w" shortcut and default chart values.
- self.OptionParser.add_option('-w', '--what', action = 'store',
- type = 'string', dest = 'what', default = '22,11,67',
- help = 'Chart Values')
- # Define string option "--type" with "-t" shortcut.
- self.OptionParser.add_option("-t", "--type", action="store",
- type="string", dest="type", default='',
- help="Chart Type")
- # Define bool option "--blur" with "-b" shortcut.
- self.OptionParser.add_option("-b", "--blur", action="store",
- type="inkbool", dest="blur", default='True',
- help="Blur Type")
- # Define string option "--file" with "-f" shortcut.
- self.OptionParser.add_option("-f", "--filename", action="store",
- type="string", dest="filename", default='',
- help="Name of File")
- # Define string option "--input_type" with "-i" shortcut.
- self.OptionParser.add_option("-i", "--input_type", action="store",
- type="string", dest="input_type", default='file',
- help="Chart Type")
- # Define string option "--delimiter" with "-d" shortcut.
- self.OptionParser.add_option("-d", "--delimiter", action="store",
- type="string", dest="csv_delimiter", default=';',
- help="delimiter")
- # Define string option "--colors" with "-c" shortcut.
- self.OptionParser.add_option("-c", "--colors", action="store",
- type="string", dest="colors", default='default',
- help="color-scheme")
- self.OptionParser.add_option("", "--reverse_colors", action="store",
- type="inkbool", dest="reverse_colors", default='False',
- help="reverse color-scheme")
- self.OptionParser.add_option("-k", "--col_key", action="store",
- type="int", dest="col_key", default='0',
- help="column that contains the keys")
- self.OptionParser.add_option("-v", "--col_val", action="store",
- type="int", dest="col_val", default='1',
- help="column that contains the values")
- self.OptionParser.add_option("-r", "--rotate", action="store",
- type="inkbool", dest="rotate", default='False',
- help="Draw barchart horizontally")
- self.OptionParser.add_option("-W", "--bar-width", action="store",
- type="int", dest="bar_width", default='10',
- help="width of bars")
- self.OptionParser.add_option("-p", "--pie-radius", action="store",
- type="int", dest="pie_radius", default='100',
- help="radius of pie-charts")
- self.OptionParser.add_option("-q", "--pie-offset", action="store",
- type="int", dest="pie_offset", default='0',
- help="Rotation offset of pie-charts")
- self.OptionParser.add_option("-H", "--bar-height", action="store",
- type="int", dest="bar_height", default='100',
- help="height of bars")
- self.OptionParser.add_option("-O", "--bar-offset", action="store",
- type="int", dest="bar_offset", default='5',
- help="distance between bars")
- self.OptionParser.add_option("-l", "--labels", action="store",
- type="inkbool", dest="labels", default='False',
- help="Initial line is a description")
- self.OptionParser.add_option("", "--stroke-width", action="store",
- type="int", dest="stroke_width", default='2')
- self.OptionParser.add_option("-o", "--text-offset", action="store",
- type="int", dest="text_offset", default='5',
- help="distance between bar and descriptions")
- self.OptionParser.add_option("-F", "--font", action="store",
- type="string", dest="font", default='sans-serif',
- help="font of description")
- self.OptionParser.add_option("-S", "--font-size", action="store",
- type="int", dest="font_size", default='10',
- help="font size of description")
- self.OptionParser.add_option("-C", "--font-color", action="store",
- type="string", dest="font_color", default='black',
- help="font color of description")
- #Dummy:
- self.OptionParser.add_option("","--input_sections")
- def create_layer(self, title):
- """ Create a new (top level) named layer to put charts into """
- svg = self.document.getroot()
- layer = inkex.etree.SubElement(svg, 'g')
- layer.set(inkex.addNS('label', 'inkscape'), 'Chart-Layer: %s' % (title))
- layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer')
- return layer
- def create_blur_filter(self):
- """ Create a single reuseable filter for blur effect
- - gaussian blur
- """
- xoffset = str(-0.5) # get from options
- yoffset = str(-0.5)
- blur_factor = str(1.1)
- # Get defs of Document
- defs = self.xpathSingle('/svg:svg//svg:defs')
- if defs == None:
- defs = inkex.etree.SubElement(self.document.getroot(),inkex.addNS('defs','svg'))
- # Create new Filter
- filt = inkex.etree.SubElement(defs,inkex.addNS('filter','svg'))
- filtId = self.uniqueId('filter')
- self.filtId = 'filter:url(#%s);' % filtId
- for k, v in [ ('id', filtId), ('height', "3"), ('width', "3"),
- ('x', xoffset), ('y', yoffset) ]:
- filt.set(k, v)
- # Append Gaussian Blur to that Filter
- fe = inkex.etree.SubElement(filt,inkex.addNS('feGaussianBlur','svg'))
- fe.set('stdDeviation', blur_factor)
- return filtId
- def get_colors(self):
- """ get colors directly from UI, or use premade named sets
- - ideally use a dropdown to select premade sets.
- - ideally sample gradients to get color sets
- """
- colors=self.options.colors # make copy so reverse operation is not persistent
- if(colors[0].isalpha()):
- colors=nc_colors.get_color_scheme(colors)
- else:
- colors=re.findall("(#[0-9a-fA-F]{6})",colors)
- #to be sure we create a fallback:
- if(len(colors)==0):
- colors=nc_colors.get_color_scheme()
- # reverse ?
- colors = [c for c in colors] # safe copy
- if(self.options.reverse_colors):
- colors.reverse()
- return (colors, len(colors))
- def make_chart(self, charttype, keys, values, title, blur_filter, layer=False ):
- logf.write("Create Chart: %s %s\n" % (charttype, title))
- logf.write(" layer : %s\n" % (layer))
- logf.write(" keys,values : %s %s\n" % (keys, values))
- #
- keys_present=False
- if keys: keys_present=True
- # Get access to main SVG document element and get its dimensions.
- svg = self.document.getroot()
- # Get the page attibutes for positioning
- width = inkex.unittouu(svg.get('width'))
- height = inkex.unittouu(svg.attrib['height'])
- if layer == False:
- # no layer supplied so make one (means its a direct single chart)
- layer = self.create_layer(title)
- # make group to hold this chart
- group = inkex.etree.SubElement(layer, 'g', {inkex.addNS('label','inkscape'):title })
- # Get Colors
- Colors, color_count = self.get_colors()
- logf.write(" Colors: %s\n" % Colors)
- #
- bar_height=self.options.bar_height
- bar_width=self.options.bar_width
- bar_offset=self.options.bar_offset
- #offset of the description in stacked-bar-charts:
- text_offset=self.options.text_offset
- #get font
- font=self.options.font
- font_size=self.options.font_size
- font_color=self.options.font_color
- #get bar chart orientation
- rotate = self.options.rotate
- pie_radius = self.options.pie_radius
- pie_offset = self.options.pie_offset * 2*math.pi/360 # degrees to radians
- stroke_width = self.options.stroke_width
- if charttype=="bar" :
- #########
- ###BAR###
- #########
- #iterate all values, use offset to draw the bars in different places
- offset=0
- count=0
- # Normalize the bars to the largest value
- try:
- value_max=max(values)
- except ValueError:
- value_max=0.0
- for i in range(len(values)):
- values[i]=(values[i]/value_max)*bar_height
- # Draw Single bars with their shadows
- for value in values:
- #draw blur, if it is wanted
- if blur_filter :
- # Create shadow element
- shadow = inkex.etree.SubElement(group,inkex.addNS("rect","svg"))
- # Set chart position to center of document. Make it horizontal or vertical
- if not rotate :
- shadow.set('x', str(width / 2 + offset +1))
- shadow.set('y', str(height / 2 - int(value)+1))
- else:
- shadow.set('y', str(width / 2 + offset +1))
- shadow.set('x', str(height / 2 +1))
- # Set shadow properties
- if not rotate :
- shadow.set("width", str(bar_width))
- shadow.set("height", str(int(value)))
- else:
- shadow.set("height", str(bar_width))
- shadow.set("width", str(int(value)))
- # Set shadow blur (connect to filter object in xml path)
- shadow.set("style","filter:url(#%s)" % blur_filter)
- # Create rectangle element
- rect = inkex.etree.SubElement(group,inkex.addNS('rect','svg'))
- # Set chart position to center of document.
- if(not rotate):
- rect.set('x', str(width/2+offset))
- rect.set('y', str(height/2-int(value)))
- else:
- rect.set('y', str(width/2+offset))
- rect.set('x', str(height/2))
- # Set rectangle properties
- if(not rotate):
- rect.set("width", str(bar_width))
- rect.set("height", str(int(value)))
- else:
- rect.set("height", str(bar_width))
- rect.set("width", str(int(value)))
- rect.set("style","fill:"+Colors[count%color_count])
- # Set shadow blur (connect to filter object in xml path)
- if(blur_filter):
- shadow.set("style","filter:url(#%s)" % blur_filter)
- # If keys are given create text elements
- if(keys_present):
- text = inkex.etree.SubElement(group, inkex.addNS('text','svg'))
- if(not rotate): #=vertical
- text.set("transform","matrix(0,-1,1,0,0,0)")
- #y after rotation:
- text.set("x", "-"+str(height/2+text_offset))
- #x after rotation:
- text.set("y", str(width/2+offset+bar_width/2+font_size/3))
- else: #=horizontal
- text.set("y", str(width/2+offset+bar_width/2+font_size/3))
- text.set("x", str(height/2-text_offset))
- text.set("style","font-size:"+str(font_size)\
- +"px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:"\
- +font+";-inkscape-font-specification:Bitstream Charter;text-align:end;text-anchor:end;fill:"\
- +font_color)
- text.text=keys[count]
- # Increase Offset and Color
- offset=offset+bar_width+bar_offset
- count += 1
- #End Bar
- elif(charttype=="pie"):
- #########
- ###PIE###
- #########
- # Iterate all values to draw the different slices
- count=0
- # Add a grey background circle
- background=inkex.etree.SubElement(group, inkex.addNS("circle","svg"))
- background.set("cx", str(width/2))
- background.set("cy", str(height/2))
- background.set("r", str(pie_radius))
- background.set("style","fill:#aaaaaa;stroke:none")
- logf.write(" made BG\n")
- #create value sum in order to divide the slices
- try:
- valuesum=sum(values)
- except ValueError:
- valuesum=0
- # Set an offsetangle
- offset=pie_offset
- logf.write(" valuesum, offset =%s %s:\n" % (valuesum, offset))
- # Draw single slices with their shadow
- for value in values:
- # Calculate the PI-angles for start and end
- angle=(2*math.pi)/valuesum*float(value)
- # Create the shadow first (if it should be created):
- if(blur_filter):
- shadow=inkex.etree.SubElement(group, inkex.addNS("path","svg"))
- shadow.set(inkex.addNS('type', 'sodipodi'), 'arc')
- shadow.set(inkex.addNS('cx', 'sodipodi'), str(width/2))
- shadow.set(inkex.addNS('cy', 'sodipodi'), str(height/2))
- shadow.set(inkex.addNS('rx', 'sodipodi'), str(pie_radius))
- shadow.set(inkex.addNS('ry', 'sodipodi'), str(pie_radius))
- shadow.set(inkex.addNS('start', 'sodipodi'), str(offset))
- shadow.set(inkex.addNS('end', 'sodipodi'), str(offset+angle))
- shadow.set("style","filter:url(#%s);fill:#000000" % blur_filter)
- #then add the slice
- pieslice=inkex.etree.SubElement(group, inkex.addNS("path","svg"))
- pieslice.set(inkex.addNS('type', 'sodipodi'), 'arc')
- pieslice.set(inkex.addNS('cx', 'sodipodi'), str(width/2))
- pieslice.set(inkex.addNS('cy', 'sodipodi'), str(height/2))
- pieslice.set(inkex.addNS('rx', 'sodipodi'), str(pie_radius))
- pieslice.set(inkex.addNS('ry', 'sodipodi'), str(pie_radius))
- pieslice.set(inkex.addNS('start', 'sodipodi'), str(offset))
- pieslice.set(inkex.addNS('end', 'sodipodi'), str(offset+angle))
- pieslice.set("style","fill:"+Colors[count%color_count]+";stroke:none;fill-opacity:1")
- #If text is given, draw short paths and add the text
- if(keys_present):
- path=inkex.etree.SubElement(group, inkex.addNS("path","svg"))
- path.set("d","m "+str((width/2)+pie_radius*math.cos(angle/2+offset))+","+str((height/2)+pie_radius*math.sin(angle/2+offset))+" "+str((text_offset-2)*math.cos(angle/2+offset))+","+str((text_offset-2)*math.sin(angle/2+offset)))
- path.set("style","fill:none;stroke:"+font_color+";stroke-width:"+str(stroke_width)+"px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1")
- text=inkex.etree.SubElement(group, inkex.addNS("text","svg"))
- text.set("x", str((width/2)+(pie_radius+text_offset)*math.cos(angle/2+offset)))
- text.set("y", str((height/2)+(pie_radius+text_offset)*math.sin(angle/2+offset)+font_size/3))
- textstyle="font-size:"+str(font_size)+"px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:"+font+";-inkscape-font-specification:Bitstream Charter;fill:"+font_color
- #check if it is right or left of the Pie
- if(math.cos(angle/2+offset)>0):
- text.set("style",textstyle)
- else:
- text.set("style",textstyle+";text-align:end;text-anchor:end")
- text.text=keys[count]
- #increase the rotation-offset and the colorcycle-position
- offset += angle
- count += 1
- logf.write(" chart: offset,color =%s %s %s:\n" % (offset, count, count%color_count))
- #End Pie
- elif(charttype=="stbar"):
- #################
- ###STACKED BAR###
- #################
- # Iterate all values to draw the different slices
- count=0
- #create value sum in order to divide the bars
- try:
- valuesum=sum(values)
- except ValueError:
- valuesum=0.0
- for value in values:
- valuesum=valuesum+float(value)
- # Init offset
- offset=0
- stack_index = len(values)-1 #loopcounter
- # Draw Single bars with their shadows
- for value in values:
- # Calculate the individual heights normalized on 100units
- normedvalue=(bar_height/valuesum)*float(value)
- if(blur_filter):
- # Create rectangle element
- shadow = inkex.etree.SubElement(group,inkex.addNS("rect","svg"))
- # Set chart position to center of document.
- if(not rotate):
- shadow.set('x', str(width / 2 + 1))
- shadow.set('y', str(height / 2 - offset - (normedvalue)+1))
- else:
- shadow.set('x', str(width / 2 + 1 + offset))
- shadow.set('y', str(height / 2 +1))
- # Set rectangle properties
- if(not rotate):
- shadow.set("width",str(bar_width))
- shadow.set("height", str((normedvalue)))
- else:
- shadow.set("width",str((normedvalue)))
- shadow.set("height", str(bar_width))
- # Set shadow blur (connect to filter object in xml path)
- shadow.set("style","filter:url(#%s)" % blur_filter)
- # Create rectangle element
- rect = inkex.etree.SubElement(group,inkex.addNS('rect','svg'))
- # Set chart position to center of document.
- if( not rotate ):
- rect.set('x', str(width / 2 ))
- rect.set('y', str(height / 2 - offset - (normedvalue)))
- else:
- rect.set('x', str(width / 2 + offset ))
- rect.set('y', str(height / 2 ))
- # Set rectangle properties
- if( not rotate ):
- rect.set("width", str(bar_width))
- rect.set("height", str((normedvalue)))
- else:
- rect.set("height", str(bar_width))
- rect.set("width", str((normedvalue)))
- rect.set("style","fill:"+Colors[count%color_count])
- #If text is given, draw short paths and add the text
- if(keys_present):
- if(not rotate):
- path=inkex.etree.SubElement(group,inkex.addNS("path","svg"))
- path.set("d","m "+str((width+bar_width)/2)+","+str(height / 2 - offset - (normedvalue / 2))+" "+str(bar_width/2+text_offset)+",0")
- path.set("style","fill:none;stroke:"+font_color+";stroke-width:"+str(stroke_width)+"px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1")
- text = inkex.etree.SubElement(group,inkex.addNS('text','svg'))
- text.set("x", str(width/2+bar_width+text_offset+1))
- text.set("y", str(height / 2 - offset + font_size/3 - (normedvalue / 2)))
- text.set("style","font-size:"+str(font_size)+"px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:"+font+";-inkscape-font-specification:Bitstream Charter;fill:"+font_color)
- text.text=keys[color]
- else:
- path=inkex.etree.SubElement(group,inkex.addNS("path","svg"))
- path.set("d","m "+str((width)/2+offset+normedvalue/2)+","
- +str(height / 2 + bar_width/2)
- +" 0,"+str(bar_width/2+(font_size*stack_index)+text_offset)) #line
- path.set("style","fill:none;stroke:"+font_color+";stroke-width:"+str(stroke_width)+"px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1")
- text = inkex.etree.SubElement(group,inkex.addNS('text','svg'))
- text.set("x", str((width)/2+offset+normedvalue/2-font_size/3))
- text.set("y", str((height/2)+bar_width+(font_size*(stack_index+1))+text_offset ))
- text.set("style","font-size:"+str(font_size)+"px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:"+font+";-inkscape-font-specification:Bitstream Charter;fill:"+font_color)
- text.text=keys[count]
- # Increase Offset and Color
- offset=offset+normedvalue
- count += 1
- stack_index-=1 #loopcounter
- def extract_data(self, csv_data):
- """ Expecting csv data in one of several forms.
- - first line has descriptions or not.
- If so then first is title of graph, subsequent column headers are labels
- - if more than one column after col_val then treat as new charts
- - blank line is separator. new layer of graphs.
- """
- data = []
- csv_delimiter=self.options.csv_delimiter
- col_key=self.options.col_key
- col_val=self.options.col_val
- desc = self.options.labels
- #
- label = ""
- titles = []
- keys = []
- values = []
- started = False
- for line in csv_data:
- line = line.strip()
- items = line.split(csv_delimiter)
- size = len(items)
- sys.stderr.write(" items: %s\n" % items)
- foo = [i for i in items] # cheap copy
- # remove dummy if all lines not same length (thanks XLS!!)
- if size > 1:
- count = foo.count('')
- for i in range(count):
- foo.remove('')
- items = foo
- size = len(items)
- sys.stderr.write("Line = %d, %s\n"% (size,items))
- if items[0] == '':
- # blank line so start again
- if not started:
- pass
- else:
- #we have something to save so we can start a new chart.
- data.append([titles,keys,values])
- keys = []
- values = []
- started = False
- sys.stderr.write("Saving chart, false start\n")
- else: # process valid lines
- if not started and desc:
- sys.stderr.write("Grab labels\n")
- # first line so grab labels if applicable
- label = items[0]
- if len(items)>1:
- titles = [label, items[col_val:]]
- for i in range(len(items[col_val:])):
- values.append([])
- sys.stderr.write(" values: %s\n" % values)
- else: # process line normally
- sys.stderr.write("process line %s\n" % items[col_key])
- keys.append(items[col_key])
- for idx,item in enumerate(items[col_val:]):
- #sys.stderr.write(" idx,item: %s %s = %s %s\n" % (idx,item, values[idx], values))
- values[idx].append(float(item.replace('$','').replace('%','')))
- sys.stderr.write(" values=%s\n" % values)
- #
- started = True # started a block (we may have already started).
- sys.stderr.write(" started=True\n")
- # all done so store last set of charts
- if values != []:
- data.append([titles,keys,values])
- titles = []
- keys = []
- values = []
- return data
- def effect(self):
- """
- Effect behaviour.
- Overrides base class' method and inserts a nice looking chart into SVG document.
- """
- # Get script's "--what" option value and process the data type --- i confess the if term is a little bit of magic
- what = self.options.what
- keys=[]
- values=[]
- csv_file_name=self.options.filename
- csv_delimiter=self.options.csv_delimiter
- input_type=self.options.input_type
- col_key=self.options.col_key
- col_val=self.options.col_val
- blur_filter = False
- if self.options.blur:
- blur_filter = self.create_blur_filter()
- if(input_type=="\"file\""):
- csv_file=open(csv_file_name,"r")
- csv_data = csv_file.readlines()
- csv_file.close()
- data = self.extract_data(csv_data)
- # Get script's "--type" option value.
- charttype=self.options.type
- logf.write("\nData: %s\n\n" % data)
- unique = 0
- for (titles,keys,values) in data:
- # make chart for each piece of data
- layer = self.create_layer(titles[0])
- for idx,dataset in enumerate(values):
- sys.stderr.write("\nMaking: %d %s\n %s\n %s\n" % (idx, charttype, keys, dataset))
- logf.write("\nMaking: %d %s\n %s\n %s\n" % (idx, charttype, keys, dataset))
- #if unique == 0:
- self.make_chart(charttype, keys, dataset, titles[1][idx]+"_"+str(unique), blur_filter, layer)
- unique += 1
- elif(input_type=="\"direct_input\""):
- what=re.findall("([A-Z|a-z|0-9]+:[0-9]+\.?[0-9]*)",what)
- for value in what:
- value=value.split(":")
- keys.append(value[0])
- values.append(float(value[1]))
- # Get script's "--type" option value.
- charttype=self.options.type
- #
- self.make_chart(charttype, keys, values, self.options.what)
- # Create effect instance and apply it.
- effect = NiceChart()
- effect.affect()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement