NVD3Chart.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. """
  4. Python-nvd3 is a Python wrapper for NVD3 graph library.
  5. NVD3 is an attempt to build re-usable charts and chart components
  6. for d3.js without taking away the power that d3.js gives you.
  7. Project location : https://github.com/areski/python-nvd3
  8. """
  9. from __future__ import unicode_literals
  10. from optparse import OptionParser
  11. from jinja2 import Environment, PackageLoader
  12. from slugify import slugify
  13. try:
  14. import simplejson as json
  15. except ImportError:
  16. import json
  17. CONTENT_FILENAME = "./content.html"
  18. PAGE_FILENAME = "./page.html"
  19. pl = PackageLoader('nvd3', 'templates')
  20. jinja2_env = Environment(lstrip_blocks=True, trim_blocks=True, loader=pl)
  21. template_content = jinja2_env.get_template(CONTENT_FILENAME)
  22. template_page = jinja2_env.get_template(PAGE_FILENAME)
  23. def stab(tab=1):
  24. """
  25. create space tabulation
  26. """
  27. return ' ' * 4 * tab
  28. class NVD3Chart(object):
  29. """
  30. NVD3Chart Base class.
  31. """
  32. #: chart count
  33. count = 0
  34. #: directory holding the assets (bower_components)
  35. assets_directory = './bower_components/'
  36. # this attribute is overridden by children of this
  37. # class
  38. CHART_FILENAME = None
  39. template_environment = Environment(lstrip_blocks=True, trim_blocks=True,
  40. loader=pl)
  41. def __init__(self, **kwargs):
  42. """
  43. This is the base class for all the charts. The following keywords are
  44. accepted:
  45. :keyword: **display_container** - default: ``True``
  46. :keyword: **jquery_on_ready** - default: ``False``
  47. :keyword: **charttooltip_dateformat** - default: ``'%d %b %Y'``
  48. :keyword: **name** - default: the class name
  49. ``model`` - set the model (e.g. ``pieChart``, `
  50. ``LineWithFocusChart``, ``MultiBarChart``).
  51. :keyword: **color_category** - default - ``None``
  52. :keyword: **color_list** - default - ``None``
  53. used by pieChart (e.g. ``['red', 'blue', 'orange']``)
  54. :keyword: **margin_bottom** - default - ``20``
  55. :keyword: **margin_left** - default - ``60``
  56. :keyword: **margin_right** - default - ``60``
  57. :keyword: **margin_top** - default - ``30``
  58. :keyword: **height** - default - ``''``
  59. :keyword: **width** - default - ``''``
  60. :keyword: **show_values** - default - ``False``
  61. :keyword: **stacked** - default - ``False``
  62. :keyword: **focus_enable** - default - ``False``
  63. :keyword: **resize** - define - ``False``
  64. :keyword: **no_data_message** - default - ``None`` or nvd3 default
  65. :keyword: **xAxis_rotateLabel** - default - ``0``
  66. :keyword: **xAxis_staggerLabel** - default - ``False``
  67. :keyword: **xAxis_showMaxMin** - default - ``True``
  68. :keyword: **right_align_y_axis** - default - ``False``
  69. :keyword: **show_controls** - default - ``True``
  70. :keyword: **show_legend** - default - ``True``
  71. :keyword: **show_labels** - default - ``True``
  72. :keyword: **tag_script_js** - default - ``True``
  73. :keyword: **use_interactive_guideline** - default - ``False``
  74. :keyword: **chart_attr** - default - ``None``
  75. :keyword: **extras** - default - ``None``
  76. Extra chart modifiers. Use this to modify different attributes of
  77. the chart.
  78. :keyword: **x_axis_date** - default - False
  79. Signal that x axis is a date axis
  80. :keyword: **date_format** - default - ``%x``
  81. see https://github.com/mbostock/d3/wiki/Time-Formatting
  82. :keyword: **y_axis_scale_min** - default - ``''``.
  83. :keyword: **y_axis_scale_max** - default - ``''``.
  84. :keyword: **x_axis_format** - default - ``''``.
  85. :keyword: **y_axis_format** - default - ``''``.
  86. :keyword: **style** - default - ``''``
  87. Style modifiers for the DIV container.
  88. :keyword: **color_category** - default - ``category10``
  89. Acceptable values are nvd3 categories such as
  90. ``category10``, ``category20``, ``category20c``.
  91. """
  92. # set the model
  93. self.model = self.__class__.__name__ #: The chart model,
  94. #: an Instance of Jinja2 template
  95. self.template_page_nvd3 = template_page
  96. self.template_content_nvd3 = template_content
  97. self.series = []
  98. self.axislist = {}
  99. # accepted keywords
  100. self.display_container = kwargs.get('display_container', True)
  101. self.charttooltip_dateformat = kwargs.get('charttooltip_dateformat',
  102. '%d %b %Y')
  103. self._slugify_name(kwargs.get('name', self.model))
  104. self.jquery_on_ready = kwargs.get('jquery_on_ready', False)
  105. self.color_category = kwargs.get('color_category', None)
  106. self.color_list = kwargs.get('color_list', None)
  107. self.margin_bottom = kwargs.get('margin_bottom', 20)
  108. self.margin_left = kwargs.get('margin_left', 60)
  109. self.margin_right = kwargs.get('margin_right', 60)
  110. self.margin_top = kwargs.get('margin_top', 30)
  111. self.height = kwargs.get('height', '')
  112. self.width = kwargs.get('width', '')
  113. self.show_values = kwargs.get('show_values', False)
  114. self.stacked = kwargs.get('stacked', False)
  115. self.focus_enable = kwargs.get('focus_enable', False)
  116. self.resize = kwargs.get('resize', False)
  117. self.no_data_message = kwargs.get('no_data_message', None)
  118. self.xAxis_rotateLabel = kwargs.get('xAxis_rotateLabel', 0)
  119. self.xAxis_staggerLabel = kwargs.get('xAxis_staggerLabel', False)
  120. self.xAxis_showMaxMin = kwargs.get('xAxis_showMaxMin', True)
  121. self.right_align_y_axis = kwargs.get('right_align_y_axis', False)
  122. self.show_controls = kwargs.get('show_controls', True)
  123. self.show_legend = kwargs.get('show_legend', True)
  124. self.show_labels = kwargs.get('show_labels', True)
  125. self.tooltip_separator = kwargs.get('tooltip_separator')
  126. self.tag_script_js = kwargs.get('tag_script_js', True)
  127. self.use_interactive_guideline = kwargs.get("use_interactive_guideline",
  128. False)
  129. self.chart_attr = kwargs.get("chart_attr", {})
  130. self.extras = kwargs.get('extras', None)
  131. self.style = kwargs.get('style', '')
  132. self.date_format = kwargs.get('date_format', '%x')
  133. self.x_axis_date = kwargs.get('x_axis_date', False)
  134. self.y_axis_scale_min = kwargs.get('y_axis_scale_min', '')
  135. self.y_axis_scale_max = kwargs.get('y_axis_scale_max', '')
  136. #: x-axis contain date format or not
  137. # possible duplicate of x_axis_date
  138. self.date_flag = kwargs.get('date_flag', False)
  139. self.x_axis_format = kwargs.get('x_axis_format', '')
  140. # Load remote JS assets or use the local bower assets?
  141. self.remote_js_assets = kwargs.get('remote_js_assets', True)
  142. self.callback = kwargs.get('callback', None)
  143. # None keywords attribute that should be modified by methods
  144. # We should change all these to _attr
  145. self.htmlcontent = '' #: written by buildhtml
  146. self.htmlheader = ''
  147. #: Place holder for the graph (the HTML div)
  148. #: Written by ``buildcontainer``
  149. self.container = u''
  150. #: Header for javascript code
  151. self.containerheader = u''
  152. # CDN http://cdnjs.com/libraries/nvd3/ needs to make sure it's up to
  153. # date
  154. self.header_css = [
  155. '<link href="%s" rel="stylesheet" />' % h for h in
  156. (
  157. 'https://cdnjs.cloudflare.com/ajax/libs/nvd3/1.8.6/nv.d3.min.css' if self.remote_js_assets else self.assets_directory + 'nvd3/src/nv.d3.css',
  158. )
  159. ]
  160. self.header_js = [
  161. '<script src="%s"></script>' % h for h in
  162. (
  163. 'https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js' if self.remote_js_assets else self.assets_directory + 'd3/d3.min.js',
  164. 'https://cdnjs.cloudflare.com/ajax/libs/nvd3/1.8.6/nv.d3.min.js' if self.remote_js_assets else self.assets_directory + 'nvd3/nv.d3.min.js'
  165. )
  166. ]
  167. #: Javascript code as string
  168. self.jschart = None
  169. self.custom_tooltip_flag = False
  170. self.charttooltip = ''
  171. self.serie_no = 1
  172. def _slugify_name(self, name):
  173. """Slufigy name with underscore"""
  174. self.name = slugify(name, separator='_')
  175. def add_serie(self, y, x, name=None, extra=None, **kwargs):
  176. """
  177. add serie - Series are list of data that will be plotted
  178. y {1, 2, 3, 4, 5} / x {1, 2, 3, 4, 5}
  179. **Attributes**:
  180. * ``name`` - set Serie name
  181. * ``x`` - x-axis data
  182. * ``y`` - y-axis data
  183. kwargs:
  184. * ``shape`` - for scatterChart, you can set different shapes
  185. (circle, triangle etc...)
  186. * ``size`` - for scatterChart, you can set size of different shapes
  187. * ``type`` - for multiChart, type should be bar
  188. * ``bar`` - to display bars in Chart
  189. * ``color_list`` - define list of colors which will be
  190. used by pieChart
  191. * ``color`` - set axis color
  192. * ``disabled`` -
  193. extra:
  194. * ``tooltip`` - set tooltip flag
  195. * ``date_format`` - set date_format for tooltip if x-axis is in
  196. date format
  197. """
  198. if not name:
  199. name = "Serie %d" % (self.serie_no)
  200. # For scatterChart shape & size fields are added in serie
  201. if 'shape' in kwargs or 'size' in kwargs:
  202. csize = kwargs.get('size', 1)
  203. cshape = kwargs.get('shape', 'circle')
  204. serie = [{
  205. 'x': x[i],
  206. 'y': j,
  207. 'shape': cshape,
  208. 'size': csize[i] if isinstance(csize, list) else csize
  209. } for i, j in enumerate(y)]
  210. else:
  211. if self.model == 'pieChart':
  212. serie = [{'label': x[i], 'value': y} for i, y in enumerate(y)]
  213. else:
  214. serie = [{'x': x[i], 'y': y} for i, y in enumerate(y)]
  215. data_keyvalue = {'values': serie, 'key': name}
  216. # multiChart
  217. # Histogram type='bar' for the series
  218. if 'type' in kwargs and kwargs['type']:
  219. data_keyvalue['type'] = kwargs['type']
  220. # Define on which Y axis the serie is related
  221. # a chart can have 2 Y axis, left and right, by default only one Y Axis is used
  222. if 'yaxis' in kwargs and kwargs['yaxis']:
  223. data_keyvalue['yAxis'] = kwargs['yaxis']
  224. else:
  225. if self.model != 'pieChart':
  226. data_keyvalue['yAxis'] = '1'
  227. if 'bar' in kwargs and kwargs['bar']:
  228. data_keyvalue['bar'] = 'true'
  229. if 'disabled' in kwargs and kwargs['disabled']:
  230. data_keyvalue['disabled'] = 'true'
  231. if 'color' in kwargs and kwargs['color']:
  232. data_keyvalue['color'] = kwargs['color']
  233. if extra:
  234. if self.model == 'pieChart':
  235. if 'color_list' in extra and extra['color_list']:
  236. self.color_list = extra['color_list']
  237. if extra.get('date_format'):
  238. self.charttooltip_dateformat = extra['date_format']
  239. if extra.get('tooltip'):
  240. self.custom_tooltip_flag = True
  241. if self.model != 'pieChart':
  242. _start = extra['tooltip']['y_start']
  243. _end = extra['tooltip']['y_end']
  244. _start = ("'" + str(_start) + "' + ") if _start else ''
  245. _end = (" + '" + str(_end) + "'") if _end else ''
  246. if self.model == 'pieChart':
  247. _start = extra['tooltip']['y_start']
  248. _end = extra['tooltip']['y_end']
  249. _start = ("'" + str(_start) + "' + ") if _start else ''
  250. _end = (" + '" + str(_end) + "'") if _end else ''
  251. # Increment series counter & append
  252. self.serie_no += 1
  253. self.series.append(data_keyvalue)
  254. def add_chart_extras(self, extras):
  255. """
  256. Use this method to add extra d3 properties to your chart.
  257. For example, you want to change the text color of the graph::
  258. chart = pieChart(name='pieChart', color_category='category20c', height=400, width=400)
  259. xdata = ["Orange", "Banana", "Pear", "Kiwi", "Apple", "Strawberry", "Pineapple"]
  260. ydata = [3, 4, 0, 1, 5, 7, 3]
  261. extra_serie = {"tooltip": {"y_start": "", "y_end": " cal"}}
  262. chart.add_serie(y=ydata, x=xdata, extra=extra_serie)
  263. The above code will create graph with a black text, the following will change it::
  264. text_white="d3.selectAll('#pieChart text').style('fill', 'white');"
  265. chart.add_chart_extras(text_white)
  266. The above extras will be appended to the java script generated.
  267. Alternatively, you can use the following initialization::
  268. chart = pieChart(name='pieChart',
  269. color_category='category20c',
  270. height=400, width=400,
  271. extras=text_white)
  272. """
  273. self.extras = extras
  274. def set_graph_height(self, height):
  275. """Set Graph height"""
  276. self.height = str(height)
  277. def set_graph_width(self, width):
  278. """Set Graph width"""
  279. self.width = str(width)
  280. def set_containerheader(self, containerheader):
  281. """Set containerheader"""
  282. self.containerheader = containerheader
  283. def set_date_flag(self, date_flag=False):
  284. """Set date flag"""
  285. self.date_flag = date_flag
  286. def set_custom_tooltip_flag(self, custom_tooltip_flag):
  287. """Set custom_tooltip_flag & date_flag"""
  288. self.custom_tooltip_flag = custom_tooltip_flag
  289. def __str__(self):
  290. """return htmlcontent"""
  291. self.buildhtml()
  292. return self.htmlcontent
  293. def buildcontent(self):
  294. """Build HTML content only, no header or body tags. To be useful this
  295. will usually require the attribute `jquery_on_ready` to be set which
  296. will wrap the js in $(function(){<regular_js>};)
  297. """
  298. self.buildcontainer()
  299. # if the subclass has a method buildjs this method will be
  300. # called instead of the method defined here
  301. # when this subclass method is entered it does call
  302. # the method buildjschart defined here
  303. self.buildjschart()
  304. self.htmlcontent = self.template_content_nvd3.render(chart=self)
  305. def buildhtml(self):
  306. """Build the HTML page
  307. Create the htmlheader with css / js
  308. Create html page
  309. Add Js code for nvd3
  310. """
  311. self.buildcontent()
  312. self.content = self.htmlcontent
  313. self.htmlcontent = self.template_page_nvd3.render(chart=self)
  314. # this is used by django-nvd3
  315. def buildhtmlheader(self):
  316. """generate HTML header content"""
  317. self.htmlheader = ''
  318. # If the JavaScript assets have already been injected, don't bother re-sourcing them.
  319. global _js_initialized
  320. if '_js_initialized' not in globals() or not _js_initialized:
  321. for css in self.header_css:
  322. self.htmlheader += css
  323. for js in self.header_js:
  324. self.htmlheader += js
  325. def buildcontainer(self):
  326. """generate HTML div"""
  327. if self.container:
  328. return
  329. # Create SVG div with style
  330. if self.width:
  331. if self.width[-1] != '%':
  332. self.style += 'width:%spx;' % self.width
  333. else:
  334. self.style += 'width:%s;' % self.width
  335. if self.height:
  336. if self.height[-1] != '%':
  337. self.style += 'height:%spx;' % self.height
  338. else:
  339. self.style += 'height:%s;' % self.height
  340. if self.style:
  341. self.style = 'style="%s"' % self.style
  342. self.container = self.containerheader + \
  343. '<div id="%s"><svg %s></svg></div>\n' % (self.name, self.style)
  344. def buildjschart(self):
  345. """generate javascript code for the chart"""
  346. self.jschart = ''
  347. # Include data
  348. self.series_js = json.dumps(self.series)
  349. def create_x_axis(self, name, label=None, format=None, date=False, custom_format=False):
  350. """Create X-axis"""
  351. axis = {}
  352. if custom_format and format:
  353. axis['tickFormat'] = format
  354. elif format:
  355. if format == 'AM_PM':
  356. axis['tickFormat'] = "function(d) { return get_am_pm(parseInt(d)); }"
  357. else:
  358. axis['tickFormat'] = "d3.format(',%s')" % format
  359. if label:
  360. axis['axisLabel'] = "'" + label + "'"
  361. # date format : see https://github.com/mbostock/d3/wiki/Time-Formatting
  362. if date:
  363. self.dateformat = format
  364. axis['tickFormat'] = ("function(d) { return d3.time.format('%s')"
  365. "(new Date(parseInt(d))) }\n"
  366. "" % self.dateformat)
  367. # flag is the x Axis is a date
  368. if name[0] == 'x':
  369. self.x_axis_date = True
  370. # Add new axis to list of axis
  371. self.axislist[name] = axis
  372. # Create x2Axis if focus_enable
  373. if name == "xAxis" and self.focus_enable:
  374. self.axislist['x2Axis'] = axis
  375. def create_y_axis(self, name, label=None, format=None, custom_format=False):
  376. """
  377. Create Y-axis
  378. """
  379. axis = {}
  380. if custom_format and format:
  381. axis['tickFormat'] = format
  382. elif format:
  383. axis['tickFormat'] = "d3.format(',%s')" % format
  384. if label:
  385. axis['axisLabel'] = "'" + label + "'"
  386. # Add new axis to list of axis
  387. self.axislist[name] = axis
  388. class TemplateMixin(object):
  389. """
  390. A mixin that override buildcontent. Instead of building the complex
  391. content template we exploit Jinja2 inheritance. Thus each chart class
  392. renders it's own chart template which inherits from content.html
  393. """
  394. def buildcontent(self):
  395. """Build HTML content only, no header or body tags. To be useful this
  396. will usually require the attribute `jquery_on_ready` to be set which
  397. will wrap the js in $(function(){<regular_js>};)
  398. """
  399. self.buildcontainer()
  400. # if the subclass has a method buildjs this method will be
  401. # called instead of the method defined here
  402. # when this subclass method is entered it does call
  403. # the method buildjschart defined here
  404. self.buildjschart()
  405. self.htmlcontent = self.template_chart_nvd3.render(chart=self)
  406. def _main():
  407. """
  408. Parse options and process commands
  409. """
  410. # Parse arguments
  411. usage = "usage: nvd3.py [options]"
  412. parser = OptionParser(usage=usage,
  413. version=("python-nvd3 - Charts generator with "
  414. "nvd3.js and d3.js"))
  415. parser.add_option("-q", "--quiet",
  416. action="store_false", dest="verbose", default=True,
  417. help="don't print messages to stdout")
  418. (options, args) = parser.parse_args()
  419. if __name__ == '__main__':
  420. _main()