Example #1
0
class RadarChart(Chart):
    def __init__(self,
                 blank_labels=options.get_option('chart.blank_labels'),
                 layout='slide_50%'):
        """Create a Radar Chart instance.

        Note:
            Radar charts plot each vertex in counter-clockwise order starting
            from the top.

        Args:
            blank_labels (bool): When true removes the title,
                subtitle, axes, and source labels from the chart.
                Default False.
            layout (str): Change size & aspect ratio of the chart for
                fitting into slides.
                - 'slide_100%'
                - 'slide_75%'
                - 'slide_50%' (Suggested for Radar Charts)
                - 'slide_25%'
        """
        # Validate axis type input
        valid_axis_types = ['linear', 'log']
        self._axis_type = 'linear'
        self._x_axis_type, self._y_axis_type = self._axis_type, self._axis_type
        if self._axis_type not in valid_axis_types:
            raise ValueError('axis_type must be one of {options}'.format(
                options=valid_axis_types))
        self._blank_labels = options._get_value(blank_labels)
        self.style = Style(self, layout)
        self.figure = self._initialize_figure(self._axis_type, self._axis_type)
        self.style._apply_settings('chart')
        self.callout = Callout(self)
        self.axes = BaseAxes._get_axis_class(self._axis_type,
                                             self._axis_type)(self)
        self.plot = PlotRadar(self)
        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.
        # Reverse the order of vertical legends. Used with stacked plot types
        # to ensure that the stack order is consistent with the legend order.
        self._reverse_vertical_legend = False
        # Logos disabled for now.
        # self.logo = Logo(self)
        # Set default for title
        title = """ch.set_title('Takeaway')"""
        if self._blank_labels:
            title = ""
        self.set_title(title)
Example #2
0
    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:
            blank_labels (bool): When true removes the title,
                subtitle, axes, and source labels from the chart.
                Default False.
            layout (str): Change size & aspect ratio of the chart for
                fitting into slides.
                - 'slide_100%'
                - 'slide_75%'
                - 'slide_50%'
                - 'slide_25%'
            x_axis_type (enum, str): Type of data plotted on the X-axis.
                - 'linear':
                - 'log':
                - 'datetime': Use for datetime formatted data.
                - 'categorical':
                - 'density'

            y_axis_type (enum, str): Type of data plotted on the Y-axis.
                - 'linear':
                - 'log':
                - 'categorical':
                - 'density'
        Note:
            Combination of x_axis_type and y_axis_type will determine the
            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']
        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)
        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.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.
        # Reverse the order of vertical legends. Used with stacked plot types
        # to ensure that the stack order is consistent with the legend order.
        self._reverse_vertical_legend = False
        # Logos disabled for now.
        # self.logo = Logo(self)
        # Set default for title
        title = """ch.set_title('Takeaway')"""
        if self._blank_labels:
            title = ""
        self.set_title(title)
Example #3
0
class Chart:
    """Class Docstring

    - Styling (.style)
    - Plotting (.plot)
    - Callouts (.callout)
    - Axes (.axes)
    - Bokeh figure (.figure)

    """
    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:
            blank_labels (bool): When true removes the title,
                subtitle, axes, and source labels from the chart.
                Default False.
            layout (str): Change size & aspect ratio of the chart for
                fitting into slides.
                - 'slide_100%'
                - 'slide_75%'
                - 'slide_50%'
                - 'slide_25%'
            x_axis_type (enum, str): Type of data plotted on the X-axis.
                - 'linear':
                - 'log':
                - 'datetime': Use for datetime formatted data.
                - 'categorical':
                - 'density'

            y_axis_type (enum, str): Type of data plotted on the Y-axis.
                - 'linear':
                - 'log':
                - 'categorical':
                - 'density'
        Note:
            Combination of x_axis_type and y_axis_type will determine the
            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']
        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)
        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.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.
        # Reverse the order of vertical legends. Used with stacked plot types
        # to ensure that the stack order is consistent with the legend order.
        self._reverse_vertical_legend = False
        # Logos disabled for now.
        # self.logo = Logo(self)
        # Set default for title
        title = """ch.set_title('Takeaway')"""
        if self._blank_labels:
            title = ""
        self.set_title(title)

    def __repr__(self):
        return """
chartify.Chart(blank_labels={blank_labels},
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)

    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'
        figure = bokeh.plotting.figure(
            x_range=x_range,
            y_range=y_range,
            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',
            # toolbar_location='right',
            active_drag=None)
        return figure

    def _add_subtitle_to_figure(self, subtitle_text=None):
        """Create the subtitle glyph and add it to the bokeh figure."""
        if subtitle_text is None:
            if self._blank_labels:
                subtitle_text = ""
            else:
                subtitle_text = """ch.set_subtitle('Data Description')"""
        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'])
        return _subtitle_glyph

    def _add_source_to_figure(self):
        """Create the source glyph and add it to the bokeh figure."""
        source_text = """ch.set_source_label('Source')"""
        if self._blank_labels:
            source_text = ""
        source_text_color = '#898989'
        source_font_size = '10px'
        _source = bokeh.models.Label(x=self.style.plot_width * .9,
                                     y=0,
                                     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')
        return _source

    @property
    def data(self):
        """Return a list of dictionaries of the data that have be plotted on the chart.

        Note:
            The format will depend on the types of plots that have been added.
        """
        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))
        return datasources_list

    @property
    def source_text(self):
        """str: Data source of the chart."""
        return self._source.text

    def set_source_label(self, source):
        """Set the chart data source.

        Args:
            source (str): Data source.

        Returns:
            Current chart object
        """
        self._source.text = source
        return self

    @property
    def title(self):
        """str: Title text of the chart."""
        return self.figure.title.text

    def set_title(self, title):
        """Set the chart title.

        Args:
            title (str): Title text.

        Returns:
            Current chart object
        """
        self.figure.title.text = title
        return self

    @property
    def subtitle(self):
        """str: Subtitle text of the chart."""
        return self._subtitle_glyph.text

    def set_subtitle(self, subtitle):
        """Set the chart subtitle.

        Args:
            subtitle (str): Subtitle text.

        Note:
            Set value to "" to remove subtitle.

        Returns:
            Current chart object
        """
        self._subtitle_glyph.text = subtitle
        return self

    @property
    def legend_location(self):
        """str: Legend location."""
        return self.figure.legend[0].location

    def set_legend_location(self, location, orientation='horizontal'):
        """Set the legend location.

        Args:
            location (str or tuple): Legend location. One of:
            - Outside of the chart: 'outside_top', 'outside_bottom',
                  'outside_right'
            - Within the chart area: 'top_left', 'top_center',
                  'top_right', 'center_left', 'center', 'center_right',
                  'bottom_left', 'bottom_center', 'bottom_right'
            - Coordinates: Tuple(Float, Float)
            - None: Removes the legend.
            orientation (str): 'horizontal' or 'vertical'

        Returns:
            Current chart object
        """
        def add_outside_legend(legend_location, layout_location):
            self.figure.legend.location = legend_location
            if not self.figure.legend:
                warnings.warn(
                    """
                    Legend location will not apply.
                    Set the legend after plotting data.
                    """, UserWarning)
                return self
            new_legend = self.figure.legend[0]
            new_legend.plot = None
            new_legend.orientation = orientation
            self.figure.add_layout(new_legend, layout_location)

        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')
        elif location is None:
            self.figure.legend.visible = False
        else:
            self.figure.legend.location = location
            self.figure.legend.orientation = orientation

        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))
        return self

    def show(self, format='html'):
        """Show the chart.

        Args:
            format (str):
                - 'html': Output chart as HTML.
                    Renders faster and allows for interactivity.
                    Charts saved as HTML in a Jupyter notebooks
                    WILL NOT display on Github.
                    Logos will not display on HTML charts.
                    Recommended when drafting plots.

                - 'png': Output chart as PNG.
                    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)

        if format == 'html':
            return bokeh.io.show(self.figure)
        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':
            return self._show_svg()

    def save(self, filename, format='html'):
        """Save the chart.

        Args:
            filename (str): Name of output file.
            format (str):
                - 'html': Output chart as HTML.
                    Renders faster and allows for interactivity.
                    Charts saved as HTML in a Jupyter notebook WILL NOT display
                    on Github.
                    Logos will not display on HTML charts.
                    Recommended when drafting plots.

                - 'png': Output chart as PNG.
                    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)

        if format == 'html':
            bokeh.io.saving.save(self.figure,
                                 filename=filename,
                                 resources=INLINE,
                                 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':
            image = self._figure_to_svg()
            self._save_svg(image, 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'):
            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' or 'png'.""")

    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("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("--disable-extensions")
        options.add_argument('--headless')
        options.add_argument('--hide-scrollbars')
        driver = webdriver.Chrome(options=options)
        return driver

    def _figure_to_png(self):
        """Convert figure object to PNG
        Bokeh can only save figure objects as html.
        To convert to PNG the HTML file is opened in a headless browser.
        """
        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.write(html)
        fp.flush()
        # Open html file in the browser.
        driver.get("file:///" + fp.name)
        driver.execute_script("document.body.style.margin = '0px';")
        png = driver.get_screenshot_as_png()
        driver.quit()
        fp.close()
        # Resize image if necessary.
        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)
        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'
            return f(self, *args, **kwargs)
            self.figure.output_backend = old_backend

        return wrapper

    @_set_svg_backend_decorator
    def _show_svg(self):
        """Show the chart figure with an svg output backend."""
        return bokeh.io.show(self.figure)

    @_set_svg_backend_decorator
    def _figure_to_svg(self):
        """
        Convert the figure to an svg so that it can be saved to a file.
        https://github.com/bokeh/bokeh/blob/master/bokeh/io/export.py
        """
        driver = self._initialize_webdriver()
        html = file_html(self.figure, resources=INLINE, title="")

        fp = tempfile.NamedTemporaryFile('w',
                                         prefix='chartify',
                                         suffix='.html',
                                         encoding='utf-8')
        fp.write(html)
        fp.flush()
        driver.get("file:///" + fp.name)
        svgs = driver.execute_script(_SVG_SCRIPT)
        fp.close()

        driver.quit()
        return svgs[0]

    def _save_svg(self, svg, filename):
        """Write the svg to a file"""
        with io.open(filename, mode="w", encoding="utf-8") as f:
            f.write(svg)