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

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

## 706 statements 656 run 50 missing 0 excluded 56 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'''Python-PPTX customized module.'''

2 import os

3 import six

4 import copy

5 import logging

6 import requests

7 import tempfile

8 import operator

9 import collections

10 import numpy as np

11 import pandas as pd

12 import matplotlib.cm

13 import matplotlib.colors

14 from lxml import etree

15 from tornado.template import Template

16 from tornado.escape import to_unicode

17 from pptx.chart import data as pptxcd

18 from pptx.dml.color import RGBColor

19 from pptx.enum.shapes import MSO_SHAPE

20 from pptx.oxml.xmlchemy import OxmlElement

21 from six.moves.urllib_parse import urlparse

22 from gramex.transforms import build_transform

23 from . import utils

24 from . import fontwidth

25 from . import color as _color

26

27

28 _template_cache = {}

29

30

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

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

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

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

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

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

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

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

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

192 def _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

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

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

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

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

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

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

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

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

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

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

965 cmdlist = {

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}