Skip to content

Commit c2e6e38

Browse files
committed
refactor into format
1 parent df44c92 commit c2e6e38

File tree

3 files changed

+240
-53
lines changed

3 files changed

+240
-53
lines changed

pandas/core/format.py

+214-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# pylint: disable=W0141
55

66
import sys
7+
import uuid
78

89
from pandas.core.base import PandasObject
910
from pandas.core.common import adjoin, notnull
@@ -848,10 +849,9 @@ class HTMLFormatter(TableFormatter):
848849
indent_delta = 2
849850

850851
def __init__(self, formatter, classes=None, max_rows=None, max_cols=None,
851-
notebook=False, style=style):
852+
notebook=False):
852853
self.fmt = formatter
853854
self.classes = classes
854-
self.style = style
855855

856856
self.frame = self.fmt.frame
857857
self.columns = self.fmt.tr_frame.columns
@@ -1229,6 +1229,218 @@ def _write_hierarchical_rows(self, fmt_values, indent):
12291229
nindex_levels=frame.index.nlevels)
12301230

12311231

1232+
1233+
class StyleFormatter(object):
1234+
1235+
def __init__(self, data, rows=None, columns=None, table=None):
1236+
self.data = data
1237+
self.u = str(uuid.uuid1()).replace("-", "_")
1238+
rows, columns, table = self.format_style_args(data, rows, columns,
1239+
table)
1240+
self.rows = rows
1241+
self.columns = columns
1242+
self.table = table
1243+
1244+
from jinja2 import Template
1245+
1246+
self.t = Template("""
1247+
<style type="text/css" >
1248+
#T_{{uuid}} tr {
1249+
border: none;
1250+
}
1251+
#T_{{uuid}} {
1252+
border: none;
1253+
}
1254+
#T_{{uuid}} th.blank {
1255+
border: none;
1256+
}
1257+
1258+
{% for s in style %}
1259+
#T_{{uuid}} {{s.selector}} {
1260+
{% for p,val in s.props %}
1261+
{{p}}: {{val}};
1262+
{% endfor %}
1263+
}
1264+
{% endfor %}
1265+
{% for s in cellstyle %}
1266+
#T_{{uuid}}{{s.selector}} {
1267+
{% for p,val in s.props %}
1268+
{{p}}: {{val}};
1269+
{% endfor %}
1270+
}
1271+
{% endfor %}
1272+
</style>
1273+
1274+
<table id="T_{{uuid}}">
1275+
{% if caption %}
1276+
<caption>{{caption}}</caption>
1277+
{% endif %}
1278+
1279+
<thead>
1280+
{% for r in head %}
1281+
<tr>
1282+
{% for c in r %}
1283+
<{{c.type}} class="{{c.class}}">{{c.value}}</th>
1284+
{% endfor %}
1285+
</tr>
1286+
{% endfor %}
1287+
</thead>
1288+
<tbody>
1289+
{% for r in body %}
1290+
<tr>
1291+
{% for c in r %}
1292+
<{{c.type}} id="T_{{uuid}}{{c.id}}" class="{{c.class}}">{{c.value}}</th>
1293+
{% endfor %}
1294+
</tr>
1295+
{% endfor %}
1296+
</tbody>
1297+
</table>
1298+
""")
1299+
1300+
@staticmethod
1301+
def format_style_args(data, rows=None, columns=None, table=None):
1302+
"""
1303+
rows/columns:
1304+
- function
1305+
- list
1306+
- dict
1307+
dict : {row/col}: [function]
1308+
"""
1309+
def cond(v):
1310+
return (callable(v) or com.is_list_like(v)
1311+
and not isinstance(v, dict))
1312+
1313+
if cond(rows):
1314+
rows = {r: rows for r in data.index}
1315+
if cond(columns):
1316+
columns = {c: columns for c in data.columns}
1317+
if callable(table) or com.is_list_like(table):
1318+
table = list(table)
1319+
1320+
rows = rows or {}
1321+
columns = columns or {}
1322+
table = table or {}
1323+
1324+
rows = {row: [x] if not com.is_list_like(x) else x
1325+
for row, x in rows.items()}
1326+
columns = {col: [x] if not com.is_list_like(x) else x
1327+
for col, x in columns.items()}
1328+
table = [table] if not com.is_list_like(table) else table
1329+
1330+
return rows, columns, table
1331+
1332+
def _build_styles(self, rows=None, columns=None, table=None):
1333+
"""
1334+
Build a DataFrame indexed like ``self.data`` containing the CSS
1335+
property: value pairs.
1336+
"""
1337+
from pandas.tools.merge import concat
1338+
data = self.data
1339+
final = [] # (row, col, prop, value)
1340+
1341+
# rows, columns, table = self._format_style_args(rows, columns, table)
1342+
# TODO: ensure index handling is correct
1343+
if rows is not None:
1344+
for row in rows:
1345+
for func in rows[row]:
1346+
props, values = zip(*func(data.loc[row]))
1347+
final.append(data.__class__({'row': row, 'col': data.columns,
1348+
'prop': props, 'value': values}))
1349+
if columns is not None:
1350+
for col in columns:
1351+
for func in columns[col]:
1352+
props, values = zip(*func(data[col]))
1353+
final.append(data.__class__({'row': data.index, 'col': col,
1354+
'prop': props, 'value': values}))
1355+
1356+
if table is not None:
1357+
for func in table:
1358+
r = func(data)
1359+
for col in r:
1360+
try:
1361+
prop, values = zip(*r[col])
1362+
except ValueError:
1363+
props, values = '', ''
1364+
final.append(data.__clas__({'row': r.index, 'col': col,
1365+
'prop': props, 'value': values}))
1366+
1367+
r = concat(final)[['row', 'col', 'prop', 'value']]
1368+
# I don't feel great about this.
1369+
# Could use a MultiIndex of [(row/col), (prop, value)]
1370+
# Or a dumber structure like a dict.
1371+
r = (r['prop'] + ':' + r['value']).groupby([r['row'], r['col']]).agg(
1372+
lambda x: '; '.join(x))
1373+
1374+
return r.unstack()
1375+
1376+
def translate(self):
1377+
"""
1378+
Convert the DataFrame in `self.data` and the attrs from `_build_styles`
1379+
into a dictionary of {head, body, uuid, cellstyle}
1380+
"""
1381+
attrs = self._build_styles(self.rows, self.columns, self.table)
1382+
ROW_HEADING_CLASS = "row_heading"
1383+
COL_HEADING_CLASS = "col_heading"
1384+
DATA_CLASS = "data"
1385+
BLANK_CLASS = "blank"
1386+
BLANK_VALUE = ""
1387+
1388+
cell_context = dict()
1389+
1390+
n_rlvls = self.data.index.nlevels
1391+
n_clvls = self.data.columns.nlevels
1392+
rlabels = self.data.index.tolist()
1393+
clabels = self.data.columns.tolist()
1394+
1395+
if n_rlvls == 1:
1396+
rlabels = [[x] for x in rlabels]
1397+
if n_clvls == 1:
1398+
clabels = [[x] for x in clabels]
1399+
clabels=list(zip(*clabels))
1400+
1401+
style = []
1402+
head = []
1403+
1404+
for r in range(n_clvls):
1405+
row_es = [{"type": "th", "value": BLANK_VALUE,
1406+
"class": " ".join([BLANK_CLASS])}] * n_rlvls
1407+
for c in range(len(clabels[0])):
1408+
cs = [COL_HEADING_CLASS, "level%s" % r, "col%s" % c]
1409+
cs.extend(cell_context.get("col_headings", {}).get(r, {}).get(c, []))
1410+
row_es.append({"type": "th", "value": clabels[r][c],
1411+
"class": " ".join(cs)})
1412+
head.append(row_es)
1413+
1414+
body = []
1415+
for r, idx in enumerate(self.data.index):
1416+
cs = [ROW_HEADING_CLASS, "level%s" % c, "row%s" % r]
1417+
cs.extend(cell_context.get("row_headings", {}).get(r, {}).get(c, []))
1418+
row_es = [{"type": "th", "value": rlabels[r][c], "class": " ".join(cs)}
1419+
for c in range(len(rlabels[r]))]
1420+
1421+
for c, col in enumerate(self.data.columns):
1422+
cs = [DATA_CLASS,"row%s" % r, "col%s" % c]
1423+
cs.extend(cell_context.get("data", {}).get(r, {}).get(c, []))
1424+
row_es.append({"type": "td", "value": self.data.iloc[r][c],
1425+
"class": " ".join(cs), "id": "_".join(cs[1:])})
1426+
try:
1427+
style.append(
1428+
{'props': [x.split(":")
1429+
for x in attrs.loc[idx, col].split('; ')],
1430+
'selector': "row%s_col%s" % (r, c)})
1431+
except KeyError:
1432+
pass
1433+
body.append(row_es)
1434+
1435+
# uuid required to isolate table styling from others
1436+
# in same notebook in ipnb
1437+
u = str(uuid.uuid1()).replace("-","_")
1438+
return dict(head=head, cellstyle=style, body=body, uuid=u)
1439+
1440+
def render(self):
1441+
return(self.t.render(**self.translate()))
1442+
1443+
12321444
def _get_level_lengths(levels, sentinel=''):
12331445
from itertools import groupby
12341446

pandas/core/frame.py

+5-51
Original file line numberDiff line numberDiff line change
@@ -1446,7 +1446,7 @@ def to_html(self, buf=None, columns=None, col_space=None, colSpace=None,
14461446
header=True, index=True, na_rep='NaN', formatters=None,
14471447
float_format=None, sparsify=None, index_names=True,
14481448
justify=None, bold_rows=True, classes=None, escape=True,
1449-
max_rows=None, max_cols=None, show_dimensions=False, style=None,
1449+
max_rows=None, max_cols=None, show_dimensions=False,
14501450
notebook=False):
14511451
"""
14521452
Render a DataFrame as an HTML table.
@@ -1487,61 +1487,15 @@ def to_html(self, buf=None, columns=None, col_space=None, colSpace=None,
14871487
max_cols=max_cols,
14881488
show_dimensions=show_dimensions)
14891489
# TODO: a generic formatter wld b in DataFrameFormatter
1490-
formatter.to_html(classes=classes, style=style, notebook=notebook)
1490+
formatter.to_html(classes=classes, notebook=notebook)
14911491

14921492
if buf is None:
14931493
return formatter.buf.getvalue()
14941494

14951495
def style(self, rows=None, columns=None, table=None):
1496-
styles = self._build_styles(
1497-
rows=rows, columns=columns, table=table
1498-
)
1499-
return self.to_html(styles=styles)
1500-
1501-
def _format_sytle_args(self, rows=None, columns=None, frame=None):
1502-
# TODO: hanlde rows=func.
1503-
rows = {row: [x] if not com.is_list_like(x) else x
1504-
for row, x in rows.items()}
1505-
columns = {col: [x] if not com.is_list_like(x) else x
1506-
for col, x in columns.items()}
1507-
frame = [x] if not com.is_list_like(x) else x
1508-
return rows, columns, frame
1509-
1510-
1511-
def _build_styles(self, rows=None, columns=None, frame=None):
1512-
final = [] # (row, col, prop, value)
1513-
1514-
rows, columns, frame = self._format_sytle_args(rows, columns, frame)
1515-
1516-
if rows is not None:
1517-
for row in rows:
1518-
for func in rows[row]:
1519-
props, values = zip(*func(self.loc[row]))
1520-
final.append(pd.DataFrame({'row': row, 'col': self.columns,
1521-
'prop': props, 'value': values}))
1522-
if columns is not None:
1523-
for col in columns:
1524-
for func in columns[col]:
1525-
props, values = zip(*func(self[col]))
1526-
final.append(pd.DataFrame({'row': self.index, 'col': col,
1527-
'prop': props, 'value': values}))
1528-
1529-
if frame is not None:
1530-
for func in frame:
1531-
r = func(self)
1532-
for col in r:
1533-
try:
1534-
prop, values = zip(*r[col])
1535-
except ValueError:
1536-
prop, values = '', ''
1537-
final.append(pd.DataFrame({'row': r.index, 'col': col,
1538-
'prop': props, 'value': values}))
1539-
1540-
r = pd.concat(final)[['row', 'col', 'prop', 'value']]
1541-
r = (r['prop'] + ':' + r['value']).groupby([r['row'], r['col']]).agg(
1542-
lambda x: '; '.join(x))
1543-
1544-
return r.unstack()
1496+
styler = fmt.StylerFormatter(self, rows=rows, columns=columns,
1497+
table=table)
1498+
return styler
15451499

15461500
@Appender(fmt.common_docstring + fmt.return_docstring, indents=1)
15471501
def to_latex(self, buf=None, columns=None, col_space=None, colSpace=None,

pandas/tests/test_format.py

+21
Original file line numberDiff line numberDiff line change
@@ -4071,6 +4071,27 @@ def test_tz_dateutil(self):
40714071
dt_datetime_us = datetime(2013, 1, 2, 12, 1, 3, 45, tzinfo=utc)
40724072
self.assertEqual(str(dt_datetime_us), str(Timestamp(dt_datetime_us)))
40734073

4074+
4075+
class TestStyler(tm.TestCase):
4076+
4077+
def setUp(self):
4078+
np.random.seed(24)
4079+
self.s = pd.DataFrame({'A': np.random.permutation(range(6))})
4080+
self.df = pd.DataFrame({'A': np.linspace(1, 5, 5), 'B': np.random.randn(5)})
4081+
4082+
@staticmethod
4083+
def color_bg_range(s, cmap='PuBu'):
4084+
"""Color background in a range."""
4085+
import matplotlib.pyplot as plt
4086+
from matplotlib.colors import rgb2hex
4087+
colors = [rgb2hex(x) for x in plt.cm.get_cmap(cmap)(s)]
4088+
return pd.Series([('backgroud-color', color) for color in colors])
4089+
4090+
def test_format_style_args(self):
4091+
expected = ({}, {'A': [self.color_bg_range]}, {})
4092+
result = self.df._format_style_args(columns={"A": self.color_bg_range})
4093+
self.assertEqual(result, expected)
4094+
40744095
if __name__ == '__main__':
40754096
nose.runmodule(argv=[__file__, '-vvs', '-x', '--pdb', '--pdb-failure'],
40764097
exit=False)

0 commit comments

Comments
 (0)