class Chart(object): """ Simple and clean facade to Matplotlib's plotting API. A chart instance abstracts a plotting device, on which one or multiple related plots can be drawn. Charts can be exported as images, or visualized interactively. Each chart instance will always open in its own GUI window, and this window will never block the execution of the rest of the program, or interfere with other L{Chart}s. The GUI can be safely opened in the background and closed infinite number of times, as long as the client program is still running. By default, a chart contains a single plot: >>> chart.plot matplotlib.axes.AxesSubplot >>> chart.plot.hist(...) If C{rows} and C{columns} are defined, the chart will contain C{rows} x C{columns} number of plots (equivalent to MPL's sub-plots). Each plot can be assessed by its index: >>> chart.plots[0] first plot or by its position in the grid: >>> chart.plots[0, 1] plot at row=0, column=1 @param number: chart number; by default this a L{Chart.AUTONUMBER} @type number: int or None @param title: chart master title @type title: str @param rows: number of rows in the chart window @type rows: int @param columns: number of columns in the chart window @type columns: int @note: additional arguments are passed directly to Matplotlib's Figure constructor. """ AUTONUMBER = None _serial = 0 def __init__(self, number=None, title='', rows=1, columns=1, backend=Backends.WX_WIDGETS, *fa, **fk): if number == Chart.AUTONUMBER: Chart._serial += 1 number = Chart._serial if rows < 1: rows = 1 if columns < 1: columns = 1 self._rows = int(rows) self._columns = int(columns) self._number = int(number) self._title = str(title) self._figure = Figure(*fa, **fk) self._figure._figure_number = self._number self._figure.suptitle(self._title) self._beclass = backend self._hasgui = False self._plots = PlotsCollection(self._figure, self._rows, self._columns) self._canvas = FigureCanvasAgg(self._figure) formats = [(f.upper(), f) for f in self._canvas.get_supported_filetypes()] self._formats = csb.core.Enum.create('OutputFormats', **dict(formats)) def __getitem__(self, i): if i in self._plots: return self._plots[i] else: raise KeyError('No such plot number: {0}'.format(i)) def __enter__(self): return self def __exit__(self, *a, **k): self.dispose() @property def _backend(self): return Backend.get(self._beclass, started=True) @property def _backend_started(self): return Backend.query(self._beclass) @property def title(self): """ Chart title @rtype: str """ return self._title @property def number(self): """ Chart number @rtype: int """ return self._number @property def plots(self): """ Index-based access to the plots in this chart @rtype: L{PlotsCollection} """ return self._plots @property def plot(self): """ First plot @rtype: matplotlib.AxesSubplot """ return self._plots[0] @property def rows(self): """ Number of rows in this chart @rtype: int """ return self._rows @property def columns(self): """ Number of columns in this chart @rtype: int """ return self._columns @property def width(self): """ Chart's width in inches @rtype: int """ return self._figure.get_figwidth() @width.setter def width(self, inches): self._figure.set_figwidth(inches) if self._backend_started: self._backend.resize(self._figure) @property def height(self): """ Chart's height in inches @rtype: int """ return self._figure.get_figheight() @height.setter def height(self, inches): self._figure.set_figheight(inches) if self._backend_started: self._backend.resize(self._figure) @property def dpi(self): """ Chart's DPI @rtype: int """ return self._figure.get_dpi() @dpi.setter def dpi(self, dpi): self._figure.set_dpi(dpi) self._backend.resize(self._figure) @property def formats(self): """ Supported output file formats @rtype: L{csb.core.enum} """ return self._formats def show(self): """ Show the GUI window (non-blocking). """ if not self._hasgui: self._backend.add(self._figure) self._hasgui = True self._backend.show(self._figure) def hide(self): """ Hide (but do not dispose) the GUI window. """ self._backend.hide(self._figure) def dispose(self): """ Dispose the GUI interface. Must be called at the end if any chart.show() calls have been made. Automatically called if using the chart in context manager ("with" statement). @note: Failing to call this method if show() has been called at least once may cause backend-related errors. """ if self._backend_started: service = self._backend if service and service.running: service.destroy(self._figure, wait=True) service.client_disposed(self) def save(self, file, format='png', crop=False, dpi=None, *a, **k): """ Save all plots to an image. @param file: destination file name @type file: str @param format: output image format; see C{chart.formats} for enumeration @type format: str or L{csb.core.EnumItem} @param crop: if True, crop the image (equivalent to MPL's bbox=tight) @type crop: bool @note: additional arguments are passed directly to MPL's savefig method """ if 'bbox_inches' in k: bbox = k['bbox_inches'] del k['bbox_inches'] else: if crop: bbox = 'tight' else: bbox = None self._canvas.print_figure(file, format=str(format), bbox_inches=bbox, dpi=dpi, *a, **k)
class ReducePyMatplotlibHistogram: # pylint: disable=R0903 """ @class ReducePyMatplotlibHistogram.PyMatplotlibHistogram is a base class for classes that create histograms using matplotlib. Histograms are output as JSON documents of form: @verbatim {"image": {"keywords": [...list of image keywords...], "description":"...a description of the image...", "tag": TAG, "image_type": "eps", "data": "...base 64 encoded image..."}} @endverbatim "TAG" is specified by the sub-class. If "histogram_auto_number" (see below) is "true" then the TAG will have a number N appended where N means that the histogram was produced as a consequence of the (N + 1)th spill processed by the worker. The number will be zero-padded to form a six digit string e.g. "00000N". If "histogram_auto_number" is false then no such number is appended. In cases where a spill is input that contains errors (e.g. is badly formatted or is missing the data needed to update a histogram) then a spill is output which is just the input spill with an "errors" field containing the error e.g. @verbatim {"errors": {..., "bad_json_document": "unable to do json.loads on input"}} {"errors": {..., "...": "..."}} @endverbatim The caller can configure the worker and specify: -Image type ("histogram_image_type"). Must be one of those supported by matplot lib (currently "svg", "ps", "emf", "rgba", "raw", "svgz", "pdf", "eps", "png"). Default: "eps". -Auto-number ("histogram_auto_number"). Default: false. Flag that determines if the image tag (see above) has the spill count appended to it or not. -Sub-classes may support additional configuration parameter Sub-classes must override: -_configure_at_birth - to extract any additional sub-class-specific configuration from data cards. -_update_histograms. This checks that a spill has the data necessary to update any histograms then creates JSON documents in the format described above. -_cleanup_at_death - to do any sub-class-specific cleanup. """ def __init__(self): """ Set initial attribute values. @param self Object reference. """ # matplotlib histogram - for validation. figure = Figure(figsize=(6, 6)) self.__histogram = FigureCanvas(figure) self.spill_count = 0 # Number of spills processed to date. self.image_type = "eps" self.auto_number = False def birth(self, config_json): """ Configure worker from data cards. If "image_type" is not in those supported then a ValueError is thrown. @param self Object reference. @param config_json JSON document string. @returns True if configuration succeeded. """ config_doc = json.loads(config_json) key = "histogram_auto_number" if key in config_doc: self.auto_number = config_doc[key] key = "histogram_image_type" if key in config_doc: self.image_type = config_doc[key] else: self.image_type = "eps" if self.image_type not in \ self.__histogram.get_supported_filetypes().keys(): error = "Unsupported histogram image type: %s Expect one of %s" \ % (self.image_type, self.__histogram.get_supported_filetypes().keys()) raise ValueError(error) self.spill_count = 0 # Do sub-class-specific configuration. return self._configure_at_birth(config_doc) def _configure_at_birth(self, config_doc): """ Perform sub-class-specific configuration from data cards. Sub-classes must define this function. @param self Object reference. @param config_json JSON document. @returns True if configuration succeeded. """ def process(self, json_string): """ Update the histogram with data from the current spill and output the histogram. @param self Object reference. @param json_string String with current JSON document. @returns JSON document containing current histogram. """ # Load and validate the JSON document. try: json_doc = json.loads(json_string.rstrip()) except Exception: # pylint:disable=W0703 json_doc = {} ErrorHandler.HandleException(json_doc, self) return unicode(json.dumps(json_doc)) self.spill_count = self.spill_count + 1 # Process spill and update histograms. try: result = self._update_histograms(json_doc) except Exception: # pylint:disable=W0703 ErrorHandler.HandleException(json_doc, self) return unicode(json.dumps(json_doc)) image_list = [image['image'] for image in result] # Convert results to strings. return json.dumps({"maus_event_type":"Image", "image_list":image_list}) def _update_histograms(self, spill): """ Check that the spill has the data necessary to update the histograms then creates JSON documents in the format described above. Sub-classes must define this function. @param self Object reference. @param spill Current spill. @returns list of JSON documents. If the sub-class only updates histograms every N spills then this list can just contain the input spill. Otherwise it should consist of 1 or more JSON documents containing image data in the form described above. @throws Exception if various sub-class specific errors arise. """ def death(self): #pylint: disable=R0201 """ Invokes _cleanup_at_death(). @returns True """ return self._cleanup_at_death() def _cleanup_at_death(self): #pylint: disable=R0201 """ A no-op. Sub-classes can override this function to do sub-class-specific clean-up at death time. @param self Object reference. @returns True """ return True def _create_histogram(self): #pylint: disable=R0201 """ Create a histogram using matplotlib. @param self Object reference. @returns matplotlib FigureCanvas representing the histogram. """ figure = Figure(figsize=(6, 6)) histogram = FigureCanvas(figure) axes = figure.add_subplot(111) axes.grid(True, linestyle="-", color="0.75") return histogram def _rescale_axes(self, histogram, xmin, xmax, ymin, ymax, xfudge = 0.5, yfudge = 0.5): #pylint: disable=C0301, R0913, R0201 """ Rescale the X and Y axes of the histogram to show the given axis ranges. Fudge factors are used avoid matplotlib warning about "Attempting to set identical bottom==top" which arises if the axes are set to be exactly the maximum of the data. @param self Object reference. @param histogram FigureCanvas representing a histogram. @param xmin Minimum X value. @param xmax Maximum X value. @param ymin Minimum Y value. @param ymin Maximum Y value. @param xfudge X fudge factor. @param yfudge Y fudge factor. """ # Fudge factors are used histogram.figure.get_axes()[0].set_xlim( \ [xmin, xmax + xfudge]) histogram.figure.get_axes()[0].set_ylim( \ [ymin, ymax + yfudge]) def _get_image_doc(self, keywords, description, tag, canvas): #pylint: disable=C0301 """ Build a JSON document holding image data. @param self Object reference. @param keywords List of image keywords. @param description String describing the image. @param tag Image tag. @param histogram FigureCanvas representing a histogram. @returns JSON document. """ json_doc = {} json_doc["maus_event_type"] = "Image" json_doc["image"] = {} if (self.auto_number): image_tag = "%s%06d" % (tag, self.spill_count) else: image_tag = tag data = self.__convert_to_binary(canvas) json_doc["image"]["keywords"] = keywords json_doc["image"]["description"] = description json_doc["image"]["tag"] = image_tag json_doc["image"]["image_type"] = self.image_type json_doc["image"]["data"] = data return json_doc def __convert_to_binary(self, canvas): #pylint: disable=R0201 """ Convert histogram to binary format. @param self Object reference. @param canvas matplotlib FigureCanvas representing a histogram. @returns representation of histogram in base 64-encoded image type format. """ data_file = StringIO.StringIO() canvas.print_figure(data_file, dpi=500, format=self.image_type) data_file.seek(0) data = data_file.read() return base64.b64encode(data)
class Chart(object): """ Simple and clean facade to Matplotlib's plotting API. A chart instance abstracts a plotting device, on which one or multiple related plots can be drawn. Charts can be exported as images, or visualized interactively. Each chart instance will always open in its own GUI window, and this window will never block the execution of the rest of the program, or interfere with other L{Chart}s. The GUI can be safely opened in the background and closed infinite number of times, as long as the client program is still running. By default, a chart contains a single plot: >>> chart.plot matplotlib.axes.AxesSubplot >>> chart.plot.hist(...) If C{rows} and C{columns} are defined, the chart will contain C{rows} x C{columns} number of plots (equivalent to MPL's sub-plots). Each plot can be assessed by its index: >>> chart.plots[0] first plot or by its position in the grid: >>> chart.plots[0, 1] plot at row=0, column=1 @param number: chart number; by default this a L{Chart.AUTONUMBER} @type number: int or None @param title: chart master title @type title: str @param rows: number of rows in the chart window @type rows: int @param columns: number of columns in the chart window @type columns: int @note: additional arguments are passed directly to Matplotlib's Figure constructor. """ AUTONUMBER = None _serial = 0 def __init__(self, number=None, title='', rows=1, columns=1, backend=Backends.WX_WIDGETS, *fa, **fk): if number == Chart.AUTONUMBER: Chart._serial += 1 number = Chart._serial if rows < 1: rows = 1 if columns < 1: columns = 1 self._rows = int(rows) self._columns = int(columns) self._number = int(number) self._title = str(title) self._figure = Figure(*fa, **fk) self._figure._figure_number = self._number self._figure.suptitle(self._title) self._beclass = backend self._hasgui = False self._plots = PlotsCollection(self._figure, self._rows, self._columns) self._canvas = FigureCanvasAgg(self._figure) formats = [ (f.upper(), f) for f in self._canvas.get_supported_filetypes() ] self._formats = csb.core.Enum.create('OutputFormats', **dict(formats)) def __getitem__(self, i): if i in self._plots: return self._plots[i] else: raise KeyError('No such plot number: {0}'.format(i)) def __enter__(self): return self def __exit__(self, *a, **k): self.dispose() @property def _backend(self): return Backend.get(self._beclass, started=True) @property def _backend_started(self): return Backend.query(self._beclass) @property def title(self): """ Chart title @rtype: str """ return self._title @property def number(self): """ Chart number @rtype: int """ return self._number @property def plots(self): """ Index-based access to the plots in this chart @rtype: L{PlotsCollection} """ return self._plots @property def plot(self): """ First plot @rtype: matplotlib.AxesSubplot """ return self._plots[0] @property def rows(self): """ Number of rows in this chart @rtype: int """ return self._rows @property def columns(self): """ Number of columns in this chart @rtype: int """ return self._columns @property def width(self): """ Chart's width in inches @rtype: int """ return self._figure.get_figwidth() @width.setter def width(self, inches): self._figure.set_figwidth(inches) if self._backend_started: self._backend.resize(self._figure) @property def height(self): """ Chart's height in inches @rtype: int """ return self._figure.get_figheight() @height.setter def height(self, inches): self._figure.set_figheight(inches) if self._backend_started: self._backend.resize(self._figure) @property def dpi(self): """ Chart's DPI @rtype: int """ return self._figure.get_dpi() @dpi.setter def dpi(self, dpi): self._figure.set_dpi(dpi) self._backend.resize(self._figure) @property def formats(self): """ Supported output file formats @rtype: L{csb.core.enum} """ return self._formats def show(self): """ Show the GUI window (non-blocking). """ if not self._hasgui: self._backend.add(self._figure) self._hasgui = True self._backend.show(self._figure) def hide(self): """ Hide (but do not dispose) the GUI window. """ self._backend.hide(self._figure) def dispose(self): """ Dispose the GUI interface. Must be called at the end if any chart.show() calls have been made. Automatically called if using the chart in context manager ("with" statement). @note: Failing to call this method if show() has been called at least once may cause backend-related errors. """ if self._backend_started: service = self._backend if service and service.running: service.destroy(self._figure, wait=True) service.client_disposed(self) def save(self, file, format='png', crop=False, dpi=None, *a, **k): """ Save all plots to an image. @param file: destination file name @type file: str @param format: output image format; see C{chart.formats} for enumeration @type format: str or L{csb.core.EnumItem} @param crop: if True, crop the image (equivalent to MPL's bbox=tight) @type crop: bool @note: additional arguments are passed directly to MPL's savefig method """ if 'bbox_inches' in k: bbox = k['bbox_inches'] del k['bbox_inches'] else: if crop: bbox = 'tight' else: bbox = None self._canvas.print_figure(file, format=str(format), bbox_inches=bbox, dpi=dpi, *a, **k)
class ReducePyMatplotlibHistogram: # pylint: disable=R0903 """ @class ReducePyMatplotlibHistogram.PyMatplotlibHistogram is a base class for classes that create histograms using matplotlib. Histograms are output as JSON documents of form: @verbatim {"image": {"keywords": [...list of image keywords...], "description":"...a description of the image...", "tag": TAG, "image_type": "eps", "data": "...base 64 encoded image..."}} @endverbatim "TAG" is specified by the sub-class. If "histogram_auto_number" (see below) is "true" then the TAG will have a number N appended where N means that the histogram was produced as a consequence of the (N + 1)th spill processed by the worker. The number will be zero-padded to form a six digit string e.g. "00000N". If "histogram_auto_number" is false then no such number is appended. In cases where a spill is input that contains errors (e.g. is badly formatted or is missing the data needed to update a histogram) then a spill is output which is just the input spill with an "errors" field containing the error e.g. @verbatim {"errors": {..., "bad_json_document": "unable to do json.loads on input"}} {"errors": {..., "...": "..."}} @endverbatim The caller can configure the worker and specify: -Image type ("histogram_image_type"). Must be one of those supported by matplot lib (currently "svg", "ps", "emf", "rgba", "raw", "svgz", "pdf", "eps", "png"). Default: "eps". -Auto-number ("histogram_auto_number"). Default: false. Flag that determines if the image tag (see above) has the spill count appended to it or not. -Sub-classes may support additional configuration parameter Sub-classes must override: -_configure_at_birth - to extract any additional sub-class-specific configuration from data cards. -_update_histograms. This checks that a spill has the data necessary to update any histograms then creates JSON documents in the format described above. -_cleanup_at_death - to do any sub-class-specific cleanup. """ def __init__(self): """ Set initial attribute values. @param self Object reference. """ # matplotlib histogram - for validation. figure = Figure(figsize=(6, 6)) self.__histogram = FigureCanvas(figure) self.spill_count = 0 # Number of spills processed to date. self.image_type = "eps" self.auto_number = False def birth(self, config_json): """ Configure worker from data cards. If "image_type" is not in those supported then a ValueError is thrown. @param self Object reference. @param config_json JSON document string. @returns True if configuration succeeded. """ config_doc = json.loads(config_json) key = "histogram_auto_number" if key in config_doc: self.auto_number = config_doc[key] key = "histogram_image_type" if key in config_doc: self.image_type = config_doc[key] else: self.image_type = "eps" if self.image_type not in \ self.__histogram.get_supported_filetypes().keys(): error = "Unsupported histogram image type: %s Expect one of %s" \ % (self.image_type, self.__histogram.get_supported_filetypes().keys()) raise ValueError(error) self.spill_count = 0 # Do sub-class-specific configuration. return self._configure_at_birth(config_doc) def _configure_at_birth(self, config_doc): """ Perform sub-class-specific configuration from data cards. Sub-classes must define this function. @param self Object reference. @param config_json JSON document. @returns True if configuration succeeded. """ def process(self, json_string): """ Update the histogram with data from the current spill and output the histogram. @param self Object reference. @param json_string String with current JSON document. @returns JSON document containing current histogram. """ # Load and validate the JSON document. try: json_doc = json.loads(json_string.rstrip()) except Exception: # pylint:disable=W0703 json_doc = {} ErrorHandler.HandleException(json_doc, self) return unicode(json.dumps(json_doc)) self.spill_count = self.spill_count + 1 # Process spill and update histograms. try: result = self._update_histograms(json_doc) except Exception: # pylint:disable=W0703 ErrorHandler.HandleException(json_doc, self) return unicode(json.dumps(json_doc)) image_list = [image['image'] for image in result] # Convert results to strings. return json.dumps({ "maus_event_type": "Image", "image_list": image_list }) def _update_histograms(self, spill): """ Check that the spill has the data necessary to update the histograms then creates JSON documents in the format described above. Sub-classes must define this function. @param self Object reference. @param spill Current spill. @returns list of JSON documents. If the sub-class only updates histograms every N spills then this list can just contain the input spill. Otherwise it should consist of 1 or more JSON documents containing image data in the form described above. @throws Exception if various sub-class specific errors arise. """ def death(self): #pylint: disable=R0201 """ Invokes _cleanup_at_death(). @returns True """ return self._cleanup_at_death() def _cleanup_at_death(self): #pylint: disable=R0201 """ A no-op. Sub-classes can override this function to do sub-class-specific clean-up at death time. @param self Object reference. @returns True """ return True def _create_histogram(self): #pylint: disable=R0201 """ Create a histogram using matplotlib. @param self Object reference. @returns matplotlib FigureCanvas representing the histogram. """ figure = Figure(figsize=(6, 6)) histogram = FigureCanvas(figure) axes = figure.add_subplot(111) axes.grid(True, linestyle="-", color="0.75") return histogram def _rescale_axes(self, histogram, xmin, xmax, ymin, ymax, xfudge=0.5, yfudge=0.5): #pylint: disable=C0301, R0913, R0201 """ Rescale the X and Y axes of the histogram to show the given axis ranges. Fudge factors are used avoid matplotlib warning about "Attempting to set identical bottom==top" which arises if the axes are set to be exactly the maximum of the data. @param self Object reference. @param histogram FigureCanvas representing a histogram. @param xmin Minimum X value. @param xmax Maximum X value. @param ymin Minimum Y value. @param ymin Maximum Y value. @param xfudge X fudge factor. @param yfudge Y fudge factor. """ # Fudge factors are used histogram.figure.get_axes()[0].set_xlim( \ [xmin, xmax + xfudge]) histogram.figure.get_axes()[0].set_ylim( \ [ymin, ymax + yfudge]) def _get_image_doc(self, keywords, description, tag, canvas): #pylint: disable=C0301 """ Build a JSON document holding image data. @param self Object reference. @param keywords List of image keywords. @param description String describing the image. @param tag Image tag. @param histogram FigureCanvas representing a histogram. @returns JSON document. """ json_doc = {} json_doc["maus_event_type"] = "Image" json_doc["image"] = {} if (self.auto_number): image_tag = "%s%06d" % (tag, self.spill_count) else: image_tag = tag data = self.__convert_to_binary(canvas) json_doc["image"]["keywords"] = keywords json_doc["image"]["description"] = description json_doc["image"]["tag"] = image_tag json_doc["image"]["image_type"] = self.image_type json_doc["image"]["data"] = data return json_doc def __convert_to_binary(self, canvas): #pylint: disable=R0201 """ Convert histogram to binary format. @param self Object reference. @param canvas matplotlib FigureCanvas representing a histogram. @returns representation of histogram in base 64-encoded image type format. """ data_file = StringIO.StringIO() canvas.print_figure(data_file, dpi=500, format=self.image_type) data_file.seek(0) data = data_file.read() return base64.b64encode(data)