Really nice script for generating personal genre preferences stats from LastFM
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

875 lines
30 KiB

# Copyright(c) 2007-2010 by Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com>
#
# This file is part of PyCha.
#
# PyCha 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.
#
# PyCha 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 PyCha. If not, see <http://www.gnu.org/licenses/>.
import copy
import inspect
import math
import cairo
from pycha.color import ColorScheme, hex2rgb, DEFAULT_COLOR
from pycha.utils import safe_unicode
class Chart(object):
def __init__(self, surface, options={}, debug=False):
# this flag is useful to reuse this chart for drawing different data
# or use different options
self.resetFlag = False
# initialize storage
self.datasets = []
# computed values used in several methods
self.layout = Layout()
self.minxval = None
self.maxxval = None
self.minyval = None
self.maxyval = None
self.xscale = 1.0
self.yscale = 1.0
self.xrange = None
self.yrange = None
self.origin = 0.0
self.xticks = []
self.yticks = []
# set the default options
self.options = copy.deepcopy(DEFAULT_OPTIONS)
if options:
self.options.merge(options)
# initialize the surface
self._initSurface(surface)
self.colorScheme = None
# debug mode to draw aditional hints
self.debug = debug
def addDataset(self, dataset):
"""Adds an object containing chart data to the storage hash"""
self.datasets += dataset
def _getDatasetsKeys(self):
"""Return the name of each data set"""
return [d[0] for d in self.datasets]
def _getDatasetsValues(self):
"""Return the data (value) of each data set"""
return [d[1] for d in self.datasets]
def setOptions(self, options={}):
"""Sets options of this chart"""
self.options.merge(options)
def getSurfaceSize(self):
cx = cairo.Context(self.surface)
x, y, w, h = cx.clip_extents()
return w, h
def reset(self):
"""Resets options and datasets.
In the next render the surface will be cleaned before any drawing.
"""
self.resetFlag = True
self.options = copy.deepcopy(DEFAULT_OPTIONS)
self.datasets = []
def render(self, surface=None, options={}):
"""Renders the chart with the specified options.
The optional parameters can be used to render a chart in a different
surface with new options.
"""
self._update(options)
if surface:
self._initSurface(surface)
cx = cairo.Context(self.surface)
# calculate area data
surface_width, surface_height = self.getSurfaceSize()
self.layout.update(cx, self.options, surface_width, surface_height,
self.xticks, self.yticks)
self._renderBackground(cx)
if self.debug:
self.layout.render(cx)
self._renderChart(cx)
self._renderAxis(cx)
self._renderTitle(cx)
self._renderLegend(cx)
def clean(self):
"""Clears the surface with a white background."""
cx = cairo.Context(self.surface)
cx.save()
cx.set_source_rgb(1, 1, 1)
cx.paint()
cx.restore()
def _setColorscheme(self):
"""Sets the colorScheme used for the chart using the
options.colorScheme option
"""
name = self.options.colorScheme.name
keys = self._getDatasetsKeys()
colorSchemeClass = ColorScheme.getColorScheme(name, None)
if colorSchemeClass is None:
raise ValueError('Color scheme "%s" is invalid!' % name)
# Remove invalid args before calling the constructor
kwargs = dict(self.options.colorScheme.args)
validArgs = inspect.getargspec(colorSchemeClass.__init__)[0]
kwargs = dict([(k, v) for k, v in kwargs.items() if k in validArgs])
self.colorScheme = colorSchemeClass(keys, **kwargs)
def _initSurface(self, surface):
self.surface = surface
if self.resetFlag:
self.resetFlag = False
self.clean()
def _update(self, options={}):
"""Update all the information needed to render the chart"""
self.setOptions(options)
self._setColorscheme()
self._updateXY()
self._updateChart()
self._updateTicks()
def _updateXY(self):
"""Calculates all kinds of metrics for the x and y axis"""
x_range_is_defined = self.options.axis.x.range is not None
y_range_is_defined = self.options.axis.y.range is not None
if not x_range_is_defined or not y_range_is_defined:
stores = self._getDatasetsValues()
# gather data for the x axis
if x_range_is_defined:
self.minxval, self.maxxval = self.options.axis.x.range
else:
xdata = [pair[0] for pair in reduce(lambda a, b: a+b, stores)]
self.minxval = float(min(xdata))
self.maxxval = float(max(xdata))
if self.minxval * self.maxxval > 0 and self.minxval > 0:
self.minxval = 0.0
self.xrange = self.maxxval - self.minxval
if self.xrange == 0:
self.xscale = 1.0
else:
self.xscale = 1.0 / self.xrange
# gather data for the y axis
if y_range_is_defined:
self.minyval, self.maxyval = self.options.axis.y.range
else:
ydata = [pair[1] for pair in reduce(lambda a, b: a+b, stores)]
self.minyval = float(min(ydata))
self.maxyval = float(max(ydata))
if self.minyval * self.maxyval > 0 and self.minyval > 0:
self.minyval = 0.0
self.yrange = self.maxyval - self.minyval
if self.yrange == 0:
self.yscale = 1.0
else:
self.yscale = 1.0 / self.yrange
if self.minyval * self.maxyval < 0: # different signs
self.origin = abs(self.minyval) * self.yscale
else:
self.origin = 0.0
def _updateChart(self):
raise NotImplementedError
def _updateTicks(self):
"""Evaluates ticks for x and y axis.
You should call _updateXY before because that method computes the
values of xscale, minxval, yscale, and other attributes needed for
this method.
"""
stores = self._getDatasetsValues()
# evaluate xTicks
self.xticks = []
if self.options.axis.x.ticks:
for tick in self.options.axis.x.ticks:
if not isinstance(tick, Option):
tick = Option(tick)
if tick.label is None:
label = str(tick.v)
else:
label = tick.label
pos = self.xscale * (tick.v - self.minxval)
if 0.0 <= pos <= 1.0:
self.xticks.append((pos, label))
elif self.options.axis.x.interval > 0:
interval = self.options.axis.x.interval
label = (divmod(self.minxval, interval)[0] + 1) * interval
pos = self.xscale * (label - self.minxval)
prec = self.options.axis.x.tickPrecision
while 0.0 <= pos <= 1.0:
pretty_label = round(label, prec)
if prec == 0:
pretty_label = int(pretty_label)
self.xticks.append((pos, pretty_label))
label += interval
pos = self.xscale * (label - self.minxval)
elif self.options.axis.x.tickCount > 0:
uniqx = range(len(uniqueIndices(stores)) + 1)
roughSeparation = self.xrange / self.options.axis.x.tickCount
i = j = 0
while i < len(uniqx) and j < self.options.axis.x.tickCount:
if (uniqx[i] - self.minxval) >= (j * roughSeparation):
pos = self.xscale * (uniqx[i] - self.minxval)
if 0.0 <= pos <= 1.0:
self.xticks.append((pos, uniqx[i]))
j += 1
i += 1
# evaluate yTicks
self.yticks = []
if self.options.axis.y.ticks:
for tick in self.options.axis.y.ticks:
if not isinstance(tick, Option):
tick = Option(tick)
if tick.label is None:
label = str(tick.v)
else:
label = tick.label
pos = 1.0 - (self.yscale * (tick.v - self.minyval))
if 0.0 <= pos <= 1.0:
self.yticks.append((pos, label))
elif self.options.axis.y.interval > 0:
interval = self.options.axis.y.interval
label = (divmod(self.minyval, interval)[0] + 1) * interval
pos = 1.0 - (self.yscale * (label - self.minyval))
prec = self.options.axis.y.tickPrecision
while 0.0 <= pos <= 1.0:
pretty_label = round(label, prec)
if prec == 0:
pretty_label = int(pretty_label)
self.yticks.append((pos, pretty_label))
label += interval
pos = 1.0 - (self.yscale * (label - self.minyval))
elif self.options.axis.y.tickCount > 0:
prec = self.options.axis.y.tickPrecision
num = self.yrange / self.options.axis.y.tickCount
if (num < 1 and prec == 0):
roughSeparation = 1
else:
roughSeparation = round(num, prec)
for i in range(self.options.axis.y.tickCount + 1):
yval = self.minyval + (i * roughSeparation)
pos = 1.0 - ((yval - self.minyval) * self.yscale)
if 0.0 <= pos <= 1.0:
pretty_label = round(yval, prec)
if prec == 0:
pretty_label = int(pretty_label)
self.yticks.append((pos, pretty_label))
def _renderBackground(self, cx):
"""Renders the background area of the chart"""
if self.options.background.hide:
return
cx.save()
if self.options.background.baseColor:
cx.set_source_rgb(*hex2rgb(self.options.background.baseColor))
cx.paint()
if self.options.background.chartColor:
cx.set_source_rgb(*hex2rgb(self.options.background.chartColor))
surface_width, surface_height = self.getSurfaceSize()
cx.rectangle(self.options.padding.left, self.options.padding.top,
surface_width - (self.options.padding.left
+ self.options.padding.right),
surface_height - (self.options.padding.top
+ self.options.padding.bottom))
cx.fill()
if self.options.background.lineColor:
cx.set_source_rgb(*hex2rgb(self.options.background.lineColor))
cx.set_line_width(self.options.axis.lineWidth)
self._renderLines(cx)
cx.restore()
def _renderLines(self, cx):
"""Aux function for _renderBackground"""
if self.options.axis.y.showLines and self.yticks:
for tick in self.yticks:
self._renderLine(cx, tick, False)
if self.options.axis.x.showLines and self.xticks:
for tick in self.xticks:
self._renderLine(cx, tick, True)
def _renderLine(self, cx, tick, horiz):
"""Aux function for _renderLines"""
x1, x2, y1, y2 = (0, 0, 0, 0)
if horiz:
x1 = x2 = tick[0] * self.layout.chart.w + self.layout.chart.x
y1 = self.layout.chart.y
y2 = y1 + self.layout.chart.h
else:
x1 = self.layout.chart.x
x2 = x1 + self.layout.chart.w
y1 = y2 = tick[0] * self.layout.chart.h + self.layout.chart.y
cx.new_path()
cx.move_to(x1, y1)
cx.line_to(x2, y2)
cx.close_path()
cx.stroke()
def _renderChart(self, cx):
raise NotImplementedError
def _renderTick(self, cx, tick, x, y, x2, y2, rotate, text_position):
"""Aux method for _renderXTick and _renderYTick"""
if callable(tick):
return
cx.new_path()
cx.move_to(x, y)
cx.line_to(x2, y2)
cx.close_path()
cx.stroke()
cx.select_font_face(self.options.axis.tickFont,
cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_NORMAL)
cx.set_font_size(self.options.axis.tickFontSize)
label = safe_unicode(tick[1], self.options.encoding)
xb, yb, width, height, xa, ya = cx.text_extents(label)
x, y = text_position
if rotate:
cx.save()
cx.translate(x, y)
cx.rotate(math.radians(rotate))
x = -width / 2.0
y = -height / 2.0
cx.move_to(x - xb, y - yb)
cx.show_text(label)
if self.debug:
cx.rectangle(x, y, width, height)
cx.stroke()
cx.restore()
else:
x -= width / 2.0
y -= height / 2.0
cx.move_to(x - xb, y - yb)
cx.show_text(label)
if self.debug:
cx.rectangle(x, y, width, height)
cx.stroke()
return label
def _renderYTick(self, cx, tick):
"""Aux method for _renderAxis"""
x = self.layout.y_ticks.x + self.layout.y_ticks.w
y = self.layout.y_ticks.y + tick[0] * self.layout.y_ticks.h
text_position = ((self.layout.y_tick_labels.x
+ self.layout.y_tick_labels.w / 2.0), y)
return self._renderTick(cx, tick,
x, y,
x - self.options.axis.tickSize, y,
self.options.axis.y.rotate,
text_position)
def _renderXTick(self, cx, tick):
"""Aux method for _renderAxis"""
x = self.layout.x_ticks.x + tick[0] * self.layout.x_ticks.w
y = self.layout.x_ticks.y
text_position = (x, (self.layout.x_tick_labels.y
+ self.layout.x_tick_labels.h / 2.0))
return self._renderTick(cx, tick,
x, y,
x, y + self.options.axis.tickSize,
self.options.axis.x.rotate,
text_position)
def _renderAxisLabel(self, cx, label, x, y, vertical=False):
cx.save()
cx.select_font_face(self.options.axis.labelFont,
cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_BOLD)
cx.set_font_size(self.options.axis.labelFontSize)
xb, yb, width, height, xa, ya = cx.text_extents(label)
if vertical:
y = y + width / 2.0
cx.move_to(x - xb, y - yb)
cx.translate(x, y)
cx.rotate(-math.radians(90))
cx.move_to(-xb, -yb)
cx.show_text(label)
if self.debug:
cx.rectangle(0, 0, width, height)
cx.stroke()
else:
x = x - width / 2.0
cx.move_to(x - xb, y - yb)
cx.show_text(label)
if self.debug:
cx.rectangle(x, y, width, height)
cx.stroke()
cx.restore()
def _renderYAxisLabel(self, cx, label_text):
label = safe_unicode(label_text, self.options.encoding)
x = self.layout.y_label.x
y = self.layout.y_label.y + self.layout.y_label.h / 2.0
self._renderAxisLabel(cx, label, x, y, True)
def _renderYAxis(self, cx):
"""Draws the vertical line represeting the Y axis"""
cx.new_path()
cx.move_to(self.layout.chart.x, self.layout.chart.y)
cx.line_to(self.layout.chart.x,
self.layout.chart.y + self.layout.chart.h)
cx.close_path()
cx.stroke()
def _renderXAxisLabel(self, cx, label_text):
label = safe_unicode(label_text, self.options.encoding)
x = self.layout.x_label.x + self.layout.x_label.w / 2.0
y = self.layout.x_label.y
self._renderAxisLabel(cx, label, x, y, False)
def _renderXAxis(self, cx):
"""Draws the horizontal line representing the X axis"""
cx.new_path()
y = self.layout.chart.y + (1.0 - self.origin) * self.layout.chart.h
cx.move_to(self.layout.chart.x, y)
cx.line_to(self.layout.chart.x + self.layout.chart.w, y)
cx.close_path()
cx.stroke()
def _renderAxis(self, cx):
"""Renders axis"""
if self.options.axis.x.hide and self.options.axis.y.hide:
return
cx.save()
cx.set_source_rgb(*hex2rgb(self.options.axis.lineColor))
cx.set_line_width(self.options.axis.lineWidth)
if not self.options.axis.y.hide:
if self.yticks:
for tick in self.yticks:
self._renderYTick(cx, tick)
if self.options.axis.y.label:
self._renderYAxisLabel(cx, self.options.axis.y.label)
self._renderYAxis(cx)
if not self.options.axis.x.hide:
if self.xticks:
for tick in self.xticks:
self._renderXTick(cx, tick)
if self.options.axis.x.label:
self._renderXAxisLabel(cx, self.options.axis.x.label)
self._renderXAxis(cx)
cx.restore()
def _renderTitle(self, cx):
if self.options.title:
cx.save()
cx.select_font_face(self.options.titleFont,
cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_BOLD)
cx.set_font_size(self.options.titleFontSize)
cx.set_source_rgb(*hex2rgb(self.options.titleColor))
title = safe_unicode(self.options.title, self.options.encoding)
extents = cx.text_extents(title)
title_width = extents[2]
x = (self.layout.title.x
+ self.layout.title.w / 2.0
- title_width / 2.0)
y = self.layout.title.y - extents[1]
cx.move_to(x, y)
cx.show_text(title)
cx.restore()
def _renderLegend(self, cx):
"""This function adds a legend to the chart"""
if self.options.legend.hide:
return
surface_width, surface_height = self.getSurfaceSize()
# Compute legend dimensions
padding = 4
bullet = 15
width = 0
height = padding
keys = self._getDatasetsKeys()
for key in keys:
extents = cx.text_extents(key)
width = max(extents[2], width)
height += max(extents[3], bullet) + padding
width = padding + bullet + padding + width + padding
# Compute legend position
legend = self.options.legend
if legend.position.right is not None:
legend.position.left = (surface_width
- legend.position.right
- width)
if legend.position.bottom is not None:
legend.position.top = (surface_height
- legend.position.bottom
- height)
# Draw the legend
cx.save()
cx.rectangle(self.options.legend.position.left,
self.options.legend.position.top,
width, height)
cx.set_source_rgba(1, 1, 1, self.options.legend.opacity)
cx.fill_preserve()
cx.set_line_width(self.options.legend.borderWidth)
cx.set_source_rgb(*hex2rgb(self.options.legend.borderColor))
cx.stroke()
def drawKey(key, x, y, text_height):
cx.rectangle(x, y, bullet, bullet)
cx.set_source_rgb(*self.colorScheme[key])
cx.fill_preserve()
cx.set_source_rgb(0, 0, 0)
cx.stroke()
cx.move_to(x + bullet + padding,
y + bullet / 2.0 + text_height / 2.0)
cx.show_text(key)
cx.set_line_width(1)
x = self.options.legend.position.left + padding
y = self.options.legend.position.top + padding
for key in keys:
extents = cx.text_extents(key)
drawKey(key, x, y, extents[3])
y += max(extents[3], bullet) + padding
cx.restore()
def uniqueIndices(arr):
"""Return a list with the indexes of the biggest element of arr"""
return range(max([len(a) for a in arr]))
class Area(object):
"""Simple rectangle to hold an area coordinates and dimensions"""
def __init__(self, x=0.0, y=0.0, w=0.0, h=0.0):
self.x, self.y, self.w, self.h = x, y, w, h
def __str__(self):
msg = "<pycha.chart.Area@(%.2f, %.2f) %.2f x %.2f>"
return msg % (self.x, self.y, self.w, self.h)
def get_text_extents(cx, text, font, font_size, encoding):
if text:
cx.save()
cx.select_font_face(font,
cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
cx.set_font_size(font_size)
safe_text = safe_unicode(text, encoding)
extents = cx.text_extents(safe_text)
cx.restore()
return extents[2:4]
return (0.0, 0.0)
class Layout(object):
"""Set of chart areas"""
def __init__(self):
self.title = Area()
self.x_label = Area()
self.y_label = Area()
self.x_tick_labels = Area()
self.y_tick_labels = Area()
self.x_ticks = Area()
self.y_ticks = Area()
self.chart = Area()
self._areas = (
(self.title, (1, 126/255.0, 0)), # orange
(self.y_label, (41/255.0, 91/255.0, 41/255.0)), # grey
(self.x_label, (41/255.0, 91/255.0, 41/255.0)), # grey
(self.y_tick_labels, (0, 115/255.0, 0)), # green
(self.x_tick_labels, (0, 115/255.0, 0)), # green
(self.y_ticks, (229/255.0, 241/255.0, 18/255.0)), # yellow
(self.x_ticks, (229/255.0, 241/255.0, 18/255.0)), # yellow
(self.chart, (75/255.0, 75/255.0, 1.0)), # blue
)
def update(self, cx, options, width, height, xticks, yticks):
self.title.x = options.padding.left
self.title.y = options.padding.top
self.title.w = width - (options.padding.left + options.padding.right)
self.title.h = get_text_extents(cx,
options.title,
options.titleFont,
options.titleFontSize,
options.encoding)[1]
x_axis_label_height = get_text_extents(cx,
options.axis.x.label,
options.axis.labelFont,
options.axis.labelFontSize,
options.encoding)[1]
y_axis_label_width = get_text_extents(cx,
options.axis.y.label,
options.axis.labelFont,
options.axis.labelFontSize,
options.encoding)[1]
x_axis_tick_labels_height = self._getAxisTickLabelsSize(cx, options,
options.axis.x,
xticks)[1]
y_axis_tick_labels_width = self._getAxisTickLabelsSize(cx, options,
options.axis.y,
yticks)[0]
self.y_label.x = options.padding.left
self.y_label.y = options.padding.top + self.title.h
self.y_label.w = y_axis_label_width
self.y_label.h = height - (options.padding.bottom
+ options.padding.top
+ x_axis_label_height
+ x_axis_tick_labels_height
+ options.axis.tickSize
+ self.title.h)
self.x_label.x = (options.padding.left
+ y_axis_label_width
+ y_axis_tick_labels_width
+ options.axis.tickSize)
self.x_label.y = height - (options.padding.bottom
+ x_axis_label_height)
self.x_label.w = width - (options.padding.left
+ options.padding.right
+ options.axis.tickSize
+ y_axis_label_width
+ y_axis_tick_labels_width)
self.x_label.h = x_axis_label_height
self.y_tick_labels.x = self.y_label.x + self.y_label.w
self.y_tick_labels.y = self.y_label.y
self.y_tick_labels.w = y_axis_tick_labels_width
self.y_tick_labels.h = self.y_label.h
self.x_tick_labels.x = self.x_label.x
self.x_tick_labels.y = self.x_label.y - x_axis_tick_labels_height
self.x_tick_labels.w = self.x_label.w
self.x_tick_labels.h = x_axis_tick_labels_height
self.y_ticks.x = self.y_tick_labels.x + self.y_tick_labels.w
self.y_ticks.y = self.y_tick_labels.y
self.y_ticks.w = options.axis.tickSize
self.y_ticks.h = self.y_label.h
self.x_ticks.x = self.x_tick_labels.x
self.x_ticks.y = self.x_tick_labels.y - options.axis.tickSize
self.x_ticks.w = self.x_label.w
self.x_ticks.h = options.axis.tickSize
self.chart.x = self.y_ticks.x + self.y_ticks.w
self.chart.y = self.title.y + self.title.h
self.chart.w = self.x_ticks.w
self.chart.h = self.y_ticks.h
def render(self, cx):
def draw_area(area, r, g, b):
cx.rectangle(area.x, area.y, area.w, area.h)
cx.set_source_rgba(r, g, b, 0.5)
cx.fill()
cx.save()
for area, color in self._areas:
draw_area(area, *color)
cx.restore()
def _getAxisTickLabelsSize(self, cx, options, axis, ticks):
cx.save()
cx.select_font_face(options.axis.tickFont,
cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_NORMAL)
cx.set_font_size(options.axis.tickFontSize)
max_width = max_height = 0.0
if not axis.hide:
extents = [cx.text_extents(safe_unicode(
tick[1], options.encoding,
))[2:4] # get width and height as a tuple
for tick in ticks]
if extents:
widths, heights = zip(*extents)
max_width, max_height = max(widths), max(heights)
if axis.rotate:
radians = math.radians(axis.rotate)
sin = math.sin(radians)
cos = math.cos(radians)
max_width, max_height = (
max_width * cos + max_height * sin,
max_width * sin + max_height * cos,
)
cx.restore()
return max_width, max_height
class Option(dict):
"""Useful dict that allow attribute-like access to its keys"""
def __getattr__(self, name):
if name in self.keys():
return self[name]
else:
raise AttributeError(name)
def merge(self, other):
"""Recursive merge with other Option or dict object"""
for key, value in other.items():
if key in self:
if isinstance(self[key], Option):
self[key].merge(other[key])
else:
self[key] = other[key]
DEFAULT_OPTIONS = Option(
axis=Option(
lineWidth=1.0,
lineColor='#0f0000',
tickSize=3.0,
labelColor='#666666',
labelFont='Tahoma',
labelFontSize=9,
labelWidth=50.0,
tickFont='Tahoma',
tickFontSize=9,
x=Option(
hide=False,
ticks=None,
tickCount=10,
tickPrecision=1,
range=None,
rotate=None,
label=None,
interval=0,
showLines=False,
),
y=Option(
hide=False,
ticks=None,
tickCount=10,
tickPrecision=1,
range=None,
rotate=None,
label=None,
interval=0,
showLines=True,
),
),
background=Option(
hide=False,
baseColor=None,
chartColor='#f5f5f5',
lineColor='#ffffff',
lineWidth=1.5,
),
legend=Option(
opacity=0.8,
borderColor='#000000',
borderWidth=2,
hide=False,
position=Option(top=20, left=40, bottom=None, right=None),
),
padding=Option(
left=10,
right=10,
top=10,
bottom=10,
),
stroke=Option(
color='#ffffff',
hide=False,
shadow=True,
width=2
),
yvals=Option(
show=False,
inside=False,
fontSize=11,
fontColor='#000000',
skipSmallValues=True,
snapToOrigin=False,
renderer=None
),
fillOpacity=1.0,
shouldFill=True,
barWidthFillFraction=0.75,
pieRadius=0.4,
colorScheme=Option(
name='gradient',
args=Option(
initialColor=DEFAULT_COLOR,
colors=None,
),
),
title=None,
titleColor='#000000',
titleFont='Tahoma',
titleFontSize=12,
encoding='utf-8',
)