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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# 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 <http://www.gnu.org/licenses/>.
# --------------------------------------------------------------------

"""
charts
======

Provides matplotlib figure that are chart templates

Provides
--------

* 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

try:
    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 io.seek(0) 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() datetime.date(2000, 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])