Commit 181ab1de authored by S Anand's avatar S Anand

Merge branch 'dev'

parents 957787c3 43ee2def
Pipeline #32595 failed with stage
in 52 seconds
......@@ -15,7 +15,6 @@
*.sqlite3-journal
# Data files. Try not to commit data into the repo
*.csv
*.dat
*.mdb
......@@ -95,3 +94,4 @@ data
# Allow data files and PPTX templates in tests/
!tests/*-input.pptx
!tests/*-data.*
*.bat
\ No newline at end of file
This diff is collapsed.
......@@ -6,6 +6,7 @@ import sys
import copy
import json
import collections
import gramex.data
import pandas as pd
import gramex.cache
from . import commands
......@@ -23,6 +24,8 @@ with io.open(os.path.join(_folder, 'release.json'), encoding='utf-8') as _releas
_release = json.load(_release_file)
__version__ = _release['version']
COMMANDS_LIST = commands.cmdlist
def commandline():
'''
......@@ -74,15 +77,10 @@ def load_data(data_config, handler=None):
for key, conf in data_config.items():
if 'function' in conf:
data[key] = build_transform(conf, vars={'handler': None})(handler=handler)[0]
elif conf.get('format') == 'database':
query = conf.pop('query')
sqlengine = conf.pop('engine')
state = conf.pop('state') if 'state' in conf else None
data[key] = gramex.cache.query(query, sqlengine, state=state)
elif conf.get('ext') in {'yaml', 'yml', 'json'}:
data[key] = gramex.cache.open(conf.pop('url'), conf.pop('ext'), **dict(conf))
else:
path = conf.pop('path')
fmt = conf.pop('format')
data[key] = gramex.cache.open(path, fmt, **conf)
data[key] = gramex.data.filter(conf.pop('url'), **dict(conf))
return data
......@@ -103,6 +101,21 @@ def replicate_slides(data, prs, change, slide, slides_to_remove, index):
copy_slide = None
def register(config):
"""Function to register a new `command` to command list."""
global COMMANDS_LIST
if 'register' in config:
resister_command = config.pop('register')
if not isinstance(resister_command, (dict,)):
raise NotImplementedError()
for command_name, command_function in resister_command.items():
if command_name not in COMMANDS_LIST:
if not isinstance(command_function, (dict,)):
command_function = {'function': command_function}
_vars = {'shape': None, 'spec': None, 'data': None}
COMMANDS_LIST[command_name] = build_transform(command_function, vars=_vars)
def pptgen(source, target, **config):
'''
Process a configuration. This loads a Presentation from source, applies the
......@@ -112,7 +125,11 @@ def pptgen(source, target, **config):
# removed from yaml config.
handler = config.pop('handler') if 'handler' in config else None
_config = copy.deepcopy(config)
data = AttrDict(load_data(_config.pop('data', {}), handler=handler))
if _config.get('is_formhandler', False):
data = _config.pop('data')
_config.pop('is_formhandler')
else:
data = AttrDict(load_data(_config.pop('data', {}), handler=handler))
prs = Presentation(source)
slides = prs.slides
# Loop through each change configuration
......@@ -121,6 +138,7 @@ def pptgen(source, target, **config):
for key, change in _config.items():
# Apply it to every slide
register(change)
slide_data = copy.deepcopy(data)
slide_data['handler'] = handler
if 'data' in change and change['data'] is not None:
......@@ -132,8 +150,7 @@ def pptgen(source, target, **config):
# Restrict to specific slides, if specified
if not is_slide_allowed(change, slide, index + 1):
continue
if 'replicate' in change and change['replicate'] is not None:
if change.get('replicate'):
is_grp = isinstance(slide_data, pd.core.groupby.DataFrameGroupBy)
if isinstance(slide_data, collections.Iterable):
for _slide_data in slide_data:
......@@ -187,7 +204,6 @@ def change_shapes(collection, change, data, **kwargs):
source_slide = kwargs.get('source_slide')
dest = prs.slides.add_slide(new_slide) if copy_slide else None
mapping = {}
for shape in collection:
if shape.name not in change:
......@@ -199,7 +215,7 @@ def change_shapes(collection, change, data, **kwargs):
mapping[shape.name] = 0
handler = data.pop('handler') if 'handler' in data else None
if 'data' in spec and spec['data'] is not None:
if spec.get('data'):
if not isinstance(spec['data'], (dict,)):
spec['data'] = {'function': '{}'.format(spec['data']) if not isinstance(
spec['data'], (str, six.string_types,)) else spec['data']}
......@@ -210,7 +226,7 @@ def change_shapes(collection, change, data, **kwargs):
if isinstance(shape_data, (dict, AttrDict,)):
shape_data['handler'] = handler
if 'stack' in spec and spec.get('stack') is not None:
if spec.get('stack'):
shape_data = shape_data[mapping[shape.name]]
mapping[shape.name] = mapping[shape.name] + 1
......@@ -220,9 +236,8 @@ def change_shapes(collection, change, data, **kwargs):
change_shapes(sub_shapes, spec, shape_data)
# Run commands in the spec
for cmd, method in commands.cmdlist.items():
for cmd, method in COMMANDS_LIST.items():
if cmd in spec:
method(shape, spec, shape_data)
commands.copy_slide_elem(shape, dest)
commands.add_new_slide(dest, source_slide)
......@@ -293,7 +293,7 @@ def distinct(count):
return _DISTINCTS[:]
def contrast(color, white='#fff', black='#000'):
def contrast(color, white='#ffffff', black='#000000'):
"""
Returns the colour (white or black) that contrasts best with a given
color.
......@@ -361,8 +361,8 @@ def brighten(color, percent):
_min = -1
_mid = 0
_max = +1
black = '#000'
white = '#fff'
black = '#0000000'
white = '#ffffff'
return gradient(percent, ((_min, black), (_mid, color), (_max, white)))
......
This diff is collapsed.
......@@ -4,7 +4,7 @@
"url": "http://code.gramener.com/sanjay.yadav/pptgen",
"author": "Sanjay Yadav",
"author_email": "sanjay.yadav@gramener.com",
"version": "0.1.1",
"version": "0.2.0",
"license": "Gramener",
"keywords": [
"powerpoint", "ppt", "pptx", "office", "open xml"
......
......@@ -8,6 +8,8 @@ import pandas as pd
from six import iteritems
from lxml import objectify
from pptx.util import Inches
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
from lxml.builder import ElementMaker
......@@ -177,9 +179,32 @@ def decimals(series):
smallest = (abs(nonnan[0]) or 1) if len(nonnan) > 0 else 1
return int(max(0, np.floor(min_float - np.log10(smallest))))
def conver_color_code(colorcode):
"""Convert color code to valid PPTX color code."""
colorcode = colorcode.rsplit('#')[-1].lower()
return colorcode + ('0' * (6 - len(colorcode)))
# Custom Charts Functions below(Sankey, Treemap, Calendarmap).
def apply_text_css(run, paragraph, **kwargs):
"""Apply css."""
pixcel_to_inch = 10000
if kwargs.get('color'):
rows_text = run.font.fill
rows_text.solid()
run.font.color.rgb = RGBColor.from_string(conver_color_code(kwargs['color']))
if kwargs.get('font-family'):
run.font.name = kwargs['font-family']
if kwargs.get('font-size'):
run.font.size = pixcel_to_inch * kwargs['font-size']
if kwargs.get('text-align'):
paragraph.alignment = getattr(PP_ALIGN, kwargs['text-align'].upper())
for prop in {'bold', 'italic', 'underline'}:
setattr(run.font, prop, kwargs.get(prop))
def make_element():
"""Function to create element structure."""
nsmap = {
......@@ -488,7 +513,7 @@ class SubTreemap(object):
summary[key] = summary.index
# If specified, sort the aggregated data
if 'sort' in self.args and self.args['sort'] is callable:
if 'sort' in self.args and callable(self.args['sort']):
summary = self.args['sort'](summary)
pad = self.args.get('padding', 0)
......@@ -520,3 +545,75 @@ class SubTreemap(object):
# Once we've finished yielding smaller boxes, yield the parent box
yield x2, y2, w2, h2, (level, v2)
class TableProperties():
"""Get/Set Table's properties."""
def extend_table(self, shape, data, total_rows, total_columns):
"""Function to extend table rows and columns if required."""
avail_rows = len(shape.table.rows)
avail_cols = len(shape.table.columns)
col_width = shape.table.columns[0].width
row_height = shape.table.rows[0].height
# Extending Table Rows if required based on the data
while avail_rows < total_rows:
shape.table.rows._tbl.add_tr(row_height)
avail_rows += 1
# Extending Table Columns if required based on the data
while avail_cols < total_columns:
shape.table._tbl.tblGrid.add_gridCol(col_width)
avail_cols += 1
def get_default_css(self, shape):
"""Function to get Table style for rows and columns."""
pixel_inch = 10000
tbl_style = {}
mapping = {0: 'header', 1: 'row'}
for row_num in range(len(list(shape.table.rows)[:2])):
style = {}
txt = shape.table.rows[row_num].cells[0].text_frame.paragraphs[0]
if txt.alignment:
style['text-align'] = '{}'.format(txt.alignment).split()[0]
if not hasattr(txt, 'runs'):
txt.add_run()
if txt.runs:
txt = txt.runs[0].font
style['bold'] = txt.bold
style['italic'] = txt.italic
style['font-size'] = (txt.size / pixel_inch) if txt.size else txt.size
style['font-family'] = txt.name
style['underline'] = txt.underline
tbl_style[mapping[row_num]] = style
if 'row' not in tbl_style or not len(tbl_style['row']):
tbl_style['row'] = copy.deepcopy(tbl_style['header'])
if 'font-size' not in tbl_style['row']:
tbl_style['row']['font-size'] = tbl_style['header'].get('font-size', None)
return tbl_style
def get_css(self, info, column_list, data):
"""Get Table CSS from config."""
columns = info.get('columns', {})
table_css = {}
for col in column_list:
common_css = copy.deepcopy(info.get('style', {}))
common_css.update(columns.get(col, {}))
if 'gradient' in common_css:
common_css['min'] = common_css.get('min', data[col].min())
common_css['max'] = common_css.get('min', data[col].max())
table_css[col] = common_css
return table_css
def apply_table_css(self, cell, paragraph, run, info):
"""Apply Table style."""
if info.get('fill'):
cell_fill = cell.fill
cell_fill.solid()
cell_fill.fore_color.rgb = RGBColor.from_string(conver_color_code(info['fill']))
apply_text_css(run, paragraph, **info)
source: bar-circle-input.pptx
target: bar-circle-output.pptx
data:
bardata: {ext: csv, url: bardata.csv}
circledata: {ext: csv, url: circledata.csv}
barcircle-config:
bar-circle:
bar_circle:
bar:
data: data['bardata']
y: FTE
x: Region
text:
function: "lambda v: '{:.2f}'.format(v['FTE'])"
circle:
data: data['circledata']
y: "Y"
size: Size
x: Category
style:
bar:
font-size: 14
color: '#ff0000'
fill: '#1A9850'
stroke: '#cccccc'
opacity: 0.3
circle:
font-size: 14
opacity: 0.7
fill: '#FE9929'
stroke: '#00441B'
Region,FTE
NE,87
Central,49
SE,80
SW,60
West,65
FSO,45
NTD,85
TAS,56
PAS-US,51
source: bubble-input.pptx
target: bubble-output.pptx
data:
bubble_data: {ext: csv, url: charts-data.csv}
change-bubble:
bubble_chart:
chart:
data: data['bubble_data']
x: Category
size: PPresY
color:
function: "{'CurY': '#D73027', 'PreY': '#1A9850', 'PPresY': '#FFFFBF'}"
source: bullet-input.pptx
target: bullet-output.pptx
data:
bullet_data: {'format': 'csv', 'path': 'bullet-data.csv'}
bullet_data: {ext: 'csv', url: 'bullet-data.csv'}
draw-bullet:
bullet-chart-horizontal:
......@@ -14,6 +14,9 @@ draw-bullet:
orient: horizontal
text:
function: "lambda v: '%.1f' % v"
style:
font-size: 10
color: '#ff0000'
bullet-chart-vertical:
bullet:
......@@ -26,3 +29,15 @@ draw-bullet:
gradient: 'Oranges'
text:
function: "lambda v: '%.1f' % v"
style:
data:
font-size: 16
color: '#ff00ff'
bold: True
fill: '#ff0000'
stroke: '00ff00'
target:
font-size: 20
color: '#0000ff'
bold: False
fill: '#00ff00'
\ No newline at end of file
source: calendarmap-input.pptx
target: calendarmap-output.pptx
data:
calendar_data: {format: csv, path: calendarmap-data.csv, parse_dates: ['date_time'], encoding: 'utf-8'}
calendar_data: {ext: csv, url: calendarmap-data.csv, parse_dates: ['date_time'], encoding: 'utf-8'}
draw-calendar:
Calendar_Map:
......
source: charts-input.pptx
target: charts-output.pptx
data:
chart_data: {format: csv, path: charts-data.csv}
chart_data: {ext: csv, url: charts-data.csv}
edit-chart:
bar_chart:
chart:
data: data['chart_data']
# query: "(Category in ('Hyderabad', 'Mumbai')) & (PPresY + PreY >= 2)"
columns:
Cities: {value: Category, description: Number of people in the district}
Size: {formula: PreY / PPresY, description: Number of people in each household}
x: Cities
size: Size
color: Colors
x: Category
size: PreY
color:
function: "{'CurY': '#D73027', 'PreY': '#1A9850', 'PPresY': '#FFFFBF', 'Size': '#cccccc'}"
No preview for this file type
X,Y,Size,Category
1,87,1,NE
2,49,2,Central
3,80,1,SE
4,60,1,SW
5,65,3,West
6,45,1,FSO
7,85,1,NTD
8,56,2,TAS
9,51,1,PAS-US
1,45,4,NE
2,85,3,Central
3,56,1,SE
4,49,1,SW
5,80,1,West
6,87,3,FSO
7,87,1,NTD
8,45,1,TAS
9,85,4,PAS-US
1,56,1,NE
2,49,2,Central
3,80,1,SE
4,60,1,SW
5,80,1,West
6,56,3,FSO
7,45,1,NTD
8,85,1,TAS
9,56,1,PAS-US
1,80,1,NE
2,60,4,Central
3,56,1,SE
4,45,1,SW
5,85,4,West
6,56,1,FSO
7,87,1,NTD
8,49,1,TAS
9,80,1,PAS-US
source: combo-input.pptx
target: combo-output.pptx
data:
combo_data: {ext: csv, url: charts-data.csv}
change-combo:
combo-charts:
area:
chart:
data: data['combo_data'][['Category', 'CurY']]
x: Category
color:
function: "{'CurY': '#D73027', 'PreY': '#1A9850', 'PPresY': '#FFFFBF'}"
opacity: 0.50
bar_chart:
chart:
data: data['combo_data']
x: Category
size: PreY
color:
function: "{'CurY': '#D73027', 'PreY': '#1A9850', 'PPresY': '#FFFFBF'}"
# opacity: 0.5
\ No newline at end of file
source: complex-input.pptx
target: complex-output.pptx
data:
complex_data: {format: yaml, path: complex-data.yaml}
complex_data: {ext: yaml, url: complex-data.yaml}
edit-complex:
data: data['complex_data']
......
Practices,BOTs,BOT Utilization,Process Hrs,FTE Equivalent,Clients Coverage,Exceptions,Revenue,Margin,Savings
All Practices,34,50,68,30,52,37,46,50,51
Mobility,34,70,79,60,90,97,70,62,59
SUTC,56,48,58,52,96,35,68,92,58
FSO,54,95,41,45,71,85,71,87,92
WOTC,77,56,60,71,46,75,65,30,66
GCS,33,77,72,85,33,98,79,57,93
EYOS,76,95,57,86,77,82,47,53,97
source: custom-table-input.pptx
target: custom-table-output.pptx
data:
table_data: {ext: csv, url: custom-table-data.csv}
edit-table:
custom-table:
custom_table:
data: data['table_data']
source: heatgrid-input.pptx
target: heatgrid-output.pptx
data:
heatgrid_data: {ext: csv, url: heatgrid-data.csv}
change-heatgrid:
heatgrid_shape:
heatgrid:
data: data['heatgrid_data']
row: name
column: hour
value: value
text: True
left-margin: 0.20
cell-width: 30
cell-height: 30
na-text: NA
na-color: '#cccccc'
style:
gradient: RdYlGn
color: '#ff0000'
font-size: 14
margin: 10
text-align: center
\ No newline at end of file
hour,name,value
1,Process 1,16
2,Process 1,20
3,Process 1,80
4,Process 1,80
5,Process 1,80
6,Process 1,2
7,Process 1,80
8,Process 1,9
9,Process 1,25
10,Process 1,49
11,Process 1,57
12,Process 1,61
13,Process 1,37
14,Process 1,66
15,Process 1,70
16,Process 1,55
17,Process 1,51
18,Process 1,55
19,Process 1,77
20,Process 1,20
21,Process 1,95
22,Process 1,94
23,Process 1,80
24,Process 1,12
1,Process 2,86
2,Process 2,92
3,Process 2,80
4,Process 2,80
5,Process 2,80
6,Process 2,72
7,Process 2,74
8,Process 2,11
9,Process 2,28
10,Process 2,49
11,Process 2,51
12,Process 2,47
13,Process 2,38
14,Process 2,65
15,Process 2,60
16,Process 2,50
17,Process 2,65
18,Process 2,50
19,Process 2,22
20,Process 2,11
21,Process 2,12
22,Process 2,92
23,Process 2,80
24,Process 2,13
1,Process 3,5
2,Process 3,8
3,Process 3,8
4,Process 3,80
5,Process 3,80
6,Process 3,2
7,Process 3,75
8,Process 3,12
9,Process 3,34
10,Process 3,43
11,Process 3,54
12,Process 3,44
13,Process 3,40
14,Process 3,48
15,Process 3,54
16,Process 3,59