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'''Utility file.''' 

2import re 

3import ast 

4import copy 

5import platform 

6import six 

7from six import iteritems 

8import numpy as np 

9import pandas as pd 

10from lxml import objectify 

11from lxml.builder import ElementMaker 

12from pptx.util import Inches 

13from pptx.dml.color import RGBColor 

14from pptx.enum.base import EnumValue 

15from pptx.enum.text import PP_ALIGN, MSO_ANCHOR 

16from gramex.transforms import build_transform 

17 

18 

19def is_slide_allowed(change, slide, number): 

20 ''' 

21 Given a change like one of the below:: 

22 

23 slide-number: 1 

24 slide-number: [1, 2, 3] 

25 slide-title: 'company' 

26 slide-title: ['company', 'industry'] 

27 

28 ... return True if: 

29 

30 1. ``number`` matches a slide-number 

31 2. ``slide`` title matches a slide-title regex (case-insensitive) 

32 

33 If none of these are specified, return True. 

34 ''' 

35 match = True 

36 # Restrict to specific slide number(s), if specified 

37 if 'slide-number' in change: 

38 slide_number = change['slide-number'] 

39 if isinstance(slide_number, (list, dict)): 

40 match = match and number in slide_number 

41 elif isinstance(slide_number, six.integer_types): 41 ↛ 45line 41 didn't jump to line 45, because the condition on line 41 was never false

42 match = match and number == slide_number 

43 

44 # Restrict to specific slide title(s), if specified 

45 if 'slide-title' in change: 

46 slide_title = change['slide-title'] 

47 title = slide.shapes.title 

48 title = title.text if title is not None else '' 

49 if isinstance(slide_title, (list, dict)): 49 ↛ 50line 49 didn't jump to line 50, because the condition on line 49 was never true

50 match = match and any( 

51 re.search(expr, title, re.IGNORECASE) for expr in slide_title) 

52 elif isinstance(slide_title, six.string_types): 52 ↛ 54line 52 didn't jump to line 54, because the condition on line 52 was never false

53 match = match and re.search(slide_title, title, re.IGNORECASE) 

54 return match 

55 

56 

57def stack_elements(replica, shape, stack=False, margin=None): 

58 '''Function to extend elements horizontally or vertically.''' 

59 if not stack: 

60 return 

61 config = {'vertical': {'axis': 'y', 'attr': 'height'}, 

62 'horizontal': {'axis': 'x', 'attr': 'width'}} 

63 grp_sp = shape.element 

64 # Adding a 15% default margin between original and new object. 

65 default_margin = 0.15 

66 margin = default_margin if not margin else margin 

67 for index in range(replica): 

68 # Adding a cloned object to shape 

69 extend_shape = copy.deepcopy(grp_sp) 

70 # Getting attributes and axis values from config based on stack. 

71 attr = config.get(stack, {}).get('attr', 0) 

72 axis = config.get(stack, {}).get('axis', 0) 

73 # Taking width or height based on stack value and setting a margin. 

74 metric_val = getattr(shape, attr) 

75 axis_val = getattr(extend_shape, axis) 

76 # Setting margin accordingly either vertically or horizontally. 

77 axis_pos = metric_val * index 

78 set_attr = axis_val + axis_pos + (axis_pos * margin) 

79 # Setting graphic position of newly created object to slide. 

80 setattr(extend_shape, axis, int(set_attr)) 

81 # Adding newly created object to slide. 

82 # grp_sp.addnext(extend_shape) 

83 grp_sp.addprevious(extend_shape) 

84 shape.element.delete() 

85 

86 

87def stack_shapes(collection, change, data, handler): 

88 ''' 

89 Function to stack Shapes if required. 

90 ''' 

91 data_len = len(data) 

92 for shape in collection: 

93 if shape.name not in change: 

94 continue 

95 info = change[shape.name] 

96 if 'data' in info and info.get('stack') is not None: 

97 _vars = {'_color': None, 'data': None, 'handler': None} 

98 if not isinstance(info['data'], (dict,)): 98 ↛ 100line 98 didn't jump to line 100, because the condition on line 98 was never false

99 info['data'] = {'function': '{}'.format(info['data'])} 

100 elif isinstance(info['data'], (dict,)) and 'function' not in info['data']: 

101 info['data'] = {'function': '{}'.format(info['data'])} 

102 args = {'data': data, 'handler': handler} 

103 data_len = len(build_transform(info['data'], vars=_vars)(**args)[0]) 

104 stack_elements(data_len, shape, stack=info.get('stack'), margin=info.get('margin')) 

105 

106 

107def delete_paragraph(paragraph): 

108 '''Delete a paragraph.''' 

109 p = paragraph._p 

110 parent_element = p.getparent() 

111 parent_element.remove(p) 

112 

113 

114def delete_run(run): 

115 '''Delete a run from paragraph.''' 

116 r = run._r 

117 r.getparent().remove(r) 

118 

119 

120def generate_slide(prs, source): 

121 '''Create a slide layout.''' 

122 layout_items_count = [ 

123 len(layout.placeholders) for layout in prs.slide_layouts] 

124 min_items = min(layout_items_count) 

125 blank_layout_id = layout_items_count.index(min_items) 

126 return prs.slide_layouts[blank_layout_id] 

127 

128 

129def copy_slide_elem(shape, dest): 

130 ''' 

131 Function to copy slide elements into a newly created slide. 

132 ''' 

133 if dest is None: 

134 return 

135 new_elem = copy.deepcopy(shape.element) 

136 dest.shapes._spTree.insert_element_before(new_elem, 'p:extLst') 

137 

138 

139def add_new_slide(dest, source_slide): 

140 '''Function to add a new slide to presentation.''' 

141 if dest is None: 

142 return 

143 for key, value in six.iteritems(source_slide.part.rels): 

144 # Make sure we don't copy a notesSlide relation as that won't exist 

145 if "notesSlide" in value.reltype: 145 ↛ 146line 145 didn't jump to line 146, because the condition on line 145 was never true

146 continue 

147 dest.part.rels.add_relationship(value.reltype, value._target, value.rId) 

148 

149 

150def move_slide(presentation, old_index, new_index): 

151 '''Move a slide's index number.''' 

152 xml_slides = presentation.slides._sldIdLst 

153 slides = list(xml_slides) 

154 xml_slides.remove(slides[old_index]) 

155 xml_slides.insert(new_index, slides[old_index]) 

156 

157 

158def delete_slide(presentation, index): 

159 '''Delete a slide from Presentation.''' 

160 # xml_slides = presentation.slides._sldIdLst 

161 # slides = list(xml_slides) 

162 # del presentation.slides[index] 

163 # xml_slides.remove(slides[index]) 

164 rid = presentation.slides._sldIdLst[index].rId 

165 presentation.part.drop_rel(rid) 

166 del presentation.slides._sldIdLst[index] 

167 

168 

169def manage_slides(prs, config): 

170 ''' 

171 Delete not required slides from presentation. 

172 

173 if `config.only` is present then remove the other slides apart from `config.only` 

174 slides from the presentation. 

175 `config.only` accepts a slide number or list of slide numbers starting from 1. 

176 ''' 

177 slide_numbers = config.pop('only', None) 

178 if slide_numbers: 

179 if isinstance(slide_numbers, six.integer_types): 179 ↛ 181line 179 didn't jump to line 181, because the condition on line 179 was never false

180 slide_numbers = set([int(slide_numbers) - 1]) 

181 elif isinstance(slide_numbers, list): 

182 slide_numbers = set([int(i) - 1 for i in slide_numbers]) 

183 else: 

184 raise ValueError('Slide numbers must be a list of integers or a single slide number.') 

185 slides = set(range(len(prs.slides))) 

186 remove_status = 0 

187 for slide_num in sorted(slides - slide_numbers): 

188 delete_slide(prs, slide_num - remove_status) 

189 remove_status += 1 

190 return prs 

191 

192 

193def is_group(shape): 

194 # TODO: implement this 

195 return shape.element.tag.endswith('}grpSp') 

196 

197 

198def pixel_to_inch(pixel): 

199 '''Function to convert Pixel to Inches based on OS.''' 

200 linux_width = 72.0 

201 windows_width = 96.0 

202 os_name = platform.system().lower().strip() 

203 if os_name == 'windows': 

204 return Inches(pixel / windows_width) 

205 return Inches(pixel / linux_width) 

206 

207 

208def scale(series, lo=None, hi=None): 

209 ''' 

210 Returns the values linearly scaled from 0 - 1. 

211 

212 The lowest value becomes 0, the highest value becomes 1, and all other 

213 values are proportionally multiplied and have a range between 0 and 1. 

214 

215 :arg Series series: Data to scale. Pandas Series, numpy array, list or iterable 

216 :arg float lo: Value that becomes 0. Values lower than ``lo`` in ``series`` 

217 will be mapped to negative numbers. 

218 :arg float hi: Value that becomes 1. Values higher than ``hi`` in ``series`` 

219 will be mapped to numbers greater than 1. 

220 

221 Examples:: 

222 

223 >>> stats.scale([1, 2, 3, 4, 5]) 

224 # array([ 0. , 0.25, 0.5 , 0.75, 1. ]) 

225 

226 >>> stats.scale([1, 2, 3, 4, 5], lo=2, hi=4) 

227 # array([-0.5, 0. , 0.5, 1. , 1.5]) 

228 ''' 

229 series = np.array(series, dtype=float) 

230 lo = np.nanmin(series) if lo is None or np.isnan(lo) else lo 

231 hi = np.nanmax(series) if hi is None or np.isnan(hi) else hi 

232 return (series - lo) / ((hi - lo) or np.nan) 

233 

234 

235def decimals(series): 

236 ''' 

237 Given a ``series`` of numbers, returns the number of decimals 

238 *just enough* to differentiate between most numbers. 

239 

240 :arg Series series: Pandas Series, numpy array, list or iterable. 

241 Data to find the required decimal precision for 

242 :return: The minimum number of decimals required to differentiate between 

243 most numbers 

244 

245 Examples:: 

246 

247 stats.decimals([1, 2, 3]) # 0: All integers. No decimals needed 

248 stats.decimals([.1, .2, .3]) # 1: 1 decimal is required 

249 stats.decimals([.01, .02, .3]) # 2: 2 decimals are required 

250 stats.decimals(.01) # 2: Only 1 no. of 2 decimal precision 

251 

252 Note: This function first calculates the smallest difference between any pair 

253 of numbers (ignoring floating-point errors). It then finds the log10 of that 

254 difference, which represents the minimum decimals required to differentiate 

255 between these numbers. 

256 ''' 

257 series = np.ma.masked_array(series, mask=np.isnan(series)).astype(float) 

258 series = series.reshape((series.size,)) 

259 diffs = np.diff(series[series.argsort()]) 

260 inf_diff = 1e-10 

261 min_float = .999999 

262 diffs = diffs[diffs > inf_diff] 

263 if len(diffs) > 0: 263 ↛ 266line 263 didn't jump to line 266, because the condition on line 263 was never false

264 smallest = np.nanmin(diffs.filled(np.Inf)) 

265 else: 

266 nonnan = series.compressed() 

267 smallest = (abs(nonnan[0]) or 1) if len(nonnan) > 0 else 1 

268 return int(max(0, np.floor(min_float - np.log10(smallest)))) 

269 

270 

271def convert_color_code(colorcode): 

272 '''Convert color code to valid PPTX color code.''' 

273 colorcode = colorcode.rsplit('#')[-1].lower() 

274 return colorcode + ('0' * (6 - len(colorcode))) 

275 

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

277 

278 

279def apply_text_css(shape, run, paragraph, **kwargs): 

280 '''Apply css.''' 

281 pixcel_to_inch = 10000 

282 if kwargs.get('color'): 

283 rows_text = run.font.fill 

284 rows_text.solid() 

285 run.font.color.rgb = RGBColor.from_string(convert_color_code(kwargs['color'])) 

286 if kwargs.get('font-family'): 286 ↛ 287line 286 didn't jump to line 287, because the condition on line 286 was never true

287 run.font.name = kwargs['font-family'] 

288 if kwargs.get('font-size'): 

289 run.font.size = pixcel_to_inch * float(kwargs['font-size']) 

290 if kwargs.get('text-align'): 

291 if isinstance(kwargs['text-align'], EnumValue) or None: 

292 paragraph.alignment = kwargs['text-align'] 

293 else: 

294 paragraph.alignment = getattr(PP_ALIGN, kwargs['text-align'].upper()) 

295 for prop in {'bold', 'italic', 'underline'}: 

296 update_prop = kwargs.get(prop) 

297 if update_prop and not isinstance(update_prop, bool): 

298 update_prop = ast.literal_eval(update_prop) 

299 setattr(run.font, prop, update_prop) 

300 if kwargs.get('text-anchor'): 300 ↛ 301line 300 didn't jump to line 301, because the condition on line 300 was never true

301 shape.vertical_anchor = getattr(MSO_ANCHOR, shape['text-anchor'].upper()) 

302 

303 

304def make_element(): 

305 '''Function to create element structure.''' 

306 nsmap = { 

307 'p': 'http://schemas.openxmlformats.org/presentationml/2006/main', 

308 'a': 'http://schemas.openxmlformats.org/drawingml/2006/main', 

309 'r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' 

310 } 

311 a = ElementMaker(namespace=nsmap['a'], nsmap=nsmap) 

312 p = ElementMaker(namespace=nsmap['p'], nsmap=nsmap) 

313 r = ElementMaker(namespace=nsmap['r'], nsmap=nsmap) 

314 return {'nsmap': nsmap, 'a': a, 'p': p, 'r': r} 

315 

316 

317def fill_color(**kwargs): 

318 ''' 

319 Return a new color object. 

320 

321 You may use any one of the following ways of specifying colour: 

322 

323 color(schemeClr='accent2') # = second theme color 

324 color(prstClr='black') # = #000000 

325 color(hslClr=[14400000, 100.0, 50.0]) # = #000080 

326 color(sysClr='windowText') # = window text color 

327 color(scrgbClr=(50000, 50000, 50000)) # = #808080 

328 color(srgbClr='aaccff') # = #aaccff 

329 

330 One or more of these modifiers may be specified: 

331 

332 - alpha : '10%' indicates 10% opacity 

333 - alphaMod : '10%' increased alpha by 10% (50% becomes 55%) 

334 - alphaOff : '10%' increases alpha by 10 points (50% becomes 60%) 

335 - blue : '10%' sets the blue component to 10% 

336 - blueMod : '10%' increases blue by 10% (50% becomes 55%) 

337 - blueOff : '10%' increases blue by 10 points (50% becomes 60%) 

338 - comp : True for opposite hue on the color wheel (e.g. red -> cyan) 

339 - gamma : True for the sRGB gamma shift of the input color 

340 - gray : True for the grayscale version of the color 

341 - green : '10%' sets the green component to 10% 

342 - greenMod : '10%' increases green by 10% (50% becomes 55%) 

343 - greenOff : '10%' increases green by 10 points (50% becomes 60%) 

344 - hue : '14400000' sets the hue component to 14400000 

345 - hueMod : '600000' increases hue by 600000 (14400000 becomes 20000000) 

346 - hueOff : '10%' increases hue by 10 points (50% becomes 60%) 

347 - inv : True for the inverse color. R, G, B are all inverted 

348 - invGamma : True for the inverse sRGB gamma shift of the input color 

349 - lum : '10%' sets the luminance component to 10% 

350 - lumMod : '10%' increases luminance by 10% (50% becomes 55%) 

351 - lumOff : '10%' increases luminance by 10 points (50% becomes 60%) 

352 - red : '10%' sets the red component to 10% 

353 - redMod : '10%' increases red by 10% (50% becomes 55%) 

354 - redOff : '10%' increases red by 10 points (50% becomes 60%) 

355 - sat : '100000' sets the saturation component to 100% 

356 - satMod : '10%' increases saturation by 10% (50% becomes 55%) 

357 - satOff : '10%' increases saturation by 10 points (50% becomes 60%) 

358 - shade : '10%' is 10% of input color, 90% black 

359 - tint : '10%' is 10% of input color, 90% white 

360 

361 Refer 

362 <http://msdn.microsoft.com/en-in/library/documentformat.openxml.drawing(v=office.14).aspx> 

363 ''' 

364 hslclr = kwargs.get('hslclr') 

365 sysclr = kwargs.get('sysclr') 

366 srgbclr = kwargs.get('srgbclr') 

367 prstclr = kwargs.get('prstclr') 

368 scrgbclr = kwargs.get('scrgbclr') 

369 schemeclr = kwargs.get('schemeclr') 

370 

371 ns = xmlns('a') 

372 srgbclr = srgbclr.rsplit('#')[-1].lower() 

373 srgbclr = srgbclr + ('0' * (6 - len(srgbclr))) 

374 if schemeclr: 374 ↛ 375line 374 didn't jump to line 375, because the condition on line 374 was never true

375 s = '<a:schemeClr %s val="%s"/>' % (ns, schemeclr) 

376 elif srgbclr: 376 ↛ 378line 376 didn't jump to line 378, because the condition on line 376 was never false

377 s = '<a:srgbClr %s val="%s"/>' % (ns, srgbclr) 

378 elif prstclr: 

379 s = '<a:prstClr %s val="%s"/>' % (ns, prstclr) 

380 elif hslclr: 

381 s = '<a:hslClr %s hue="%.0f" sat="%.2f%%" lum="%.2f%%"/>' % ( 

382 (ns,) + tuple(hslclr)) 

383 elif sysclr: 

384 s = '<a:sysClr %s val="%s"/>' % (ns, sysclr) 

385 elif scrgbclr: 

386 s = '<a:scrgbClr %s r="%.0f" g="%.0f" b="%.0f"/>' % ((ns,) + tuple( 

387 scrgbclr)) 

388 color = objectify.fromstring(s) 

389 return color 

390 

391 

392def xmlns(*prefixes): 

393 '''XML ns.''' 

394 elem_schema = make_element() 

395 return ' '.join('xmlns:%s="%s"' % (pre, elem_schema['nsmap'][pre]) for pre in prefixes) 

396 

397 

398def call(val, g, group, default): 

399 '''Callback.''' 

400 if callable(val): 400 ↛ 402line 400 didn't jump to line 402, because the condition on line 400 was never false

401 return val(g) 

402 return default 

403 

404 

405def cust_shape(x, y, w, h, _id): 

406 '''Custom shapes.''' 

407 _cstmshape = '<p:sp ' + xmlns('p', 'a') + '>' 

408 _cstmshape = _cstmshape + '''<p:nvSpPr> 

409 <p:cNvPr id='%s' name='%s'/> 

410 <p:cNvSpPr/> 

411 <p:nvPr/> 

412 </p:nvSpPr> 

413 <p:spPr> 

414 <a:xfrm> 

415 <a:off x='%s' y='%s'/> 

416 <a:ext cx='%s' cy='%s'/> 

417 </a:xfrm> 

418 <a:custGeom> 

419 <a:avLst/> 

420 <a:gdLst/> 

421 <a:ahLst/> 

422 <a:cxnLst/> 

423 <a:rect l='0' t='0' r='0' b='0'/> 

424 </a:custGeom> 

425 </p:spPr> 

426 </p:sp>''' 

427 shp = _cstmshape % (_id, 'Freeform %d' % _id, x, y, w, h) 

428 return objectify.fromstring(shp) 

429 

430 

431def draw_sankey(data, spec): 

432 '''Create sankey data logic.''' 

433 x0 = spec['x0'] 

434 size = spec['size'] 

435 group = spec['group'] 

436 width = spec['width'] 

437 default_color = '#ccfccf' 

438 default_stroke = '#ffffff' 

439 attrs = spec.get('attrs', {}) 

440 sort = spec.get('sort', False) 

441 

442 text = spec.get('text') 

443 order = spec.get('order') 

444 fill_color = spec.get('color') 

445 

446 g = data.groupby(group) 

447 frame = pd.DataFrame({ 

448 'size': g[group[0]].count() if size is None else g[size].sum(), 

449 'seq': 0 if order is None else order(g), 

450 }) 

451 frame['width'] = frame['size'] / float(frame['size'].sum()) * width 

452 frame['fill'] = call(fill_color, g, group, default_color) 

453 result = call(text, g, group, '') 

454 frame['text'] = result 

455 # Add all attrs to the frame as well 

456 for key, val in iteritems(attrs): 456 ↛ 457line 456 didn't jump to line 457, because the loop on line 456 never started

457 frame[key] = call(val, g, group, None) 

458 if 'stroke' not in attrs: 458 ↛ 461line 458 didn't jump to line 461, because the condition on line 458 was never false

459 frame['stroke'] = default_stroke 

460 # Compute frame['x'] only after sorting 

461 if order and sort: 

462 frame.sort_values('seq', inplace=True) 

463 frame['x'] = x0 + frame['width'].cumsum() - frame['width'] 

464 return frame 

465 

466 

467def squarified(x, y, w, h, data): 

468 ''' 

469 Draw a squarified treemap. 

470 

471 See <http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.36.6685> 

472 Returns a numpy array with (x, y, w, h) for each item in data. 

473 

474 Examples:: 

475 

476 # The result is a 2x2 numpy array:: 

477 >>> squarified(x=0, y=0, w=6, h=4, data=[6, 6, 4, 3, 2, 2, 1]) 

478 array([[ 0. , 0. , 3. , 2. ], 

479 [ 0. , 2. , 3. , 2. ], 

480 [ 3. , 0. , 1.71428571, 2.33333333], 

481 [ 4.71428571, 0. , 1.28571429, 2.33333333], 

482 [ 3. , 2.33333333, 1.2 , 1.66666667], 

483 [ 4.2 , 2.33333333, 1.2 , 1.66666667], 

484 [ 5.4 , 2.33333333, 0.6 , 1.66666667]]) 

485 

486 >>> squarified(x=0, y=0, w=1, h=1, data=[np.nan, 0, 1, 2]) 

487 array([[ 0. , 0. , 0. , 0. ], 

488 [ 0. , 0. , 0. , 0. ], 

489 [ 0. , 0. , 0.33333333, 1. ], 

490 [ 0.33333333, 0. , 0.66666667, 1. ]]) 

491 ''' 

492 w, h = float(w), float(h) 

493 size = np.nan_to_num(np.array(data).astype(float)) 

494 start, end = 0, len(size) 

495 result = np.zeros([end, 4]) 

496 if w <= 0 or h <= 0: 496 ↛ 497line 496 didn't jump to line 497, because the condition on line 496 was never true

497 return result 

498 

499 cumsize = np.insert(size.cumsum(), 0, 0) 

500 while start < end: 

501 # We lay out out blocks of rects on either the left or the top edge of 

502 # the remaining rectangle. But how many rects in the block? We take as 

503 # many as we can as long as the worst aspect ratio of the block's 

504 # rectangles keeps improving. 

505 

506 # This section is (and should be) be heavily optimised. Each operation 

507 # is run on every element in data. 

508 last_aspect, newstart = np.Inf, start + 1 

509 startsize = cumsize[start] 

510 blockmin = blockmax = size[newstart - 1] 

511 blocksum = cumsize[newstart] - startsize 

512 datasum = cumsize[end] - startsize 

513 ratio = datasum * (h / w if w > h else w / h) 

514 while True: 

515 f = blocksum * blocksum / ratio 

516 aspect = blockmax / f if blockmax > f else f / blockmax 

517 aspect2 = blockmin / f if blockmin > f else f / blockmin 

518 if aspect2 > aspect: 

519 aspect = aspect2 

520 if aspect <= last_aspect: 

521 if newstart < end: 

522 last_aspect = aspect 

523 newstart += 1 

524 val = size[newstart - 1] 

525 if val < blockmin: 525 ↛ 527line 525 didn't jump to line 527, because the condition on line 525 was never false

526 blockmin = val 

527 if val > blockmax: 527 ↛ 528line 527 didn't jump to line 528, because the condition on line 527 was never true

528 blockmax = val 

529 blocksum += val 

530 else: 

531 break 

532 else: 

533 if newstart > start + 1: 533 ↛ 535line 533 didn't jump to line 535, because the condition on line 533 was never false

534 newstart = newstart - 1 

535 break 

536 

537 # Now, lay out the block = start:newstart on the left or top edge. 

538 block = slice(start, newstart) 

539 blocksum = cumsize[newstart] - startsize 

540 scale = blocksum / datasum 

541 blockcumsize = cumsize[block] - startsize 

542 

543 if w > h: 

544 # Layout left-edge, downwards 

545 r = h / blocksum 

546 result[block, 0] = x 

547 result[block, 1] = y + r * blockcumsize 

548 result[block, 2] = dx = w * scale 

549 result[block, 3] = r * size[block] 

550 x, w = x + dx, w - dx 

551 else: 

552 # Layout top-edge, rightwards 

553 r = w / blocksum 

554 result[block, 0] = x + r * blockcumsize 

555 result[block, 1] = y 

556 result[block, 2] = r * size[block] 

557 result[block, 3] = dy = h * scale 

558 y, h = y + dy, h - dy 

559 

560 start = newstart 

561 

562 return np.nan_to_num(result) 

563 

564 

565class SubTreemap(object): 

566 ''' 

567 Yield a hierarchical treemap at multiple levels. 

568 

569 Usage: 

570 SubTreemap( 

571 data=data, 

572 keys=['Parent', 'Child'], 

573 values={'Value':sum}, 

574 size=lambda x: x['Value'], 

575 sort=None, 

576 padding=0, 

577 aspect=1) 

578 

579 yields: 

580 x, y, w, h, (level, data) 

581 ''' 

582 

583 def __init__(self, **args): 

584 '''Default Constructor.''' 

585 self.args = args 

586 

587 def draw(self, width, height, x=0, y=0, filter={}, level=0): 

588 '''Function to draw rectanfles.''' 

589 # We recursively into each column in `keys` and stop there 

590 if level >= len(self.args['keys']): 

591 return 

592 

593 # Start with the base dataset. Filter by each key applied so far 

594 summary = self.args['data'] 

595 for key in filter: 595 ↛ 596line 595 didn't jump to line 596, because the loop on line 595 never started

596 summary = summary[summary[key] == filter[key]] 

597 

598 # Aggregate by the key up to the current level 

599 summary = summary.groupby( 

600 self.args['keys'][:level + 1] 

601 ).agg(self.args.get('values', {})) 

602 for key in self.args['keys'][:level + 1]: 

603 if hasattr(summary, 'reset_index'): 603 ↛ 608line 603 didn't jump to line 608, because the condition on line 603 was never false

604 # Just pop the key out. .reset_index(key) should do this. 

605 # But on Pandas 0.20.1, this fails 

606 summary = summary.reset_index([summary.index.names.index(key)]) 

607 else: 

608 summary[key] = summary.index 

609 

610 # If specified, sort the aggregated data 

611 if 'sort' in self.args and callable(self.args['sort']): 611 ↛ 614line 611 didn't jump to line 614, because the condition on line 611 was never false

612 summary = self.args['sort'](summary) 

613 

614 pad = self.args.get('padding', 0) 

615 aspect = self.args.get('aspect', 1) 

616 

617 # Find the positions of each box at this level 

618 key = self.args['keys'][level] 

619 rows = (summary.to_records() if hasattr(summary, 'to_records') else summary) 

620 

621 rects = squarified(x, y * aspect, width, height * aspect, self.args['size'](rows)) 

622 for i2, (x2, y2, w2, h2) in enumerate(rects): 

623 v2 = rows[i2] 

624 y2, h2 = y2 / aspect, h2 / aspect 

625 # Ignore invalid boxes generated by Squarified 

626 if ( 626 ↛ 631line 626 didn't jump to line 631

627 np.isnan([x2, y2, w2, h2]).any() or 

628 np.isinf([x2, y2, w2, h2]).any() or 

629 w2 < 0 or h2 < 0 

630 ): 

631 continue 

632 

633 # For each box, dive into the next level 

634 filter2 = dict(filter) 

635 filter2.update({key: v2[key]}) 

636 for output in self.draw(w2 - 2 * pad, h2 - 2 * pad, x=x2 + pad, y=y2 + pad, 636 ↛ 638line 636 didn't jump to line 638, because the loop on line 636 never started

637 filter=filter2, level=level + 1): 

638 yield output 

639 

640 # Once we've finished yielding smaller boxes, yield the parent box 

641 yield x2, y2, w2, h2, (level, v2) 

642 

643 

644class TableProperties(): 

645 '''Get/Set Table's properties.''' 

646 

647 def extend_table(self, shape, data, total_rows, total_columns): 

648 '''Function to extend table rows and columns if required.''' 

649 avail_rows = len(shape.table.rows) 

650 avail_cols = len(shape.table.columns) 

651 

652 col_width = shape.table.columns[0].width 

653 row_height = shape.table.rows[0].height 

654 # Extending Table Rows if required based on the data 

655 while avail_rows < total_rows: 

656 shape.table.rows._tbl.add_tr(row_height) 

657 avail_rows += 1 

658 # Extending Table Columns if required based on the data 

659 while avail_cols < total_columns: 

660 shape.table._tbl.tblGrid.add_gridCol(col_width) 

661 avail_cols += 1 

662 

663 def get_default_css(self, shape): 

664 '''Function to get Table style for rows and columns.''' 

665 pixel_inch = 10000 

666 tbl_style = {} 

667 mapping = {0: 'header', 1: 'row'} 

668 for row_num in range(len(list(shape.table.rows)[:2])): 

669 style = {} 

670 txt = shape.table.rows[row_num].cells[0].text_frame.paragraphs[0] 

671 

672 if txt.alignment: 672 ↛ 675line 672 didn't jump to line 675, because the condition on line 672 was never false

673 style['text-align'] = '{}'.format(txt.alignment).split()[0] 

674 

675 if not hasattr(txt, 'runs'): 675 ↛ 676line 675 didn't jump to line 676, because the condition on line 675 was never true

676 txt.add_run() 

677 if txt.runs: 677 ↛ 684line 677 didn't jump to line 684, because the condition on line 677 was never false

678 txt = txt.runs[0].font 

679 style['bold'] = txt.bold 

680 style['italic'] = txt.italic 

681 style['font-size'] = (txt.size / pixel_inch) if txt.size else txt.size 

682 style['font-family'] = txt.name 

683 style['underline'] = txt.underline 

684 tbl_style[mapping[row_num]] = style 

685 

686 if 'row' not in tbl_style or not len(tbl_style['row']): 686 ↛ 687line 686 didn't jump to line 687, because the condition on line 686 was never true

687 tbl_style['row'] = copy.deepcopy(tbl_style['header']) 

688 

689 if 'font-size' not in tbl_style['row']: 689 ↛ 690line 689 didn't jump to line 690, because the condition on line 689 was never true

690 tbl_style['row']['font-size'] = tbl_style['header'].get('font-size', None) 

691 

692 return tbl_style 

693 

694 def get_css(self, info, column_list, data): 

695 '''Get Table CSS from config.''' 

696 columns = info.get('columns', {}) 

697 table_css = {} 

698 for col in column_list: 

699 common_css = copy.deepcopy(info.get('style', {})) 

700 common_css.update(columns.get(col, {})) 

701 if 'gradient' in common_css: 701 ↛ 702line 701 didn't jump to line 702, because the condition on line 701 was never true

702 common_css['min'] = common_css.get('min', data[col].min()) 

703 common_css['max'] = common_css.get('max', data[col].max()) 

704 table_css[col] = common_css 

705 return table_css 

706 

707 def apply_table_css(self, cell, paragraph, run, info): 

708 '''Apply Table style.''' 

709 if info.get('fill'): 

710 cell_fill = cell.fill 

711 cell_fill.solid() 

712 cell_fill.fore_color.rgb = RGBColor.from_string(convert_color_code(info['fill'])) 

713 apply_text_css(cell, run, paragraph, **info)