{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# A Grid class to deal with any finite difference model grid\n", "*Prof. dr.ir.T.N.Olsthoorn*\n", "\n", "*Heemstede, Sept. 2016, 24 May 2017*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Using a Grid class to handle spatial information regarding the grid" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As we saw we often have to compute values of the grid, like the `xm`, `dx`, `Nx`, `Ny`, `shape` etc. A much better, more convenient and far less error-prone method of dealing with anything that has to do with the spacial dimensions of the finite difference grid is to define a Grid class, whose instances are created with the grid coordinates like so\n", "\n", " gr = Grid(x, y, z)\n", "\n", "after which any spatial information can be obtained from the actual Grid instanceuse, called gr in this example.\n", "\n", "Requesting values then work like this:\n", "\n", " gr.Nx # int, len(x)-1\n", " gr.shape # tuple (Nt, Nx, Nz)\n", " gr.xm # ndarray, (len(x)-1\n", " gr.Xm # ndarray, (Ny, Nx) of x-coordinates of cell centers\n", " gr.XM # ndarray, (Nz, Ny, Nx) of x-coordinates of cell centers\n", " gr.YM # ndarray, (Nz, Ny, Nx) of y-coordinates of cell centers\n", " gr.xm[3:10] # indexing gr.xm\n", " gr.shape # tuple, (Nz, Ny, Nx)\n", " gr.area # scalar, total area of the model\n", " gr.Area # ndarray, (Ny, Nx)\n", " gr.volume # scalar total volume of the model\n", " gr.Volume # ndarray, (Nz, Ny, Nx)\n", "\n", "A large number of spacial or grid-specific variables, in fact, anything that can be computed from the coordinates can then be obtained.\n", "\n", "The Grid class will take care of error checking and house-keeping. It can even be told to interpret the grid as\n", "axially symmetric\n", "\n", " gr = Grid(x, y, z, axial=True)\n", "\n", "It can guarantee a minimum layer thickness like so\n", "\n", " gr = Grid(x, y, z, min_dz=0.001)\n", " \n", "No matter if z used to invoke the Grid was specified as a vector telling the elevation of the tops and bottoms of uniform layers, or if z is a full-fledged 3D ndarray telling the top and bottom of all cells, Grid will handle it, in any case yielding a full 3D array of tops and bottoms when requested\n", "\n", " gr.Z # full 3D array: shape (Ny, Nx, Nz+1)\n", "\n", "and any other like grid related quantities that one may think of.\n", "\n", "All grid related information is then contained in the grid object, where the z needs not be limited to a vector but may be a full 3D array of the tops and bottoms of all cells, so that each cell column can have elevations different from its neighbors. This approach is definitely much more flexible. Also, the grid can carry out all necessary error checking behing the scene which is effective as well.\n", "\n", "The Grid class also has methods,like\n", "\n", " A = gr.const(v)\n", " \n", "This generates a ndarray of the size of the model with all values `v` if `v` is a scalar or with\n", "the values cells in layer `i` the value `v[i]` if `v` is a vector of length `gr.Nz`.\n", "\n", " gr.plot(linespec)\n", "\n", "will plot itself using the specified linespec, i.e. combination of color and linetype like used in ``matplotilb.plot``.\n", "\n", "Additionally, other functions like `fdm3` can be adapted to simply accept a `Grid` object as input instead of individual `x`, `y` and `z` coordinates. This requires less preparation and less clutting insize fdm3, while error checking then is delegated to the Grid object.\n", "\n", "You can learn about the Grid class by introspection or by simply loading it in an editor and studying how it was implemented. Simply typing\n", "\n", " Grid?\n", "\n", "Provides the help from its docstring.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Grid-adapted model fdm3\n", "\n", "The module fdm_b of the previous chapter contains the functions `unique()`, `quivdata()` and `fdm3`. This model is copied below but only the functions `fdm3` and `quivdata` have been adapted to deal with the grid object for its grid information.\n", "\n", "The new module will be save as ``fdm_c.py``" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Overwriting fdm_c.py\n" ] } ], "source": [ "%%writefile fdm_c.py\n", "\n", "import numpy as np\n", "import pdb\n", "import scipy.sparse as sp\n", "from scipy.sparse.linalg import spsolve # to use its short name\n", "from collections import namedtuple\n", "\n", "class InputError(Exception):\n", " pass\n", "\n", "def quivdata(Out, gr, iz=0):\n", " \"\"\"Returns coordinates and velocity components to show velocity with quiver\n", " \n", " Compute arrays to display velocity vectors using matplotlib's quiver.\n", " The quiver is always drawn in the xy-plane for a specific layer. Default iz=0\n", " \n", " Parameters\n", " ----------\n", " `Out` : namedtuple holding arrays `Qx`, `Qy`, `Qz` as defined in `fdm3`\n", " `Qx` : ndarray, shape: (Nz, Ny, Nx-1), [L3/T]\n", " Interfacial flows in finite difference model in x-direction from `fdm3'\n", " `Qy` : ndarray, shape: (Nz, Ny-1, Nx), [L3/T]\n", " Interfacial flows in finite difference model in y-direction from `fdm3`\n", " `Qz` : ndarray, shape: (Nz-1, Ny, Nx), [L3/T]\n", " Interfacial flows in finite difference model in z-direction from `fdm3` \n", " `gr` : `grid_object` generated by Grid\n", " `iz` : int [-]\n", " iz is the number of the layer for which the data are requested,\n", " and all output arrays will be 2D for that layer.\n", " if iz==None, then all outputs will be full 3D arrays and cover all layers\n", " simultaneously\n", "\n", " Returns\n", " -------\n", " `Xm` : ndarray, shape: (Nz, Ny, Nx), [L]\n", " x-coordinates of cell centers\n", " `Ym` : ndarray, shape: (Nz, Ny, Nx), [L]\n", " y-coodinates of cell centers\n", " `ZM` : ndarray, shape: (Nz, Ny, Nx), [L]\n", " `z`-coordinates at cell centers\n", " `U` : ndarray, shape: (Nz, Ny, Nx), [L3/d]\n", " Flow in `x`-direction at cell centers\n", " `V` : ndarray, shape: (Nz, Ny, Nx), [L3/T]\n", " Flow in `y`-direction at cell centers\n", " `W` : ndarray, shape: (Nz, Ny, Nx), [L3/T]\n", " Flow in `z`-direction at cell centers.\n", " \n", " \"\"\"\n", " \n", " X, Y = np.meshgrid(gr.xm, gr.ym) # coordinates of cell centers\n", " \n", " shp = (gr.Ny, gr.Nx) # 2D tuple to select a single layer\n", " \n", " # Flows at cell centers\n", " U = np.concatenate((Out.Qx[iz, :, 0].reshape((1, gr.Ny, 1)), \\\n", " 0.5 * (Out.Qx[iz, :, :-1].reshape((1, gr.Ny, gr.Nx-2)) +\\\n", " Out.Qx[iz, :, 1: ].reshape((1, gr.Ny, gr.Nx-2))), \\\n", " Out.Qx[iz, :, -1].reshape((1, gr.Ny, 1))), axis=2).reshape(shp)\n", " V = np.concatenate((Out.Qy[iz, 0, :].reshape((1, 1, gr.Nx)), \\\n", " 0.5 * (Out.Qy[iz, :-1, :].reshape((1, gr.Ny-2, gr.Nx)) +\\\n", " Out.Qy[iz, 1:, :].reshape((1, gr.Ny-2, gr.Nx))), \\\n", " Out.Qy[iz, -1, :].reshape((1, 1, gr.Nx))), axis=1).reshape(shp)\n", " return X, Y, U, V\n", "\n", "\n", "def unique(x, tol=0.0001):\n", " \"\"\"return sorted unique values of x, keeping ascending or descending direction\"\"\"\n", " if x[0]>x[-1]: # vector is reversed\n", " x = np.sort(x)[::-1] # sort and reverse\n", " return x[np.hstack((np.diff(x) < -tol, True))]\n", " else:\n", " x = np.sort(x)\n", " return x[np.hstack((np.diff(x) > +tol, True))]\n", "\n", " \n", "def fdm3(gr, kx, ky, kz, FQ, HI, IBOUND):\n", " '''Steady state 3D Finite Difference Model returning computed heads and flows.\n", " \n", " Heads and flows are returned as 3D arrays as specified under output parmeters.\n", " \n", " Parameters\n", " ----------\n", " 'gr' : `grid_object`, generated by gr = Grid(x, y, z, ..)\n", " `kx`, `ky`, `kz` : ndarray, shape: (Nz, Ny, Nx), [L/T]\n", " hydraulic conductivities along the three axes, 3D arrays.\n", " `FQ` : ndarray, shape: (Nz, Ny, Nx), [L3/T]\n", " prescrived cell flows (injection positive, zero of no inflow/outflow)\n", " `IH` : ndarray, shape: (Nz, Ny, Nx), [L]\n", " initial heads. `IH` has the prescribed heads for the cells with prescribed head.\n", " `IBOUND` : ndarray, shape: (Nz, Ny, Nx) of int\n", " boundary array like in MODFLOW with values denoting\n", " * IBOUND>0 the head in the corresponding cells will be computed\n", " * IBOUND=0 cells are inactive, will be given value NaN\n", " * IBOUND<0 coresponding cells have prescribed head\n", " \n", " outputs\n", " ------- \n", " `Out` : namedtuple containing heads and flows:\n", " `Out.Phi` : ndarray, shape: (Nz, Ny, Nx), [L3/T] \n", " computed heads. Inactive cells will have NaNs\n", " `Out.Q` : ndarray, shape: (Nz, Ny, Nx), [L3/T]\n", " net inflow in all cells, inactive cells have 0\n", " `Out.Qx : ndarray, shape: (Nz, Ny, Nx-1), [L3/T] \n", " intercell flows in x-direction (parallel to the rows)\n", " `Out.Qy` : ndarray, shape: (Nz, Ny-1, Nx), [L3/T] \n", " intercell flows in y-direction (parallel to the columns)\n", " `Out.Qz` : ndarray, shape: (Nz-1, Ny, Nx), [L3/T] \n", " intercell flows in z-direction (vertially upward postitive)\n", " the 3D array with the final heads with `NaN` at inactive cells.\n", " \n", " TO 160905\n", " '''\n", "\n", " # define the named tuple to hold all the output of the model fdm3\n", " Out = namedtuple('Out',['Phi', 'Q', 'Qx', 'Qy', 'Qz'])\n", " Out.__doc__ = \"\"\"fdm3 output, , containing fields Phi, Qx, Qy and Qz\\n \\\n", " Use Out.Phi, Out.Q, Out.Qx, Out.Qy and Out.Qz\"\"\" \n", " \n", " if kx.shape != gr.shape:\n", " raise AssertionError(\"shape of kx {0} differs from that of model {1}\".format(kx.shape,SHP))\n", " if ky.shape != gr.shape:\n", " raise AssertionError(\"shape of ky {0} differs from that of model {1}\".format(ky.shape,SHP))\n", " if kz.shape != gr.shape:\n", " raise AssertionError(\"shape of kz {0} differs from that of model {1}\".format(kz.shape,SHP))\n", " \n", " active = (IBOUND > 0).reshape(gr.Nod,) # boolean vector denoting the active cells\n", " inact = (IBOUND ==0).reshape(gr.Nod,) # boolean vector denoting inacive cells\n", " fxhd = (IBOUND < 0).reshape(gr.Nod,) # boolean vector denoting fixed-head cells\n", "\n", " # reshaping shorthands\n", " rx = lambda a : np.reshape(a, (1, 1, gr.Nx))\n", " ry = lambda a : np.reshape(a, (1, gr.Ny, 1))\n", " rz = lambda a : np.reshape(a, (gr.Nz, 1, 1))\n", " \n", " # half cell flow resistances\n", " Rx = 0.5 * rx(gr.dx) / (ry(gr.dy) * rz(gr.dz)) / kx\n", " Ry = 0.5 * ry(gr.dy) / (rz(gr.dz) * rx(gr.dx)) / ky\n", " Rz = 0.5 * rz(gr.dz) / (rx(gr.dx) * ry(gr.dy)) / kz\n", " \n", " # set flow resistance in inactive cells to infinite\n", " Rx = Rx.reshape(gr.Nod,); Rx[inact] = np.Inf; Rx=Rx.reshape(gr.shape)\n", " Ry = Ry.reshape(gr.Nod,); Ry[inact] = np.Inf; Ry=Ry.reshape(gr.shape)\n", " Rz = Rz.reshape(gr.Nod,); Rz[inact] = np.Inf; Rz=Rz.reshape(gr.shape)\n", " \n", " # conductances between adjacent cells\n", " Cx = 1 / (Rx[:, :, :-1] + Rx[:, :, 1:])\n", " Cy = 1 / (Ry[:, :-1, :] + Ry[:, 1:, :])\n", " Cz = 1 / (Rz[:-1, :, :] + Rz[1:, :, :])\n", " \n", " IE = gr.NOD[:, :, 1:] # east neighbor cell numbers\n", " IW = gr.NOD[:, :, :-1] # west neighbor cell numbers\n", " IN = gr.NOD[:, :-1, :] # north neighbor cell numbers\n", " IS = gr.NOD[:, 1:, :] # south neighbor cell numbers\n", " IT = gr.NOD[:-1, :, :] # top neighbor cell numbers\n", " IB = gr.NOD[1:, :, :] # bottom neighbor cell numbers\n", " \n", " R = lambda x : x.ravel() # generate anonymous function R(x) as shorthand for x.ravel()\n", "\n", " # notice the call csc_matrix( (data, (rowind, coind) ), (M,N)) tuple within tupple\n", " # also notice that Cij = negative but that Cii will be postive, namely -sum(Cij)\n", " A = sp.csc_matrix(( -np.concatenate(( R(Cx), R(Cx), R(Cy), R(Cy), R(Cz), R(Cz)) ),\\\n", " (np.concatenate(( R(IE), R(IW), R(IN), R(IS), R(IB), R(IT)) ),\\\n", " np.concatenate(( R(IW), R(IE), R(IS), R(IN), R(IT), R(IB)) ),\\\n", " )),(gr.Nod,gr.Nod))\n", " \n", " # to use the vector of diagonal values in a call of sp.diags() we need to have it aa a \n", " # standard nondimensional numpy vector.\n", " # To get this:\n", " # - first turn the matrix obtained by A.sum(axis=1) into a np.array by np.array( .. )\n", " # - then take the whole column to loose the array orientation (to get a dimensionless numpy vector)\n", " adiag = np.array(-A.sum(axis=1))[:,0]\n", " \n", " Adiag = sp.diags(adiag) # diagonal matrix with a[i,i]\n", " \n", " RHS = FQ.reshape((gr.Nod,1)) - A[:,fxhd].dot(HI.reshape((gr.Nod,1))[fxhd]) # Right-hand side vector\n", " \n", " Out.Phi = HI.flatten() # allocate space to store heads\n", " \n", " Out.Phi[active] = spsolve( (A+Adiag)[active][:,active] ,RHS[active] ) # solve heads at active locations\n", " \n", " # net cell inflow\n", " Out.Q = (A+Adiag).dot(Out.Phi).reshape(gr.shape)\n", "\n", " # set inactive cells to NaN\n", " Out.Phi[inact] = np.NaN # put NaN at inactive locations\n", " \n", " # reshape Phi to shape of grid\n", " Out.Phi = Out.Phi.reshape(gr.shape)\n", " \n", " #Flows across cell faces\n", " Out.Qx = -np.diff( Out.Phi, axis=2) * Cx\n", " Out.Qy = +np.diff( Out.Phi, axis=1) * Cy\n", " Out.Qz = +np.diff( Out.Phi, axis=0) * Cz\n", " \n", " return Out # all outputs in a named tuple for easy access" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import fdm_c\n", "from importlib import reload\n", "reload(fdm_c)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Example\n", "\n", "We use the same 3D example as before but now apply the grid object." ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "collapsed": true }, "outputs": [], "source": [ "# Make sure that your modules like grid are in the sys.path\n", "import sys\n", "\n", "path2modules = './modules/'\n", "\n", "if not path2modules in sys.path:\n", " sys.path.append(path2modules)\n" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "reload(mfgrid)" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([ 20., 0., -10., -100.])" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "gr.z" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "application/javascript": [ "/* Put everything inside the global mpl namespace */\n", "window.mpl = {};\n", "\n", "mpl.get_websocket_type = function() {\n", " if (typeof(WebSocket) !== 'undefined') {\n", " return WebSocket;\n", " } else if (typeof(MozWebSocket) !== 'undefined') {\n", " return MozWebSocket;\n", " } else {\n", " alert('Your browser does not have WebSocket support.' +\n", " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", " 'Firefox 4 and 5 are also supported but you ' +\n", " 'have to enable WebSockets in about:config.');\n", " };\n", "}\n", "\n", "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", " this.id = figure_id;\n", "\n", " this.ws = websocket;\n", "\n", " this.supports_binary = (this.ws.binaryType != undefined);\n", "\n", " if (!this.supports_binary) {\n", " var warnings = document.getElementById(\"mpl-warnings\");\n", " if (warnings) {\n", " warnings.style.display = 'block';\n", " warnings.textContent = (\n", " \"This browser does not support binary websocket messages. \" +\n", " \"Performance may be slow.\");\n", " }\n", " }\n", "\n", " this.imageObj = new Image();\n", "\n", " this.context = undefined;\n", " this.message = undefined;\n", " this.canvas = undefined;\n", " this.rubberband_canvas = undefined;\n", " this.rubberband_context = undefined;\n", " this.format_dropdown = undefined;\n", "\n", " this.image_mode = 'full';\n", "\n", " this.root = $('
');\n", " this._root_extra_style(this.root)\n", " this.root.attr('style', 'display: inline-block');\n", "\n", " $(parent_element).append(this.root);\n", "\n", " this._init_header(this);\n", " this._init_canvas(this);\n", " this._init_toolbar(this);\n", "\n", " var fig = this;\n", "\n", " this.waiting = false;\n", "\n", " this.ws.onopen = function () {\n", " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", " fig.send_message(\"send_image_mode\", {});\n", " fig.send_message(\"refresh\", {});\n", " }\n", "\n", " this.imageObj.onload = function() {\n", " if (fig.image_mode == 'full') {\n", " // Full images could contain transparency (where diff images\n", " // almost always do), so we need to clear the canvas so that\n", " // there is no ghosting.\n", " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", " }\n", " fig.context.drawImage(fig.imageObj, 0, 0);\n", " };\n", "\n", " this.imageObj.onunload = function() {\n", " this.ws.close();\n", " }\n", "\n", " this.ws.onmessage = this._make_on_message_function(this);\n", "\n", " this.ondownload = ondownload;\n", "}\n", "\n", "mpl.figure.prototype._init_header = function() {\n", " var titlebar = $(\n", " '
');\n", " var titletext = $(\n", " '
');\n", " titlebar.append(titletext)\n", " this.root.append(titlebar);\n", " this.header = titletext[0];\n", "}\n", "\n", "\n", "\n", "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", "\n", "}\n", "\n", "\n", "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", "\n", "}\n", "\n", "mpl.figure.prototype._init_canvas = function() {\n", " var fig = this;\n", "\n", " var canvas_div = $('
');\n", "\n", " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", "\n", " function canvas_keyboard_event(event) {\n", " return fig.key_event(event, event['data']);\n", " }\n", "\n", " canvas_div.keydown('key_press', canvas_keyboard_event);\n", " canvas_div.keyup('key_release', canvas_keyboard_event);\n", " this.canvas_div = canvas_div\n", " this._canvas_extra_style(canvas_div)\n", " this.root.append(canvas_div);\n", "\n", " var canvas = $('');\n", " canvas.addClass('mpl-canvas');\n", " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", "\n", " this.canvas = canvas[0];\n", " this.context = canvas[0].getContext(\"2d\");\n", "\n", " var rubberband = $('');\n", " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", "\n", " var pass_mouse_events = true;\n", "\n", " canvas_div.resizable({\n", " start: function(event, ui) {\n", " pass_mouse_events = false;\n", " },\n", " resize: function(event, ui) {\n", " fig.request_resize(ui.size.width, ui.size.height);\n", " },\n", " stop: function(event, ui) {\n", " pass_mouse_events = true;\n", " fig.request_resize(ui.size.width, ui.size.height);\n", " },\n", " });\n", "\n", " function mouse_event_fn(event) {\n", " if (pass_mouse_events)\n", " return fig.mouse_event(event, event['data']);\n", " }\n", "\n", " rubberband.mousedown('button_press', mouse_event_fn);\n", " rubberband.mouseup('button_release', mouse_event_fn);\n", " // Throttle sequential mouse events to 1 every 20ms.\n", " rubberband.mousemove('motion_notify', mouse_event_fn);\n", "\n", " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", "\n", " canvas_div.on(\"wheel\", function (event) {\n", " event = event.originalEvent;\n", " event['data'] = 'scroll'\n", " if (event.deltaY < 0) {\n", " event.step = 1;\n", " } else {\n", " event.step = -1;\n", " }\n", " mouse_event_fn(event);\n", " });\n", "\n", " canvas_div.append(canvas);\n", " canvas_div.append(rubberband);\n", "\n", " this.rubberband = rubberband;\n", " this.rubberband_canvas = rubberband[0];\n", " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", " this.rubberband_context.strokeStyle = \"#000000\";\n", "\n", " this._resize_canvas = function(width, height) {\n", " // Keep the size of the canvas, canvas container, and rubber band\n", " // canvas in synch.\n", " canvas_div.css('width', width)\n", " canvas_div.css('height', height)\n", "\n", " canvas.attr('width', width);\n", " canvas.attr('height', height);\n", "\n", " rubberband.attr('width', width);\n", " rubberband.attr('height', height);\n", " }\n", "\n", " // Set the figure to an initial 600x600px, this will subsequently be updated\n", " // upon first draw.\n", " this._resize_canvas(600, 600);\n", "\n", " // Disable right mouse context menu.\n", " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", " return false;\n", " });\n", "\n", " function set_focus () {\n", " canvas.focus();\n", " canvas_div.focus();\n", " }\n", "\n", " window.setTimeout(set_focus, 100);\n", "}\n", "\n", "mpl.figure.prototype._init_toolbar = function() {\n", " var fig = this;\n", "\n", " var nav_element = $('
')\n", " nav_element.attr('style', 'width: 100%');\n", " this.root.append(nav_element);\n", "\n", " // Define a callback function for later on.\n", " function toolbar_event(event) {\n", " return fig.toolbar_button_onclick(event['data']);\n", " }\n", " function toolbar_mouse_event(event) {\n", " return fig.toolbar_button_onmouseover(event['data']);\n", " }\n", "\n", " for(var toolbar_ind in mpl.toolbar_items) {\n", " var name = mpl.toolbar_items[toolbar_ind][0];\n", " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", " var image = mpl.toolbar_items[toolbar_ind][2];\n", " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", "\n", " if (!name) {\n", " // put a spacer in here.\n", " continue;\n", " }\n", " var button = $('