Skip to content

Commit

Permalink
Add tick formatter plot options (#3042)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr authored and jlstevens committed Oct 5, 2018
1 parent 299ee5b commit 89ca901
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 39 deletions.
27 changes: 1 addition & 26 deletions examples/user_guide/Plotting_with_Bokeh.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -144,32 +144,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"### Controlling axes"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Bokeh provides a variety of options to control the axes. Here we provide a quick overview of linked plots for the same data displayed differently by applying log axes, disabling axes, rotating ticks, specifying the number of ticks, and supplying an explicit list of ticks."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"points = hv.Points(np.exp(xs)) \n",
"axes_opts = [('Plain', {}),\n",
" ('Log', {'logy': True}),\n",
" ('None', {'yaxis': None}),\n",
" ('Rotate', {'xrotation': 90}),\n",
" ('N Ticks', {'xticks': 3}),\n",
" ('List Ticks', {'xticks': [0, 20, 50, 90]})]\n",
"\n",
"hv.Layout([points.relabel(group=group).options(**opts)\n",
" for group, opts in axes_opts]).cols(3)"
"## Controlling axes"
]
},
{
Expand Down
64 changes: 64 additions & 0 deletions examples/user_guide/Styling_Plots.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,70 @@
"\n",
"categorical_points.sort('Category').options(color_index='Category', cmap=explicit_mapping, size=5)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Controlling axes"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"A number of options to control the axes are shared across backend. Here we provide a quick overview of linked plots for the same data displayed differently by applying log axes, disabling axes, rotating ticks, specifying the number of ticks, and supplying an explicit list of ticks."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"points = hv.Points(np.exp(xs)) \n",
"axes_opts = [('Plain', {}),\n",
" ('Log', {'logy': True}),\n",
" ('None', {'yaxis': None}),\n",
" ('Rotate', {'xrotation': 90}),\n",
" ('N Ticks', {'xticks': 3}),\n",
" ('List Ticks', {'xticks': [0, 20, 50, 90]})]\n",
"\n",
"hv.Layout([points.relabel(group=group).options(**opts)\n",
" for group, opts in axes_opts]).cols(3)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Tick formatters"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Tick formatting works very differently in different backends, however the ``xformatter`` and ``yformatter`` options try to minimize these differences. Tick formatters may be defined in one of three formats:\n",
"\n",
"* A classic format string such as ``'%d'``, ``'%.3f'`` or ``'%d'`` which may also contain other characters (``'$%.2f'``)\n",
"* A function which will be compiled to JS using flexx (if installed) when using bokeh\n",
"* A ``bokeh.models.TickFormatter`` in bokeh and a ``matplotlib.ticker.Formatter`` instance in matplotlib\n",
"\n",
"Here is a small example demonstrating how to use the string format and function approaches:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def formatter(value):\n",
" return str(value) + ' %'\n",
"\n",
"points.options(xformatter=formatter, yformatter='$%.2f')"
]
}
],
"metadata": {
Expand Down
30 changes: 28 additions & 2 deletions holoviews/plotting/bokeh/element.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import warnings
from types import FunctionType

import param
import numpy as np
import bokeh
import bokeh.plotting
from bokeh.core.properties import value
from bokeh.models import (HoverTool, Renderer, Range1d, DataRange1d, Title,
FactorRange, FuncTickFormatter, Tool, Legend)
FactorRange, FuncTickFormatter, Tool, Legend,
TickFormatter, PrintfTickFormatter)
from bokeh.models.tickers import Ticker, BasicTicker, FixedTicker, LogTicker
from bokeh.models.widgets import Panel, Tabs
from bokeh.models.mappers import LinearColorMapper
Expand Down Expand Up @@ -107,6 +109,14 @@ class ElementPlot(BokehPlot, GenericElementPlot):
The toolbar location, must be one of 'above', 'below',
'left', 'right', None.""")

xformatter = param.ClassSelector(
default=None, class_=(util.basestring, TickFormatter, FunctionType), doc="""
Formatter for ticks along the x-axis.""")

yformatter = param.ClassSelector(
default=None, class_=(util.basestring, TickFormatter, FunctionType), doc="""
Formatter for ticks along the x-axis.""")

_categorical = False

# Declares the default types for continuous x- and y-axes
Expand Down Expand Up @@ -421,7 +431,23 @@ def _axis_properties(self, axis, key, plot, dimension=None,
else:
axis_props['ticker'] = FixedTicker(ticks=ticker)

if FuncTickFormatter is not None and ax_mapping and dimension:
formatter = self.xformatter if axis == 'x' else self.yformatter
if formatter:
if isinstance(formatter, TickFormatter):
pass
elif isinstance(formatter, FunctionType):
msg = ('%sformatter could not be '
'converted to tick formatter. ' % axis)
jsfunc = py2js_tickformatter(formatter, msg)
if jsfunc:
formatter = FuncTickFormatter(code=jsfunc)
else:
formatter = None
else:
formatter = PrintfTickFormatter(format=formatter)
if formatter is not None:
axis_props['formatter'] = formatter
elif FuncTickFormatter is not None and ax_mapping and dimension:
formatter = None
if dimension.value_format:
formatter = dimension.value_format
Expand Down
30 changes: 22 additions & 8 deletions holoviews/plotting/mpl/element.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import math
from types import FunctionType

import param
import numpy as np
Expand Down Expand Up @@ -40,6 +41,18 @@ class ElementPlot(GenericElementPlot, MPLPlot):
logz = param.Boolean(default=False, doc="""
Whether to apply log scaling to the y-axis of the Chart.""")

xformatter = param.ClassSelector(
default=None, class_=(util.basestring, ticker.Formatter, FunctionType), doc="""
Formatter for ticks along the x-axis.""")

yformatter = param.ClassSelector(
default=None, class_=(util.basestring, ticker.Formatter, FunctionType), doc="""
Formatter for ticks along the y-axis.""")

zformatter = param.ClassSelector(
default=None, class_=(util.basestring, ticker.Formatter, FunctionType), doc="""
Formatter for ticks along the z-axis.""")

zaxis = param.Boolean(default=True, doc="""
Whether to display the z-axis.""")

Expand Down Expand Up @@ -111,7 +124,7 @@ def _finalize_axis(self, key, element=None, title=None, dimensions=None, ranges=
self._subplot_label(axis)

# Apply axis options if axes are enabled
if element and not any(not sp._has_axes for sp in [self] + subplots):
if element is not None and not any(not sp._has_axes for sp in [self] + subplots):
# Set axis labels
if dimensions:
self._set_labels(axis, dimensions, xlabel, ylabel, zlabel)
Expand Down Expand Up @@ -164,13 +177,13 @@ def _finalize_ticks(self, axis, dimensions, xticks, yticks, zticks):

# Tick formatting
if xdim:
self._set_axis_formatter(axis.xaxis, xdim)
self._set_axis_formatter(axis.xaxis, xdim, self.xformatter)
if ydim:
self._set_axis_formatter(axis.yaxis, ydim)
self._set_axis_formatter(axis.yaxis, ydim, self.yformatter)
if self.projection == '3d':
zdim = dimensions[2] if ndims > 2 else None
if zdim:
self._set_axis_formatter(axis.zaxis, zdim)
if zdim or self.zformatter is not None:
self._set_axis_formatter(axis.zaxis, zdim, self.zformatter)

xticks = xticks if xticks else self.xticks
self._set_axis_ticks(axis.xaxis, xticks, log=self.logx,
Expand Down Expand Up @@ -222,13 +235,14 @@ def _set_labels(self, axes, dimensions, xlabel=None, ylabel=None, zlabel=None):
axes.set_zlabel(zlabel, **self._fontsize('zlabel'))


def _set_axis_formatter(self, axis, dim):
def _set_axis_formatter(self, axis, dim, formatter):
"""
Set axis formatter based on dimension formatter.
"""
if isinstance(dim, list): dim = dim[0]
formatter = None
if dim.value_format:
if formatter is not None:
pass
elif dim.value_format:
formatter = dim.value_format
elif dim.type in dim.type_formatters:
formatter = dim.type_formatters[dim.type]
Expand Down
3 changes: 3 additions & 0 deletions holoviews/plotting/mpl/raster.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,13 @@ class RasterGridPlot(GridPlot, OverlayPlot):
ylim = param.Parameter(precedence=-1)
zlim = param.Parameter(precedence=-1)
xticks = param.Parameter(precedence=-1)
xformatter = param.Parameter(precedence=-1)
yticks = param.Parameter(precedence=-1)
yformatter = param.Parameter(precedence=-1)
zticks = param.Parameter(precedence=-1)
zaxis = param.Parameter(precedence=-1)
zrotation = param.Parameter(precedence=-1)
zformatter = param.Parameter(precedence=-1)


def __init__(self, layout, keys=None, dimensions=None, create_axes=False, ranges=None,
Expand Down
48 changes: 46 additions & 2 deletions tests/plotting/bokeh/testelementplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

try:
from bokeh.document import Document
from bokeh.models import FuncTickFormatter
from bokeh.models import FuncTickFormatter, PrintfTickFormatter, NumeralTickFormatter
except:
pass

Expand All @@ -24,6 +24,50 @@ def test_element_show_frame_disabled(self):
plot = bokeh_renderer.get_plot(curve).state
self.assertEqual(plot.outline_line_alpha, 0)

def test_element_xformatter_string(self):
curve = Curve(range(10)).options(xformatter='%d')
plot = bokeh_renderer.get_plot(curve)
xaxis = plot.handles['xaxis']
self.assertIsInstance(xaxis.formatter, PrintfTickFormatter)
self.assertEqual(xaxis.formatter.format, '%d')

def test_element_yformatter_string(self):
curve = Curve(range(10)).options(yformatter='%d')
plot = bokeh_renderer.get_plot(curve)
yaxis = plot.handles['yaxis']
self.assertIsInstance(yaxis.formatter, PrintfTickFormatter)
self.assertEqual(yaxis.formatter.format, '%d')

def test_element_xformatter_function(self):
def formatter(value):
return str(value) + ' %'
curve = Curve(range(10)).options(xformatter=formatter)
plot = bokeh_renderer.get_plot(curve)
xaxis = plot.handles['xaxis']
self.assertIsInstance(xaxis.formatter, FuncTickFormatter)

def test_element_yformatter_function(self):
def formatter(value):
return str(value) + ' %'
curve = Curve(range(10)).options(yformatter=formatter)
plot = bokeh_renderer.get_plot(curve)
yaxis = plot.handles['yaxis']
self.assertIsInstance(yaxis.formatter, FuncTickFormatter)

def test_element_xformatter_instance(self):
formatter = NumeralTickFormatter()
curve = Curve(range(10)).options(xformatter=formatter)
plot = bokeh_renderer.get_plot(curve)
xaxis = plot.handles['xaxis']
self.assertIs(xaxis.formatter, formatter)

def test_element_yformatter_instance(self):
formatter = NumeralTickFormatter()
curve = Curve(range(10)).options(yformatter=formatter)
plot = bokeh_renderer.get_plot(curve)
yaxis = plot.handles['yaxis']
self.assertIs(yaxis.formatter, formatter)

def test_empty_element_visibility(self):
curve = Curve([])
plot = bokeh_renderer.get_plot(curve)
Expand Down Expand Up @@ -151,7 +195,7 @@ def test_colormapper_symmetric(self):
cmapper = plot.handles['color_mapper']
self.assertEqual(cmapper.low, -3)
self.assertEqual(cmapper.high, 3)

def test_colormapper_color_levels(self):
cmap = process_cmap('viridis', provider='bokeh')
img = Image(np.array([[0, 1], [2, 3]])).options(color_levels=5, cmap=cmap)
Expand Down
Loading

0 comments on commit 89ca901

Please sign in to comment.