Source code for lib.charts

# -*- coding: utf-8 -*-

# Copyright Martin Manns
# Distributed under the terms of the GNU General Public License

# --------------------------------------------------------------------
# pyspread is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# pyspread is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with pyspread. If not, see <>.
# --------------------------------------------------------------------


Provides matplotlib figure that are chart templates


* ChartFigure: Main chart class


from collections import OrderedDict
from copy import copy
from io import StringIO
import datetime
from pathlib import Path
from typing import IO, List, Union

    from matplotlib.figure import Figure
    from matplotlib.sankey import Sankey
    from matplotlib import dates
except ImportError:
    Figure = Sankey = dates = object

[docs]def fig2x(figure: Figure, format: Union[str, Path, IO]) -> str: """Returns svg from matplotlib chart :param figure: Matplotlib figure object :param format: matplotlib.pyplot.savefig format, normally filename suffix """ # Save svg to file like object svg_io io = StringIO() figure.savefig(io, format=format) # Rewind the file like object data = io.getvalue() io.close() return data
[docs]class ChartFigure(Figure): """Chart figure class with drawing method **This class is deprecated and exists solely for compatibility with pyspread <1.99.0** """ plot_type_fixed_attrs = { "plot": ["xdata", "ydata"], "bar": ["left", "height"], "boxplot": ["x"], "hist": ["x"], "pie": ["x"], "contour": ["X", "Y", "Z"], "contourf": ["X", "Y", "Z"], "Sankey": [], } plot_type_xy_mapping = { "plot": ["xdata", "ydata"], "bar": ["left", "height"], "boxplot": ["x", "x"], "hist": ["label", "x"], "pie": ["labels", "x"], "annotate": ["xy", "xy"], "contour": ["X", "Y"], "contourf": ["X", "Y", "Z"], "Sankey": ["flows", "orientations"], } contour_label_attrs = { "contour_labels": "contour_labels", "contour_label_fontsize": "fontsize", "contour_label_colors": "colors", } contourf_attrs = { "contour_fill": "contour_fill", "hatches": "hatches", } def __init__(self, *attributes: List[dict]): """ :param attributes: List of dicts that contain matplotlib attributes The first list element is defining the axes The following list elements are defining plots """ Figure.__init__(self, (5.0, 4.0), facecolor="white") self.attributes = attributes self.__axes = self.add_subplot(111) # Insert empty attributes with a dict for figure attributes if not self.attributes: self.attributes = [{}] self.draw_chart() self.tight_layout(pad=1.5)
[docs] def _xdate_setter(self, xdate_format: str = '%Y-%m-%d'): """Makes x axis a date axis with auto format :param xdate_format: Sets date formatting """ if xdate_format: # We have to validate xdate_format. If wrong then bail out. try: self.autofmt_xdate(), 1, 1).strftime(xdate_format) except ValueError: self.autofmt_xdate() return self.__axes.xaxis_date() formatter = dates.DateFormatter(xdate_format) self.__axes.xaxis.set_major_formatter(formatter)
# The autofmt method does not work in matplotlib 1.3.0 # self.autofmt_xdate()
[docs] def _setup_axes(self, axes_data: dict): """Sets up axes for drawing chart :param axes_data: Dicts with keys that match matplotlib axes attributes """ self.__axes.clear() key_setter = [ ("title", self.__axes.set_title), ("xlabel", self.__axes.set_xlabel), ("ylabel", self.__axes.set_ylabel), ("xscale", self.__axes.set_xscale), ("yscale", self.__axes.set_yscale), ("xticks", self.__axes.set_xticks), ("xtick_labels", self.__axes.set_xticklabels), ("xtick_params", self.__axes.tick_params), ("yticks", self.__axes.set_yticks), ("ytick_labels", self.__axes.set_yticklabels), ("ytick_params", self.__axes.tick_params), ("xlim", self.__axes.set_xlim), ("ylim", self.__axes.set_ylim), ("xgrid", self.__axes.xaxis.grid), ("ygrid", self.__axes.yaxis.grid), ("xdate_format", self._xdate_setter), ] key2setter = OrderedDict(key_setter) for key in key2setter: if key in axes_data and axes_data[key]: try: kwargs_key = key + "_kwargs" kwargs = axes_data[kwargs_key] except KeyError: kwargs = {} if key == "title": # Shift title up kwargs["y"] = 1.08 key2setter[key](axes_data[key], **kwargs)
[docs] def _setup_legend(self, axes_data: dict): """Sets up legend for drawing chart :param axes_data: Dicts with keys that match matplotlib axes attributes """ if "legend" in axes_data and axes_data["legend"]: self.__axes.legend()
[docs] def draw_chart(self): """Plots chart from self.attributes""" if not hasattr(self, "attributes"): return # The first element is always axes data self._setup_axes(self.attributes[0]) for attribute in self.attributes[1:]: series = copy(attribute) # Extract chart type chart_type_string = series.pop("type") x_str, y_str = self.plot_type_xy_mapping[chart_type_string] # Check xdata length if x_str in series and \ len(series[x_str]) != len(series[y_str]): # Wrong length --> ignore xdata series[x_str] = list(range(len(series[y_str]))) else: # Solve the problem that the series data may contain utf-8 data series_list = list(series[x_str]) series_unicode_list = [] for ele in series_list: if isinstance(ele, bytes): try: series_unicode_list.append(ele.decode('utf-8')) except Exception: series_unicode_list.append(ele) else: series_unicode_list.append(ele) series[x_str] = tuple(series_unicode_list) fixed_attrs = [] if chart_type_string in self.plot_type_fixed_attrs: for attr in self.plot_type_fixed_attrs[chart_type_string]: # Remove attr if it is a fixed (non-kwd) attr # If a fixed attr is missing, insert a dummy try: fixed_attrs.append(tuple(series.pop(attr))) except KeyError: fixed_attrs.append(()) # Remove contour chart label info from series cl_attrs = {} for contour_label_attr in self.contour_label_attrs: if contour_label_attr in series: cl_attrs[self.contour_label_attrs[contour_label_attr]] = \ series.pop(contour_label_attr) # Remove contourf attributes from series cf_attrs = {} for contourf_attr in self.contourf_attrs: if contourf_attr in series: cf_attrs[self.contourf_attrs[contourf_attr]] = \ series.pop(contourf_attr) if not fixed_attrs or all(fixed_attrs): # Draw series to axes # Do we have a Sankey plot --> build it if chart_type_string == "Sankey": Sankey(self.__axes, **series).finish() else: chart_method = getattr(self.__axes, chart_type_string) plot = chart_method(*fixed_attrs, **series) # Do we have a filled contour? try: if cf_attrs.pop("contour_fill"): cf_attrs.update(series) if "linewidths" in cf_attrs: cf_attrs.pop("linewidths") if "linestyles" in cf_attrs: cf_attrs.pop("linestyles") if not cf_attrs["hatches"]: cf_attrs.pop("hatches") self.__axes.contourf(plot, **cf_attrs) except KeyError: pass # Do we have a contour chart label? try: if cl_attrs.pop("contour_labels"): self.__axes.clabel(plot, **cl_attrs) except KeyError: pass # The legend has to be set up after all series are drawn self._setup_legend(self.attributes[0])