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