Hide keyboard shortcuts

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 

26 

27 

28_template_cache = {} 

29 

30 

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)) 

36 

37 

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) 

46 

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) 

58 

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 

99 

100 

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} 

108 

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) 

115 

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 

121 

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) 

127 

128 

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 

144 

145 

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 

153 

154 

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) 

163 

164 

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) 

179 

180 

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.) 

185 

186 

187def rect(shape, x, y, width, height): 

188 '''Add rectangle to slide.''' 

189 return shape.add_shape(MSO_SHAPE.RECTANGLE, x, y, width, height) 

190 

191 

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 

219 

220 

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 

231 

232 

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] 

244 

245 

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) 

298 

299 

300def chart(shape, spec, data): 

301 '''Replacing chart Data.''' 

302 if not shape.has_chart: 

303 raise AttributeError('Shape must be a chart object.') 

304 

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] 

308 

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 } 

323 

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() 

326 

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) 

335 

336 style = {'color': info.pop('color', None), 'opacity': info.pop('opacity', None), 

337 'stroke': info.pop('stroke', None)} 

338 

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) 

344 

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

350 

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)) 

353 

354 chart_data = _update_chart( 

355 info, change_data, getattr(pptxcd, chart_name)(), series_cols, chart=chart_name) 

356 

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)))) 

361 

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 

394 

395 

396# Custom Charts Functions below(Sankey, Treemap, Calendarmap). 

397 

398 

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} 

415 

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) 

446 

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] 

461 

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)))) 

472 

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)))) 

480 

481 path.append(elem_schema['a'].lnTo( 

482 elem_schema['a'].pt(x='{:.0f}'.format(x2), y='{:.0f}'.format(y2)))) 

483 

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)))) 

488 

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'] 

495 

496 

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) 

540 

541 

542def calendarmap(shape, spec, data): 

543 '''Draw calendar map in PPT.''' 

544 if shape.auto_shape_type != MSO_SHAPE.RECTANGLE: 

545 raise NotImplementedError() 

546 

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) 

553 

554 pixel_inch = 10000 

555 size = spec.get('size', None) 

556 

557 label_top = spec.get('label_top', 0) * pixel_inch 

558 label_left = spec.get('label_left', 0) * pixel_inch 

559 

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 

565 

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) 

574 

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) 

584 

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) 

590 

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 

611 

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) 

626 

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) 

685 

686 

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']) 

692 

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() 

696 

697 font_aspect = 5 

698 pixel_inch = 10000 

699 x = shape.left 

700 y = shape.top 

701 

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 

705 

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 

711 

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) 

727 

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', {})} 

731 

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 

739 

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}) 

753 

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]] 

756 

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}) 

768 

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) 

788 

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) 

820 

821 

822def heatgrid(shape, spec, data): 

823 '''Create a heat grid.''' 

824 if shape.auto_shape_type != MSO_SHAPE.RECTANGLE: 

825 raise NotImplementedError() 

826 

827 spec = copy.deepcopy(spec['heatgrid']) 

828 

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() 

837 

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()) 

850 

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} 

856 

857 styles = copy.deepcopy(spec.get('style', {})) 

858 

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) 

880 

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 

898 

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']) 

913 

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) 

933 

934 

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) 

954 

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) 

963 

964 

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}