PageRenderTime 63ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/bayes/pygooglechart.py

http://myrpm.googlecode.com/
Python | 1067 lines | 918 code | 83 blank | 66 comment | 49 complexity | 186942cd56b1a333e76e3d8e2aeb63d8 MD5 | raw file
Possible License(s): GPL-2.0
  1. """
  2. pygooglechart - A complete Python wrapper for the Google Chart API
  3. http://pygooglechart.slowchop.com/
  4. Copyright 2007-2008 Gerald Kaszuba
  5. This program is free software: you can redistribute it and/or modify
  6. it under the terms of the GNU General Public License as published by
  7. the Free Software Foundation, either version 3 of the License, or
  8. (at your option) any later version.
  9. This program is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. GNU General Public License for more details.
  13. You should have received a copy of the GNU General Public License
  14. along with this program. If not, see <http://www.gnu.org/licenses/>.
  15. """
  16. import os
  17. import urllib
  18. import urllib2
  19. import math
  20. import random
  21. import re
  22. import warnings
  23. import copy
  24. # Helper variables and functions
  25. # -----------------------------------------------------------------------------
  26. __version__ = '0.2.1'
  27. __author__ = 'Gerald Kaszuba'
  28. reo_colour = re.compile('^([A-Fa-f0-9]{2,2}){3,4}$')
  29. def _check_colour(colour):
  30. if not reo_colour.match(colour):
  31. raise InvalidParametersException('Colours need to be in ' \
  32. 'RRGGBB or RRGGBBAA format. One of your colours has %s' % \
  33. colour)
  34. def _reset_warnings():
  35. """Helper function to reset all warnings. Used by the unit tests."""
  36. globals()['__warningregistry__'] = None
  37. # Exception Classes
  38. # -----------------------------------------------------------------------------
  39. class PyGoogleChartException(Exception):
  40. pass
  41. class DataOutOfRangeException(PyGoogleChartException):
  42. pass
  43. class UnknownDataTypeException(PyGoogleChartException):
  44. pass
  45. class NoDataGivenException(PyGoogleChartException):
  46. pass
  47. class InvalidParametersException(PyGoogleChartException):
  48. pass
  49. class BadContentTypeException(PyGoogleChartException):
  50. pass
  51. class AbstractClassException(PyGoogleChartException):
  52. pass
  53. class UnknownChartType(PyGoogleChartException):
  54. pass
  55. # Data Classes
  56. # -----------------------------------------------------------------------------
  57. class Data(object):
  58. def __init__(self, data):
  59. if type(self) == Data:
  60. raise AbstractClassException('This is an abstract class')
  61. self.data = data
  62. @classmethod
  63. def float_scale_value(cls, value, range):
  64. lower, upper = range
  65. assert(upper > lower)
  66. scaled = (value - lower) * (float(cls.max_value) / (upper - lower))
  67. return scaled
  68. @classmethod
  69. def clip_value(cls, value):
  70. return max(0, min(value, cls.max_value))
  71. @classmethod
  72. def int_scale_value(cls, value, range):
  73. return int(round(cls.float_scale_value(value, range)))
  74. @classmethod
  75. def scale_value(cls, value, range):
  76. scaled = cls.int_scale_value(value, range)
  77. clipped = cls.clip_value(scaled)
  78. Data.check_clip(scaled, clipped)
  79. return clipped
  80. @staticmethod
  81. def check_clip(scaled, clipped):
  82. if clipped != scaled:
  83. warnings.warn('One or more of of your data points has been '
  84. 'clipped because it is out of range.')
  85. class SimpleData(Data):
  86. max_value = 61
  87. enc_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
  88. def __repr__(self):
  89. encoded_data = []
  90. for data in self.data:
  91. sub_data = []
  92. for value in data:
  93. if value is None:
  94. sub_data.append('_')
  95. elif value >= 0 and value <= self.max_value:
  96. sub_data.append(SimpleData.enc_map[value])
  97. else:
  98. raise DataOutOfRangeException('cannot encode value: %d'
  99. % value)
  100. encoded_data.append(''.join(sub_data))
  101. return 'chd=s:' + ','.join(encoded_data)
  102. class TextData(Data):
  103. max_value = 100
  104. def __repr__(self):
  105. encoded_data = []
  106. for data in self.data:
  107. sub_data = []
  108. for value in data:
  109. if value is None:
  110. sub_data.append(-1)
  111. elif value >= 0 and value <= self.max_value:
  112. sub_data.append("%.1f" % float(value))
  113. else:
  114. raise DataOutOfRangeException()
  115. encoded_data.append(','.join(sub_data))
  116. return 'chd=t:' + '|'.join(encoded_data)
  117. @classmethod
  118. def scale_value(cls, value, range):
  119. # use float values instead of integers because we don't need an encode
  120. # map index
  121. scaled = cls.float_scale_value(value, range)
  122. clipped = cls.clip_value(scaled)
  123. Data.check_clip(scaled, clipped)
  124. return clipped
  125. class ExtendedData(Data):
  126. max_value = 4095
  127. enc_map = \
  128. 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
  129. def __repr__(self):
  130. encoded_data = []
  131. enc_size = len(ExtendedData.enc_map)
  132. for data in self.data:
  133. sub_data = []
  134. for value in data:
  135. if value is None:
  136. sub_data.append('__')
  137. elif value >= 0 and value <= self.max_value:
  138. first, second = divmod(int(value), enc_size)
  139. sub_data.append('%s%s' % (
  140. ExtendedData.enc_map[first],
  141. ExtendedData.enc_map[second]))
  142. else:
  143. raise DataOutOfRangeException( \
  144. 'Item #%i "%s" is out of range' % (data.index(value), \
  145. value))
  146. encoded_data.append(''.join(sub_data))
  147. return 'chd=e:' + ','.join(encoded_data)
  148. # Axis Classes
  149. # -----------------------------------------------------------------------------
  150. class Axis(object):
  151. BOTTOM = 'x'
  152. TOP = 't'
  153. LEFT = 'y'
  154. RIGHT = 'r'
  155. TYPES = (BOTTOM, TOP, LEFT, RIGHT)
  156. def __init__(self, axis_index, axis_type, **kw):
  157. assert(axis_type in Axis.TYPES)
  158. self.has_style = False
  159. self.axis_index = axis_index
  160. self.axis_type = axis_type
  161. self.positions = None
  162. def set_index(self, axis_index):
  163. self.axis_index = axis_index
  164. def set_positions(self, positions):
  165. self.positions = positions
  166. def set_style(self, colour, font_size=None, alignment=None):
  167. _check_colour(colour)
  168. self.colour = colour
  169. self.font_size = font_size
  170. self.alignment = alignment
  171. self.has_style = True
  172. def style_to_url(self):
  173. bits = []
  174. bits.append(str(self.axis_index))
  175. bits.append(self.colour)
  176. if self.font_size is not None:
  177. bits.append(str(self.font_size))
  178. if self.alignment is not None:
  179. bits.append(str(self.alignment))
  180. return ','.join(bits)
  181. def positions_to_url(self):
  182. bits = []
  183. bits.append(str(self.axis_index))
  184. bits += [str(a) for a in self.positions]
  185. return ','.join(bits)
  186. class LabelAxis(Axis):
  187. def __init__(self, axis_index, axis_type, values, **kwargs):
  188. Axis.__init__(self, axis_index, axis_type, **kwargs)
  189. self.values = [str(a) for a in values]
  190. def __repr__(self):
  191. return '%i:|%s' % (self.axis_index, '|'.join(self.values))
  192. class RangeAxis(Axis):
  193. def __init__(self, axis_index, axis_type, low, high, **kwargs):
  194. Axis.__init__(self, axis_index, axis_type, **kwargs)
  195. self.low = low
  196. self.high = high
  197. def __repr__(self):
  198. return '%i,%s,%s' % (self.axis_index, self.low, self.high)
  199. # Chart Classes
  200. # -----------------------------------------------------------------------------
  201. class Chart(object):
  202. """Abstract class for all chart types.
  203. width are height specify the dimensions of the image. title sets the title
  204. of the chart. legend requires a list that corresponds to datasets.
  205. """
  206. BASE_URL = 'http://chart.apis.google.com/chart?'
  207. BACKGROUND = 'bg'
  208. CHART = 'c'
  209. ALPHA = 'a'
  210. VALID_SOLID_FILL_TYPES = (BACKGROUND, CHART, ALPHA)
  211. SOLID = 's'
  212. LINEAR_GRADIENT = 'lg'
  213. LINEAR_STRIPES = 'ls'
  214. def __init__(self, width, height, title=None, legend=None, colours=None,
  215. auto_scale=True, x_range=None, y_range=None,
  216. colours_within_series=None):
  217. if type(self) == Chart:
  218. raise AbstractClassException('This is an abstract class')
  219. assert(isinstance(width, int))
  220. assert(isinstance(height, int))
  221. self.width = width
  222. self.height = height
  223. self.data = []
  224. self.set_title(title)
  225. self.set_legend(legend)
  226. self.set_legend_position(None)
  227. self.set_colours(colours)
  228. self.set_colours_within_series(colours_within_series)
  229. # Data for scaling.
  230. self.auto_scale = auto_scale # Whether to automatically scale data
  231. self.x_range = x_range # (min, max) x-axis range for scaling
  232. self.y_range = y_range # (min, max) y-axis range for scaling
  233. self.scaled_data_class = None
  234. self.scaled_x_range = None
  235. self.scaled_y_range = None
  236. self.fill_types = {
  237. Chart.BACKGROUND: None,
  238. Chart.CHART: None,
  239. Chart.ALPHA: None,
  240. }
  241. self.fill_area = {
  242. Chart.BACKGROUND: None,
  243. Chart.CHART: None,
  244. Chart.ALPHA: None,
  245. }
  246. self.axis = []
  247. self.markers = []
  248. self.line_styles = {}
  249. self.grid = None
  250. # URL generation
  251. # -------------------------------------------------------------------------
  252. def get_url(self, data_class=None):
  253. url_bits = self.get_url_bits(data_class=data_class)
  254. return self.BASE_URL + '&'.join(url_bits)
  255. def get_url_bits(self, data_class=None):
  256. url_bits = []
  257. # required arguments
  258. url_bits.append(self.type_to_url())
  259. url_bits.append('chs=%ix%i' % (self.width, self.height))
  260. url_bits.append(self.data_to_url(data_class=data_class))
  261. # optional arguments
  262. if self.title:
  263. url_bits.append('chtt=%s' % self.title)
  264. if self.legend:
  265. url_bits.append('chdl=%s' % '|'.join(self.legend))
  266. if self.legend_position:
  267. url_bits.append('chdlp=%s' % (self.legend_position))
  268. if self.colours:
  269. url_bits.append('chco=%s' % ','.join(self.colours))
  270. if self.colours_within_series:
  271. url_bits.append('chco=%s' % '|'.join(self.colours_within_series))
  272. ret = self.fill_to_url()
  273. if ret:
  274. url_bits.append(ret)
  275. ret = self.axis_to_url()
  276. if ret:
  277. url_bits.append(ret)
  278. if self.markers:
  279. url_bits.append(self.markers_to_url())
  280. if self.line_styles:
  281. style = []
  282. for index in xrange(max(self.line_styles) + 1):
  283. if index in self.line_styles:
  284. values = self.line_styles[index]
  285. else:
  286. values = ('1', )
  287. style.append(','.join(values))
  288. url_bits.append('chls=%s' % '|'.join(style))
  289. if self.grid:
  290. url_bits.append('chg=%s' % self.grid)
  291. return url_bits
  292. # Downloading
  293. # -------------------------------------------------------------------------
  294. def download(self, file_name):
  295. print "URL:" +str(self.get_url())
  296. opener = urllib2.urlopen(self.get_url())
  297. if opener.headers['content-type'] != 'image/png':
  298. raise BadContentTypeException('Server responded with a ' \
  299. 'content-type of %s' % opener.headers['content-type'])
  300. open(file_name, 'wb').write(opener.read())
  301. # Simple settings
  302. # -------------------------------------------------------------------------
  303. def set_title(self, title):
  304. if title:
  305. self.title = urllib.quote(title)
  306. else:
  307. self.title = None
  308. def set_legend(self, legend):
  309. """legend needs to be a list, tuple or None"""
  310. assert(isinstance(legend, list) or isinstance(legend, tuple) or
  311. legend is None)
  312. if legend:
  313. self.legend = [urllib.quote(a) for a in legend]
  314. else:
  315. self.legend = None
  316. def set_legend_position(self, legend_position):
  317. if legend_position:
  318. self.legend_position = urllib.quote(legend_position)
  319. else:
  320. self.legend_position = None
  321. # Chart colours
  322. # -------------------------------------------------------------------------
  323. def set_colours(self, colours):
  324. # colours needs to be a list, tuple or None
  325. assert(isinstance(colours, list) or isinstance(colours, tuple) or
  326. colours is None)
  327. # make sure the colours are in the right format
  328. if colours:
  329. for col in colours:
  330. _check_colour(col)
  331. self.colours = colours
  332. def set_colours_within_series(self, colours):
  333. # colours needs to be a list, tuple or None
  334. assert(isinstance(colours, list) or isinstance(colours, tuple) or
  335. colours is None)
  336. # make sure the colours are in the right format
  337. if colours:
  338. for col in colours:
  339. _check_colour(col)
  340. self.colours_within_series = colours
  341. # Background/Chart colours
  342. # -------------------------------------------------------------------------
  343. def fill_solid(self, area, colour):
  344. assert(area in Chart.VALID_SOLID_FILL_TYPES)
  345. _check_colour(colour)
  346. self.fill_area[area] = colour
  347. self.fill_types[area] = Chart.SOLID
  348. def _check_fill_linear(self, angle, *args):
  349. assert(isinstance(args, list) or isinstance(args, tuple))
  350. assert(angle >= 0 and angle <= 90)
  351. assert(len(args) % 2 == 0)
  352. args = list(args) # args is probably a tuple and we need to mutate
  353. for a in xrange(len(args) / 2):
  354. col = args[a * 2]
  355. offset = args[a * 2 + 1]
  356. _check_colour(col)
  357. assert(offset >= 0 and offset <= 1)
  358. args[a * 2 + 1] = str(args[a * 2 + 1])
  359. return args
  360. def fill_linear_gradient(self, area, angle, *args):
  361. assert(area in Chart.VALID_SOLID_FILL_TYPES)
  362. args = self._check_fill_linear(angle, *args)
  363. self.fill_types[area] = Chart.LINEAR_GRADIENT
  364. self.fill_area[area] = ','.join([str(angle)] + args)
  365. def fill_linear_stripes(self, area, angle, *args):
  366. assert(area in Chart.VALID_SOLID_FILL_TYPES)
  367. args = self._check_fill_linear(angle, *args)
  368. self.fill_types[area] = Chart.LINEAR_STRIPES
  369. self.fill_area[area] = ','.join([str(angle)] + args)
  370. def fill_to_url(self):
  371. areas = []
  372. for area in (Chart.BACKGROUND, Chart.CHART, Chart.ALPHA):
  373. if self.fill_types[area]:
  374. areas.append('%s,%s,%s' % (area, self.fill_types[area], \
  375. self.fill_area[area]))
  376. if areas:
  377. return 'chf=' + '|'.join(areas)
  378. # Data
  379. # -------------------------------------------------------------------------
  380. def data_class_detection(self, data):
  381. """Determines the appropriate data encoding type to give satisfactory
  382. resolution (http://code.google.com/apis/chart/#chart_data).
  383. """
  384. assert(isinstance(data, list) or isinstance(data, tuple))
  385. if not isinstance(self, (LineChart, BarChart, ScatterChart)):
  386. # From the link above:
  387. # Simple encoding is suitable for all other types of chart
  388. # regardless of size.
  389. return SimpleData
  390. elif self.height < 100:
  391. # The link above indicates that line and bar charts less
  392. # than 300px in size can be suitably represented with the
  393. # simple encoding. I've found that this isn't sufficient,
  394. # e.g. examples/line-xy-circle.png. Let's try 100px.
  395. return SimpleData
  396. else:
  397. return ExtendedData
  398. def _filter_none(self, data):
  399. return [r for r in data if r is not None]
  400. def data_x_range(self):
  401. """Return a 2-tuple giving the minimum and maximum x-axis
  402. data range.
  403. """
  404. try:
  405. lower = min([min(self._filter_none(s))
  406. for type, s in self.annotated_data()
  407. if type == 'x'])
  408. upper = max([max(self._filter_none(s))
  409. for type, s in self.annotated_data()
  410. if type == 'x'])
  411. return (lower, upper)
  412. except ValueError:
  413. return None # no x-axis datasets
  414. def data_y_range(self):
  415. """Return a 2-tuple giving the minimum and maximum y-axis
  416. data range.
  417. """
  418. try:
  419. lower = min([min(self._filter_none(s))
  420. for type, s in self.annotated_data()
  421. if type == 'y'])
  422. upper = max([max(self._filter_none(s)) + 1
  423. for type, s in self.annotated_data()
  424. if type == 'y'])
  425. return (lower, upper)
  426. except ValueError:
  427. return None # no y-axis datasets
  428. def scaled_data(self, data_class, x_range=None, y_range=None):
  429. """Scale `self.data` as appropriate for the given data encoding
  430. (data_class) and return it.
  431. An optional `y_range` -- a 2-tuple (lower, upper) -- can be
  432. given to specify the y-axis bounds. If not given, the range is
  433. inferred from the data: (0, <max-value>) presuming no negative
  434. values, or (<min-value>, <max-value>) if there are negative
  435. values. `self.scaled_y_range` is set to the actual lower and
  436. upper scaling range.
  437. Ditto for `x_range`. Note that some chart types don't have x-axis
  438. data.
  439. """
  440. self.scaled_data_class = data_class
  441. # Determine the x-axis range for scaling.
  442. if x_range is None:
  443. x_range = self.data_x_range()
  444. if x_range and x_range[0] > 0:
  445. x_range = (x_range[0], x_range[1])
  446. self.scaled_x_range = x_range
  447. # Determine the y-axis range for scaling.
  448. if y_range is None:
  449. y_range = self.data_y_range()
  450. if y_range and y_range[0] > 0:
  451. y_range = (y_range[0], y_range[1])
  452. self.scaled_y_range = y_range
  453. scaled_data = []
  454. for type, dataset in self.annotated_data():
  455. if type == 'x':
  456. scale_range = x_range
  457. elif type == 'y':
  458. scale_range = y_range
  459. elif type == 'marker-size':
  460. scale_range = (0, max(dataset))
  461. scaled_dataset = []
  462. for v in dataset:
  463. if v is None:
  464. scaled_dataset.append(None)
  465. else:
  466. scaled_dataset.append(
  467. data_class.scale_value(v, scale_range))
  468. scaled_data.append(scaled_dataset)
  469. return scaled_data
  470. def add_data(self, data):
  471. self.data.append(data)
  472. return len(self.data) - 1 # return the "index" of the data set
  473. def data_to_url(self, data_class=None):
  474. if not data_class:
  475. data_class = self.data_class_detection(self.data)
  476. if not issubclass(data_class, Data):
  477. raise UnknownDataTypeException()
  478. if self.auto_scale:
  479. data = self.scaled_data(data_class, self.x_range, self.y_range)
  480. else:
  481. data = self.data
  482. return repr(data_class(data))
  483. def annotated_data(self):
  484. for dataset in self.data:
  485. yield ('x', dataset)
  486. # Axis Labels
  487. # -------------------------------------------------------------------------
  488. def set_axis_labels(self, axis_type, values):
  489. assert(axis_type in Axis.TYPES)
  490. values = [urllib.quote(str(a)) for a in values]
  491. axis_index = len(self.axis)
  492. axis = LabelAxis(axis_index, axis_type, values)
  493. self.axis.append(axis)
  494. return axis_index
  495. def set_axis_range(self, axis_type, low, high):
  496. assert(axis_type in Axis.TYPES)
  497. axis_index = len(self.axis)
  498. axis = RangeAxis(axis_index, axis_type, low, high)
  499. self.axis.append(axis)
  500. return axis_index
  501. def set_axis_positions(self, axis_index, positions):
  502. try:
  503. self.axis[axis_index].set_positions(positions)
  504. except IndexError:
  505. raise InvalidParametersException('Axis index %i has not been ' \
  506. 'created' % axis)
  507. def set_axis_style(self, axis_index, colour, font_size=None, \
  508. alignment=None):
  509. try:
  510. self.axis[axis_index].set_style(colour, font_size, alignment)
  511. except IndexError:
  512. raise InvalidParametersException('Axis index %i has not been ' \
  513. 'created' % axis)
  514. def axis_to_url(self):
  515. available_axis = []
  516. label_axis = []
  517. range_axis = []
  518. positions = []
  519. styles = []
  520. index = -1
  521. for axis in self.axis:
  522. available_axis.append(axis.axis_type)
  523. if isinstance(axis, RangeAxis):
  524. range_axis.append(repr(axis))
  525. if isinstance(axis, LabelAxis):
  526. label_axis.append(repr(axis))
  527. if axis.positions:
  528. positions.append(axis.positions_to_url())
  529. if axis.has_style:
  530. styles.append(axis.style_to_url())
  531. if not available_axis:
  532. return
  533. url_bits = []
  534. url_bits.append('chxt=%s' % ','.join(available_axis))
  535. if label_axis:
  536. url_bits.append('chxl=%s' % '|'.join(label_axis))
  537. if range_axis:
  538. url_bits.append('chxr=%s' % '|'.join(range_axis))
  539. if positions:
  540. url_bits.append('chxp=%s' % '|'.join(positions))
  541. if styles:
  542. url_bits.append('chxs=%s' % '|'.join(styles))
  543. return '&'.join(url_bits)
  544. # Markers, Ranges and Fill area (chm)
  545. # -------------------------------------------------------------------------
  546. def markers_to_url(self):
  547. return 'chm=%s' % '|'.join([','.join(a) for a in self.markers])
  548. def add_marker(self, index, point, marker_type, colour, size, priority=0):
  549. self.markers.append((marker_type, colour, str(index), str(point), \
  550. str(size), str(priority)))
  551. def add_horizontal_range(self, colour, start, stop):
  552. self.markers.append(('r', colour, '0', str(start), str(stop)))
  553. def add_data_line(self, colour, data_set, size, priority=0):
  554. self.markers.append(('D', colour, str(data_set), '0', str(size), str(priority)))
  555. def add_marker_text(self, string, colour, data_set, data_point, size, priority=0):
  556. self.markers.append((str(string), colour, str(data_set), str(data_point), str(size), str(priority)))
  557. def add_vertical_range(self, colour, start, stop):
  558. self.markers.append(('R', colour, '0', str(start), str(stop)))
  559. def add_fill_range(self, colour, index_start, index_end):
  560. self.markers.append(('b', colour, str(index_start), str(index_end), \
  561. '1'))
  562. def add_fill_simple(self, colour):
  563. self.markers.append(('B', colour, '1', '1', '1'))
  564. # Line styles
  565. # -------------------------------------------------------------------------
  566. def set_line_style(self, index, thickness=1, line_segment=None, \
  567. blank_segment=None):
  568. value = []
  569. value.append(str(thickness))
  570. if line_segment:
  571. value.append(str(line_segment))
  572. value.append(str(blank_segment))
  573. self.line_styles[index] = value
  574. # Grid
  575. # -------------------------------------------------------------------------
  576. def set_grid(self, x_step, y_step, line_segment=1, \
  577. blank_segment=0):
  578. self.grid = '%s,%s,%s,%s' % (x_step, y_step, line_segment, \
  579. blank_segment)
  580. class ScatterChart(Chart):
  581. def type_to_url(self):
  582. return 'cht=s'
  583. def annotated_data(self):
  584. yield ('x', self.data[0])
  585. yield ('y', self.data[1])
  586. if len(self.data) > 2:
  587. # The optional third dataset is relative sizing for point
  588. # markers.
  589. yield ('marker-size', self.data[2])
  590. class LineChart(Chart):
  591. def __init__(self, *args, **kwargs):
  592. if type(self) == LineChart:
  593. raise AbstractClassException('This is an abstract class')
  594. Chart.__init__(self, *args, **kwargs)
  595. class SimpleLineChart(LineChart):
  596. def type_to_url(self):
  597. return 'cht=lc'
  598. def annotated_data(self):
  599. # All datasets are y-axis data.
  600. for dataset in self.data:
  601. yield ('y', dataset)
  602. class SparkLineChart(SimpleLineChart):
  603. def type_to_url(self):
  604. return 'cht=ls'
  605. class XYLineChart(LineChart):
  606. def type_to_url(self):
  607. return 'cht=lxy'
  608. def annotated_data(self):
  609. # Datasets alternate between x-axis, y-axis.
  610. for i, dataset in enumerate(self.data):
  611. if i % 2 == 0:
  612. yield ('x', dataset)
  613. else:
  614. yield ('y', dataset)
  615. class BarChart(Chart):
  616. def __init__(self, *args, **kwargs):
  617. if type(self) == BarChart:
  618. raise AbstractClassException('This is an abstract class')
  619. Chart.__init__(self, *args, **kwargs)
  620. self.bar_width = None
  621. self.zero_lines = {}
  622. def set_bar_width(self, bar_width):
  623. self.bar_width = bar_width
  624. def set_zero_line(self, index, zero_line):
  625. self.zero_lines[index] = zero_line
  626. def get_url_bits(self, data_class=None, skip_chbh=False):
  627. url_bits = Chart.get_url_bits(self, data_class=data_class)
  628. if not skip_chbh and self.bar_width is not None:
  629. url_bits.append('chbh=%i' % self.bar_width)
  630. zero_line = []
  631. if self.zero_lines:
  632. for index in xrange(max(self.zero_lines) + 1):
  633. if index in self.zero_lines:
  634. zero_line.append(str(self.zero_lines[index]))
  635. else:
  636. zero_line.append('0')
  637. url_bits.append('chp=%s' % ','.join(zero_line))
  638. return url_bits
  639. class StackedHorizontalBarChart(BarChart):
  640. def type_to_url(self):
  641. return 'cht=bhs'
  642. class StackedVerticalBarChart(BarChart):
  643. def type_to_url(self):
  644. return 'cht=bvs'
  645. def annotated_data(self):
  646. for dataset in self.data:
  647. yield ('y', dataset)
  648. class GroupedBarChart(BarChart):
  649. def __init__(self, *args, **kwargs):
  650. if type(self) == GroupedBarChart:
  651. raise AbstractClassException('This is an abstract class')
  652. BarChart.__init__(self, *args, **kwargs)
  653. self.bar_spacing = None
  654. self.group_spacing = None
  655. def set_bar_spacing(self, spacing):
  656. """Set spacing between bars in a group."""
  657. self.bar_spacing = spacing
  658. def set_group_spacing(self, spacing):
  659. """Set spacing between groups of bars."""
  660. self.group_spacing = spacing
  661. def get_url_bits(self, data_class=None):
  662. # Skip 'BarChart.get_url_bits' and call Chart directly so the parent
  663. # doesn't add "chbh" before we do.
  664. url_bits = BarChart.get_url_bits(self, data_class=data_class,
  665. skip_chbh=True)
  666. if self.group_spacing is not None:
  667. if self.bar_spacing is None:
  668. raise InvalidParametersException('Bar spacing is required ' \
  669. 'to be set when setting group spacing')
  670. if self.bar_width is None:
  671. raise InvalidParametersException('Bar width is required to ' \
  672. 'be set when setting bar spacing')
  673. url_bits.append('chbh=%i,%i,%i'
  674. % (self.bar_width, self.bar_spacing, self.group_spacing))
  675. elif self.bar_spacing is not None:
  676. if self.bar_width is None:
  677. raise InvalidParametersException('Bar width is required to ' \
  678. 'be set when setting bar spacing')
  679. url_bits.append('chbh=%i,%i' % (self.bar_width, self.bar_spacing))
  680. elif self.bar_width:
  681. url_bits.append('chbh=%i' % self.bar_width)
  682. return url_bits
  683. class GroupedHorizontalBarChart(GroupedBarChart):
  684. def type_to_url(self):
  685. return 'cht=bhg'
  686. class GroupedVerticalBarChart(GroupedBarChart):
  687. def type_to_url(self):
  688. return 'cht=bvg'
  689. def annotated_data(self):
  690. for dataset in self.data:
  691. yield ('y', dataset)
  692. class PieChart(Chart):
  693. def __init__(self, *args, **kwargs):
  694. if type(self) == PieChart:
  695. raise AbstractClassException('This is an abstract class')
  696. Chart.__init__(self, *args, **kwargs)
  697. self.pie_labels = []
  698. if self.y_range:
  699. warnings.warn('y_range is not used with %s.' % \
  700. (self.__class__.__name__))
  701. def set_pie_labels(self, labels):
  702. self.pie_labels = [urllib.quote(a) for a in labels]
  703. def get_url_bits(self, data_class=None):
  704. url_bits = Chart.get_url_bits(self, data_class=data_class)
  705. if self.pie_labels:
  706. url_bits.append('chl=%s' % '|'.join(self.pie_labels))
  707. return url_bits
  708. def annotated_data(self):
  709. # Datasets are all y-axis data. However, there should only be
  710. # one dataset for pie charts.
  711. for dataset in self.data:
  712. yield ('x', dataset)
  713. def scaled_data(self, data_class, x_range=None, y_range=None):
  714. if not x_range:
  715. x_range = [0, sum(self.data[0])]
  716. return Chart.scaled_data(self, data_class, x_range, self.y_range)
  717. class PieChart2D(PieChart):
  718. def type_to_url(self):
  719. return 'cht=p'
  720. class PieChart3D(PieChart):
  721. def type_to_url(self):
  722. return 'cht=p3'
  723. class VennChart(Chart):
  724. def type_to_url(self):
  725. return 'cht=v'
  726. def annotated_data(self):
  727. for dataset in self.data:
  728. yield ('y', dataset)
  729. class RadarChart(Chart):
  730. def type_to_url(self):
  731. return 'cht=r'
  732. class SplineRadarChart(RadarChart):
  733. def type_to_url(self):
  734. return 'cht=rs'
  735. class MapChart(Chart):
  736. def __init__(self, *args, **kwargs):
  737. Chart.__init__(self, *args, **kwargs)
  738. self.geo_area = 'world'
  739. self.codes = []
  740. def type_to_url(self):
  741. return 'cht=t'
  742. def set_codes(self, codes):
  743. self.codes = codes
  744. def get_url_bits(self, data_class=None):
  745. url_bits = Chart.get_url_bits(self, data_class=data_class)
  746. url_bits.append('chtm=%s' % self.geo_area)
  747. if self.codes:
  748. url_bits.append('chld=%s' % ''.join(self.codes))
  749. return url_bits
  750. class GoogleOMeterChart(PieChart):
  751. """Inheriting from PieChart because of similar labeling"""
  752. def __init__(self, *args, **kwargs):
  753. PieChart.__init__(self, *args, **kwargs)
  754. if self.auto_scale and not self.x_range:
  755. warnings.warn('Please specify an x_range with GoogleOMeterChart, '
  756. 'otherwise one arrow will always be at the max.')
  757. def type_to_url(self):
  758. return 'cht=gom'
  759. class QRChart(Chart):
  760. def __init__(self, *args, **kwargs):
  761. Chart.__init__(self, *args, **kwargs)
  762. self.encoding = None
  763. self.ec_level = None
  764. self.margin = None
  765. def type_to_url(self):
  766. return 'cht=qr'
  767. def data_to_url(self, data_class=None):
  768. if not self.data:
  769. raise NoDataGivenException()
  770. return 'chl=%s' % urllib.quote(self.data[0])
  771. def get_url_bits(self, data_class=None):
  772. url_bits = Chart.get_url_bits(self, data_class=data_class)
  773. if self.encoding:
  774. url_bits.append('choe=%s' % self.encoding)
  775. if self.ec_level:
  776. url_bits.append('chld=%s|%s' % (self.ec_level, self.margin))
  777. return url_bits
  778. def set_encoding(self, encoding):
  779. self.encoding = encoding
  780. def set_ec(self, level, margin):
  781. self.ec_level = level
  782. self.margin = margin
  783. class ChartGrammar(object):
  784. def __init__(self):
  785. self.grammar = None
  786. self.chart = None
  787. def parse(self, grammar):
  788. self.grammar = grammar
  789. self.chart = self.create_chart_instance()
  790. for attr in self.grammar:
  791. if attr in ('w', 'h', 'type', 'auto_scale', 'x_range', 'y_range'):
  792. continue # These are already parsed in create_chart_instance
  793. attr_func = 'parse_' + attr
  794. if not hasattr(self, attr_func):
  795. warnings.warn('No parser for grammar attribute "%s"' % (attr))
  796. continue
  797. getattr(self, attr_func)(grammar[attr])
  798. return self.chart
  799. def parse_data(self, data):
  800. self.chart.data = data
  801. @staticmethod
  802. def get_possible_chart_types():
  803. possible_charts = []
  804. for cls_name in globals().keys():
  805. if not cls_name.endswith('Chart'):
  806. continue
  807. cls = globals()[cls_name]
  808. # Check if it is an abstract class
  809. try:
  810. a = cls(1, 1, auto_scale=False)
  811. del a
  812. except AbstractClassException:
  813. continue
  814. # Strip off "Class"
  815. possible_charts.append(cls_name[:-5])
  816. return possible_charts
  817. def create_chart_instance(self, grammar=None):
  818. if not grammar:
  819. grammar = self.grammar
  820. assert(isinstance(grammar, dict)) # grammar must be a dict
  821. assert('w' in grammar) # width is required
  822. assert('h' in grammar) # height is required
  823. assert('type' in grammar) # type is required
  824. chart_type = grammar['type']
  825. w = grammar['w']
  826. h = grammar['h']
  827. auto_scale = grammar.get('auto_scale', None)
  828. x_range = grammar.get('x_range', None)
  829. y_range = grammar.get('y_range', None)
  830. types = ChartGrammar.get_possible_chart_types()
  831. if chart_type not in types:
  832. raise UnknownChartType('%s is an unknown chart type. Possible '
  833. 'chart types are %s' % (chart_type, ','.join(types)))
  834. return globals()[chart_type + 'Chart'](w, h, auto_scale=auto_scale,
  835. x_range=x_range, y_range=y_range)
  836. def download(self):
  837. pass