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