Skip to content

Commit

Permalink
Implement OHLC plot (#623)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed May 22, 2021
1 parent 8d3c7ec commit e1aeb0c
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 3 deletions.
123 changes: 123 additions & 0 deletions examples/reference/pandas/ohlc.ipynb
@@ -0,0 +1,123 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import hvplot.pandas # noqa"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`ohlc` is a useful chart type to visualize stock movements"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from bokeh.sampledata import stocks\n",
"\n",
"df = pd.DataFrame(stocks.AAPL)\n",
"df['date'] = pd.to_datetime(df.date)\n",
"\n",
"df.iloc[-50:].hvplot.ohlc(grid=True)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can control the `neg_color`, `pos_color`, `line_color` and `bar_width`:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"df.iloc[-50:].hvplot.ohlc(neg_color='indianred', pos_color='chartreuse', line_color='gray', bar_width=0.9, grid=True)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"By default `ohlc` will assume the `index` OR the first datetime column should be mapped to the x-axis and the first four non-datetime columns correspond to the O (open), H (high), L (low) and C (close) components. The default call is therefore equivalent to:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"df.iloc[-50:].hvplot.ohlc('date', ['open', 'low', 'high', 'close'], grid=True)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Using the HoloViews `RangeToolLink` we can make it easy to scroll through the data while still seeing an overview of the overall timeseries:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from holoviews.plotting.links import RangeToolLink\n",
"\n",
"df_2012 = df[df.date > pd.to_datetime('2012')]\n",
"\n",
"ohlc = df_2012.hvplot.ohlc(ylabel='Price ($)', grid=True, xaxis=None)\n",
"overview = df_2012.hvplot.ohlc(yaxis=None, height=150, fields={'date': 'Date'})\n",
"volume = df_2012.hvplot.step('date', 'volume', height=100, xaxis=None)\n",
"\n",
"RangeToolLink(overview.get(0), ohlc.get(0))\n",
"\n",
"layout = (volume + ohlc + overview).cols(1)\n",
"\n",
"layout.opts(merge_tools=False)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.8"
},
"widgets": {
"application/vnd.jupyter.widget-state+json": {
"state": {},
"version_major": 2,
"version_minor": 0
}
}
},
"nbformat": 4,
"nbformat_minor": 4
}
54 changes: 51 additions & 3 deletions hvplot/converter.py
Expand Up @@ -20,7 +20,7 @@
Curve, Scatter, Area, Bars, BoxWhisker, Dataset, Distribution,
Table, HeatMap, Image, HexTiles, QuadMesh, Bivariate, Histogram,
Violin, Contours, Polygons, Points, Path, Labels, RGB, ErrorBars,
VectorField
VectorField, Rectangles, Segments
)
from holoviews.plotting.bokeh import OverlayPlot, colormap_generator
from holoviews.plotting.util import process_cmap
Expand Down Expand Up @@ -224,7 +224,9 @@ class HoloViewsConverter(object):
the default is 'Wikipedia'.
"""

_gridded_types = ['image', 'contour', 'contourf', 'quadmesh', 'rgb', 'points', 'dataset']
_gridded_types = [
'image', 'contour', 'contourf', 'quadmesh', 'rgb', 'points', 'dataset'
]

_geom_types = ['paths', 'polygons']

Expand Down Expand Up @@ -258,6 +260,7 @@ class HoloViewsConverter(object):
'step' : ['where'],
'area' : ['y2'],
'errorbars': ['yerr1', 'yerr2'],
'ohlc' : ['bar_width', 'pos_color', 'neg_color', 'line_color'],
'hist' : ['bins', 'bin_range', 'normed', 'cumulative'],
'heatmap' : ['C', 'reduce_function', 'logz'],
'hexbin' : ['C', 'reduce_function', 'gridsize', 'logz', 'min_count'],
Expand All @@ -284,7 +287,7 @@ class HoloViewsConverter(object):
'bar': Bars, 'barh': Bars, 'contour': Contours, 'contourf': Polygons,
'points': Points, 'polygons': Polygons, 'paths': Path, 'step': Curve,
'labels': Labels, 'rgb': RGB, 'errorbars': ErrorBars,
'vectorfield': VectorField,
'vectorfield': VectorField, 'ohlc': Rectangles
}

_colorbar_types = ['image', 'hexbin', 'heatmap', 'quadmesh', 'bivariate',
Expand Down Expand Up @@ -1743,6 +1746,51 @@ def bivariate(self, x=None, y=None, data=None):
opts = self._get_opts('Bivariate', **self.kwds)
return Bivariate(data, [x, y]).redim(**self._redim).opts(**opts)

def ohlc(self, x=None, y=None, data=None):
data = self.data if data is None else data
if x is None:
variables = [var for var in self.variables if var not in self.indexes]
if data[variables[0]].dtype.kind == 'M':
x = variables[0]
else:
x = self.indexes[0]
width = self.kwds.get('bar_width', 0.5)
if y is None:
o, h, l, c = [col for col in data.columns if col != x][:4]
else:
o, h, l, c = y
neg, pos = self.kwds.get('neg_color', 'red'), self.kwds.get('pos_color', 'green')
color_exp = (dim(o)>dim(c)).categorize({True: neg, False: pos})
ds = Dataset(data, [x], [o, h, l, c])
if ds.data[x].dtype.kind in 'SUO':
rects = Rectangles(ds, [x, o, x, c])
else:
if len(ds):
sampling = np.min(np.diff(ds[x])) * width/2.
ds = ds.transform(lbound=dim(x)-sampling, ubound=dim(x)+sampling)
else:
ds = ds.transform(lbound=dim(x), ubound=dim(x))
rects = Rectangles(ds, ['lbound', o, 'ubound', c])
segments = Segments(ds, [x, l, x, h])
rect_opts = self._get_opts('Rectangles')
rect_opts.pop('tools')
rect_opts['color'] = color_exp
seg_opts = self._get_opts('Segments')
tools = seg_opts.pop('tools', [])
if 'hover' in tools:
tools[tools.index('hover')] = HoverTool(tooltips=[
(x, '@{%s}' % x), ('Open', '@{%s}' % o), ('High', '@{%s}' % h),
('Low', '@{%s}' % l), ('Close', '@{%s}' % c)
])
seg_opts['tools'] = tools
seg_opts ['color'] = self.kwds.get('line_color', 'black')
if 'xlabel' not in seg_opts:
seg_opts['xlabel'] = '' if x == 'index' else x
if 'ylabel' not in seg_opts:
seg_opts['ylabel'] = ''
return (segments.redim(**self._redim).opts(**seg_opts) *
rects.redim(**self._redim).opts(**rect_opts))

def table(self, x=None, y=None, data=None):
data = self.data if data is None else data
if isinstance(data.index, (DatetimeIndex, MultiIndex)):
Expand Down
21 changes: 21 additions & 0 deletions hvplot/plotting/core.py
Expand Up @@ -118,6 +118,7 @@ class hvPlotTabular(hvPlotBase):
'scatter',
'area',
'errorbars',
'ohlc',
'heatmap',
'hexbin',
'bivariate',
Expand Down Expand Up @@ -242,6 +243,26 @@ def errorbars(self, x=None, y=None, yerr1=None, yerr2=None, **kwds):
return self(x, y, kind='errorbars',
yerr1=yerr1, yerr2=yerr2, **kwds)

def ohlc(self, x=None, y=None, **kwds):
"""
OHLC
Parameters
----------
x: string, optional
Field name to draw x coordinates from.
y: list or tuple, optional
Field names of the OHLC columns
**kwds : optional
Keyword arguments to pass on to
:py:meth:`hvplot.converter.HoloViewsConverter`.
Returns
-------
obj : HoloViews object
The HoloViews representation of the plot.
"""
return self(kind='ohlc', x=x, y=y, **kwds)

def heatmap(self, x=None, y=None, C=None, colorbar=True, **kwds):
"""
HeatMap plot
Expand Down

0 comments on commit e1aeb0c

Please sign in to comment.