Coverage for gramex\pptgen\commands.py : 89%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1'''Python-PPTX customized module.'''
2import os
3import six
4import copy
5import logging
6import requests
7import tempfile
8import operator
9import collections
10import numpy as np
11import pandas as pd
12import matplotlib.cm
13import matplotlib.colors
14from lxml import etree
15from tornado.template import Template
16from tornado.escape import to_unicode
17from pptx.chart import data as pptxcd
18from pptx.dml.color import RGBColor
19from pptx.enum.shapes import MSO_SHAPE
20from pptx.oxml.xmlchemy import OxmlElement
21from six.moves.urllib_parse import urlparse
22from gramex.transforms import build_transform
23from . import utils
24from . import fontwidth
25from . import color as _color
28_template_cache = {}
31def template(tmpl, data):
32 '''Execute tornado template'''
33 if tmpl not in _template_cache:
34 _template_cache[tmpl] = Template(tmpl, autoescape=None)
35 return to_unicode(_template_cache[tmpl].generate(**data))
38def text(shape, spec, data):
39 '''Replace entire text of shape with spec['text']'''
40 if not shape.has_text_frame: 40 ↛ 41line 40 didn't jump to line 41, because the condition on line 40 was never true
41 logging.error('"%s" is not a TextShape to apply text:', shape.name)
42 return
43 if not isinstance(data, (dict,)): 43 ↛ 44line 43 didn't jump to line 44, because the condition on line 43 was never true
44 data = {'data': data}
45 handler = data.pop('handler', None)
47 style = copy.deepcopy(spec.get('style', {}))
48 style = generate_style(style, data, handler)
49 pixel_inch = 10000
50 # Get paragraph
51 paragraph = shape.text_frame.paragraphs[0]
52 # Removing the extra paragraphs if more than one present
53 for para in shape.text_frame.paragraphs[1:]: 53 ↛ 54line 53 didn't jump to line 54, because the loop on line 53 never started
54 utils.delete_paragraph(para)
55 # Removing the extra run in paragraph if more than one present
56 for _run in paragraph.runs[1:]: 56 ↛ 57line 56 didn't jump to line 57, because the loop on line 56 never started
57 utils.delete_run(_run)
59 theme, brightness, default_css = None, None, {}
60 # Calculatiing default style
61 clrtype = paragraph.runs[0].font.color.type
62 if clrtype and 'rgb' in '{}'.format(clrtype).lower().split(): 62 ↛ 63line 62 didn't jump to line 63, because the condition on line 62 was never true
63 default_css['color'] = '{}'.format(paragraph.runs[0].font.color.rgb)
64 elif clrtype and 'scheme' in '{}'.format(clrtype).lower().split(): 64 ↛ 65line 64 didn't jump to line 65, because the condition on line 64 was never true
65 theme = paragraph.runs[0].font.color.theme_color
66 brightness = paragraph.runs[0].font.color.brightness
67 for prop in {'bold', 'italic', 'underline'}:
68 default_css[prop] = getattr(paragraph.runs[0].font, prop)
69 if paragraph.runs[0].font.size: 69 ↛ 70line 69 didn't jump to line 70, because the condition on line 69 was never true
70 default_css['font-size'] = paragraph.runs[0].font.size / pixel_inch
71 default_css['text-align'] = paragraph.alignment
72 default_css['font-family'] = paragraph.runs[0].font.name
73 # Updating default css with css from config.
74 default_css.update(style)
75 default_css['color'] = default_css.get('color', '#0000000')
76 update_text = etree.fromstring('<root>{}</root>'.format(template(spec['text'], data)))
77 paragraph.runs[0].text = update_text.text if update_text.text else ''
78 utils.apply_text_css(shape, paragraph.runs[0], paragraph, **default_css)
79 index = 1
80 for child in update_text.getchildren():
81 if not child.tag == 'text': 81 ↛ 82line 81 didn't jump to line 82, because the condition on line 81 was never true
82 raise ValueError('XML elemet must contain only "text" tag.')
83 # Adding runs for each text item
84 for idx, new_txt in enumerate([child.text, child.tail]):
85 if not new_txt:
86 continue
87 common_css = {key: val for key, val in default_css.items()}
88 paragraph.add_run()
89 # Adding text to run
90 paragraph.runs[index].text = new_txt
91 # Updating the text css
92 common_css.update(dict(child.items())) if not idx else None
93 if theme: 93 ↛ 94line 93 didn't jump to line 94, because the condition on line 93 was never true
94 paragraph.runs[index].font.color.theme_color = theme
95 if brightness: 95 ↛ 96line 95 didn't jump to line 96, because the condition on line 95 was never true
96 paragraph.runs[index].font.color.brightness = brightness
97 utils.apply_text_css(shape, paragraph.runs[index], paragraph, **common_css)
98 index += 1
101def replace(shape, spec, data):
102 '''Replace keywords in shape using the dictionary at spec['replace']'''
103 if not shape.has_text_frame: 103 ↛ 104line 103 didn't jump to line 104, because the condition on line 103 was never true
104 logging.error('"%s" is not a TextShape to apply text:', shape.name)
105 return
106 if not isinstance(data, (dict,)): 106 ↛ 107line 106 didn't jump to line 107, because the condition on line 106 was never true
107 data = {'data': data}
109 handler = spec.get('handler', None)
110 common_css = spec.get('style', {})
111 style = {}
112 for old, new in spec['replace'].items():
113 _style = common_css.pop(old, {})
114 style[old] = generate_style(_style, {'val': template(new, data), 'data': data}, handler)
116 common_css = generate_style(common_css, data, handler)
117 for key, val in style.items():
118 css = copy.deepcopy(common_css)
119 css.update(val)
120 style[key] = css
122 for paragraph in shape.text_frame.paragraphs:
123 for run in paragraph.runs:
124 for old, new in spec['replace'].items():
125 run.text = run.text.replace(old, template(new, data))
126 utils.apply_text_css(shape, run, paragraph, **style)
129def image(shape, spec, data):
130 '''Replace image with a different file specified in spec['image']'''
131 image = template(spec['image'], data)
132 # If it's a URL, use the requests library's raw stream as a file-like object
133 if urlparse(image).netloc:
134 r = requests.get(image)
135 with tempfile.NamedTemporaryFile(delete=False) as handle:
136 handle.write(r.content)
137 new_img_part, new_rid = shape.part.get_or_add_image_part(handle.name)
138 os.unlink(handle.name)
139 else:
140 new_img_part, new_rid = shape.part.get_or_add_image_part(image)
141 # old_rid = shape._pic.blip_rId
142 shape._pic.blipFill.blip.rEmbed = new_rid
143 shape.part.related_parts[new_rid].blob = new_img_part.blob
146def generate_style(style, data, handler):
147 '''Function to conpile style section from kwargs.'''
148 for key, value in style.items():
149 if isinstance(value, (dict,)) and 'function' in value: 149 ↛ 150line 149 didn't jump to line 150, because the condition on line 149 was never true
150 result = compile_function(style, key, data, handler)
151 style[key] = result(data) if callable(result) else result
152 return style
155def rect_css(shape, **kwargs):
156 '''Function to add text to shape.'''
157 for key in {'fill', 'stroke'}:
158 if kwargs.get(key): 158 ↛ 157line 158 didn't jump to line 157, because the condition on line 158 was never false
159 fill = shape.fill if key == 'fill' else shape.line.fill
160 rectcss = kwargs[key].rsplit('#')[-1].lower()
161 rectcss = rectcss + ('0' * (6 - len(rectcss)))
162 chart_css(fill, kwargs, rectcss)
165def add_text_to_shape(shape, textval, **kwargs):
166 '''Function to add text to shape.'''
167 min_inc = 13000
168 pixel_inch = 10000
169 # kwargs['font-size'] = max(kwargs.get('font-size', 16), min_inc)
170 if (kwargs.get('font-size', 14) * pixel_inch) < min_inc: 170 ↛ 171line 170 didn't jump to line 171, because the condition on line 170 was never true
171 return
172 paragraph = shape.text_frame.paragraphs[0]
173 paragraph.add_run()
174 for run in paragraph.runs:
175 run.text = textval
176 shape_txt = run.font.fill
177 shape_txt.solid()
178 utils.apply_text_css(shape, run, paragraph, **kwargs)
181def scale_data(data, lo, hi, factor=None):
182 '''Function to scale data.'''
183 data = np.array(data, dtype=float)
184 return ((data - lo) / ((hi - lo) or np.nan)) * (factor or 1.)
187def rect(shape, x, y, width, height):
188 '''Add rectangle to slide.'''
189 return shape.add_shape(MSO_SHAPE.RECTANGLE, x, y, width, height)
192def _update_chart(info, data, chart_data, series_columns, chart='ChartData'):
193 '''Updating Chart data.'''
194 if chart == 'ChartData':
195 chart_data.categories = data[info['x']].dropna().unique().tolist()
196 for series in series_columns:
197 if np.issubdtype(data[series].dtype, np.number): 197 ↛ 196line 197 didn't jump to line 196, because the condition on line 197 was never false
198 chart_data.add_series(series, tuple(data[series].fillna(0).values.tolist()))
199 return chart_data
200 series_dict = {}
201 columns = data.columns.difference([info['x']])
202 is_numeric_x = np.issubdtype(data[info['x']].dtype, np.number)
203 xindex = data[info['x']].astype(float) if is_numeric_x else pd.Series(data.index + 1)
204 for index, row in data.fillna(0).iterrows():
205 x = xindex.loc[index]
206 for col in series_columns:
207 if col not in columns or not np.issubdtype(data[col].dtype, np.number): 207 ↛ 208line 207 didn't jump to line 208, because the condition on line 207 was never true
208 continue
209 serieslist = [series.name for series in chart_data._series]
210 if col not in serieslist:
211 series_dict[col] = chart_data.add_series(col)
212 if chart == 'XyChartData':
213 series_dict[col].add_data_point(x, row[col])
214 elif chart == 'BubbleChartData': 214 ↛ 206line 214 didn't jump to line 206, because the condition on line 214 was never false
215 bubble_size = row[info['size']] if info.get('size') else 1
216 if col != info.get('size'): 216 ↛ 206line 216 didn't jump to line 206, because the condition on line 216 was never false
217 series_dict[col].add_data_point(x, row[col], bubble_size)
218 return chart_data
221def chart_css(fill, style, color):
222 '''Function to add opacity to charts.'''
223 fill.solid()
224 pix_to_inch = 100000
225 fill.fore_color.rgb = RGBColor.from_string(utils.convert_color_code(color))
226 solid_fill = fill.fore_color._xFill
227 alpha = OxmlElement('a:alpha')
228 alpha.set('val', '%d' % (pix_to_inch * style.get('opacity', 1.0)))
229 solid_fill.srgbClr.append(alpha)
230 return fill
233def compile_function(spec, key, data, handler):
234 '''A function to compile configuration.'''
235 if key not in spec:
236 return None
237 _vars = {'_color': None, 'data': None, 'handler': None}
238 if not isinstance(spec[key], (dict,)):
239 spec[key] = {'function': '{}'.format(spec[key])}
240 elif isinstance(spec[key], (dict,)) and 'function' not in spec[key]: 240 ↛ 241line 240 didn't jump to line 241, because the condition on line 240 was never true
241 spec[key] = {'function': '{}'.format(spec[key])}
242 args = {'data': data, 'handler': handler, '_color': _color}
243 return build_transform(spec[key], vars=_vars)(**args)[0]
246def table(shape, spec, data):
247 '''Update an existing Table shape with data.'''
248 if not shape.has_table:
249 raise AttributeError('Shape must be a table object.')
250 if not spec.get('table', {}).get('data'): 250 ↛ 251line 250 didn't jump to line 251, because the condition on line 250 was never true
251 return
252 spec = copy.deepcopy(spec['table'])
253 handler = data.pop('handler') if 'handler' in data else None
254 data = compile_function(spec, 'data', data, handler)
255 if not len(data): 255 ↛ 256line 255 didn't jump to line 256, because the condition on line 255 was never true
256 return
257 data_cols = data.columns
258 data_cols_len = len(data_cols)
259 table_properties = utils.TableProperties()
260 # Extending table if required.
261 table_properties.extend_table(shape, data, len(data) + 1, data_cols_len)
262 # Fetching Table Style for All Cells and texts.
263 tbl_style = table_properties.get_default_css(shape)
264 input_colspec = spec.get('columns', {})
265 input_cols = input_colspec.keys()
266 if all(isinstance(x, int) for x in input_cols): 266 ↛ 270line 266 didn't jump to line 270, because the condition on line 266 was never false
267 for x in list(input_cols): 267 ↛ 268line 267 didn't jump to line 268, because the loop on line 267 never started
268 if x < data_cols_len:
269 input_colspec[data_cols[x]] = input_colspec.pop(x)
270 styled_cols = data.columns.intersection(input_cols or data_cols)
271 cell_style = table_properties.get_css(spec, styled_cols, data)
272 data = data.to_dict(orient='records')
273 for row_num, row in enumerate(shape.table.rows):
274 cols = len(row.cells._tr.tc_lst)
275 # Extending cells in newly added rows.
276 while cols < len(data_cols):
277 row.cells._tr.add_tc()
278 cols += 1
279 for col_num, cell in enumerate(row.cells):
280 colname = data_cols[col_num]
281 for paragraph in cell.text_frame.paragraphs:
282 if not paragraph.text.strip():
283 paragraph.add_run()
284 for run in paragraph.runs:
285 txt = colname if row_num == 0 else data[row_num - 1][colname]
286 run.text = '{}'.format(txt)
287 cellcss = {} if row_num == 0 else copy.deepcopy(cell_style.get(colname, {}))
288 txt_css = copy.deepcopy(tbl_style.get('header' if row_num == 0 else 'row', {}))
289 if row_num > 0 and 'gradient' in cellcss: 289 ↛ 290line 289 didn't jump to line 290, because the condition on line 289 was never true
290 grad_txt = scale_data(txt, cellcss['min'], cellcss['max'])
291 gradient = matplotlib.cm.get_cmap(cellcss['gradient'])
292 cellcss['fill'] = matplotlib.colors.to_hex(gradient(grad_txt))
293 if cellcss.get('fill'):
294 txt_css['color'] = cellcss.get('color', _color.contrast(cellcss['fill']))
295 cellcss['color'] = cellcss.get('color', _color.contrast(cellcss['fill']))
296 txt_css.update(cellcss)
297 table_properties.apply_table_css(cell, paragraph, run, txt_css)
300def chart(shape, spec, data):
301 '''Replacing chart Data.'''
302 if not shape.has_chart:
303 raise AttributeError('Shape must be a chart object.')
305 chart_type = None
306 if hasattr(shape.chart, 'chart_type'): 306 ↛ 309line 306 didn't jump to line 309
307 chart_type = '{}'.format(shape.chart.chart_type).split()[0]
309 chart_types = {
310 'ChartData': {
311 'AREA', 'AREA_STACKED', 'AREA_STACKED_100', 'BAR_CLUSTERED',
312 'BAR_OF_PIE', 'BAR_STACKED', 'BAR_STACKED_100', 'COLUMN_CLUSTERED',
313 'COLUMN_STACKED', 'COLUMN_STACKED_100', 'LINE',
314 'LINE_MARKERS', 'LINE_MARKERS_STACKED', 'LINE_MARKERS_STACKED_100',
315 'LINE_STACKED', 'LINE_STACKED_100', 'RADAR_MARKERS',
316 'RADAR', 'RADAR_FILLED', 'PIE', 'PIE_EXPLODED', 'PIE_OF_PIE',
317 'DOUGHNUT', 'DOUGHNUT_EXPLODED'},
318 'XyChartData': {
319 'XY_SCATTER', 'XY_SCATTER_LINES', 'XY_SCATTER_LINES_NO_MARKERS',
320 'XY_SCATTER_SMOOTH', 'XY_SCATTER_SMOOTH_NO_MARKERS'},
321 'BubbleChartData': {'BUBBLE', 'BUBBLE_THREE_D_EFFECT'}
322 }
324 if not chart_type: 324 ↛ 325line 324 didn't jump to line 325, because the condition on line 324 was never true
325 raise NotImplementedError()
327 info = copy.deepcopy(spec['chart'])
328 # Load data
329 handler = data.pop('handler') if 'handler' in data else None
330 for prop in {'x', 'size', 'usecols'}:
331 if prop in info and isinstance(info[prop], (dict,)): 331 ↛ 332line 331 didn't jump to line 332, because the condition on line 331 was never true
332 if 'function' not in info[prop]:
333 info[prop]['function'] = '{}'.format(info[prop])
334 info[prop] = compile_function(info, prop, data, handler)
336 style = {'color': info.pop('color', None), 'opacity': info.pop('opacity', None),
337 'stroke': info.pop('stroke', None)}
339 for key in {'color', 'stroke', 'opacity'}:
340 if key in style and isinstance(style[key], (dict,)):
341 if 'function' not in style[key]: 341 ↛ 343line 341 didn't jump to line 343, because the condition on line 341 was never false
342 style[key]['function'] = '{}'.format(style[key])
343 style[key] = compile_function(style, key, data, handler)
345 data = compile_function(info, 'data', data, handler)
346 # Getting subset of data if `usecols` is defined.
347 change_data = data.reset_index(drop=True)[info.get('usecols', data.columns)]
348 series_cols = change_data.columns.drop(info['x']) if info['x'] else change_data.columns
349 chart_name = next((k for k in chart_types if chart_type in chart_types[k]), None) 349 ↛ exitline 349 didn't finish the generator expression on line 349
351 if not chart_name: 351 ↛ 352line 351 didn't jump to line 352, because the condition on line 351 was never true
352 raise NotImplementedError('Input Chart Type {} is not supported'.format(chart_type))
354 chart_data = _update_chart(
355 info, change_data, getattr(pptxcd, chart_name)(), series_cols, chart=chart_name)
357 shape.chart.replace_data(chart_data)
358 if chart_name == 'scatter' and not style.get('color'): 358 ↛ 359line 358 didn't jump to line 359, because the condition on line 358 was never true
359 series_names = [series.name for series in shape.chart.series]
360 style['color'] = dict(zip(series_names, _color.distinct(len(series_names))))
362 if style.get('color'): 362 ↛ exitline 362 didn't return from function 'chart', because the condition on line 362 was never false
363 color_mapping = {'XyChartData': 'point.marker.format', 'ChartData': 'point.format',
364 'BubbleChartData': 'point.format', 'area': 'series.format'}
365 is_area = {'AREA', 'AREA_STACKED', 'AREA_STACKED_100'}
366 chart_name = 'area' if chart_type in is_area else chart_name
367 is_donut = {'PIE', 'PIE_EXPLODED', 'PIE_OF_PIE', 'DOUGHNUT', 'DOUGHNUT_EXPLODED'}
368 for series in shape.chart.series:
369 for index, point in enumerate(series.points):
370 row = data.loc[index]
371 args = {
372 'handler': handler,
373 'row': row.to_dict(),
374 'name': row[info['x']] if chart_type in is_donut else series.name
375 }
376 point_css = {}
377 for key in {'opacity', 'color', 'stroke'}:
378 if style.get(key) is None:
379 continue
380 point_css[key] = style[key]
381 if callable(style[key]): 381 ↛ 382line 381 didn't jump to line 382, because the condition on line 381 was never true
382 prop = style[key](index)
383 point_css[key] = prop(**args) if callable(prop) else prop
384 elif isinstance(style[key], (dict)):
385 point_css[key] = style[key].get(args['name'], '#cccccc')
386 # Evaluatinig line and shape color format
387 for series_point in {'point', 'series'}:
388 # Replacing point with series to change color in legend
389 fillpoint = color_mapping[chart_name].replace('point', series_point)
390 chart_css(eval(fillpoint).fill, point_css, point_css['color']) # nosec
391 # Will apply on outer line of chart shape line(like stroke in html)
392 _stroke = point_css.get('stroke', point_css['color'])
393 chart_css(eval(fillpoint).line.fill, point_css, _stroke) # nosec
396# Custom Charts Functions below(Sankey, Treemap, Calendarmap).
399def sankey(shape, spec, data):
400 '''Draw sankey in PPT.'''
401 # Shape must be a rectangle.
402 if shape.auto_shape_type != MSO_SHAPE.RECTANGLE:
403 raise NotImplementedError()
404 # Getting parent shapes
405 pxl_to_inch = 10000
406 default_thickness = 40
407 spec = copy.deepcopy(spec['sankey'])
408 handler = data.pop('handler') if 'handler' in data else None
409 y0 = shape.top
410 x0 = shape.left
411 width = shape.width
412 height = shape.height
413 shapes = shape._parent
414 shape_ids = {'shape': 0}
416 groups = compile_function(spec, 'groups', data, handler)
417 thickness = spec.get('thickness', default_thickness) * pxl_to_inch
418 h = (height - (thickness * len(groups))) / (len(groups) - 1) + thickness
419 frames = {}
420 # Sankey Rectangles and texts.
421 sankey_conf = {}
422 for k in ['size', 'order', 'text', 'color']:
423 sankey_conf[k] = compile_function(spec, k, data, handler)
424 sankey_conf['x0'] = x0
425 sankey_conf['width'] = width
426 sankey_conf['attrs'] = spec.get('attrs', {})
427 sankey_conf['sort'] = spec.get('sort')
428 stroke = spec.get('stroke', '#ffffff')
429 # Delete rectangle after geting width, height, x-position and y-position
430 shape._sp.delete()
431 elem_schema = utils.make_element()
432 data = compile_function(spec, 'data', data, handler)
433 for ibar, group in enumerate(groups):
434 y = y0 + h * ibar
435 sankey_conf['group'] = [group]
436 df = frames[group] = utils.draw_sankey(data, sankey_conf)
437 # Adding rectangle
438 for key, row in df.iterrows():
439 shp = shapes.add_shape(
440 MSO_SHAPE.RECTANGLE, row['x'], y, row['width'], thickness)
441 rectstyle = {'fill': row['fill'], 'stroke': stroke}
442 rect_css(shp, **rectstyle)
443 text_style = {'color': _color.contrast(row['fill'])}
444 text_style.update(spec.get('style', {}))
445 add_text_to_shape(shp, row['text'], **text_style)
447 # Sankey Connection Arcs.
448 for ibar, (group1, group2) in enumerate(zip(groups[:-1], groups[1:])):
449 sankey_conf['group'] = [group1, group2]
450 sankey_conf['sort'] = False
451 df = utils.draw_sankey(data, sankey_conf)
452 pos = collections.defaultdict(float)
453 for key1, row1 in frames[group1].iterrows():
454 for key2, row2 in frames[group2].iterrows():
455 if (key1, key2) in df.index:
456 row = df.loc[(key1, key2)]
457 y1, y2 = y0 + h * ibar + thickness, y0 + h * (ibar + 1)
458 ym = (y1 + y2) / 2
459 x1 = row1['x'] + pos[0, key1]
460 x2 = row2['x'] + pos[1, key2]
462 _id = shape_ids['shape'] = shape_ids['shape'] + 1
463 shp = utils.cust_shape(
464 0, 0, '{:.0f}'.format(row['width']), '{:.0f}'.format(ym), _id)
465 path = elem_schema['a'].path(
466 w='{:.0f}'.format(row['width']), h='{:.0f}'.format(ym))
467 shp.find('.//a:custGeom', namespaces=elem_schema['nsmap']).append(
468 elem_schema['a'].pathLst(path))
469 path.append(
470 elem_schema['a'].moveTo(elem_schema['a'].pt(
471 x='{:.0f}'.format(x1 + row['width']), y='{:.0f}'.format(y1))))
473 path.append(elem_schema['a'].cubicBezTo(
474 elem_schema['a'].pt(x='{:.0f}'.format(x1 + row['width']),
475 y='{:.0f}'.format(ym)),
476 elem_schema['a'].pt(x='{:.0f}'.format(x2 + row['width']),
477 y='{:.0f}'.format(ym)),
478 elem_schema['a'].pt(x='{:.0f}'.format(x2 + row['width']),
479 y='{:.0f}'.format(y2))))
481 path.append(elem_schema['a'].lnTo(
482 elem_schema['a'].pt(x='{:.0f}'.format(x2), y='{:.0f}'.format(y2))))
484 path.append(elem_schema['a'].cubicBezTo(
485 elem_schema['a'].pt(x='{:.0f}'.format(x2), y='{:.0f}'.format(ym)),
486 elem_schema['a'].pt(x='{:.0f}'.format(x1), y='{:.0f}'.format(ym)),
487 elem_schema['a'].pt(x='{:.0f}'.format(x1), y='{:.0f}'.format(y1))))
489 path.append(elem_schema['a'].close())
490 shp.spPr.append(elem_schema['a'].solidFill(
491 utils.fill_color(srgbclr=row['fill'])))
492 shapes._spTree.append(shp)
493 pos[0, key1] += row['width']
494 pos[1, key2] += row['width']
497def treemap(shape, spec, data):
498 '''Function to download data as ppt.'''
499 # Shape must be a rectangle.
500 if shape.auto_shape_type != MSO_SHAPE.RECTANGLE:
501 raise NotImplementedError()
502 shapes = shape._parent
503 x0 = shape.left
504 y0 = shape.top
505 width = shape.width
506 height = shape.height
507 spec = copy.deepcopy(spec['treemap'])
508 stroke = spec.get('stroke', '#ffffff')
509 # Load data
510 handler = data.pop('handler') if 'handler' in data else None
511 for k in ['keys', 'values', 'size', 'sort', 'color', 'text', 'data']:
512 spec[k] = compile_function(spec, k, data, handler)
513 # Getting rectangle's width and height using `squarified` algorithm.
514 treemap_data = utils.SubTreemap(**spec)
515 # Delete rectangle after geting width, height, x-position and y-position
516 shape._sp.delete()
517 font_aspect = 14.5
518 pixel_inch = 10000
519 default_rect_color = '#cccccc'
520 for x, y, w, h, (level, v) in treemap_data.draw(width, height):
521 if level == 0: 521 ↛ 520line 521 didn't jump to line 520, because the condition on line 521 was never false
522 shp = shapes.add_shape(
523 MSO_SHAPE.RECTANGLE, x + x0, y + y0, w, h)
524 rect_color = default_rect_color
525 if spec.get('color'): 525 ↛ 527line 525 didn't jump to line 527, because the condition on line 525 was never false
526 rect_color = spec['color'](v) if callable(spec['color']) else spec['color']
527 if spec.get('text'): 527 ↛ 530line 527 didn't jump to line 530, because the condition on line 527 was never false
528 text = spec['text'](v) if callable(spec['text']) else spec['text']
529 else:
530 text = '{}'.format(v[1])
531 rectstyle = {'fill': rect_color, 'stroke': stroke}
532 rect_css(shp, **rectstyle)
533 font_size = min(h, w * font_aspect / fontwidth.fontwidth('{}'.format(text)), pd.np.Inf)
534 text_style = {}
535 text_style['color'] = _color.contrast(rect_color)
536 text_style.update(spec.get('style', {}))
537 text_style['font-size'] = font_size / pixel_inch
538 # Adding text inside rectangles
539 add_text_to_shape(shp, text, **text_style)
542def calendarmap(shape, spec, data):
543 '''Draw calendar map in PPT.'''
544 if shape.auto_shape_type != MSO_SHAPE.RECTANGLE:
545 raise NotImplementedError()
547 shapes = shape._parent
548 spec = copy.deepcopy(spec['calendarmap'])
549 handler = data.get('handler')
550 # Load data
551 data = compile_function(spec, 'data', data, handler).fillna(0)
552 startdate = compile_function(spec, 'startdate', data, handler)
554 pixel_inch = 10000
555 size = spec.get('size', None)
557 label_top = spec.get('label_top', 0) * pixel_inch
558 label_left = spec.get('label_left', 0) * pixel_inch
560 width = spec['width'] * pixel_inch
561 shape_top = label_top + shape.top
562 shape_left = label_left + shape.left
563 y0 = width + shape_top
564 x0 = width + shape_left
566 # Deleting the shape
567 shape.element.delete()
568 # Style
569 default_color = '#ffffff'
570 default_line_color = '#787C74'
571 default_txt_color = '#000000'
572 style = copy.deepcopy(spec.get('style', {}))
573 style = generate_style(style, data, handler)
575 font_size = style.get('font-size', 12)
576 stroke = style.get('stroke', '#ffffff')
577 fill_rect = style.get('fill', '#cccccc')
578 text_color = style.get('color', '#000000')
579 # Treat infinities as nans when calculating scale
580 scaledata = pd.Series(data).replace([pd.np.inf, -pd.np.inf], pd.np.nan)
581 for key in {'lo', 'hi', 'weekstart'}:
582 if isinstance(spec.get(key), (dict,)) and 'function' in spec.get(key): 582 ↛ 583line 582 didn't jump to line 583, because the condition on line 582 was never true
583 spec[key] = compile_function(spec, key, data, handler)
585 lo_data = spec.get('lo', scaledata.min())
586 range_data = spec.get('hi', scaledata.max()) - lo_data
587 gradient = matplotlib.cm.get_cmap(spec.get('gradient', 'RdYlGn'))
588 color = style.get('fill', lambda v: matplotlib.colors.to_hex(
589 gradient((float(v) - lo_data) / range_data)) if not pd.isnull(v) else default_color)
591 startweekday = (startdate.weekday() - spec.get('weekstart', 0)) % 7
592 # Weekday Mean and format
593 weekday_mean = pd.Series(
594 [scaledata[(x - startweekday) % 7::7].mean() for x in range(7)])
595 weekday_format = spec.get('format', '{:,.%df}' % utils.decimals(weekday_mean.values))
596 # Weekly Mean and format
597 weekly_mean = pd.Series([scaledata[max(0, x):x + 7].mean()
598 for x in range(-startweekday, len(scaledata), 7)])
599 weekly_format = spec.get('format', '{:,.%df}' % utils.decimals(weekly_mean.values))
600 # Scale sizes as square roots from 0 to max (not lowest to max -- these
601 # should be an absolute scale)
602 sizes = width * utils.scale( 602 ↛ exitline 602 didn't jump to the function exit
603 [v ** .5 for v in size], lo=0) if size is not None else [width] * len(scaledata)
604 for i, val in enumerate(data):
605 nx = (i + startweekday) // 7
606 ny = (i + startweekday) % 7
607 d = startdate + pd.DateOffset(days=i)
608 fill = '#cccccc'
609 if not pd.isnull(val): 609 ↛ 612line 609 didn't jump to line 612, because the condition on line 609 was never false
610 fill = color(val) if callable(color) else color
612 shp = shapes.add_shape(
613 MSO_SHAPE.RECTANGLE,
614 x0 + (width * nx) + (width - sizes[i]) / 2,
615 y0 + (width * ny) + (width - sizes[i]) / 2,
616 sizes[i], sizes[i])
617 rectstyle = {'fill': fill, 'stroke': stroke(val) if callable(stroke) else stroke}
618 rect_css(shp, **rectstyle)
619 text_style = {}
620 text_style['color'] = style.get('color')(val) if callable(
621 style.get('color')) else spec.get('color', _color.contrast(fill))
622 text_style['font-size'] = font_size(val) if callable(font_size) else font_size
623 for k in ['bold', 'italic', 'underline', 'font-family']:
624 text_style[k] = style.get(k)
625 add_text_to_shape(shp, '%02d' % d.day, **text_style)
627 # Draw the boundary lines between months
628 if i >= 7 and d.day == 1 and ny > 0:
629 border = shapes.add_shape(
630 MSO_SHAPE.RECTANGLE,
631 x0 + width * nx, y0 + (width * ny), width, 2 * pixel_inch)
632 border.name = 'border'
633 rectstyle = {'fill': default_line_color, 'stroke': default_line_color}
634 rect_css(border, **rectstyle)
635 if i >= 7 and d.day <= 7 and nx > 0:
636 border = shapes.add_shape(
637 MSO_SHAPE.RECTANGLE,
638 x0 + (width * nx), y0 + (width * ny), 2 * pixel_inch, width)
639 border.name = 'border'
640 rectstyle = {'fill': default_line_color, 'stroke': default_line_color}
641 rect_css(border, **rectstyle)
642 # Adding weekdays text to the chart (left side)
643 if i < 7:
644 txt = shapes.add_textbox(
645 x0 - (width / 2), y0 + (width * ny) + (width / 2), width, width)
646 text_style['color'] = default_txt_color
647 add_text_to_shape(txt, d.strftime('%a')[0], **text_style)
648 # Adding months text to the chart (top)
649 if d.day <= 7 and ny == 0:
650 txt = shapes.add_textbox(
651 x0 + (width * nx), y0 - (width / 2), width, width)
652 text_style['color'] = default_txt_color
653 add_text_to_shape(txt, d.strftime('%b %Y'), **text_style)
654 if label_top:
655 lo_weekly = spec.get('lo', weekly_mean.min())
656 range_weekly = spec.get('hi', weekly_mean.max()) - lo_weekly
657 for nx, val in enumerate(weekly_mean.fillna(0)):
658 w = label_top * ((val - lo_weekly) / range_weekly)
659 px = x0 + (width * nx)
660 bar = shapes.add_shape(
661 MSO_SHAPE.RECTANGLE, px, shape_top - w, width, w)
662 bar.name = 'summary.top.bar'
663 rectstyle = {'fill': fill_rect(val) if callable(fill_rect) else fill_rect,
664 'stroke': stroke(val) if callable(stroke) else stroke}
665 rect_css(bar, **rectstyle)
666 label = shapes.add_textbox(px, shape_top - width, width, width)
667 label.name = 'summary.top.label'
668 text_style['color'] = text_color(val) if callable(text_color) else text_color
669 add_text_to_shape(label, weekly_format.format(weekly_mean[nx]), **text_style)
670 if label_left:
671 lo_weekday = spec.get('lo', weekday_mean.min())
672 range_weekday = spec.get('hi', weekday_mean.max()) - lo_weekday
673 for ny, val in enumerate(weekday_mean.fillna(0)):
674 w = label_left * ((val - lo_weekday) / range_weekday)
675 bar = shapes.add_shape(
676 MSO_SHAPE.RECTANGLE, shape_left - w, y0 + (width * ny), w, width)
677 bar.name = 'summary.left.bar'
678 rectstyle = {'fill': fill_rect(val) if callable(fill_rect) else fill_rect,
679 'stroke': stroke(val) if callable(stroke) else stroke}
680 rect_css(bar, **rectstyle)
681 label = shapes.add_textbox(shape_left - width, y0 + (width * ny), w, width)
682 label.name = 'summary.left.label'
683 text_style['color'] = text_color(val) if callable(text_color) else text_color
684 add_text_to_shape(label, weekday_format.format(weekday_mean[ny]), **text_style)
687def bullet(shape, spec, data):
688 '''Function to plot bullet chart.'''
689 if shape.auto_shape_type != MSO_SHAPE.RECTANGLE:
690 raise NotImplementedError()
691 spec = copy.deepcopy(spec['bullet'])
693 orient = spec.get('orient', 'horizontal')
694 if orient not in {'horizontal', 'vertical'}: 694 ↛ 695line 694 didn't jump to line 695, because the condition on line 694 was never true
695 raise NotImplementedError()
697 font_aspect = 5
698 pixel_inch = 10000
699 x = shape.left
700 y = shape.top
702 handler = data.get('handler')
703 for met in ['poor', 'average', 'good', 'target']:
704 spec[met] = compile_function(spec, met, data, handler) if spec.get(met) else np.nan
706 height = shape.height if orient == 'horizontal' else shape.width
707 width = shape.width if orient == 'horizontal' else shape.height
708 if spec.get('max-width'): 708 ↛ 712line 708 didn't jump to line 712, because the condition on line 708 was never false
709 max_width = compile_function(spec, 'max-width', data, handler)
710 width = max_width(width) if callable(max_width) else max_width * width
712 spec['data'] = compile_function(spec, 'data', data, handler)
713 gradient = spec.get('gradient', 'RdYlGn')
714 shapes = shape._parent
715 shape._sp.delete()
716 lo = spec.get('lo', 0)
717 hi = spec.get('hi', np.nanmax([spec['data'], spec['target'], spec['poor'],
718 spec['average'], spec['good']]))
719 style = {}
720 common_style = copy.deepcopy(spec.get('style', {}))
721 data_text = common_style.get('data', {}).pop('text', spec.get('text', True))
722 target_text = common_style.get('target', {}).pop('text', spec.get('text', True))
723 if data_text:
724 data_text = compile_function({'text': data_text}, 'text', data, handler)
725 if target_text:
726 target_text = compile_function({'text': target_text}, 'text', data, handler)
728 css = {'data': common_style.pop('data', {}), 'target': common_style.pop('target', {}),
729 'poor': common_style.pop('poor', {}), 'good': common_style.pop('good', {}),
730 'average': common_style.pop('average', {})}
732 for key, val in css.items():
733 _style = copy.deepcopy(common_style)
734 _style.update(val)
735 for css_prop, css_val in _style.items(): 735 ↛ 736line 735 didn't jump to line 736, because the loop on line 735 never started
736 if isinstance(css_val, (dict,)) and 'function' in css_val:
737 _style[css_prop] = compile_function(_style, css_prop, data, handler)
738 style[key] = _style
740 gradient = matplotlib.cm.get_cmap(gradient)
741 percentage = {'good': 0.125, 'average': 0.25, 'poor': 0.50, 'data': 1.0, 'target': 1.0}
742 for index, metric in enumerate(['good', 'average', 'poor']):
743 scaled = scale_data(spec.get(metric, np.nan), lo, hi, factor=width)
744 if not np.isnan(scaled):
745 _width = scaled if orient == 'horizontal' else height
746 _hight = height if orient == 'horizontal' else scaled
747 yaxis = y if orient == 'horizontal' else y + (width - scaled)
748 _rect = rect(shapes, x, yaxis, _width, _hight)
749 fill = style.get(metric, {})
750 stroke = fill.get('stroke')
751 fill = fill.get('fill', matplotlib.colors.to_hex(gradient(percentage[metric])))
752 rect_css(_rect, **{'fill': fill, 'stroke': stroke or fill})
754 getmax = {key: spec.get(key, np.nan) for key in ['data', 'target', 'good', 'average', 'poor']}
755 max_data_val = percentage[max(getmax.items(), key=operator.itemgetter(1))[0]]
757 scaled = scale_data(spec['data'], lo, hi, factor=width)
758 if not np.isnan(scaled):
759 _width = scaled if orient == 'horizontal' else height / 2.0
760 yaxis = y + height / 4.0 if orient == 'horizontal' else y + (width - scaled)
761 xaxis = x if orient == 'horizontal' else x + height / 4.0
762 _hight = height / 2.0 if orient == 'horizontal' else scaled
763 data_rect = rect(shapes, xaxis, yaxis, _width, _hight)
764 fill = style.get('data', {})
765 stroke = fill.get('stroke')
766 fill = fill.get('fill', matplotlib.colors.to_hex(gradient(1.0)))
767 rect_css(data_rect, **{'fill': fill, 'stroke': stroke or fill})
769 if data_text and not np.isnan(scaled):
770 if callable(data_text): 770 ↛ 773line 770 didn't jump to line 773, because the condition on line 770 was never false
771 _data_text = '{}'.format(data_text(spec['data']))
772 else:
773 _data_text = '{}'.format(spec['data']) if data_text is True else data_text
774 parent = data_rect._parent
775 text_width = (_width if orient == 'vertical' else _hight * 2) * len(_data_text)
776 _xaxis = xaxis if orient == 'vertical' else x + scaled - text_width
777 parent = parent.add_textbox(_xaxis, yaxis, text_width, text_width / len(_data_text))
778 data_txt_style = style.get('data', {})
779 data_txt_style['color'] = data_txt_style.get('color', _color.contrast(fill))
780 default_align = 'left' if orient == 'vertical' else 'right'
781 data_txt_style['text-align'] = data_txt_style.get('text-align', default_align)
782 # Setting default font-size
783 font_size = (text_width / pixel_inch) * font_aspect / fontwidth.fontwidth(
784 '{}'.format(_data_text))
785 font_size = min(text_width / pixel_inch, font_size, pd.np.Inf)
786 data_txt_style['font-size'] = data_txt_style.get('font-size', font_size)
787 add_text_to_shape(parent, _data_text, **data_txt_style)
789 scaled = scale_data(spec['target'], lo, hi, factor=width)
790 if not np.isnan(scaled):
791 line_hight = 10000
792 _width = line_hight if orient == 'horizontal' else height
793 _hight = height if orient == 'horizontal' else line_hight
794 yaxis = y if orient == 'horizontal' else (width - scaled) + y
795 xaxis = x + scaled if orient == 'horizontal' else x
796 target_line = rect(shapes, xaxis, yaxis, _width, _hight)
797 fill = style.get('target', {})
798 stroke = fill.get('stroke')
799 fill_target_rect = fill.get('fill', matplotlib.colors.to_hex(gradient(1.0)))
800 rect_css(target_line, **{'fill': fill_target_rect, 'stroke': stroke or fill_target_rect})
801 if target_text:
802 if callable(target_text): 802 ↛ 805line 802 didn't jump to line 805, because the condition on line 802 was never false
803 _target_text = '{}'.format(target_text(spec['target']))
804 else:
805 _target_text = '{}'.format(spec['target']) if target_text is True else target_text
806 handler = data.get('handler')
807 parent = target_line._parent
808 yaxis = yaxis - (_width / 2) if orient == 'vertical' else yaxis
809 text_width = (_width if orient == 'vertical' else _hight) * len(_target_text)
810 parent = parent.add_textbox(xaxis, yaxis, text_width, text_width / len(_target_text))
811 target_txt_style = style.get('target', {})
812 fill_max = fill.get('fill', matplotlib.colors.to_hex(gradient(max_data_val)))
813 target_txt_style['color'] = target_txt_style.get('color', _color.contrast(fill_max))
814 # Setting default font-size
815 font_size = font_aspect / fontwidth.fontwidth('{}'.format(_target_text))
816 font_size = min(text_width / pixel_inch,
817 (text_width / pixel_inch) * font_size, pd.np.Inf)
818 target_txt_style['font-size'] = target_txt_style.get('font-size', font_size)
819 add_text_to_shape(parent, _target_text, **target_txt_style)
822def heatgrid(shape, spec, data):
823 '''Create a heat grid.'''
824 if shape.auto_shape_type != MSO_SHAPE.RECTANGLE:
825 raise NotImplementedError()
827 spec = copy.deepcopy(spec['heatgrid'])
829 top = shape.top
830 left = shape.left
831 width = shape.width
832 pixel_inch = 10000
833 default_height = 20
834 height = spec.get('cell-height', default_height) * pixel_inch
835 parent = shape._parent
836 shape.element.delete()
838 # Loading config
839 handler = data.pop('handler') if 'handler' in data else None
840 for key in ['row', 'column', 'value', 'column-order', 'row-order']:
841 if key not in spec:
842 continue
843 if isinstance(spec[key], (dict,)) and 'function' in spec[key]: 843 ↛ 844line 843 didn't jump to line 844, because the condition on line 843 was never true
844 spec[key] = compile_function(spec, key, data, handler)
845 # Loading data
846 data = compile_function(spec, 'data', data, handler)
847 data = data.sort_values(by=[spec['column']])
848 rows = spec.get('row-order') or sorted(data[spec['row']].unique().tolist())
849 columns = spec.get('column-order') or sorted(data[spec['column']].unique().tolist())
851 left_margin = (width * spec.get('left-margin', 0.15))
852 padding = spec.get('style', {}).get('padding', 5)
853 if not isinstance(padding, (dict,)): 853 ↛ 854line 853 didn't jump to line 854, because the condition on line 853 was never true
854 padding = {'left': padding, 'right': padding,
855 'top': padding, 'bottom': padding}
857 styles = copy.deepcopy(spec.get('style', {}))
859 if styles.get('gradient'): 859 ↛ 862line 859 didn't jump to line 862, because the condition on line 859 was never false
860 _min, _max = data[spec['value']].min(), data[spec['value']].max()
861 # Compiling style elements if required
862 for key in ['gradient', 'color', 'fill', 'font-size', 'font-family', 'stroke']:
863 if isinstance(styles.get(key), (dict,)) and 'function' in styles[key]:
864 prop = compile_function(styles, key, data, handler)
865 styles[key] = prop(**{'data': data, 'handler': handler}) if callable(prop) else prop
866 # Calculating cell's width based on config
867 _width = (width - left_margin) / float(len(columns)) / pixel_inch
868 _width = spec.get('cell-width', _width) * pixel_inch
869 # Adding Columns to the HeatGrid.
870 for idx, column in enumerate(columns):
871 txt = parent.add_textbox(
872 left + _width * idx + left_margin, top - height, _width, height)
873 add_text_to_shape(txt, '{}'.format(column), **styles)
874 # Cell width
875 for index, row in enumerate(rows):
876 _data = data[data[spec['row']] == row].dropna()
877 _data = pd.merge(
878 pd.DataFrame({spec['column']: list(columns)}), _data,
879 left_on=spec['column'], right_on=spec['column'], how='left').reset_index(drop=True)
881 for _idx, _row in _data.iterrows():
882 style = copy.deepcopy(styles)
883 # Setting callable padding args
884 _vars = {'handler': None, 'row': None,
885 'column': None, 'value': None}
886 args = {'handler': handler, 'row': row,
887 'column': _row[spec['column']],
888 'value': _row[spec['value']]}
889 # Setting padding if callable.
890 _pad = copy.deepcopy(padding)
891 for key, val in _pad.items():
892 if isinstance(val, (dict,)) and 'function' in val: 892 ↛ 893line 892 didn't jump to line 893, because the condition on line 892 was never true
893 _pad[key] = build_transform(val, vars=_vars)(**args)[0]
894 top_pad = _pad.get('top', 5) * pixel_inch
895 left_pad = _pad.get('left', 5) * pixel_inch
896 right_pad = _pad.get('right', 5) * pixel_inch
897 bottom_pad = _pad.get('bottom', 5) * pixel_inch
899 # Adding cells
900 xaxis = left + (_width * _idx) + left_margin + left_pad
901 yaxis = top + (height * index) + (top_pad) * index
902 _rect = rect(parent, xaxis, yaxis, _width - left_pad - right_pad,
903 height - top_pad)
904 # Adding color gradient to cell if gradient is True
905 if style.get('gradient'): 905 ↛ 910line 905 didn't jump to line 910, because the condition on line 905 was never false
906 grad_txt = scale_data(_row[spec['value']], _min, _max)
907 gradient = matplotlib.cm.get_cmap(style['gradient'])
908 style['fill'] = matplotlib.colors.to_hex(gradient(grad_txt))
909 style['color'] = _color.contrast(style['fill'])
910 if np.isnan(_row[spec['value']]) and spec.get('na-color'):
911 style['fill'] = spec.get('na-color')
912 style['color'] = _color.contrast(style['fill'])
914 style['stroke'] = style.get('stroke', style['fill'])
915 rect_css(_rect, **style)
916 # Adding text to cells if required.
917 if spec.get('text'): 917 ↛ 881line 917 didn't jump to line 881, because the condition on line 917 was never false
918 _txt = parent.add_textbox(
919 xaxis, yaxis, _width - left_pad - right_pad,
920 height - top_pad - bottom_pad)
921 if isinstance(spec['text'], dict) and 'function' in spec['text']: 921 ↛ 924line 921 didn't jump to line 924, because the condition on line 921 was never false
922 cell_txt = compile_function(spec, 'text', _row, handler)
923 else:
924 cell_txt = '{}'.format(_row[spec['value']])
925 if pd.isnull(cell_txt) and spec.get('na-text'): 925 ↛ 926line 925 didn't jump to line 926, because the condition on line 925 was never true
926 cell_txt = spec.get('na-text')
927 add_text_to_shape(_txt, cell_txt, **style)
928 # Adding row's text in left side
929 txt = parent.add_textbox(
930 left, top + (height * index) + top_pad * index,
931 _width + left_margin, height)
932 add_text_to_shape(txt, row, **styles)
935def css(shape, spec, data):
936 '''Function to modify a rectangle's property in PPT.'''
937 pxl_to_inch = 10000
938 handler = data.pop('handler') if 'handler' in data else None
939 spec = copy.deepcopy(spec['css'])
940 data = compile_function(spec, 'data', data, handler)
941 style = copy.deepcopy(spec.get('style', {}))
942 shape_prop = {'width', 'height', 'top', 'left'}
943 for prop in shape_prop:
944 setprop = style.get(prop)
945 if setprop: 945 ↛ 952line 945 didn't jump to line 952, because the condition on line 945 was never false
946 if not isinstance(style[prop], (dict,)): 946 ↛ 949line 946 didn't jump to line 949, because the condition on line 946 was never false
947 style[prop] = {'function': '{}'.format(style[prop]) if not isinstance(
948 style[prop], (str, six.string_types,)) else style[prop]}
949 setprop = compile_function(style, prop, data, handler)
950 setprop = setprop * pxl_to_inch
951 else:
952 setprop = getattr(shape, prop)
953 setattr(shape, prop, setprop)
955 _style = {}
956 for key, val in style.items():
957 if key not in shape_prop:
958 _style[key] = val
959 if isinstance(val, (dict,)): 959 ↛ 960line 959 didn't jump to line 960, because the condition on line 959 was never true
960 _style[key] = compile_function(style, key, data, handler)
961 _style[key] = _style[key](data) if callable(_style[key]) else _style[key]
962 rect_css(shape, **_style)
965cmdlist = {
966 'css': css,
967 'text': text,
968 'image': image,
969 'chart': chart,
970 'table': table,
971 'sankey': sankey,
972 'bullet': bullet,
973 'replace': replace,
974 'treemap': treemap,
975 'heatgrid': heatgrid,
976 'calendarmap': calendarmap,
977}