Source code for epygram._plugins.with_cartopy.H2DField

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) Météo France (2014-)
# This software is governed by the CeCILL-C license under French law.
# http://www.cecill.info
"""
Extend H2DField with plotting methods using cartopy.
"""
from __future__ import print_function, absolute_import, unicode_literals, division
import six

import numpy
import copy
import cartopy.crs as ccrs
import cartopy.feature as cfeature
from cartopy.mpl.gridliner import (LATITUDE_FORMATTER,
                                   LONGITUDE_FORMATTER)

import footprints

from epygram import epygramError, config, util
from epygram import colormapping

epylog = footprints.loggers.getLogger(__name__)


def activate():
    """Activate extension."""
    from . import __name__ as plugin_name
    from epygram._plugins.util import notify_doc_requires_plugin
    notify_doc_requires_plugin([cartoplot, cartoplot_fig_init,
                                cartoplot_background],
                               plugin_name)
    from epygram.fields import H2DField
    # defaults arguments for cartopy plots
    H2DField.default_NEfeatures = [dict(category='cultural',
                                        name='admin_0_countries',
                                        facecolor='none',
                                        edgecolor='k'),]
    H2DField.default_scatter_kw = {'s':20,
                                   'marker':',',
                                   'linewidths':0}
    H2DField.default_contour_kw = {'linewidths':1}
    H2DField.default_clabel_kw = {'fmt':'%0i'}
    H2DField.default_gridlines_kw = {'draw_labels':True,
                                     'linewidth':1,
                                     'color':'k',
                                     'linestyle':'--'}
    # methods
    H2DField.cartoplot_fig_init = cartoplot_fig_init
    H2DField.cartoplot_background = cartoplot_background
    H2DField._cartoplot_treat_minmax = classmethod(_cartoplot_treat_minmax)
    H2DField._cartoplot_mesh_coords = _cartoplot_mesh_coords
    H2DField._cartoplot_get_coords = _cartoplot_get_coords
    H2DField._cartoplot_shape = _cartoplot_shape
    H2DField._cartoplot_get_ColormapHelper = classmethod(_cartoplot_get_ColormapHelper)
    H2DField._cartoplot_colormap_kwargs = classmethod(_cartoplot_colormap_kwargs)
    H2DField._cartoplot_actualplot = _cartoplot_actualplot
    H2DField._cartoplot_colorbar = classmethod(_cartoplot_colorbar)
    H2DField._cartoplot_text = _cartoplot_text
    H2DField.cartoplot = cartoplot


def meshable_geometry(geometry):
    """Returns True is the geometry is meshable (rectangularly)."""
    return geometry.rectangular_grid and geometry.name != 'unstructured'


def cartoplot_fig_init(self,
                       fig=None,
                       ax=None,
                       projection=None,
                       figsize=config.plotsizes,
                       rcparams=config.default_rcparams,
                       set_global=False):
    """Consistently set figure, ax and projection."""
    import matplotlib.pyplot as plt
    for args, kwargs in rcparams:
        plt.rc(*args, **kwargs)
    # fig, ax, proj
    if fig is None:
        assert ax is None, "Cannot specify an **ax** without a **fig**."
        if projection is None:
            projection = self.geometry.default_cartopy_CRS()
        fig = plt.figure(figsize=figsize)
        ax = fig.add_subplot(1,1,1, projection=projection)
    else:
        if ax is None:
            if projection is None:
                projection = self.geometry.default_cartopy_CRS()
            ax = fig.add_subplot(1,1,1, projection=projection)
        else:
            assert hasattr(ax, 'projection'), "Cannot provide an **ax** which does not hold a projection."
            if projection is not None:
                assert ax.projection == projection
            else:
                projection = ax.projection
    # enlarge ?
    if set_global:
        ax.set_global()
    return fig, ax, projection


def cartoplot_background(self,
                         ax,
                         projection,
                         cartopy_features=[],
                         natural_earth_features='__default__',
                         meridians='auto',
                         parallels='auto',
                         gridlines_kw=None,
                         epygram_departments=False,
                         subzone=None):
    """Set cartography features, such as borders, coastlines, meridians and parallels..."""
    if natural_earth_features == '__default__':
        natural_earth_features = self.default_NEfeatures,
    if 'gauss' in self.geometry.name:
        default_scale = '110m'
    else:
        default_scale = '50m'
    for f in cartopy_features:
        if isinstance(f, six.string_types):
            f = getattr(cfeature, f)
        ax.add_feature(f.with_scale(default_scale))
    for f in natural_earth_features:
        f = copy.copy(f)
        if 'scale' not in f:
            f['scale'] = default_scale
        ax.add_feature(cfeature.NaturalEarthFeature(**f))
    # meridians and parallels
    meridians, parallels = util.auto_meridians_parallels(self.geometry,
                                                         meridians,
                                                         parallels)
    if gridlines_kw is None:
        gridlines_kw = copy.copy(self.default_gridlines_kw)
    if (meridians, parallels) != ([], []):
        if not isinstance(projection, (ccrs.PlateCarree, ccrs.Mercator)):
            # only these projections have labels available in cartopy
            gridlines_kw.pop('draw_labels', False)
            # home-made workaround for Lambert
            if isinstance(projection, ccrs.LambertConformal):
                from cartopy_plus import lambert_parallels_meridians_labels
                lambert_parallels_meridians_labels(ax, self.geometry, projection,
                                                   meridians, parallels,
                                                   subzone=subzone)
            else:
                pass  # TODO: workarounds for other geometries ?
        gl = ax.gridlines(xlocs=meridians,
                          ylocs=parallels,
                          **gridlines_kw)
        gl.xlabels_top = False
        gl.ylabels_right = False
        gl.xformatter = LONGITUDE_FORMATTER
        gl.yformatter = LATITUDE_FORMATTER
        # stereo-polar, get more round circles
        if isinstance(projection, (ccrs.Stereographic,
                                   ccrs.NorthPolarStereo,
                                   ccrs.SouthPolarStereo)):
            gl.n_steps = 90
    if epygram_departments:
        if not isinstance(epygram_departments, dict):
            epygram_departments = dict(color='k')
        import json
        with open(config.installdir + '/data/french_departments.json', 'r') as dp:
            depts = json.load(dp)[1]
        for d in list(range(len(depts)))[:]:
            for part in range(len(depts[d])):
                dlon = numpy.array(depts[d][part][0])
                dlat = numpy.array(depts[d][part][1])
                xyz = projection.transform_points(ccrs.PlateCarree(), dlon, dlat)
                x = xyz[..., 0]
                y = xyz[..., 1]
                ax.plot(x, y, **epygram_departments)


def _cartoplot_treat_minmax(cls,
                            data,
                            mask_threshold,
                            minmax,
                            colormap_helper,
                            minmax_along_colorbar):
    """Mask according to threshold, get min/max, mask according to colormap."""
    # 1/ mask values or explicit thresholding
    mask_outside = {'min':-config.mask_outside,
                    'max':config.mask_outside}
    if mask_threshold is not None:
        mask_outside.update(mask_threshold)
    data = numpy.ma.masked_outside(data,
                                   mask_outside['min'],
                                   mask_outside['max'])
    # 2/ get min/max for plot
    pmin = data.min()
    pmax = data.max()
    if minmax_along_colorbar and (minmax is not None or
                                  colormap_helper.explicit_colorbounds is not None):
        minmax_along_colorbar = '(min: {: .{precision}{type}} // max: {: .{precision}{type}})'.format(
            pmin, pmax, type='E', precision=3)
    else:
        minmax_along_colorbar = ''
    if minmax is not None:
        try:
            pmin = float(minmax[0])
        except ValueError:
            pass
        try:
            pmax = float(minmax[1])
        except ValueError:
            pass
    if colormap_helper.explicit_colorbounds is not None:
        if minmax is not None:
            epylog.warning('**minmax** overwritten by **colormap_helper.explicit_colorbounds**')
        pmin = min(colormap_helper.explicit_colorbounds)
        pmax = max(colormap_helper.explicit_colorbounds)
    # 3/ mask outside colorbounds/minmax
    data = numpy.ma.masked_outside(data, pmin, pmax)
    return data, pmin, pmax, minmax_along_colorbar


def _cartoplot_mesh_coords(self, subzone):
    """Get coordinates of mesh corners."""
    if meshable_geometry(self.geometry):
        # In this case we need the coordinates of mesh borders
        lons, lats = self.geometry.get_lonlat_grid(subzone=subzone,
                                                   position='lower-left')
        corners_ij = self.geometry.gimme_corners_ij(subzone=subzone)
        Imax, Jmax = corners_ij['ur']
        Imin, Jmin = corners_ij['ll']
        ii = numpy.arange(Imin, Imax + 1)
        jj = numpy.arange(Jmin, Jmax + 1)
        # compute upper line, upper border
        up_lon, up_lat = self.geometry.ij2ll(ii,
                                             numpy.full((len(ii),), Jmax, dtype=numpy.int),
                                             position='upper-right')
        ul = self.geometry.ij2ll(Imin, Jmax,
                                 position='upper-left')
        up_lon = numpy.ma.hstack((ul[0], up_lon))
        up_lat = numpy.ma.hstack((ul[1], up_lat))
        up_lon = up_lon.reshape((1, len(up_lon)))
        up_lat = up_lat.reshape((1, len(up_lat)))
        # compute right line, right border
        right_lon, right_lat = self.geometry.ij2ll(numpy.full((len(jj),), Imax, dtype=numpy.int),
                                                   jj,
                                                   position='upper-right')
        lr = self.geometry.ij2ll(Imax, Jmin,
                                 position='lower-right')
        right_lon = numpy.ma.hstack((lr[0], right_lon))
        right_lat = numpy.ma.hstack((lr[1], right_lat))
        right_lon = right_lon.reshape((len(right_lon), 1))
        right_lat = right_lat.reshape((len(right_lat), 1))
        # augment numpy array
        lons = numpy.ma.vstack((lons, up_lon[:, :-1]))
        lats = numpy.ma.vstack((lats, up_lat[:, :-1]))
        lons = numpy.ma.hstack((lons, right_lon))
        lats = numpy.ma.hstack((lats, right_lat))
        mask = numpy.ma.array(lats > 90.).filled(False)  # don't replace value where masked
        lats[mask] = 90.
        mask = numpy.ma.array(lats < -90.).filled(False)  # don't replace value where masked
        lats[mask] = -90.
    else:
        raise NotImplementedError("plot_method='pcolormesh' for Gauss or unstructured grids. Any idea ?")  # TODO: ?
    return lons, lats


def _cartoplot_get_coords(self,
                          plot_method,
                          subzone,
                          projection):
    # 1/ coordinates
    if plot_method == 'pcolormesh':
        # In this case we need the coordinates of mesh borders
        lons, lats = self._cartoplot_mesh_coords(subzone)
    else:
        # else the center
        lons, lats = self.geometry.get_lonlat_grid(subzone=subzone)
    if self.geometry.name == 'academic':
        x = (lons - lons.min()) * self.geometry.grid['X_resolution']
        y = (lats - lats.min()) * self.geometry.grid['Y_resolution']
    else:
        xyz = projection.transform_points(ccrs.PlateCarree(), lons, lats)
        x = numpy.ma.array(xyz[..., 0])
        y = numpy.ma.array(xyz[..., 1])
        masked = numpy.logical_or(x == numpy.inf, y == numpy.inf)
        x[masked] = numpy.ma.masked
        y[masked] = numpy.ma.masked
        if isinstance(lons, numpy.ma.masked_array):
            x = numpy.ma.masked_where(lons.mask, x)
            y = numpy.ma.masked_where(lons.mask, y)
    return x, y


def _cartoplot_shape(self,
                     x, y,
                     data,
                     plot_method):
    """Shape according to plot_method."""
    xf = x
    yf = y
    zf = data
    if plot_method in ('contourf', 'contour'):
        if not self.geometry.rectangular_grid:
            # gauss grid
            _fill_value = 1e20
            _shp = [1,1] + list(data.shape)
            zf = self.geometry.fill_maskedvalues(data.reshape(_shp),  # protect actually masked values
                                                 _fill_value)
            zf = numpy.ma.masked_equal(zf.compressed(), _fill_value)  # flatten and re-masked
            xf = numpy.ma.masked_where(zf.mask, x.compressed())
            yf = numpy.ma.masked_where(zf.mask, y.compressed())
        elif self.geometry.dimensions['Y'] == 1:
            # unstructured grid
            xf = x.flatten()
            yf = y.flatten()
            zf = data.flatten()
    elif plot_method == 'scatter':
        # flatten data for scatter
        xf = x.flatten()
        yf = y.flatten()
        zf = data.flatten()
    return xf, yf, zf


def _cartoplot_get_ColormapHelper(cls,
                                  colormap,
                                  colorbounds):
    """Build ColormapHelper object."""
    if colormap in config.colormaps:
        cmapfile = config.colormaps[colormap]
        if cmapfile.endswith('.json'):
            colormaphelper = colormapping.get_ColormapHelper_fromfile(cmapfile)  # potentially already loaded
        elif cmapfile.endswith('.cmap'):
            # deprecated
            raise epygramError(util._deprecated_cmap)
    else:
        colormaphelper = colormapping.ColormapHelper(colormap,
                                                     explicit_colorbounds=colorbounds)
    return colormaphelper


def _cartoplot_colormap_kwargs(cls,
                               colormaphelper,
                               m, M,
                               plot_method,
                               colorsnumber,
                               colorstep,
                               center_cmap_on_0,
                               contourcolor):
    """Get colors kwargs."""
    from matplotlib.colors import cnames
    plot_kwargs = colormaphelper.kwargs_for_plot(plot_method,
                                                 (m, M),
                                                 center_cmap_on_0,
                                                 number=colorsnumber,
                                                 step=colorstep)
    if plot_method == 'contour':
        if contourcolor in cnames or contourcolor[0] == '#':
            colormap = None
        else:
            util.load_cmap(contourcolor)
            colormap = contourcolor
            contourcolor = None
        plot_kwargs.update(colors=contourcolor,
                           cmap=colormap)
    return plot_kwargs


def _cartoplot_actualplot(self,
                          ax,
                          x, y,
                          data,
                          plot_method,
                          plot_kwargs,
                          uniform,
                          colormap_helper,
                          scatter_kw,
                          contour_kw,
                          contourlabel,
                          clabel_kw):
    """Actual call to matplotlib plotting functions."""
    from matplotlib.colors import cnames
    if plot_method == 'contourf':
        if not self.geometry.rectangular_grid or self.geometry.dimensions['Y'] == 1:
            # triangulate for plotting
            pf = ax.tricontourf(x, y, data,
                                **plot_kwargs)
        else:
            pf = ax.contourf(x, y, data,
                             **plot_kwargs)
    elif plot_method == 'scatter':
        # treat uniform field case
        if uniform:
            if colormap_helper.colormap in cnames or len(colormap_helper.colormap) == 1:
                colors = colormap_helper.colormap
            else:
                colors = 'silver'
        else:
            colors = data
        # plot
        if scatter_kw is None:
            scatter_kw = self.default_scatter_kw
        plot_kwargs.update(scatter_kw)
        pf = ax.scatter(x, y, c=colors,
                        **plot_kwargs)
    elif plot_method == 'pcolormesh':
        if any([isinstance(arr, numpy.ma.masked_array) for arr in [x, y]]):
            # pcolormesh cannot plot if x or y contain masked values
            # We mask data values instead of x or y
            data = numpy.ma.array(data)
            m = numpy.logical_or(numpy.ma.getmaskarray(x),
                                 numpy.ma.getmaskarray(y))
            m = numpy.logical_or(numpy.logical_or(m[:-1, :-1],
                                                  m[1:, :-1]),
                                 numpy.logical_or(m[:-1, 1:],
                                                  m[1:, 1:]))
            data[m] = numpy.ma.masked
            x = x.filled()
            y = y.filled()
        pf = ax.pcolormesh(x, y, data,
                           **plot_kwargs)
    elif plot_method == 'contour':
        if contour_kw is None:
            contour_kw = self.default_contour_kw
        plot_kwargs.update(contour_kw)
        if not self.geometry.rectangular_grid or self.geometry.dimensions['Y'] == 1:
            pf = ax.tricontour(x, y, data,
                               **plot_kwargs)
        else:
            pf = ax.contour(x, y, data,
                            **plot_kwargs)
        if contourlabel:
            if clabel_kw is None:
                clabel_kw = self.default_clabel_kw
            ax.clabel(pf,
                      colors=plot_kwargs['colors'],
                      cmap=plot_kwargs['cmap'],
                      **clabel_kw)
    else:
        raise NotImplementedError('plot_method=="{}"'.format(plot_method))
    return pf


def _cartoplot_colorbar(cls,
                        ax,
                        pf,
                        colorbar,
                        colorbar_over,
                        colorbar_ax_kw,
                        colormap_helper,
                        minmax_along_colorbar,
                        m, M,
                        colorsnumber,
                        colorstep):
    """Add colorbar."""
    import matplotlib.pyplot as plt
    from mpl_toolkits.axes_grid1 import make_axes_locatable
    # position of colorbar
    if colorbar_over is None:
        if colorbar_ax_kw is None:
            colorbar_ax_kw = dict(size="5%", pad=0.2)
        cax = make_axes_locatable(ax).append_axes(colorbar,
                                                  axes_class=plt.Axes,
                                                  **colorbar_ax_kw)
    else:
        cax = colorbar_over
    orientation = 'vertical' if colorbar in ('right', 'left') else 'horizontal'
    # ticks
    ticks_label = colormap_helper.ticks_label((m, M),
                                              number=colorsnumber,
                                              step=colorstep)
    ticks_position = colormap_helper.ticks_position((m, M),
                                                    number=colorsnumber,
                                                    step=colorstep)
    cb = plt.colorbar(pf,
                      orientation=orientation,
                      ticks=ticks_position,
                      cax=cax)
    if ticks_label != ticks_position:
        cax.set_yticklabels(ticks_label)
    if minmax_along_colorbar:
        cb.set_label(minmax_along_colorbar)
    return cb

def _cartoplot_text(self, ax,
                    title,
                    uniform,
                    uniformvalue):
    """Set legend text: title and others."""
    if title is None:
        fid = self.fid.get('short',
                           self.fid[sorted(self.fid.keys())[0]])
        title = "\n".join([str(fid),
                           str(self.validity.get())])
    ax.set_title(title)
    if uniform:
        ax.text(1.02, 0.5, '(uniform field: value={})'.format(uniformvalue),
                horizontalalignment='center',
                verticalalignment='center',
                transform=ax.transAxes,
                rotation=90.)


def cartoplot(self,
              # figure
              fig=None,
              ax=None,
              figsize=config.plotsizes,
              rcparams=config.default_rcparams,
              title=None,
              # geometry
              projection=None,
              subzone=None,
              extent='__default__',
              # graphical settings
              plot_method='__default__',
              minmax=None,
              mask_threshold=None,
              scatter_kw=None,
              contour_kw=None,
              contourlabel=False,
              clabel_kw=None,
              # cartography
              meridians='auto',
              parallels='auto',
              gridlines_kw=None,
              cartopy_features=[],
              natural_earth_features='__default__',
              epygram_departments=False,
              # colormapping
              colormap_helper=None,
              colormap='plasma',
              colorbounds=None,
              colorsnumber=None,
              colorstep=None,
              center_cmap_on_0=False,
              contourcolor='blue',
              # colorbar
              colorbar='right',
              colorbar_over=None,
              colorbar_ax_kw=None,
              minmax_along_colorbar=True,
              # takeover
              takeover=False
              ):
    """
    Plot field with **cartopy**. Returns (figure, axis).
    
    Figure settings:

    :param fig: any existing figure to be used for the plot.
        A matplotlib *figure* is a frame containing eventually several
        subplots (axes).
    :param ax: any existing axis to be used to plot on.
        A matplotlib *axis* is the subplot on which the drawing is done.
        If given, **fig** and **ax** must be consistent, i.e. **ax** being
        one of the **fig** axes.
    :param figsize: figure sizes in inches, e.g. (5, 8.5).
        Default figsize is config.plotsizes.
    :param rcparams: list of (*args, **kwargs) to be passed to pyplot.rc()
        defaults to [(('font',), dict(family='serif')),]
    :param title: title for the plot. Default is field identifier and validity.

    Geometry settings:

    :param projection: a cartopy.crs projection to be used for plot.
        Defaults to the field.geometry.default_cartopy_CRS()
    :param subzone: [LAM fields only] among ('C', 'CI'), plots the data
        resp. on the C or C+I zone.
        Default is no subzone, i.e. the whole field.
    :param extent: tune the extent of the map.
        Among ('focus', 'global', '__default__').
        'focus' will focus on the field geometry extent.
        'global' will de-zoom to have the whole globe, if possible
        (call cartopy GeoAxes.set_global()).
        '__default__' will choose one of these depending on the geometry.

    Graphical settings:

    :param plot_method: choice of the matplotlib plotting function to be
        used, among ('contourf', 'contour', 'scatter', 'pcolormesh', None).
        If None, prepare the blank figure, but skip actual plot.
        Default is 'pcolormesh' if the geometry is "meshable",
        else 'contourf'.
    :param minmax: defines the min and max values for the plot colorbar.
        Syntax: [min, max]. Strings 'min' and 'max' (default)
        will take the min and max of the field.
    :param mask_threshold: dict with min and/or max value(s) to mask outside.
    :param scatter_kw: kwargs to be passed to matplotlib's ax.scatter().
        Only for plot_method = 'scatter'.
    :param contour_kw: kwargs to be passed to matplotlib's ax.contour().
        Only for plot_method = 'contour'.
    :param contourlabel: displays labels on contours.
        Only for plot_method = 'contour'.
    :param clabel_kw: kwargs to be passed to matplotlib's ax.clabel().
        Only for plot_method = 'contour'.

    Cartography settings:

    :param meridians: enable to fine-tune the choice of lines to
        plot, with either:
          - 'auto': automatic scaling to the basemap extents
          - 'default': every 10 degree
          - a list of values
          - a grid step, e.g. 5 to plot each 5 degree.
          - None: no one is plot
    :param parallels: cf. **meridians**
    :param gridlines_kw: graphical characteristics of meridians/parallels,
        arguments to be passed to cartopy's ax.gridlines(...)
    :param cartopy_features: list of cartopy.feature.??? features.
    :param natural_earth_features: list of dicts, each of them containing
        arguments to instanciate a cartopy.feature.NaturalEarthFeature(...).
        E.g. [dict(category='cultural', name='admin_1_states_provinces', facecolor='none', linestyle=':'),]
        will add states/departments/provinces.
        Cf. https://scitools.org.uk/cartopy/docs/latest/matplotlib/feature_interface.html#cartopy.feature.NaturalEarthFeature
        for details.
    :param epygram_departments: add high-resolution french departments
        limits stored in epygram. May be a dict containing lines plotting
        arguments, such as linewidth etc...
        Warning: not consistent with natural_earth_features !

    Colormap settings:

    :param colormap_helper: an instance of the class
        epygram.colormapping.ColormapHelper.
        Has priority on the following arguments.
    :param colormap: name of the ``matplotlib`` colormap to use (or an
        ``epygram`` one, or a user-defined one, cf.
        config.usercolormaps).
    :param colorbounds: levels on which to shift color.
    :param colorsnumber: number of levels for contours and colorbar.
    :param colorstep: step in value between two color shift.
    :param center_cmap_on_0: aligns the colormap center on the value 0.
    :param contourcolor: color or colormap to be used for 'contourlines'
        plot_method. It can be either a legal html color name, or a
        colormap name.

    Colorbar settings:

    :param colorbar: if *False*, hide colorbar the plot; else, defines the
        colorbar position, among ('bottom', 'right'). Defaults to 'right'.
    :param colorbar_over: an optional existing ax to plot the colorbar on.
    :param colorbar_ax_kw: kwargs to be passed to
        make_axes_locatable(ax).append_axes(colorbar, **kwargs)
    :param minmax_along_colorbar: if True and minmax is not None,
        adds min and max values along colorbar.
        
    Takeover:
    
    :param takeover: give the user more access to the objects used in the
        plot, by returning a dict containing them instead of only fig/ax
    """
    # 0/ defaults pre-sets
    if plot_method == '__default__':
        if meshable_geometry(self.geometry):
            plot_method = 'pcolormesh'
        else:
            plot_method = 'contourf'
    if natural_earth_features == '__default__':
        natural_earth_features = self.default_NEfeatures
    if not self.geometry.grid.get('LAMzone', False):
        subzone = None
    if extent == '__default__':
        if self.geometry.isglobal:
            extent = 'global'
        else:
            extent = 'focus'
    # 1/ geometry and figure
    fig, ax, projection = self.cartoplot_fig_init(fig,
                                                  ax,
                                                  projection,
                                                  figsize,
                                                  rcparams,
                                                  set_global=(extent == 'global'))
    result = dict(fig=fig, ax=ax)
    # 2/ background
    self.cartoplot_background(ax,
                              projection,
                              cartopy_features,
                              natural_earth_features,
                              meridians,
                              parallels,
                              gridlines_kw,
                              epygram_departments,
                              subzone=subzone)
    # 3/ get data to plot
    if self.spectral:
        self.sp2gp()
    data = self.getdata(subzone=subzone)
    # 4/ handle min/max values
    if colormap_helper is None:
        colormap_helper = self._cartoplot_get_ColormapHelper(colormap,
                                                             colorbounds)
    data, pmin, pmax, minmax_along_colorbar = self._cartoplot_treat_minmax(data,
                                                                           mask_threshold,
                                                                           minmax,
                                                                           colormap_helper,
                                                                           minmax_along_colorbar)
    if abs(float(pmin) - float(pmax)) < config.epsilon and plot_method is not None:
        epylog.warning("uniform field: plot as 'points'.")
        plot_method = 'scatter'
        uniform = True
        uniformvalue = pmin
    else:
        uniform = False
        uniformvalue = None
    # 5/ get coordinates
    x, y = self._cartoplot_get_coords(plot_method,
                                      subzone,
                                      projection)
    # FIXME: (CLEANME) fixed by Sebastien ?
    assert numpy.inf not in [x.min(), x.max(), y.min(), y.max()], \
        "Domain is too large to be plotted on space view"
    assert -numpy.inf not in [x.min(), x.max(), y.min(), y.max()], \
        "Domain is too large to be plotted on space view"
    # 6/ shape data/lons/lats
    x, y, data = self._cartoplot_shape(x, y,
                                       data,
                                       plot_method)
    if extent == 'focus':
        xyz = ax.projection.transform_points(projection, x, y)
        xyz = numpy.ma.masked_where(numpy.abs(xyz) == numpy.inf, xyz)
        ax.set_xlim((xyz[..., 0].min(), xyz[..., 0].max()))
        ax.set_ylim((xyz[..., 1].min(), xyz[..., 1].max()))
        del xyz
    # 7/ colormapping
    plot_kwargs = self._cartoplot_colormap_kwargs(colormap_helper,
                                                  pmin, pmax,
                                                  plot_method,
                                                  colorsnumber,
                                                  colorstep,
                                                  center_cmap_on_0,
                                                  contourcolor)
    if plot_method is not None:
        # 8/ plot
        elements = self._cartoplot_actualplot(ax,
                                              x, y,
                                              data,
                                              plot_method,
                                              plot_kwargs,
                                              uniform,
                                              colormap_helper,
                                              scatter_kw,
                                              contour_kw,
                                              contourlabel,
                                              clabel_kw)
        result['plot_elements'] = elements
        # 9/ colorbar
        if colorbar and plot_method != 'contour':
            cb = self._cartoplot_colorbar(ax,
                                          elements,
                                          colorbar,
                                          colorbar_over,
                                          colorbar_ax_kw,
                                          colormap_helper,
                                          minmax_along_colorbar,
                                          pmin, pmax,
                                          colorsnumber,
                                          colorstep)
            result['colorbar'] = cb
    # 10/ texts
    self._cartoplot_text(ax, title, uniform, uniformvalue)
    return result if takeover else (fig, ax)