Skip to content

Commit

Permalink
Merge pull request spotify#39 from spotify/second_y_axis
Browse files Browse the repository at this point in the history
Added second axis
  • Loading branch information
cphalpert authored Feb 16, 2019
2 parents 9e5dd9e + c8465ba commit 91eed29
Show file tree
Hide file tree
Showing 11 changed files with 544 additions and 200 deletions.
16 changes: 16 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@
History
=======

2.4.0 (2019-02-16)
------------------

Improvements:

* Added second Y axis plotting.
* Removed Bokeh loading notification on import (Thanks @canavandl!)
* Added support for custom Bokeh resource loading (Thanks @canavandl!)
* Added example for Chart.save() method (Thanks @david30907d!)

Bugfixes:

* Updated documentation for saving and showing svgs.
* Fixed bug that broke plots with no difference between min and max
points. (Thanks for finding @fabioconcina!)

2.3.5 (2018-11-21)
------------------

Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Chartify
|status| |release| |python|

.. |status| image:: https://img.shields.io/badge/Status-Beta-blue.svg
.. |release| image:: https://img.shields.io/badge/Release-2.3.5-blue.svg
.. |release| image:: https://img.shields.io/badge/Release-2.4.0-blue.svg
.. |python| image:: https://img.shields.io/badge/Python-3.6-blue.svg

Chartify is a Python library that makes it easy for data scientists to create charts.
Expand Down
2 changes: 1 addition & 1 deletion chartify/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

__author__ = """Chris Halpert"""
__email__ = '[email protected]'
__version__ = '2.3.5'
__version__ = '2.4.0'

_IPYTHON_INSTANCE = False

Expand Down
121 changes: 81 additions & 40 deletions chartify/_core/axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,59 @@
import pandas as pd
import bokeh
from bokeh.models.tickers import FixedTicker
from bokeh.models import LinearAxis, LogAxis, DataRange1d
from math import pi


class BaseAxes:
class YAxisMixin:

def __init__(self):
self._y_axis_index = 0
self._y_range = self._chart.figure.y_range

@property
def yaxis_label(self):
"""Return y-axis label.
Returns:
y-axis label text
"""
return self._chart.figure.yaxis[self._y_axis_index].axis_label

def set_yaxis_label(self, label):
"""Set y-axis label text.
Args:
label (string): the text for the y-axis label
Returns:
Current chart object
"""
self._chart.figure.yaxis[self._y_axis_index].axis_label = label
return self._chart

def hide_yaxis(self):
"""Hide the tick labels, ticks, and axis lines of the y-axis.
The y-axis label will remain visible, but can be
removed with .axes.set_yaxis_label("")
"""
self._chart.figure.yaxis[self._y_axis_index].axis_line_alpha = 0
self._chart.figure.yaxis[
self._y_axis_index].major_tick_line_color = None
self._chart.figure.yaxis[
self._y_axis_index].minor_tick_line_color = None
self._chart.figure.yaxis[
self._y_axis_index].major_label_text_color = None
return self._chart


class BaseAxes(YAxisMixin):
"""Base class for axes."""

def __init__(self, chart):
self._chart = chart
super(BaseAxes, self).__init__()
self._initialize_defaults()

@classmethod
Expand Down Expand Up @@ -119,27 +164,6 @@ def set_xaxis_label(self, label):
self._chart.figure.xaxis.axis_label = label
return self._chart

@property
def yaxis_label(self):
"""Return y-axis label.
Returns:
y-axis label text
"""
return self._chart.figure.yaxis.axis_label

def set_yaxis_label(self, label):
"""Set y-axis label text.
Args:
label (string): the text for the y-axis label
Returns:
Current chart object
"""
self._chart.figure.yaxis.axis_label = label
return self._chart

def hide_xaxis(self):
"""Hide the tick labels, ticks, and axis lines of the x-axis.
Expand All @@ -156,19 +180,6 @@ def hide_xaxis(self):

return self._chart

def hide_yaxis(self):
"""Hide the tick labels, ticks, and axis lines of the y-axis.
The y-axis label will remain visible, but can be
removed with .axes.set_yaxis_label("")
"""
self._chart.figure.yaxis.axis_line_alpha = 0
self._chart.figure.yaxis.major_tick_line_color = None
self._chart.figure.yaxis.minor_tick_line_color = None
self._chart.figure.yaxis.major_label_text_color = None

return self._chart

def set_xaxis_tick_orientation(self, orientation='horizontal'):
"""Change the orientation or the x axis tick labels.
Expand Down Expand Up @@ -266,6 +277,7 @@ def set_xaxis_tick_format(self, num_format):


class NumericalYMixin:

def set_yaxis_range(self, start=None, end=None):
"""Set y-axis range.
Expand All @@ -276,8 +288,8 @@ def set_yaxis_range(self, start=None, end=None):
Returns:
Current chart object
"""
self._chart.figure.y_range.end = end
self._chart.figure.y_range.start = start
self._y_range.end = end
self._y_range.start = start
return self._chart

def set_yaxis_tick_values(self, values):
Expand All @@ -289,7 +301,8 @@ def set_yaxis_tick_values(self, values):
Returns:
Current chart object
"""
self._chart.figure.yaxis.ticker = FixedTicker(ticks=values)
self._chart.figure.yaxis[
self._y_axis_index].ticker = FixedTicker(ticks=values)
return self._chart

def set_yaxis_tick_format(self, num_format):
Expand Down Expand Up @@ -321,8 +334,8 @@ def set_yaxis_tick_format(self, num_format):
Returns:
Current chart object
"""
self._chart.figure.yaxis[
0].formatter = bokeh.models.NumeralTickFormatter(format=num_format)
self._chart.figure.yaxis[self._y_axis_index].formatter = (
bokeh.models.NumeralTickFormatter(format=num_format))
return self._chart


Expand Down Expand Up @@ -572,3 +585,31 @@ class CategoricalXYAxes(BaseAxes, CategoricalXMixin, CategoricalYMixin):
def __init__(self, chart):
super(CategoricalXYAxes, self).__init__(chart)
self._chart.style._apply_settings('categorical_xyaxis')


class SecondYNumericalAxis(YAxisMixin, NumericalYMixin):
"""Axis class for second Y numerical axes."""
def __init__(self, chart):
self._chart = chart
self._y_range_name = 'second_y'
self._chart.figure.extra_y_ranges = {
self._y_range_name: DataRange1d(bounds='auto')
}
# Add the appropriate axis type to the figure.
axis_class = LinearAxis
if self._chart._second_y_axis_type == 'log':
axis_class = LogAxis
self._chart.figure.add_layout(
axis_class(y_range_name=self._y_range_name), 'right')

self._y_axis_index = 1
self._y_range = self._chart.figure.extra_y_ranges[self._y_range_name]
self._chart.style._apply_settings('second_y_axis')


class SecondAxis:
"""Class for second axis.
- Plotting (.plot)
- Axes (.axes)
"""
26 changes: 24 additions & 2 deletions chartify/_core/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from selenium.webdriver.chrome.options import Options

from chartify._core.style import Style
from chartify._core.axes import BaseAxes
from chartify._core.axes import BaseAxes, SecondYNumericalAxis, SecondAxis
from chartify._core.plot import BasePlot
from chartify._core.callout import Callout
from chartify._core.options import options
Expand All @@ -57,7 +57,8 @@ def __init__(self,
blank_labels=options.get_option('chart.blank_labels'),
layout='slide_100%',
x_axis_type='linear',
y_axis_type='linear'):
y_axis_type='linear',
second_y_axis=False):
"""Create a chart instance.
Args:
Expand Down Expand Up @@ -91,13 +92,23 @@ def __init__(self,
'linear', 'log', 'datetime', 'categorical', 'density'
]
valid_y_axis_types = ['linear', 'log', 'categorical', 'density']
valid_second_y_axis_types = ['linear', 'log']
if x_axis_type not in valid_x_axis_types:
raise ValueError('x_axis_type must be one of {options}'.format(
options=valid_x_axis_types))
if y_axis_type not in valid_y_axis_types:
raise ValueError('y_axis_type must be one of {options}'.format(
options=valid_y_axis_types))

self._second_y_axis_type = None
if second_y_axis:
self._second_y_axis_type = y_axis_type
if self._second_y_axis_type not in valid_second_y_axis_types:
raise ValueError(
'second_y_axis can only be used when \
y_axis_type is one of {options}'.format(
options=valid_second_y_axis_types))

self._x_axis_type, self._y_axis_type = x_axis_type, y_axis_type

self._blank_labels = options._get_value(blank_labels)
Expand All @@ -110,6 +121,13 @@ def __init__(self,
self.callout = Callout(self)
self.axes = BaseAxes._get_axis_class(self._x_axis_type,
self._y_axis_type)(self)

if self._second_y_axis_type in valid_second_y_axis_types:
self.second_axis = SecondAxis()
self.second_axis.axes = SecondYNumericalAxis(self)
self.second_axis.plot = BasePlot._get_plot_class(
self._x_axis_type, self._second_y_axis_type)(
self, self.second_axis.axes._y_range_name)
self._source = self._add_source_to_figure()
self._subtitle_glyph = self._add_subtitle_to_figure()
self.figure.toolbar.logo = None # Remove bokeh logo from toolbar.
Expand Down Expand Up @@ -346,6 +364,8 @@ def show(self, format='html'):
Easy to copy+paste into slides.
Will render logos.
Recommended when the plot is in a finished state.
- 'svg': Output as SVG.
"""
self._set_toolbar_for_format(format)

Expand Down Expand Up @@ -376,6 +396,8 @@ def save(self, filename, format='html'):
Easy to paste into google slides.
Recommended when the plot is in a finished state.
Will render logos.
- 'svg': Output as SVG.
"""
self._set_toolbar_for_format(format)

Expand Down
22 changes: 15 additions & 7 deletions chartify/_core/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@
class BasePlot:
"""Base for all plot classes."""

def __init__(self, chart):
def __init__(self, chart, y_range_name="default"):
self._chart = chart
self._y_range_name = y_range_name

@staticmethod
def _axis_format_precision(max_value, min_value):
Expand Down Expand Up @@ -115,7 +116,9 @@ def _cannonical_series_name(series_name):

@staticmethod
def _named_column_data_source(data_frame, series_name):
"""Ensure consistent naming of column data sources."""
"""Ensure consistent naming of column data sources.
Naming ensures that Chart.data property will populate correctly.
"""
cannonical_series_name = BasePlot._cannonical_series_name(series_name)
return bokeh.models.ColumnDataSource(
data_frame, name=cannonical_series_name)
Expand Down Expand Up @@ -358,7 +361,8 @@ def line(self,
line_cap=line_cap,
legend=color_value,
line_dash=line_dash,
alpha=alpha)
alpha=alpha,
y_range_name=self._y_range_name)

# Set legend defaults if there are multiple series.
if color_column is not None:
Expand Down Expand Up @@ -432,7 +436,8 @@ def scatter(self,
legend=color_value,
marker=marker,
line_color=color,
alpha=alpha)
alpha=alpha,
y_range_name=self._y_range_name)

# Set legend defaults if there are multiple series.
if color_column is not None:
Expand Down Expand Up @@ -514,7 +519,8 @@ def text(self,
x_offset=x_offset,
angle=angle,
angle_units='deg',
text_font=text_font)
text_font=text_font,
y_range_name=self._y_range_name)
return self._chart

def area(self,
Expand Down Expand Up @@ -619,15 +625,17 @@ def area(self,
alpha=alpha,
source=source,
legend=color_value,
color=color)
color=color,
y_range_name=self._y_range_name)
else:
self._chart.figure.patch(
x=y_column,
y=x_column,
alpha=alpha,
source=source,
legend=color_value,
color=color)
color=color,
y_range_name=self._y_range_name)

# Set legend defaults if there are multiple series.
if color_column is not None:
Expand Down
Loading

0 comments on commit 91eed29

Please sign in to comment.