diff --git a/fitting.py b/fitting.py
index 35fbbd6e0bf70744bb93ff9b36f8306751b1e306..0ac0636ee354745df30d40337f6abad5d022719e 100644
--- a/fitting.py
+++ b/fitting.py
@@ -96,6 +96,39 @@ def fit_double_lorenzian(S, V, *, s0=None, dry_run=False, **kwargs):
     param, param_error, function, [X, _] = result
     param[3] = np.abs(param[3]) # return the positive sigma (could use bounds instead, but that's more likely to fail)
     return param, param_error, function, [X, function(X, *param)]
+
+
+def fit_moving_gaussian(S, T, VV, extend=0, **kwargs):
+    """Fit a gaussian function with linearly moving centroid position to the 2D data
+    
+    :param S: data x values
+    :param T: data y values (axis along which centroid varies)
+    :param VV: data z values of shape [x,y]
+    :param p0: initial values of parameters
+    :param odr: Fit method to use: True means ortogonal distance regression (ODR), False means ordinary least square fit, None (default) uses ODR only if either xerr or yerr is not None
+    :param extend: fraction by which the returned fit trace extends beyond the first/last data point
+    """
+    
+    #r = np.repeat([np.linspace(0, 1, m)], n, axis=0).T
+    ST = np.tile(S, (len(T), 1)).T
+    r = np.linspace(0, 1, ST.shape[1]).reshape(1, -1)
+    moving_gauss = lambda s, v0, vp, s0, ds, sigma: v0+vp*np.exp(-0.5*((s-(s0 + ds*r))/sigma)**2)
+
+    V0 = np.average(VV[:, :VV.shape[1]//5], axis=1)  # average over first 5% of data for initial mean
+    V1 = np.average(VV[:, -VV.shape[1]//5:], axis=1)  # average over last 5% of data for final mean
+    v0 = np.min(VV)
+    vp = np.max(VV)-v0
+    s0 = S[np.argmax(V0)]
+    s1 = S[np.argmax(V1)]
+    ds = s1 - s0
+    sigma = np.sqrt(np.abs(np.sum((S - s0) ** 2 * V0) / np.sum(V0)))
+    result = fit_function(lambda *a, **b: moving_gauss(*a, **b).ravel(), ST, VV.ravel(), p0=(v0, vp, s0, ds, sigma), extend=None, **kwargs) # flatten 2D arrays for fit
+    param, param_error, function, _ = result
+    param[4] = np.abs(param[4]) # return the positive sigma (could use bounds instead, but that's more likely to fail)
+    mi, ma = np.min(T), np.max(T)
+    X = np.linspace(mi - (ma-mi)*extend, ma + (ma-mi)*extend, 1000)
+    Y = param[2] + param[3]*np.linspace(0, 1, 1000)
+    return param, param_error, function, [X, Y]
     
 
 def fit_function(function, x, y, *, xerr=None, yerr=None, p0=None, odr=None, extend=0):
@@ -126,9 +159,12 @@ def fit_function(function, x, y, *, xerr=None, yerr=None, p0=None, odr=None, ext
         param, cov = scipy.optimize.curve_fit(function, x, y, p0)
         param_error = np.sqrt(np.abs(cov.diagonal()))
     
-    mi, ma = min(x), max(x)
-    X = np.linspace(mi - (ma-mi)*extend, ma + (ma-mi)*extend, 1000)
-    return param, param_error, function, [X, function(X, *param)]
+    if extend is not None:
+        mi, ma = np.min(x), np.max(x)
+        X = np.linspace(mi - (ma-mi)*extend, ma + (ma-mi)*extend, 1000)
+        return param, param_error, function, [X, function(X, *param)]
+    else:
+        return param, param_error, function, None
 
 def fit_exponential(S, V, **kwargs):
     """Fit an exponential function to the data
diff --git a/plotting.py b/plotting.py
index d404abdc88945d1963c40147c2643d6acaa32a83..a0f3111283e3df3c59a05ea6ab524ee28365762c 100644
--- a/plotting.py
+++ b/plotting.py
@@ -304,7 +304,7 @@ def plot_tune_spectrum(ax, libera_data, xy, turn_range=None, time_range=None, tu
         
 def plot_tune_spectrogram(ax, libera_data, xy, *, nperseg=2**12, noverlap=None, ninterpol=None, over_time=True, colorbar=False,
                           turn_range=None, time_range=None, tune_range=None, excitation=None, vmin=None, vmax=None,
-                          cmap='gist_heat_r', bunches=None, show_nperseg=True):
+                          cmap='gist_heat_r', bunches=None, show_nperseg=True, fit=False, fitkwarg=dict()):
     """Plot a tune spectrogram based on turn-by-turn data
     
     :param ax: Axis to plot onto
@@ -428,11 +428,27 @@ def plot_tune_spectrogram(ax, libera_data, xy, *, nperseg=2**12, noverlap=None,
             ax.plot(xdata, ex_q + ex_dq/2*np.sin(2*np.pi*ex_fc*time + ex_pc), **ex_style)
         else:
             raise NotImpementedError(f'Excitation type {ex_type} not implemented')
-            
-            
-            
+
     
     ax.set(ylim=tune_range, ylabel=f'Tune $q_{xy}$', xlabel='Time / s' if over_time else 'Turn') #  or $1-q_{xy}$')
+    
+    if fit:
+        dq_dx = None
+        try:
+            fitr = fit(tune, xdata, mag)
+            if fit in (fit_moving_gaussian, ):
+                v0, vp, q0, dq, sigma = fitr[0]
+                dx = xdata[-1]-xdata[0]
+                dq = SI.Measurement(fitr[0][3], fitr[1][3], '1/s' if over_time else '')
+                dq_dx = dq/dx
+                ax.plot(*fitr[-1], **dict(ls='--', label=f'Fit $\\partial q_{xy}/\\partial '+('t' if over_time else 'n')+f'={dq_dx:~L}$', zorder=50, color=cmap_petroff_10(9), **fitkwarg))
+                
+        except RuntimeError:
+            print('Warning: fit failed')
+        
+        return xdata, tune, mag, frev, dq_dx
+            
+    
     return xdata, tune, mag, frev