Source code for pybert.data.dataview

# -*- coding: utf-8 -*-
"""Visualize ERT data."""

import numpy as np
from numpy import ma
import matplotlib as mpl
import matplotlib.pyplot as plt

import pygimli as pg
# from pygimli.mplviewer import cmapFromName
import pygimli.utils
from pygimli.mplviewer.dataview import generateMatrix, plotMatrix, patchMatrix
from pygimli.mplviewer.dataview import patchValMap

import pybert as pb

from . datascheme import Pseudotype, DataSchemeManager


[docs]def showData(data, vals=None, **kwargs): """Utility one-liner to show a BERT datafile. Creates figure, axis and Show. Parameters ---------- data : :bertapi:`BERT::DataContainerERT` **kwargs : * axes : matplotlib.axes Axes to plot into. Default is None and a new figure and axes are created. * vals : Array[nData] Values to be plotted. Default is data('rhoa'). * schemeName : str Name for the scheme to be plotted with the old semiautomatic style. Default is 'auto' to draw the new plotting style. * : * is forwarded to plotERTData (old style) or drawData (new style) """ # remove ax keyword global ax = kwargs.pop('axes', None) if ax is None: ax = kwargs.pop('ax', None) if ax is None: fig = plt.figure() ax = None axTopo = None if 'showTopo' in kwargs: ax = fig.add_subplot(1, 1, 1) # axs = fig.subplots(2, 1, sharex=True) # # Remove horizontal space between axes # fig.subplots_adjust(hspace=0) # ax = axs[1] # axTopo = axs[0] else: ax = fig.add_subplot(1, 1, 1) pg.checkAndFixLocaleDecimal_point(verbose=False) # vals = kwargs.pop('vals', data('rhoa')) if vals is None: vals = 'rhoa' if isinstance(vals, str): if data.haveData(vals): vals = data(vals) else: raise KeyError('field not in data container: ', vals) schemeName = kwargs.pop('schemeName', 'auto') if schemeName == 'auto': ax, cbar = plotERTData(data, vals=vals, ax=ax, **kwargs) else: # print("data: ", min(vals), max(vals)) drawData(ax, data, vals, schemeName=schemeName, **kwargs) if 'xlabel' in kwargs: ax.set_xlabel(kwargs['xlabel']) if 'ylabel' in kwargs: ax.set_ylabel(kwargs['ylabel']) if 'showTopo' in kwargs: # if axTopo is not None: print(ax.get_position()) axTopo = plt.axes([ax.get_position().x0, ax.get_position().y0, ax.get_position().x0+0.2, ax.get_position().y0+0.2]) x = pg.x(data) x *= (ax.get_xlim()[1] - ax.get_xlim()[0]) / (max(x)-min(x)) x += ax.get_xlim()[0] axTopo.plot(x, pg.z(data), '-o', markersize=4) axTopo.set_ylim(min(pg.z(data)), max(pg.z(data))) axTopo.set_aspect(1) # ax.set_aspect('equal') # plt.pause(0.1) pg.mplviewer.updateAxes(ax) if schemeName == 'auto': return ax, cbar else: return ax
[docs]def plotERTData(data, **kwargs): """Plot ERT data as pseudosection matrix (position over separation). Parameters ---------- data : pybert.DataContainerERT data container with sensorPositions and a/b/m/n fields vals : iterable of data.size() [data('rhoa')] vector containing the vals to show ax : mpl.axis axis to plot, if not given a new figure is created cMin/cMax : float minimum/maximum color vals logScale : bool logarithmic colour scale [min(A)>0] label : string colorbar label **kwargs: * dx : float x-width of individual rectangles * ind : integer iterable or IVector indices to limit display * var : int [0] historical plotting styles (1, 2) * circular : bool Plot in polar coordinates when plotting via patchValMap Returns ------- ax: The used Axes cbar: The used Colorbar or None """ vals = kwargs.pop('vals', data('rhoa')) valid = data.get("valid").array().astype("bool") vals = ma.array(vals, mask=~valid) ind = kwargs.pop('ind', None) if ind is not None: vals = vals[ind] mid, sep = midconfERT(data, ind) else: mid, sep = midconfERT(data, circular=kwargs.get('circular', False)) var = kwargs.pop('var', 0) ax = None cbar = None if var == 0: # default style dx = kwargs.pop('dx', np.median(np.diff(np.unique(mid))))*2 ax, cbar, ymap = patchValMap(vals, mid, sep, dx=dx, **kwargs) else: # only here for special cases A, xmap, ymap = generateMatrix(mid, sep, vals, **kwargs) if var == 1: la = A.shape[1] B = np.zeros((A.shape[0], la*2+2)) # for i in range(2): # B[:, i:la*2+i-1:2] = A for i in range(4): B[:, i:la*2+i-1:2] += A xmap2 = {} for k in xmap: xmap2[k * 2] = xmap[k] xmap2[k*2 + 1] = xmap[k] kwargs.setdefault('aspect', 2) ax, cbar = plotMatrix(B, xmap2, ymap, showally=False, **kwargs) elif var == 2: ax, cbar = plotMatrix(A, xmap, ymap, showally=False, **kwargs) else: ax, cbar = patchMatrix(A, xmap, ymap, **kwargs) if kwargs.get('circular', False): a = np.array([np.arctan2(x[1], x[0]) for x in data.sensors()]) p = list(range(len(a))) # p.append(0) ax.plot(np.cos(a)[p], np.sin(a)[p], 'o', color='black') for i in range(len(a)): ax.text(1.15 * np.cos(a[i]), 1.15 * np.sin(a[i]), str(i+1), horizontalalignment='center', verticalalignment='center') ax.set_axis_off() ax.set_aspect(1) else: ytl = generateConfStr(np.sort([int(k) for k in ymap])) if 'DD1' in ytl and 'WB2' in ytl and 'DD2'not in ytl: ytl[ytl.index('DD1')] = 'WB1' if 'WA1' in ytl and 'SL2' in ytl and 'WA2'not in ytl: ytl[ytl.index('WA1')] = 'SL1' yt = ax.get_yticks() yt = np.unique(yt.clip(0, len(ytl)-1)) # if yt[0] == yt[1]: # yt = yt[1:] dyt = np.diff(yt) if dyt[-1] < dyt[-2]: yt = yt[:-1] ax.set_yticks(yt) ax.set_yticklabels([ytl[int(yti)] for yti in yt]) return ax, cbar
[docs]def drawData(axes, data, vals, schemeName='A_M', patchView=False, colorBar=True, cMin=None, cMax=None, linear=False, label="", **kwargs): """TODO DOCUMENTME.""" scheme = None dm = DataSchemeManager() scheme = dm.scheme(schemeName) if scheme is None: scheme = dm.scheme('unknown') gci = None if np.min(vals) == np.max(vals): drawDataAsMarker(axes, data, scheme=scheme) elif patchView: gci = pb.data.drawDataAsPatches(axes, data, vals, scheme=scheme, **kwargs) else: gci = drawDataAsMatrix(axes, data, vals, scheme=scheme, logScale=not linear, **kwargs) if colorBar: pg.mplviewer.createColorbar(gci, cMin=cMin, cMax=cMax, nLevs=5, label=label, **kwargs) if gci is not None: if cMin != cMax: gci.set_clim((cMin, cMax)) cmap = kwargs.pop('cMap', None) if cmap is not None: if type(cmap) is str: gci.set_cmap(pg.mplviewer.cmapFromName(cmap)) else: gci.set_cmap(cmap) return gci
[docs]def midconfERT(data, ind=None, rnum=1, circular=False): """Return the midpoint and configuration key for ERT data. Return the midpoint and configuration key for ERT data. Parameters ---------- data : pybert.DataContainerERT data container with sensorPositions and a/b/m/n fields ind : [] Documentme rnum : [] Documentme circular : bool Return midpoint in degree (rad) instead if meter. Returns ------- mid : np.array of float representative midpoint (middle of MN, AM depending on array) conf : np.array of float configuration/array key consisting of 1) array type (Wenner-alpha/beta, Schlumberger, PP, PD, DD, MG) 00000: pole-pole 10000: pole-dipole or dipole-pole 30000: Wenner-alpha 40000: Schlumberger or Gradient 50000: dipole-dipole or Wenner-beta 2) potential dipole length (in electrode spacings) 3) separation factor (current dipole length or (di)pole separation) """ # xe = np.hstack((pg.x(data.sensorPositions()), np.nan)) # not used anymore x0 = data.sensorPosition(0).x() xe = pg.x(data.sensorPositions()) - x0 ux = pg.unique(xe) if len(ux) * 2 > data.sensorCount(): # 2D with topography case dx = np.array(pg.utils.diff(pg.utils.cumDist(data.sensorPositions()))) dxM = pg.mean(dx) if min(pg.y(data)) != max(pg.y(data)) or \ min(pg.z(data)) != max(pg.z(data)): # Topography case if (pg.max(abs(dx-dxM)) < dxM*0.9): # if the maximum spacing < meanSpacing/2 we assume equidistant # spacing and no missing electrodes dx = np.ones(len(dx)) * dxM else: # topography with probably missing electrodes dx = np.floor(dx/np.round(dxM))*dxM pass if max(dx) < 0.5: print("Detecting small distances, using mm accuracy") rnum = 3 xe = np.hstack((0., np.cumsum(np.round(dx, rnum)), np.nan)) de = np.median(np.diff(xe[:-1])).round(rnum) ne = np.round(xe/de) else: # 3D (without topo) case => take positions directly de = np.median(np.diff(ux)).round(1) ne = np.array(xe/de, dtype=int) # a, b, m, n = data('a'), data('b'), data('m'), data('n') # check if xe[a]/a is better suited (has similar size) if circular: # for circle geometry center = np.mean(data.sensorPositions(), axis=0) x = pg.x(data)-center[0] y = pg.y(data)-center[1] a = np.array([np.arctan2(y[i], x[i]) for i in data('a')]) b = np.array([np.arctan2(y[i], x[i]) for i in data('b')]) m = np.array([np.arctan2(y[i], x[i]) for i in data('m')]) n = np.array([np.arctan2(y[i], x[i]) for i in data('n')]) a = np.unwrap(a) % (np.pi*2) b = np.unwrap(b) % (np.pi*2) m = np.unwrap(m) % (np.pi*2) n = np.unwrap(n) % (np.pi*2) else: a = np.array([ne[int(i)] for i in data('a')]) b = np.array([ne[int(i)] for i in data('b')]) m = np.array([ne[int(i)] for i in data('m')]) n = np.array([ne[int(i)] for i in data('n')]) if ind is not None: a = a[ind] b = b[ind] m = m[ind] n = n[ind] anan = np.isnan(a) a[anan] = b[anan] b[anan] = np.nan ab, am, an = np.abs(a-b), np.abs(a-m), np.abs(a-n) bm, bn, mn = np.abs(b-m), np.abs(b-n), np.abs(m-n) if circular: for v in [ab, mn, bm, an]: v[v > np.pi] = 2*np.pi - v[v > np.pi] # 2-point (default) 00000 sep = np.abs(a-m) mid = (a+m) / 2 # 3-point (PD, DP) (now only b==-1 or n==-<1, check also for a and m) imn = np.isfinite(n)*np.isnan(b) mid[imn] = (m[imn]+n[imn]) / 2 sep[imn] = np.minimum(am[imn], an[imn]) + 10000 + 100 * (mn[imn]-1) + \ (np.sign(a[imn]-m[imn])/2+0.5) * 10000 iab = np.isfinite(b)*np.isnan(n) mid[iab] = (a[iab]+b[iab]) / 2 # better 20000 or -10000? sep[iab] = np.minimum(am[iab], bm[iab]) + 10000 + 100 * (ab[iab]-1) + \ (np.sign(a[iab]-n[iab])/2+0.5) * 10000 # + 10000*(a-m) # 4-point alpha: 30000 (WE) or 4000 (SL) iabmn = np.isfinite(a) & np.isfinite(b) & np.isfinite(m) & np.isfinite(n) ialfa = np.copy(iabmn) ialfa[iabmn] = (ab[iabmn] >= mn[iabmn]+2) # old mnmid = (m[iabmn] + n[iabmn]) / 2 ialfa[iabmn] = np.sign((a[iabmn]-mnmid)*(b[iabmn]-mnmid)) < 0 mid[ialfa] = (m[ialfa] + n[ialfa]) / 2 spac = np.minimum(bn[ialfa], bm[ialfa]) abmn3 = np.round((3*mn[ialfa]-ab[ialfa])*10000)/10000 sep[ialfa] = spac + (mn[ialfa]-1)*100*(abmn3 != 0) + \ 30000 + (abmn3 < 0)*10000 # gradient # %% 4-point beta ibeta = np.copy(iabmn) ibeta[iabmn] = (bm[iabmn] >= mn[iabmn]) & (~ialfa[iabmn]) if circular: # print(ab[ibeta]) ibeta = np.copy(iabmn) def averageAngle(vs): sumsin = 0 sumcos = 0 for v in vs: sumsin += np.sin(v) sumcos += np.cos(v) return np.arctan2(sumsin, sumcos) abC = averageAngle([a[ibeta], b[ibeta]]) mnC = averageAngle([m[ibeta], n[ibeta]]) mid[ibeta] = averageAngle([abC, mnC]) # speccial case when dipoles are completly opposite iOpp = abs(abs((mnC - abC)) - np.pi) < 1e-3 mid[iOpp] = averageAngle([b[iOpp], m[iOpp]]) minAb = min(ab[ibeta]) sep[ibeta] = 50000 + (np.round(ab[ibeta]/minAb)) * 100 + \ np.round(np.minimum(np.minimum(am[ibeta], an[ibeta]), np.minimum(bm[ibeta], bn[ibeta])) / minAb) else: mid[ibeta] = (a[ibeta] + b[ibeta] + m[ibeta] + n[ibeta]) / 4 sep[ibeta] = 50000 + (ab[ibeta]-1) * 100 + np.minimum( np.minimum(am[ibeta], an[ibeta]), np.minimum(bm[ibeta], bn[ibeta])) # %% 4-point gamma # multiply with electrode distance and add first position if not circular: mid *= de mid += x0 return mid, sep
[docs]def generateConfStr(yy): """Generate configuration string to characterize array.""" types = ['PP', 'PD', 'DP', 'WA', 'SL', 'DD'] # base types spac = yy % 100 # source-receiver distance dip = np.round(yy//100) % 100 # MN dipole length typ = np.round(yy//10000) # check if SL is actually GR (multi-gradient) # check if DD-n-n should be renamed rendd = (np.mean(spac / (dip+1)) < 2.1) keys = [] for s, d, t in zip(spac, dip, typ): key = types[t] if d > 0: if rendd and d+1 == s and t == 5: key = 'WB' else: key = key + str(d+1) + '-' key = key + "{:2d}".format(s) # str(s) keys.append(key) return keys
[docs]def createPseudoPosition(data, scheme, scaleX=False): """Create pseudo x position and separation for the dataset. ScaleX: scales the x positions regarding the real electrode positions. """ nElecs = data.sensorCount() if scheme.typ == Pseudotype.DipoleDipole: x = (data('a') + data('b') + data('m') + data('n')) / 4.0 sep = pg.abs((data('n') + data('m'))/2. - (data('b') + data('a'))/2.) elif scheme.typ == Pseudotype.WennerBeta: x = (data('a') + data('b') + data('m') + data('n')) / 4.0 sep = pg.abs((data('n')+data('m'))/2. - (data('b')+data('a'))/2.)/2. sep = sep + 1. elif scheme.typ == Pseudotype.WennerAlpha: x = (data('a') + data('b') + data('m') + data('n')) / 4.0 sep = pg.abs((data('b')-data('a'))/2 + (data('n')-data('m'))/2.)/2.0 sep = sep + 1. elif scheme.typ == Pseudotype.Schlumberger: x = (data('a') + data('b') + data('m') + data('n')) / 4.0 sep = pg.abs((data('b')-data('a')) / 2.0 + (data('n')-data('m')) / 2.) elif scheme.typ == Pseudotype.PoleDipole: x = data('m') sep = pg.abs(data('a') - data('m')) sep = sep + 1. elif scheme.typ == Pseudotype.HalfWenner: x = data('m') sep = data('a') - data('m') sep = sep + 1. elif scheme.typ == Pseudotype.Gradient: x = (data('m') + data('n')) / 2.0 def psmin(vec1, vec2): ret = pg.RVector(vec1.size(), 0.0) for i in range(len(vec1)): ret[i] = min(vec1[i], vec2[i]) return ret sep = psmin((x - data('a')), (data('b') - x)) / 3.0 elif scheme.typ == Pseudotype.AB_MN: x = data('a') * float(nElecs) + data('b') sep = data('m') * float(nElecs) + data('n') elif scheme.typ == Pseudotype.AB_M: x = data('a') * float(nElecs) + data('b') sep = data('m') elif scheme.typ == Pseudotype.AB_N: x = data('a') * float(nElecs) + data('b') sep = data('n') elif scheme.typ == Pseudotype.Test: x = data('a') * float(nElecs) + data('b') sep = pg.abs(data('m') - data('n')) * float(nElecs) * float(nElecs) + \ data('m') * float(nElecs) + data('n') else: x = pg.RVector(data('a')) sep = pg.RVector(data('m')) # need a copy here , so we do not change the original data !!! x = pg.RVector(x) # I do not get this point! sep = pg.RVector(sep) if scaleX: x += data.sensorPositions()[0][0] x *= data.sensorPositions()[0].distance(data.sensorPositions()[1]) sep -= 1. sep *= -1. return x, sep
[docs]def createDataMatrix(data, vals, scheme): """Create a matrix that represents the ERT data.""" nElecs = data.sensorCount() nData = data.size() # create horizontal (separation) and vertical (x)'pseudopositions' x, sep = createPseudoPosition(data, scheme) # unique separations Sidx = pg.unique(pg.sort(sep)) # unique x-position Xidx = pg.unique(pg.sort(x)) # scale parameter dataWidthInMatrix = 1 xOffset = 0 xLength = len(Xidx) # print(min(pygimli.utils.diff(Xidx))) if scheme.typ > 2 and len(Xidx) > 1: if pg.min(pygimli.utils.diff(Xidx)) < 1.0: dataWidthInMatrix = int(1.0 / pg.min(pygimli.utils.diff(Xidx))) if dataWidthInMatrix > 1: xOffset = int(Xidx[0] * dataWidthInMatrix) - 1 xLength = (nElecs - 1) * dataWidthInMatrix # print("xLength: ", xLength) mat = np.ndarray(shape=(len(Sidx), xLength,), dtype=float, order='F') # mat = arange(0.0, len(Sidx) * xLength) mat[:] = 0.0 mat = ma.masked_where(mat == 0.0, mat) # mat = mat.reshape(len(Sidx), xLength) xMin = 1e99 xMax = -1e99 for i in range(0, nData): if data.get('valid')[i]: xPos = pg.find(Xidx == x[i])[0] mat[pg.find(Sidx == sep[i]), xPos + xOffset] = vals[i] for j in range(1, dataWidthInMatrix): mat[pg.find(Sidx == sep[i]), xPos + xOffset + j] = vals[i] xMin = min(xMin, xPos) xMax = max(xMax, xPos + xOffset + dataWidthInMatrix) # print("datasize:", data.size(), "shown: ", # len(mat[~mat.mask]) / dataWidthInMatrix) notShown = (data.size() - len(mat[~mat.mask]) / dataWidthInMatrix) if notShown > 0: print("data not shown: ", notShown) return mat, Xidx, Sidx, dataWidthInMatrix, xMin, xMax
[docs]def drawDataAsMatrix(ax, data, vals, scheme, mat=None, logScale=True, **kwargs): """Draw data as matrix image in axes ax.""" norm = None if vals is None: if mat is not None: if isinstance(mat, pg.RMatrix): # t = [] # for i in mat: t.append(pg.RVectorToList(i)) # m = array(t) # m.reshape(len(mat), len(mat[0])) t = np.zeros((len(mat), len(mat[0]))) for i, row in enumerate(mat): t[i] = row mat = t elif isinstance(mat, list): t = [] for i in mat: t.append(i) m = np.array(t) m.reshape(len(mat), len(mat[0])) mat = m else: raise BaseException('drawDataAsMatrix(...) No vals/matrix given.') else: cmin = np.min(vals) cmax = np.max(vals) if cmin <= 0: logScale = False if logScale: vals, cmin, cmax = pg.mplviewer.colorbar.findAndMaskBestClim( vals, cMin=None, cMax=None, dropColLimitsPerc=5) norm = mpl.colors.LogNorm() matSpacing = None if mat is None: if data: mat, matXidx, matSidx, matSpacing, xMin, xMax = createDataMatrix( data, vals, scheme) else: raise Exception(('no data or matrix given')) mat = ma.masked_where(mat == 0.0, mat) if min(mat.flat) < 0: norm = mpl.colors.Normalize() else: norm = mpl.colors.LogNorm() image = ax.imshow(mat, interpolation='nearest', norm=norm) image.get_cmap().set_bad([1.0, 1.0, 1.0, 0.0]) # print(mat.shape) # print(matXidx) # print(matSidx) # print(min(matXidx), max(matXidx), matSpacing) # print(min(matSidx), max(matSidx), matSpacing) ax.set_xlim(xMin - matSpacing, xMax + matSpacing) # ax.set_xlim(data.sensorPositions()[0][0]*matSpacing - matSpacing, # data.sensorPositions()[-1][0]*matSpacing + matSpacing) annotateSeparationAxis(ax, scheme, grid=True) return image
[docs]def annotateSeparationAxis(ax, scheme, grid=False): """Draw y-axes tick labels corresponding to the separation.""" prefix = scheme.prefix def sepName(sep): suffix = "" if sep == 0: return '' elif sep > 0: suffix = "'" if grid: ax.plot(ax.get_xlim(), [sep, sep], color='black', linewidth=1, linestyle='dotted') return prefix + ' $' + str(abs(int(sep))) + suffix + '$' ax.yaxis.set_ticklabels([sepName(l) for l in ax.yaxis.get_ticklocs()])
[docs]def drawElectrodesAsMarker(ax, data): """Draw electrode marker, these marker are pickable.""" elecsX = [] elecsY = [] for i in range(len(data.sensorPositions())): elecsX.append(data.sensorPositions()[i][0]) elecsY.append(data.sensorPositions()[i][1]) electrodeMarker, = ax.plot(elecsX, elecsY, 'x', color='black', picker=5.) ax.set_xlim([data.sensorPositions()[0][0]-1., data.sensorPositions()[data.sensorCount() - 1][0] + 1.]) return electrodeMarker
[docs]def drawDataAsMarker(ax, data, scheme, **kwargs): """Draw pseudosection scheme for the data using marker only.""" # first draw the electrodes electrodeMarker = drawElectrodesAsMarker(ax, data) # now draw the data Marker x, sep = createPseudoPosition(data, scheme, scaleX=True) # print((max(sep))) # print(sep) maxSepView = max(sep) + 2 if max(sep) > 0: maxSepView = maxSepView - 1 ax.set_ylim([min(sep) - 1, maxSepView]) dataMarker, = ax.plot(x, sep, '.', color='black', picker=5., **kwargs) annotateSeparationAxis(ax, scheme, grid=True) return electrodeMarker, dataMarker
[docs]def createDataPatches(ax, data, scheme, **kwargs): """Create patches for a pseudosection.""" # swatch = pg.Stopwatch(True) x, sep = createPseudoPosition(data, scheme, scaleX=True) ax.set_ylim([min(sep)-1, max(sep)+1]) # dx2 = (data.sensorPositions()[1][0] - data.sensorPositions()[0][0])/4. dx2 = (x[1]-x[0])/2. dSep2 = 0.5 polys = [] for i, xv in enumerate(x): s = sep[i] polys.append(list(zip([xv-dx2, xv+dx2, xv+dx2, xv-dx2], [s - dSep2, s - dSep2, s + dSep2, s + dSep2]))) patches = mpl.collections.PolyCollection(polys, antialiaseds=False, lod=True, **kwargs) patches.set_edgecolor('face') # patches.set_linewidth(0.001) ax.add_collection(patches) # print("Create data patches takes t = ", swatch.duration(True)) return patches
[docs]def drawDataAsPatches(ax, data, vals, scheme, writeValues=False, logScale=True, **kwargs): """Draw pseudosection as patch graphic.""" # first draw the electrodes # electrodeMarker = drawElectrodesAsMarker(ax, data) # never used! # now draw the data Marker gci = createDataPatches(ax, data, scheme, **kwargs) vals = ma.masked_where(vals == 0, vals * data.get('valid')) pg.mplviewer.setMappableData(gci, vals, logScale=logScale) # if min(vals) < 0 : # writeValues = True if writeValues: x, sep = createPseudoPosition(data, scheme, scaleX=True) for i, xv in enumerate(x): ax.text(xv, sep[i], str(round(vals[i], 2)), fontsize='8', horizontalalignment='center', verticalalignment='center') annotateSeparationAxis(ax, scheme, grid=True) return gci