diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index 5e2aba1..e9a1275 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -4,11 +4,11 @@ on: [push, pull_request] jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: max-parallel: 5 matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: ["3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v3 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 0b974ff..aeb0c92 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -106,7 +106,7 @@ Before you submit a pull request, check that it meets these guidelines: 2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst. -3. The pull request should work for Python 3.6. Check +3. The pull request should work for Python 3.8, 3.9 and 3.10. Check https://github.com/spotify/chartify/actions and make sure that the tests pass for all supported Python versions. diff --git a/HISTORY.rst b/HISTORY.rst index b383043..7e4877c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,6 +2,11 @@ History ======= +4.0.0 (2023-03-23) +------------------ + +* Dropped support for python 3.6 and 3.7 + 3.1.0 (2023-03-22) ------------------ diff --git a/chartify/__init__.py b/chartify/__init__.py index b244e6d..6581379 100644 --- a/chartify/__init__.py +++ b/chartify/__init__.py @@ -13,6 +13,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +# flake8: noqa """Top-level package for chartify.""" from chartify._core.chart import Chart from chartify._core.radar_chart import RadarChart @@ -21,8 +22,8 @@ from chartify import examples __author__ = """Chris Halpert""" -__email__ = 'chalpert@spotify.com' -__version__ = '3.1.0' +__email__ = "chalpert@spotify.com" +__version__ = "4.0.0" _IPYTHON_INSTANCE = False @@ -44,7 +45,7 @@ def set_display_settings(): if curstate().notebook_type is None: # Inline resources uses bokeh.js from the local version. # This enables offline usage. - output_notebook(Resources('inline'), hide_banner=True) + output_notebook(Resources("inline"), hide_banner=True) set_display_settings() diff --git a/chartify/_core/axes.py b/chartify/_core/axes.py index 62001de..2ade04a 100644 --- a/chartify/_core/axes.py +++ b/chartify/_core/axes.py @@ -26,7 +26,6 @@ class YAxisMixin: - def __init__(self): self._y_axis_index = 0 self._y_range = self._chart.figure.y_range @@ -59,12 +58,9 @@ def hide_yaxis(self): 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 + 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 @@ -78,23 +74,21 @@ def __init__(self, chart): @classmethod def _get_axis_class(cls, x_axis_type, y_axis_type): - if x_axis_type == 'categorical' and y_axis_type == 'categorical': + if x_axis_type == "categorical" and y_axis_type == "categorical": return CategoricalXYAxes - elif x_axis_type == 'categorical': + elif x_axis_type == "categorical": return NumericalYAxis - elif y_axis_type == 'categorical': + elif y_axis_type == "categorical": return NumericalXAxis - elif x_axis_type == 'datetime': + elif x_axis_type == "datetime": return DatetimeXNumericalYAxes return NumericalXYAxes @property def _vertical(self): - if self._chart._x_axis_type == 'density': + if self._chart._x_axis_type == "density": return False - elif isinstance(self, (NumericalYAxis, - NumericalXYAxes, - DatetimeXNumericalYAxes)): + elif isinstance(self, (NumericalYAxis, NumericalXYAxes, DatetimeXNumericalYAxes)): return True else: return False @@ -103,8 +97,8 @@ def _initialize_defaults(self): xaxis_label = """ch.axes.set_xaxis_label('label (units)')""" yaxis_label = """ch.axes.set_yaxis_label('label (units)')""" if self._chart._blank_labels: - xaxis_label = '' - yaxis_label = '' + xaxis_label = "" + yaxis_label = "" self.set_xaxis_label(xaxis_label) self.set_yaxis_label(yaxis_label) @@ -112,13 +106,12 @@ def _initialize_defaults(self): def _convert_major_orientation_labels(orientation): """Map the user inputted orientation values to the values expected by bokeh for major labels.""" - if orientation == 'vertical': + if orientation == "vertical": orientation = pi / 180 * 90 - elif orientation == 'diagonal': + elif orientation == "diagonal": orientation = pi / 180 * 45 - elif orientation != 'horizontal': - raise ValueError( - 'Orientation must be `horizontal`, `vertical`, or `diagonal`.') + elif orientation != "horizontal": + raise ValueError("Orientation must be `horizontal`, `vertical`, or `diagonal`.") return orientation def _convert_subgroup_orientation_labels(self, orientation): @@ -126,21 +119,20 @@ def _convert_subgroup_orientation_labels(self, orientation): bokeh for group labels.""" if self._vertical: - horizontal_value = 'parallel' + horizontal_value = "parallel" vertical_value = pi / 180 * 90 else: - horizontal_value = 'normal' - vertical_value = 'parallel' + horizontal_value = "normal" + vertical_value = "parallel" - if orientation == 'horizontal': + if orientation == "horizontal": orientation = horizontal_value - elif orientation == 'vertical': + elif orientation == "vertical": orientation = vertical_value - elif orientation == 'diagonal': + elif orientation == "diagonal": orientation = pi / 180 * 45 else: - raise ValueError( - 'Orientation must be `horizontal`, `vertical`, or `diagonal`.') + raise ValueError("Orientation must be `horizontal`, `vertical`, or `diagonal`.") return orientation @property @@ -180,7 +172,7 @@ def hide_xaxis(self): return self._chart - def set_xaxis_tick_orientation(self, orientation='horizontal'): + def set_xaxis_tick_orientation(self, orientation="horizontal"): """Change the orientation or the x axis tick labels. Args: @@ -194,7 +186,7 @@ def set_xaxis_tick_orientation(self, orientation='horizontal'): orientation = [orientation] * 3 level_1 = orientation[0] - level_2 = orientation[1] if len(orientation) > 1 else 'horizontal' + level_2 = orientation[1] if len(orientation) > 1 else "horizontal" level_3 = orientation[2] if len(orientation) > 2 else level_2 level_1 = self._convert_major_orientation_labels(level_1) @@ -204,11 +196,11 @@ def set_xaxis_tick_orientation(self, orientation='horizontal'): self._chart.figure.xaxis.major_label_orientation = level_1 xaxis = self._chart.figure.xaxis[0] - has_subgroup_label = getattr(xaxis, 'subgroup_label_orientation', None) + has_subgroup_label = getattr(xaxis, "subgroup_label_orientation", None) if has_subgroup_label is not None: self._chart.figure.xaxis.subgroup_label_orientation = level_2 - has_group_label = getattr(xaxis, 'group_label_orientation', None) + has_group_label = getattr(xaxis, "group_label_orientation", None) if has_group_label is not None: self._chart.figure.xaxis.group_label_orientation = level_3 return self._chart @@ -225,8 +217,10 @@ def set_xaxis_range(self, start=None, end=None): Returns: Current chart object """ - self._chart.figure.x_range.end = end - self._chart.figure.x_range.start = start + if end is not None: + self._chart.figure.x_range.end = end + if start is not None: + self._chart.figure.x_range.start = start return self._chart def set_xaxis_tick_values(self, values): @@ -270,14 +264,11 @@ def set_xaxis_tick_format(self, num_format): Returns: Current chart object """ - self._chart.figure.xaxis[0].formatter = ( - bokeh.models.NumeralTickFormatter(format=num_format) - ) + self._chart.figure.xaxis[0].formatter = bokeh.models.NumeralTickFormatter(format=num_format) return self._chart class NumericalYMixin: - def set_yaxis_range(self, start=None, end=None): """Set y-axis range. @@ -288,8 +279,10 @@ def set_yaxis_range(self, start=None, end=None): Returns: Current chart object """ - self._y_range.end = end - self._y_range.start = start + if end is not None: + self._y_range.end = end + if start is not None: + self._y_range.start = start return self._chart def set_yaxis_tick_values(self, values): @@ -301,8 +294,7 @@ def set_yaxis_tick_values(self, values): Returns: Current chart object """ - self._chart.figure.yaxis[ - self._y_axis_index].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): @@ -334,8 +326,7 @@ def set_yaxis_tick_format(self, num_format): Returns: Current chart object """ - self._chart.figure.yaxis[self._y_axis_index].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 @@ -420,7 +411,7 @@ def hide_yaxis(self): pass return self._chart - def set_yaxis_tick_orientation(self, orientation='horizontal'): + def set_yaxis_tick_orientation(self, orientation="horizontal"): """Change the orientation or the y axis tick labels. Args: @@ -434,7 +425,7 @@ def set_yaxis_tick_orientation(self, orientation='horizontal'): orientation = [orientation] * 3 level_1 = orientation[0] - level_2 = orientation[1] if len(orientation) > 1 else 'horizontal' + level_2 = orientation[1] if len(orientation) > 1 else "horizontal" level_3 = orientation[2] if len(orientation) > 2 else level_2 level_1 = self._convert_major_orientation_labels(level_1) @@ -454,15 +445,14 @@ class DatetimeXMixin: def _convert_timestamp_list_to_epoch_ms(ts_list): return list( map( - lambda x: ( - (pd.to_datetime(x) - pd.Timestamp("1970-01-01")) - // pd.Timedelta('1ms')), - ts_list)) + lambda x: ((pd.to_datetime(x) - pd.Timestamp("1970-01-01")) // pd.Timedelta("1ms")), + ts_list, + ) + ) @staticmethod def _convert_timestamp_to_epoch_ms(timestamp): - return (pd.to_datetime(timestamp) - - pd.Timestamp("1970-01-01")) // pd.Timedelta('1ms') + return (pd.to_datetime(timestamp) - pd.Timestamp("1970-01-01")) // pd.Timedelta("1ms") def set_xaxis_range(self, start=None, end=None): """Set x-axis range. @@ -541,17 +531,17 @@ def set_xaxis_tick_format(self, date_format): Returns: Current chart object """ - self._chart.figure.xaxis[ - 0].formatter = bokeh.models.DatetimeTickFormatter( - milliseconds=[date_format], - seconds=[date_format], - minsec=[date_format], - minutes=[date_format], - hourmin=[date_format], - hours=[date_format], - days=[date_format], - months=[date_format], - years=[date_format]) + self._chart.figure.xaxis[0].formatter = bokeh.models.DatetimeTickFormatter( + milliseconds=[date_format], + seconds=[date_format], + minsec=[date_format], + minutes=[date_format], + hourmin=[date_format], + hours=[date_format], + days=[date_format], + months=[date_format], + years=[date_format], + ) return self._chart @@ -560,7 +550,7 @@ class NumericalXAxis(BaseAxes, NumericalXMixin, CategoricalYMixin): def __init__(self, chart): super(NumericalXAxis, self).__init__(chart) - self._chart.style._apply_settings('categorical_yaxis') + self._chart.style._apply_settings("categorical_yaxis") class NumericalYAxis(BaseAxes, CategoricalXMixin, NumericalYMixin): @@ -568,7 +558,7 @@ class NumericalYAxis(BaseAxes, CategoricalXMixin, NumericalYMixin): def __init__(self, chart): super(NumericalYAxis, self).__init__(chart) - self._chart.style._apply_settings('categorical_xaxis') + self._chart.style._apply_settings("categorical_xaxis") class NumericalXYAxes(BaseAxes, NumericalXMixin, NumericalYMixin): @@ -584,27 +574,25 @@ class CategoricalXYAxes(BaseAxes, CategoricalXMixin, CategoricalYMixin): def __init__(self, chart): super(CategoricalXYAxes, self).__init__(chart) - self._chart.style._apply_settings('categorical_xyaxis') + 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') - } + 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': + 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._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') + self._chart.style._apply_settings("second_y_axis") class SecondAxis: diff --git a/chartify/_core/callout.py b/chartify/_core/callout.py index 590ba4c..0ce333f 100644 --- a/chartify/_core/callout.py +++ b/chartify/_core/callout.py @@ -29,13 +29,15 @@ class Callout: def __init__(self, chart): self._chart = chart - def line(self, - location, - orientation='width', - line_color='black', - line_dash='solid', - line_width=2, - line_alpha=1.0): + def line( + self, + location, + orientation="width", + line_color="black", + line_dash="solid", + line_width=2, + line_alpha=1.0, + ): """Add line callout to the chart. Args: @@ -58,11 +60,10 @@ def line(self, Current chart object """ # Convert datetime values to epoch if datetime axis. - if isinstance(self._chart.axes, - DatetimeXNumericalYAxes) and orientation == 'height': + if isinstance(self._chart.axes, DatetimeXNumericalYAxes) and orientation == "height": location = self._chart.axes._convert_timestamp_to_epoch_ms(location) line_color = colors.Color(line_color).get_hex_l() - location_units = 'data' + location_units = "data" span = bokeh.models.Span( location=location, dimension=orientation, @@ -70,19 +71,22 @@ def line(self, line_dash=line_dash, line_width=line_width, location_units=location_units, - line_alpha=line_alpha) + line_alpha=line_alpha, + ) self._chart.figure.add_layout(span) return self._chart - def line_segment(self, - x_start, - y_start, - x_end, - y_end, - line_color='black', - line_dash='solid', - line_width=2, - line_alpha=1.0): + def line_segment( + self, + x_start, + y_start, + x_end, + y_end, + line_color="black", + line_dash="solid", + line_width=2, + line_alpha=1.0, + ): """Add line segment callout to the chart. Args: @@ -119,18 +123,13 @@ def line_segment(self, line_color=line_color, line_width=line_width, line_dash=line_dash, - line_alpha=line_alpha) + line_alpha=line_alpha, + ) self._chart.figure.add_layout(segment) return self._chart - def box(self, - top=None, - bottom=None, - left=None, - right=None, - alpha=.2, - color='red'): + def box(self, top=None, bottom=None, left=None, right=None, alpha=0.2, color="red"): """Add box callout to the chart. Args: @@ -162,18 +161,21 @@ def box(self, left=left, right=right, fill_alpha=alpha, - fill_color=color) + fill_color=color, + ) self._chart.figure.add_layout(box) return self._chart - def text(self, - text, - x, - y, - text_color='black', - text_align='left', - font_size='1em', - angle=0): + def text( + self, + text, + x, + y, + text_color="black", + text_align="left", + font_size="1em", + angle=0, + ): """Add text callout to the chart. Note: @@ -194,22 +196,18 @@ def text(self, if isinstance(self._chart.axes, DatetimeXNumericalYAxes): x = self._chart.axes._convert_timestamp_to_epoch_ms(x) text_color = colors.Color(text_color).get_hex_l() - source = bokeh.models.ColumnDataSource({ - 'text': [text], - 'x': [x], - 'y': [y] - }) - text_font = self._chart.style._get_settings('text_callout_and_plot')[ - 'font'] + source = bokeh.models.ColumnDataSource({"text": [text], "x": [x], "y": [y]}) + text_font = self._chart.style._get_settings("text_callout_and_plot")["font"] self._chart.figure.text( - x='x', - y='y', - text='text', + x="x", + y="y", + text="text", text_color=text_color, text_align=text_align, angle=angle, - angle_units='deg', + angle_units="deg", text_font=text_font, source=source, - text_font_size=font_size) + text_font_size=font_size, + ) return self._chart diff --git a/chartify/_core/chart.py b/chartify/_core/chart.py index 506bd3b..4e46eba 100644 --- a/chartify/_core/chart.py +++ b/chartify/_core/chart.py @@ -32,6 +32,7 @@ from bokeh.resources import INLINE from IPython.display import display from PIL import Image +from PIL.Image import Resampling from selenium import webdriver from selenium.webdriver.chrome.options import Options @@ -53,12 +54,14 @@ class Chart: """ - def __init__(self, - blank_labels=options.get_option('chart.blank_labels'), - layout='slide_100%', - x_axis_type='linear', - y_axis_type='linear', - second_y_axis=False): + def __init__( + self, + blank_labels=options.get_option("chart.blank_labels"), + layout="slide_100%", + x_axis_type="linear", + y_axis_type="linear", + second_y_axis=False, + ): """Create a chart instance. Args: @@ -88,46 +91,41 @@ def __init__(self, plotting methods available. """ # Validate axis type input - valid_x_axis_types = [ - 'linear', 'log', 'datetime', 'categorical', 'density' - ] - valid_y_axis_types = ['linear', 'log', 'categorical', 'density'] - valid_second_y_axis_types = ['linear', 'log'] + valid_x_axis_types = ["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)) + 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)) + 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)) + "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) self.style = Style(self, layout) - self.figure = self._initialize_figure(self._x_axis_type, - self._y_axis_type) - self.style._apply_settings('chart') - self.plot = BasePlot._get_plot_class(self._x_axis_type, - self._y_axis_type)(self) + self.figure = self._initialize_figure(self._x_axis_type, self._y_axis_type) + self.style._apply_settings("chart") + self.plot = BasePlot._get_plot_class(self._x_axis_type, self._y_axis_type)(self) self.callout = Callout(self) - self.axes = BaseAxes._get_axis_class(self._x_axis_type, - self._y_axis_type)(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.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. @@ -148,33 +146,35 @@ def __repr__(self): layout='{layout}', x_axis_type='{x_axis_type}', y_axis_type='{y_axis_type}') -""".format(blank_labels=self._blank_labels, - layout=self.style._layout, - x_axis_type=self._x_axis_type, - y_axis_type=self._y_axis_type) +""".format( + blank_labels=self._blank_labels, + layout=self.style._layout, + x_axis_type=self._x_axis_type, + y_axis_type=self._y_axis_type, + ) def _initialize_figure(self, x_axis_type, y_axis_type): - x_range, y_range = None, None - if x_axis_type == 'categorical': - x_range = [] - x_axis_type = 'auto' - if y_axis_type == 'categorical': - y_range = [] - y_axis_type = 'auto' - if x_axis_type == 'density': - x_axis_type = 'linear' - if y_axis_type == 'density': - y_axis_type = 'linear' + range_args = {} + if x_axis_type == "categorical": + range_args["x_range"] = [] + x_axis_type = "auto" + if y_axis_type == "categorical": + range_args["y_range"] = [] + y_axis_type = "auto" + if x_axis_type == "density": + x_axis_type = "linear" + if y_axis_type == "density": + y_axis_type = "linear" figure = bokeh.plotting.figure( - x_range=x_range, - y_range=y_range, + **range_args, y_axis_type=y_axis_type, x_axis_type=x_axis_type, - plot_width=self.style.plot_width, - plot_height=self.style.plot_height, - tools='save', + width=self.style.plot_width, + height=self.style.plot_height, + tools="save", # toolbar_location='right', - active_drag=None) + active_drag=None + ) return figure def _add_subtitle_to_figure(self, subtitle_text=None): @@ -184,16 +184,15 @@ def _add_subtitle_to_figure(self, subtitle_text=None): subtitle_text = "" else: subtitle_text = """ch.set_subtitle('Data Description')""" - subtitle_settings = self.style._get_settings('subtitle') + subtitle_settings = self.style._get_settings("subtitle") _subtitle_glyph = bokeh.models.Title( text=subtitle_text, - align=subtitle_settings['subtitle_align'], - text_color=subtitle_settings['subtitle_text_color'], - text_font_size=subtitle_settings['subtitle_text_size'], - text_font=subtitle_settings['subtitle_text_font'], - ) - self.figure.add_layout(_subtitle_glyph, - subtitle_settings['subtitle_location']) + align=subtitle_settings["subtitle_align"], + text_color=subtitle_settings["subtitle_text_color"], + text_font_size=subtitle_settings["subtitle_text_size"], + text_font=subtitle_settings["subtitle_text_font"], + ) + self.figure.add_layout(_subtitle_glyph, subtitle_settings["subtitle_location"]) return _subtitle_glyph def _add_source_to_figure(self): @@ -201,20 +200,21 @@ def _add_source_to_figure(self): source_text = """ch.set_source_label('Source')""" if self._blank_labels: source_text = "" - source_text_color = '#898989' - source_font_size = '10px' + source_text_color = "#898989" + source_font_size = "10px" _source = bokeh.models.Label( - x=self.style.plot_width * .9, + x=self.style.plot_width * 0.9, y=0, - x_units='screen', - y_units='screen', - level='overlay', + x_units="screen", + y_units="screen", + level="overlay", text=source_text, text_color=source_text_color, text_font_size=source_font_size, - text_align='right', - name='subtitle') - self.figure.add_layout(_source, 'below') + text_align="right", + name="subtitle", + ) + self.figure.add_layout(_source, "below") return _source @property @@ -224,9 +224,7 @@ def data(self): Note: The format will depend on the types of plots that have been added. """ - datasources = self.figure.select({ - 'type': bokeh.models.ColumnDataSource - }) + datasources = self.figure.select({"type": bokeh.models.ColumnDataSource}) # Extract the data attribute from the ColumnDataSource object # and place in a list. datasources_list = list(map(lambda x: x.data, datasources)) @@ -291,7 +289,7 @@ def legend_location(self): """str: Legend location.""" return self.figure.legend[0].location - def set_legend_location(self, location, orientation='horizontal'): + def set_legend_location(self, location, orientation="horizontal"): """Set the legend location. Args: @@ -316,23 +314,24 @@ def add_outside_legend(legend_location, layout_location): """ Legend location will not apply. Set the legend after plotting data. - """, UserWarning) + """, + UserWarning, + ) return self new_legend = self.figure.legend[0] new_legend.orientation = orientation self.figure.add_layout(new_legend, layout_location) - if location == 'outside_top': - add_outside_legend('top_left', 'above') + if location == "outside_top": + add_outside_legend("top_left", "above") # Re-render the subtitle so that it appears over the legend. subtitle_index = self.figure.renderers.index(self._subtitle_glyph) self.figure.renderers.pop(subtitle_index) - self._subtitle_glyph = self._add_subtitle_to_figure( - self._subtitle_glyph.text) - elif location == 'outside_bottom': - add_outside_legend('bottom_center', 'below') - elif location == 'outside_right': - add_outside_legend('top_left', 'right') + self._subtitle_glyph = self._add_subtitle_to_figure(self._subtitle_glyph.text) + elif location == "outside_bottom": + add_outside_legend("bottom_center", "below") + elif location == "outside_right": + add_outside_legend("top_left", "right") elif location is None: self.figure.legend.visible = False else: @@ -342,12 +341,11 @@ def add_outside_legend(legend_location, layout_location): vertical = self.axes._vertical # Reverse the legend order if self._reverse_vertical_legend: - if orientation == 'vertical' and vertical: - self.figure.legend[0].items = list( - reversed(self.figure.legend[0].items)) + if orientation == "vertical" and vertical: + self.figure.legend[0].items = list(reversed(self.figure.legend[0].items)) return self - def show(self, format='html'): + def show(self, format="html"): """Show the chart. Args: @@ -365,20 +363,20 @@ def show(self, format='html'): Recommended when the plot is in a finished state. - 'svg': Output as SVG. - """ + """ self._set_toolbar_for_format(format) - if format == 'html': + if format == "html": return bokeh.io.show(self.figure) - elif format == 'png': + elif format == "png": image = self._figure_to_png() # Need to re-enable this when logos are added back. # image = self.logo._add_logo_to_image(image) return display(image) - elif format == 'svg': + elif format == "svg": return self._show_svg() - def save(self, filename, format='html'): + def save(self, filename, format="html"): """Save the chart. Args: @@ -400,48 +398,49 @@ def save(self, filename, format='html'): """ self._set_toolbar_for_format(format) - if format == 'html': + if format == "html": bokeh.io.saving.save( self.figure, filename=filename, resources=INLINE, - title='Chartify chart.') - elif format == 'png': + title="Chartify chart.", + ) + elif format == "png": image = self._figure_to_png() # Need to re-enable this when logos are added back. # image = self.logo._add_logo_to_image(image) image.save(filename) - elif format == 'svg': + elif format == "svg": image = self._figure_to_svg() self._save_svg(image, filename) - print('Saved to {filename}'.format(filename=filename)) + print("Saved to {filename}".format(filename=filename)) return self def _set_toolbar_for_format(self, format): - if format == 'html': - self.figure.toolbar_location = 'right' - elif format in ('png', 'svg'): + if format == "html": + self.figure.toolbar_location = "right" + elif format in ("png", "svg"): self.figure.toolbar_location = None elif format is None: # If format is None the chart won't be shown. pass else: - raise ValueError( - """Invalid format. Valid options are 'html', 'png' or 'svg'.""") + raise ValueError("""Invalid format. Valid options are 'html', 'png' or 'svg'.""") def _initialize_webdriver(self): """Initialize headless chrome browser""" options = Options() - options.add_argument("window-size={width},{height}".format( - width=self.style.plot_width, height=self.style.plot_height)) + options.add_argument( + "window-size={width},{height}".format(width=self.style.plot_width, height=self.style.plot_height) + ) options.add_argument("start-maximized") options.add_argument("disable-infobars") options.add_argument("disable-gpu") - options.add_argument('no-sandbox') # Required for use in docker. + options.add_argument("no-sandbox") # Required for use in docker. options.add_argument("--disable-extensions") - options.add_argument('--headless') - options.add_argument('--hide-scrollbars') + options.add_argument("--headless") + options.add_argument("--hide-scrollbars") driver = webdriver.Chrome(options=options) return driver @@ -453,8 +452,7 @@ def _figure_to_png(self): driver = self._initialize_webdriver() # Save figure as HTML html = file_html(self.figure, resources=INLINE, title="") - fp = tempfile.NamedTemporaryFile( - 'w', prefix='chartify', suffix='.html', encoding='utf-8') + fp = tempfile.NamedTemporaryFile("w", prefix="chartify", suffix=".html", encoding="utf-8") fp.write(html) fp.flush() # Open html file in the browser. @@ -467,18 +465,20 @@ def _figure_to_png(self): image = Image.open(BytesIO(png)) target_dimensions = (self.style.plot_width, self.style.plot_height) if image.size != target_dimensions: - image = image.resize(target_dimensions, resample=Image.LANCZOS) + image = image.resize(target_dimensions, resample=Resampling.LANCZOS) return image def _set_svg_backend_decorator(f): """Sets the chart backend to svg and resets after the function has run.""" + @wraps(f) def wrapper(self, *args, **kwargs): old_backend = self.figure.output_backend - self.figure.output_backend = 'svg' + self.figure.output_backend = "svg" return f(self, *args, **kwargs) self.figure.output_backend = old_backend + return wrapper @_set_svg_backend_decorator @@ -495,8 +495,7 @@ def _figure_to_svg(self): driver = self._initialize_webdriver() html = file_html(self.figure, resources=INLINE, title="") - fp = tempfile.NamedTemporaryFile( - 'w', prefix='chartify', suffix='.html', encoding='utf-8') + fp = tempfile.NamedTemporaryFile("w", prefix="chartify", suffix=".html", encoding="utf-8") fp.write(html) fp.flush() driver.get("file:///" + fp.name) @@ -514,14 +513,12 @@ def _save_svg(self, svg, filename): class Logo: - def __init__(self, chart): self._chart = chart self._logo_image = None - self._path = options.get_option('config.logos_path') + self._path = options.get_option("config.logos_path") self._logo_file_mapping = {} - self._logo_file_mapping = OrderedDict( - sorted(list(self._logo_file_mapping.items()), key=lambda t: t[0])) + self._logo_file_mapping = OrderedDict(sorted(list(self._logo_file_mapping.items()), key=lambda t: t[0])) def _add_logo_to_image(self, image): """If the logo is set then add it to the chart image.""" @@ -536,20 +533,19 @@ def _add_logo_to_image(self, image): return image def _resize_logo(self, logo_image): - logo_width, logo_height = logo_image.size # TODO smart scaling of logos - target_height = int(self._chart.style.plot_height * .1) + target_height = int(self._chart.style.plot_height * 0.1) if logo_width == logo_height: - logo_image = logo_image.resize( - (target_height, target_height), resample=Image.LANCZOS) + logo_image = logo_image.resize((target_height, target_height), resample=Image.LANCZOS) else: logo_width_to_height = logo_width * 1.0 / logo_height logo_image = logo_image.resize( (int(logo_width_to_height * target_height), target_height), - resample=Image.LANCZOS) + resample=Image.LANCZOS, + ) return logo_image def show_logo_options(self): @@ -569,8 +565,10 @@ def set_logo(self, logo=None): filename = self._logo_file_mapping[logo] except KeyError: raise KeyError( - 'Must supply a valid logo name: {valid_options}'.format( - valid_options=list(self._logo_file_mapping.keys()))) + "Must supply a valid logo name: {valid_options}".format( + valid_options=list(self._logo_file_mapping.keys()) + ) + ) logo_image = Image.open(self._path + filename) diff --git a/chartify/_core/colors.py b/chartify/_core/colors.py index 5de0daf..5fa2a1b 100644 --- a/chartify/_core/colors.py +++ b/chartify/_core/colors.py @@ -25,19 +25,18 @@ class CustomColors: def __init__(self): - - config_filename = options.get_option('config.colors') + config_filename = options.get_option("config.colors") try: self.colors = self.from_yaml(config_filename) except FileNotFoundError: self.colors = { - (232, 232, 232): 'Light Grey', - (83, 88, 95): 'Dark Grey', + (232, 232, 232): "Light Grey", + (83, 88, 95): "Dark Grey", } def to_yaml(self, filename): """Write colors to a yaml file""" - with open(filename, 'w') as outfile: + with open(filename, "w") as outfile: yaml.dump(self.colors, outfile, default_flow_style=False) def from_yaml(self, filename): @@ -61,9 +60,8 @@ def overwrite_colors(self): colour.RGB_TO_COLOR_NAMES[color_rgb] = [color_name] colour.COLOR_NAME_TO_RGB = dict( - (name.lower(), rgb) - for rgb, names in list(colour.RGB_TO_COLOR_NAMES.items()) - for name in names) + (name.lower(), rgb) for rgb, names in list(colour.RGB_TO_COLOR_NAMES.items()) for name in names + ) # Load custom colors. @@ -71,8 +69,8 @@ def overwrite_colors(self): class Color(colour.Color): - DISPLAY_HEIGHT = '20px' - DISPLAY_WIDTH = '200px' + DISPLAY_HEIGHT = "20px" + DISPLAY_WIDTH = "200px" def _html(self): return """
\\n\"+\n", - " \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n", - " \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n", - " \"
\\n\"+\n", - " \"\\n\"+\n",
- " \"from bokeh.resources import INLINE\\n\"+\n",
- " \"output_notebook(resources=INLINE)\\n\"+\n",
- " \"
\\n\"+\n",
- " \"0&&ye(a,!u&&ve(e,\"script\")),s},cleanData:function(e){for(var t,n,r,i=b.event.special,o=0;void 0!==(n=e[o]);o++)if(X(n)){if(t=n[G.expando]){if(t.events)for(r in t.events)i[r]?b.event.remove(n,r):b.removeEvent(n,r,t.handle);n[G.expando]=void 0}n[Y.expando]&&(n[Y.expando]=void 0)}}}),b.fn.extend({detach:function(e){return Me(this,e,!0)},remove:function(e){return Me(this,e)},text:function(e){return B(this,(function(e){return void 0===e?b.text(this):this.empty().each((function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)}))}),null,e,arguments.length)},append:function(){return Re(this,arguments,(function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||qe(this,e).appendChild(e)}))},prepend:function(){return Re(this,arguments,(function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=qe(this,e);t.insertBefore(e,t.firstChild)}}))},before:function(){return Re(this,arguments,(function(e){this.parentNode&&this.parentNode.insertBefore(e,this)}))},after:function(){return Re(this,arguments,(function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)}))},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(b.cleanData(ve(e,!1)),e.textContent=\"\");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map((function(){return b.clone(this,e,t)}))},html:function(e){return B(this,(function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if(\"string\"==typeof e&&!Ne.test(e)&&!ge[(de.exec(e)||[\"\",\"\"])[1].toLowerCase()]){e=b.htmlPrefilter(e);try{for(;n