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

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
19def is_slide_allowed(change, slide, number):
20 '''
21 Given a change like one of the below::
23 slide-number: 1
24 slide-number: [1, 2, 3]
25 slide-title: 'company'
26 slide-title: ['company', 'industry']
28 ... return True if:
30 1. ``number`` matches a slide-number
31 2. ``slide`` title matches a slide-title regex (case-insensitive)
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
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
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()
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'))
107def delete_paragraph(paragraph):
108 '''Delete a paragraph.'''
109 p = paragraph._p
110 parent_element = p.getparent()
111 parent_element.remove(p)
114def delete_run(run):
115 '''Delete a run from paragraph.'''
116 r = run._r
117 r.getparent().remove(r)
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]
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')
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)
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])
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]
169def manage_slides(prs, config):
170 '''
171 Delete not required slides from presentation.
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
193def is_group(shape):
194 # TODO: implement this
195 return shape.element.tag.endswith('}grpSp')
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)
208def scale(series, lo=None, hi=None):
209 '''
210 Returns the values linearly scaled from 0 - 1.
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.
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.
221 Examples::
223 >>> stats.scale([1, 2, 3, 4, 5])
224 # array([ 0. , 0.25, 0.5 , 0.75, 1. ])
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)
235def decimals(series):
236 '''
237 Given a ``series`` of numbers, returns the number of decimals
238 *just enough* to differentiate between most numbers.
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
245 Examples::
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
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))))
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)))
276# Custom Charts Functions below(Sankey, Treemap, Calendarmap).
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())
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}
317def fill_color(**kwargs):
318 '''
319 Return a new color object.
321 You may use any one of the following ways of specifying colour:
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
330 One or more of these modifiers may be specified:
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
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')
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
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)
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
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)
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)
442 text = spec.get('text')
443 order = spec.get('order')
444 fill_color = spec.get('color')
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
467def squarified(x, y, w, h, data):
468 '''
469 Draw a squarified treemap.
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.
474 Examples::
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]])
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
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.
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
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
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
560 start = newstart
562 return np.nan_to_num(result)
565class SubTreemap(object):
566 '''
567 Yield a hierarchical treemap at multiple levels.
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)
579 yields:
580 x, y, w, h, (level, data)
581 '''
583 def __init__(self, **args):
584 '''Default Constructor.'''
585 self.args = args
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
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]]
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
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)
614 pad = self.args.get('padding', 0)
615 aspect = self.args.get('aspect', 1)
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)
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
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
640 # Once we've finished yielding smaller boxes, yield the parent box
641 yield x2, y2, w2, h2, (level, v2)
644class TableProperties():
645 '''Get/Set Table's properties.'''
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)
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
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]
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]
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
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'])
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)
692 return tbl_style
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
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)