Skip to content

Commit

Permalink
Table improvements (#3160)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr authored and jlstevens committed Nov 8, 2018
1 parent 6ddd311 commit 8324e7a
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 92 deletions.
89 changes: 44 additions & 45 deletions holoviews/plotting/bokeh/plot.py
Expand Up @@ -82,6 +82,14 @@ class BokehPlot(DimensionedPlot):
_merged_tools = ['pan', 'box_zoom', 'box_select', 'lasso_select',
'poly_select', 'ypan', 'xpan']

_title_template = (
'<span style='
'"color:{color};font-family:{font};'
'font-style:{fontstyle};font-weight:{fontstyle};' # italic/bold
'font-size:{fontsize}">'
'{title}</span>'
)

backend = 'bokeh'

@property
Expand Down Expand Up @@ -315,6 +323,37 @@ def _fontsize(self, key, label='fontsize', common=True):
return {k: v if isinstance(v, basestring) else '%spt' % v
for k, v in size.items()}

def _get_title_div(self, key, default_fontsize='15pt', width=450):
title_div = None
title = self._format_title(key) if self.show_title else ''
if not title:
return title_div

title_json = theme_attr_json(self.renderer.theme, 'Title')
color = title_json.get('text_color', None)
font = title_json.get('text_font', 'Arial')
fontstyle = title_json.get('text_font_style', 'bold')
fontsize = self._fontsize('title').get('fontsize', default_fontsize)
if fontsize == default_fontsize: # if default
fontsize = title_json.get('text_font_size', default_fontsize)
if 'em' in fontsize:
# it's smaller than it shosuld be so add 0.25
fontsize = str(float(fontsize[:-2]) + 0.25) + 'em'

title_tags = self._title_template.format(
color=color,
font=font,
fontstyle=fontstyle,
fontsize=fontsize,
title=title)

if 'title' in self.handles:
title_div = self.handles['title']
else:
title_div = Div(width=width) # so it won't wrap long titles easily
title_div.text = title_tags

return title_div

def sync_sources(self):
"""
Expand Down Expand Up @@ -386,46 +425,6 @@ class CompositePlot(BokehPlot):
{'title': '15pt'}""")

_title_template = (
'<span style='
'"color:{color};font-family:{font};'
'font-style:{fontstyle};font-weight:{fontstyle};' # italic/bold
'font-size:{fontsize}">'
'{title}</span>'
)

def _get_title(self, key):
title_div = None
title = self._format_title(key) if self.show_title else ''
if not title:
return title_div

title_json = theme_attr_json(self.renderer.theme, 'Title')
color = title_json.get('text_color', None)
font = title_json.get('text_font', 'Arial')
fontstyle = title_json.get('text_font_style', 'bold')
fontsize = self._fontsize('title')['fontsize']
if fontsize == '15pt': # if default
fontsize = title_json.get('text_font_size', '15pt')
if 'em' in fontsize:
# it's smaller than it shosuld be so add 0.25
fontsize = str(float(fontsize[:-2]) + 0.25) + 'em'

title_tags = self._title_template.format(
color=color,
font=font,
fontstyle=fontstyle,
fontsize=fontsize,
title=title)

if 'title' in self.handles:
title_div = self.handles['title']
else:
title_div = Div(width=450) # so it won't wrap long titles easily
title_div.text = title_tags

return title_div

def _link_dimensioned_streams(self):
"""
Should perform any linking required to update titles when dimensioned
Expand All @@ -439,7 +438,7 @@ def _stream_update(self, **kwargs):
contents = [k for s in self.streams for k in s.contents]
key = tuple(None if d in contents else k for d, k in zip(self.dimensions, self.current_key))
key = wrap_tuple_streams(key, self.dimensions, self.streams)
self._get_title(key)
self._get_title_div(key)

@property
def current_handles(self):
Expand Down Expand Up @@ -608,7 +607,7 @@ def initialize_plot(self, ranges=None, plots=[]):
merge_tools=self.merge_tools)
plot = self._make_axes(plot)

title = self._get_title(self.keys[-1])
title = self._get_title_div(self.keys[-1])
if title:
plot = Column(title, plot)
self.handles['title'] = title
Expand Down Expand Up @@ -683,7 +682,7 @@ def update_frame(self, key, ranges=None):
subplot = self.subplots.get(wrap_tuple(coord), None)
if subplot is not None:
subplot.update_frame(key, ranges)
title = self._get_title(key)
title = self._get_title_div(key)
if title:
self.handles['title']

Expand Down Expand Up @@ -952,7 +951,7 @@ def initialize_plot(self, plots=None, ranges=None):
toolbar_location=self.toolbar,
merge_tools=self.merge_tools, **kwargs)

title = self._get_title(self.keys[-1])
title = self._get_title_div(self.keys[-1])
if title:
self.handles['title'] = title
layout_plot = Column(title, layout_plot, **kwargs)
Expand Down Expand Up @@ -983,7 +982,7 @@ def update_frame(self, key, ranges=None):
subplot = self.subplots.get((r, c), None)
if subplot is not None:
subplot.update_frame(key, ranges)
title = self._get_title(key)
title = self._get_title_div(key)
if title:
self.handles['title'] = title

Expand Down
15 changes: 12 additions & 3 deletions holoviews/plotting/bokeh/tabular.py
@@ -1,5 +1,6 @@
import param

from bokeh.models import Column
from bokeh.models.widgets import (
DataTable, TableColumn, NumberEditor, NumberFormatter, DateFormatter,
DateEditor, StringFormatter, StringEditor, IntEditor
Expand Down Expand Up @@ -67,15 +68,22 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None):
style['reorderable'] = False
table = DataTable(source=source, columns=columns, height=self.height,
width=self.width, **style)
self.handles['plot'] = table
self.handles['table'] = table
self.handles['glyph_renderer'] = table
self._execute_hooks(element)
self.drawn = True

for cb in self.callbacks:
cb.initialize()

return table
title = self._get_title_div(self.keys[-1], '10pt')
if title:
plot = Column(title, table)
self.handles['title'] = title
else:
plot = table
self.handles['plot'] = plot
return plot

def _get_columns(self, element, data):
columns = []
Expand Down Expand Up @@ -108,6 +116,7 @@ def update_frame(self, key, ranges=None, plot=None):
to the key.
"""
element = self._get_frame(key)
self._get_title_div(key, '12pt')

# Cache frame object id to skip updating data if unchanged
previous_id = self.handles.get('previous_id', None)
Expand All @@ -122,5 +131,5 @@ def update_frame(self, key, ranges=None, plot=None):
style = self.lookup_options(element, 'style')[self.cyclic_index]
data, _, style = self.get_data(element, ranges, style)
columns = self._get_columns(element, data)
self.handles['plot'].columns = columns
self.handles['table'].columns = columns
self._update_datasource(source, data)
97 changes: 55 additions & 42 deletions holoviews/plotting/mpl/tabular.py
Expand Up @@ -47,36 +47,42 @@ class TablePlot(ElementPlot):

def __init__(self, table, **params):
super(TablePlot, self).__init__(table, **params)
self.cell_widths = self._format_table()
if not self.dynamic:
self.cell_widths = self._format_table()
else:
self.cell_widths = None


def _format_table(self):
cell_widths = defaultdict(int)
for key in self.keys:
frame = self._get_frame(key)
if frame is None:
element = self._get_frame(key)
if element is None:
continue

# Mapping from the cell coordinates to the dictionary key.
summarize = frame.rows > self.max_rows
half_rows = self.max_rows//2
rows = min([self.max_rows, frame.rows])
for row in range(rows):
adjusted_row = row
for col in range(frame.cols):
if summarize and row == half_rows:
cell_text = "..."
else:
if summarize and row > half_rows:
adjusted_row = (frame.rows - self.max_rows + row)
cell_text = frame.pprint_cell(adjusted_row, col)
if len(cell_text) > self.max_value_len:
cell_text = cell_text[:(self.max_value_len-3)]+'...'
if len(cell_text) + 2 > cell_widths[col]:
cell_widths[col] = len(cell_text) + 2
self._update_cell_widths(element, cell_widths)
return cell_widths


def _update_cell_widths(self, element, cell_widths):
# Mapping from the cell coordinates to the dictionary key.
summarize = element.rows > self.max_rows
half_rows = self.max_rows//2
rows = min([self.max_rows, element.rows])
for row in range(rows):
adjusted_row = row
for col in range(element.cols):
if summarize and row == half_rows:
cell_text = "..."
else:
if summarize and row > half_rows:
adjusted_row = (element.rows - self.max_rows + row)
cell_text = element.pprint_cell(adjusted_row, col)
if len(cell_text) > self.max_value_len:
cell_text = cell_text[:(self.max_value_len-3)]+'...'
if len(cell_text) + 2 > cell_widths[col]:
cell_widths[col] = len(cell_text) + 2


def _cell_value(self, element, row, col):
summarize = element.rows > self.max_rows
half_rows = self.max_rows//2
Expand All @@ -93,14 +99,30 @@ def _cell_value(self, element, row, col):

@mpl_rc_context
def initialize_plot(self, ranges=None):

# Render table
axes = self.handles['axis']
element = self.hmap.last
axis = self.handles['axis']
table = self._render_table(element, axes)
self.handles['artist'] = table

# Add to axes
axes.set_axis_off()
axes.add_table(table)
return self._finalize_axis(self.keys[-1], element=element)


def _render_table(self, element, axes):
if self.dynamic:
cell_widths = defaultdict(int)
self._update_cell_widths(element, cell_widths)
else:
cell_widths = self.cell_widths

axis.set_axis_off()
size_factor = (1.0 - 2*self.border)
table = mpl_Table(axis, bbox=[self.border, self.border,
table = mpl_Table(axes, bbox=[self.border, self.border,
size_factor, size_factor])
total_width = sum(self.cell_widths.values())
total_width = sum(cell_widths.values())
height = size_factor / element.rows

summarize = element.rows > self.max_rows
Expand All @@ -113,27 +135,18 @@ def initialize_plot(self, ranges=None):
adjusted_row = (element.rows - self.max_rows + row)
cell_value = self._cell_value(element, row, col)
cellfont = self.font_types.get(element.cell_type(adjusted_row,col), None)
width = self.cell_widths[col] / float(total_width)
width = cell_widths[col] / float(total_width)
font_kwargs = dict(fontproperties=cellfont) if cellfont else {}
table.add_cell(row, col, width, height, text=cell_value, loc='center',
**font_kwargs)

table.set_fontsize(self.max_font_size)
table.auto_set_font_size(True)
axis.add_table(table)
return table

self.handles['artist'] = table

return self._finalize_axis(self.keys[-1], element=element)


def update_handles(self, key, axis, element, ranges, style):
table = self.handles['artist']

for coords, cell in table.get_celld().items():
value = self._cell_value(element, *coords)
cell.set_text_props(text=value)

# Resize fonts across table as necessary
table.set_fontsize(self.max_font_size)
table.auto_set_font_size(True)
def update_handles(self, key, axes, element, ranges, style):
table = self._render_table(element, axes)
self.handles['artist'].remove()
axes.add_table(table)
self.handles['artist'] = table
return {}
4 changes: 2 additions & 2 deletions holoviews/tests/plotting/bokeh/testtabular.py
Expand Up @@ -69,7 +69,7 @@ def test_table_change_columns(self):
table = DynamicMap(lambda a: Table(range(lengths[a]), a), kdims=['a']).redim.values(a=['a', 'b', 'c'])
plot = bokeh_renderer.get_plot(table)
self.assertEqual(sorted(plot.handles['source'].data.keys()), ['a'])
self.assertEqual(plot.handles['plot'].columns[0].title, 'a')
self.assertEqual(plot.handles['table'].columns[0].title, 'a')
plot.update(('b',))
self.assertEqual(sorted(plot.handles['source'].data.keys()), ['b'])
self.assertEqual(plot.handles['plot'].columns[0].title, 'b')
self.assertEqual(plot.handles['table'].columns[0].title, 'b')

0 comments on commit 8324e7a

Please sign in to comment.