Coverage for gramex\pptgen\utils.py: 82%

# Coverage for gramex\pptgen\utils.py : 82%

## 368 statements 320 run 48 missing 0 excluded 29 partial

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

2 import re

3 import ast

4 import copy

5 import platform

6 import six

7 from six import iteritems

8 import numpy as np

9 import pandas as pd

10 from lxml import objectify

11 from lxml.builder import ElementMaker

12 from pptx.util import Inches

13 from pptx.dml.color import RGBColor

14 from pptx.enum.base import EnumValue

15 from pptx.enum.text import PP_ALIGN, MSO_ANCHOR

16 from gramex.transforms import build_transform

17

18

19 def 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

57 def 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

87 def 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

107 def delete_paragraph(paragraph):

108 '''Delete a paragraph.'''

109 p = paragraph._p

110 parent_element = p.getparent()

111 parent_element.remove(p)

112

113

114 def delete_run(run):

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

116 r = run._r

117 r.getparent().remove(r)

118

119

120 def 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

129 def 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

139 def 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

150 def 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

158 def 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

169 def 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

193 def is_group(shape):

194 # TODO: implement this

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

196

197

198 def 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

208 def 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

235 def 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

271 def 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

279 def 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

304 def 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

317 def 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

392 def 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

398 def 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

405 def 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

431 def 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

467 def 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

565 class 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

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