Exemple #1
0
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)
Exemple #3
0
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)