Visualization Control System (VCS) Basic Tutorial

VCS allows scientists to produce highly customized plots. This tutorial provides an overview of VCS while other tutorials on the CDAT Tutorials page provide more specifics on additional VCS Principles, the VCS Template which controls many aspects of a plot, Text Objects in VCS and another example of using VCS to plot data from start to finish in the VCS Example tutorial. We recommend you also look at the CDMS 101 notebook.

The most direct way to work with this Jupyter Notebook is to download the notebook by right-clicking on the link below and chosing "Download Linked File As..." or "Save Link as...", activating a CDAT + Jupyter compatible environment, and running the notebook on its own or within a JupyterLab interface. For more details, see the Getting Started section below.

If you are unfamiliar with Jupyter Notebooks, they are files with an .ipynb extension that are made up of cells that can include executable code or regular text to explain what the code is doing. From "The Jupyter Notebook is an open-source web application that allows you to create and share documents that contain live code, equations, visualizations and narrative text." Users can step through each cell in the notebook by putting their cursor in the cell they wish to run and either clicking on the "Run" button at the top of the page or pressing enter on the keyboard while holding down shift at the same time (shift-enter).

To download this Jupyter Notebook, right click on the link and choose "Download Linked File As..." or "Save Link as...". Remember where you save the downloaded file which should have an .ipynb extension. (You'll need to launch the Jupyter notebook or JupyterLab from the location where you store the notebook file.)

Getting Started

This notebook uses Python 3.

If you only have Python 2 installed, the code should still work in Python 2, but if it doesn't, isn't this a good time to join the Python 3 crowd? The instructions below make it relatively painless to install a Python 3 environment.

For the code in this notebook to work, you need to install a CDAT compatible environment. See the next cell in the notebook for details on how to create an appropriate environment and activate it. If you see three grey dots, that cell has been hidden. Just click on the dots to see the contents of the cell. (To hide the cell in JupyterLab, click on the vertical blue bar to the left of the cell.)

Conda Installation

We recommend using conda to install and manage environments. Conda itself can be installed either via Anaconda (the everything-but-the-kitchen-sink version) or Miniconda (the minimalist version).

I prefer Miniconda since it takes up less room on my computer and I don't mind installing the various packages I need when I need them. Miniconda (or Anaconda) will install Python on your computer. If you already have Python installed on your computer, see this helpful page. The choice of whether to install the Python 2.x or Python 3.x version of Miniconda will affect only your root environment. You can create both Python 2.x and Python 3.x environments with either version of Miniconda installed (and many environments can exist on your computer at the same time).

When installing Miniconda or Anaconda, let the installer add the conda installation of Python to your PATH environment variable. On a Mac or Linux machine, if asked "Do you wish the installer to initialize Anaconda3 by running conda init?" or something similar, we recommend saying "yes". For more details see this page.

Create a CDAT Compatible Environment

Once you have a version of Miniconda (or Anaconda) installed, create a CDAT or Jupyter-VCDAT environment.


  1. Type the following code at a command line prompt. Note, since all output of code in this notebook will be displayed within the notebook, we do not need a separate display window so we can use the mesalib (or headless display) version of the latest version of the cdatX.x environment, which is currently cdat8.1.

     conda create -n cdat81-mesa -c cdat/label/v81 -c conda-forge python=3.6 cdat mesalib
  2. Once the cdat81-mesa environment is installed, activate it by typing:

     conda activate cdat81-mesa
  3. Install JupyterLab within your cdat81-mesa environment so you can run Jupyter notebooks:

     conda install -c conda-forge jupyterlab

For additional details on creating a CDAT environment, see the following CDAT installation page.

For VCDAT and the jupyter-vcdat environment

  1. Follow these instructions for installing VCDAT 2.0 on your personal computer.

  2. Once the jupyter-vcdat environment (and VCDAT) have been installed, activate the environment with:

     conda activate jupyter-vcdat

If "conda activate" doesn't work


    source activate name_of_environment

Start JupyterLab

Once you have a cdatXx-mesa or jupyter-vcdat environment activated, navigate to the parent folder that contains this notebook, then type:


Note: you must launch JupyterLab from the highest level folder you want to be able to access. Jupyter can see folders below the directory from which it was launched, but it cannot see directories above its launch directory.

If you do not want to use the enhanced JupyterLab interface, you can run:


to load only the notebook without the JupyterLab interface.

Once JupyterLab (or Jupyter Notebook) has started, you may be asked to pick a Kernel. Choose the generic Python 3 environment or cdatXx-mesa or jupyter-vcdat. If you chose the generic Python 3 environment that means Jupyter will use whichever Python environment started the JupyterLab (or Jupyter Notebook) session. Hence it is best to start the JupyterLab session from an environment that contains CDAT.

As a side note, VCDAT and VCS are two different CDAT modules though they have similar acronyms. VCDAT 2.0 helps you visualize, manipulate data, and test out code within the JupyterLab interface. VCS stands for Visualization Control System which allows you to create customized plots and animations.

Download Sample Data

Back To Top

First let's download some sample data, specifically the three NetCDF files:,, and, which will be stored in the same directory as this notebook.

In [1]:
from __future__ import print_function
import requests
import os

for filename in ['', '', '']:
    if not os.path.exists(filename):
        r = requests.get("{}".format(filename), stream=True)
        with open(filename,"wb") as f:
            for chunk in r.iter_content(chunk_size=1024):
                if chunk:
                    f.write(chunk) # write this chunk to a local version of the file

Opening Data Files

Back To Top

First we will open one of our demo files, which is done via the cdms2 module.

In the output below the code cell below, if you see a message that says "Allow anonymous logging usage to help improve CDAT(you can also set the environment variable CDAT_ANONYMOUS_LOG to yes or no)? [yes]/no:" either press enter (for yes) or type yes or no. If you do not respond with a return or yes or no, you will not be able to proceed.

In [2]:
import cdms2

f ="")

Querying File

Back To Top

Next we will query the file to learn which variables are available.

In [3]:
dict_keys(['bounds_time', 'bounds_latitude', 'bounds_longitude', 'ta'])

We can obtain information on a specific variable using the info method. In the next line of code we look at the ta variable which is the Air Temperature in degrees Kelvin (K). The file contains:

  • 11 months of Air Temperature data (the T axis),
  • 17 different atmospheric pressure levels (the Z axis),
  • 73 latitude bands (spanning from 90 to -90) (Y axis) and
  • 144 longitude bands (spanning from 0 to 360) (X axis).

The latitude axis is Y and the longitude axis is X. (If this seems backwards, think of the values of latitude increasing or decreasing along the Y axis though the actual lines of latitude - or parallels - follow the X axis).

As you'll see in the output, the shape of the data is (11, 17, 73, 144). Each axis is also known as a dimension.

In [4]:
*** Description of Slab ta ***
id: ta
shape: (11, 17, 73, 144)
filename: /Users/davis278/repos/Jupyter-notebooks/vcs/VCS_Basics/
missing_value: [1.e+20]
grid_name: grid_73x144
grid_type: None
title: Air Temperature [K]
Grid has Python id 0x11e9fd320.
Gridtype: generic
Grid shape: (73, 144)
Order: yx
** Dimension 1 **
   id: time
   Designated a time axis.
   units:  months since 1949-1-1 0:0
   Length: 11
   First:  461.0
   Last:   471.0
   Other axis attributes:
      calendar: proleptic_gregorian
      axis: T
   Python id:  0x11e9fd2e8
** Dimension 2 **
   id: level
   Designated a level axis.
   units:  lev
   Length: 17
   First:  1000.0
   Last:   10.0
   Other axis attributes:
      axis: Z
   Python id:  0x11e9fd278
** Dimension 3 **
   id: latitude
   Designated a latitude axis.
   units:  degrees_north
   Length: 73
   First:  -90.0
   Last:   90.0
   Other axis attributes:
      axis: Y
   Python id:  0x11e9fd240
** Dimension 4 **
   id: longitude
   Designated a longitude axis.
   units:  degrees_east
   Length: 144
   First:  0.0
   Last:   357.5
   Other axis attributes:
      axis: X
      modulo: [360.]
      topology: circular
   Python id:  0x11e9fd2b0
*** End of description for ta ***

In general, with the latitude dimension having a length of 73 (meaning there are 73 temperature values from -90 to 90 degrees) and the longitude dimension having a length of 144 (or 144 temperature values from 0 to 360 degrees), a single temperature value spans a 2.5 x 2.5 degree portion of the globe. The exception is in the latitude dimension which would normally have a length of 72 if all sections of the grid were 2.5 degrees. Instead, it has a length of 73 because the first and last grid elements (at -90 and 90 degrees) are each 1.25 degrees tall not 2.5. You can see this by displaying the latitude slices in the data using the getBounds() method.

In [5]:
array([[-90.  , -88.75],
       [-88.75, -86.25],
       [-86.25, -83.75],
       [-83.75, -81.25],
       [-81.25, -78.75],
       [-78.75, -76.25],
       [-76.25, -73.75],
       [-73.75, -71.25],
       [-71.25, -68.75],
       [-68.75, -66.25],
       [-66.25, -63.75],
       [-63.75, -61.25],
       [-61.25, -58.75],
       [-58.75, -56.25],
       [-56.25, -53.75],
       [-53.75, -51.25],
       [-51.25, -48.75],
       [-48.75, -46.25],
       [-46.25, -43.75],
       [-43.75, -41.25],
       [-41.25, -38.75],
       [-38.75, -36.25],
       [-36.25, -33.75],
       [-33.75, -31.25],
       [-31.25, -28.75],
       [-28.75, -26.25],
       [-26.25, -23.75],
       [-23.75, -21.25],
       [-21.25, -18.75],
       [-18.75, -16.25],
       [-16.25, -13.75],
       [-13.75, -11.25],
       [-11.25,  -8.75],
       [ -8.75,  -6.25],
       [ -6.25,  -3.75],
       [ -3.75,  -1.25],
       [ -1.25,   1.25],
       [  1.25,   3.75],
       [  3.75,   6.25],
       [  6.25,   8.75],
       [  8.75,  11.25],
       [ 11.25,  13.75],
       [ 13.75,  16.25],
       [ 16.25,  18.75],
       [ 18.75,  21.25],
       [ 21.25,  23.75],
       [ 23.75,  26.25],
       [ 26.25,  28.75],
       [ 28.75,  31.25],
       [ 31.25,  33.75],
       [ 33.75,  36.25],
       [ 36.25,  38.75],
       [ 38.75,  41.25],
       [ 41.25,  43.75],
       [ 43.75,  46.25],
       [ 46.25,  48.75],
       [ 48.75,  51.25],
       [ 51.25,  53.75],
       [ 53.75,  56.25],
       [ 56.25,  58.75],
       [ 58.75,  61.25],
       [ 61.25,  63.75],
       [ 63.75,  66.25],
       [ 66.25,  68.75],
       [ 68.75,  71.25],
       [ 71.25,  73.75],
       [ 73.75,  76.25],
       [ 76.25,  78.75],
       [ 78.75,  81.25],
       [ 81.25,  83.75],
       [ 83.75,  86.25],
       [ 86.25,  88.75],
       [ 88.75,  90.  ]])

First Plot

Back To Top

Now, let's visualize the ta variable. For this we will need the vcs module. We will also need a vcs canvas upon which to plot the data.

In [6]:
import vcs
canvas = vcs.init()
canvas.plot(f["ta"]) # This plots the first time interval (1987/6/1) and the first atmospheric level (1000).

Subsetting Data

Back To Top

In the above plot we used all the variable dimensions (time, level, latitude and longitude) and the resulting plot is a boxfill plot. (If the data has at least two dimensions for the variable being plotted, the default plot type is boxfill.)

You can load and use a subset of the dimensions, however. Notice how we can use either the actual dimension value (e.g. -90 for longitude) or indices (via slice). For this dataset, the longitude values range from 0 to 360, so specifying a -90 value for longintude means 360-90 = 270.

In [7]:
data = f["ta"](longitude=-90, latitude=40, level=500, time=slice(0,1))

In the previous example, we fixed all dimensions and the command returned a single value. Let's subset longitude to a range rather than a single value and plot it.

The "squeeze" parameter in the first line below prevents dimensions of length 1 (e.g. latitude, level, and time) from being plotted.

In [8]:
data = f["ta"](longitude=(0,180), latitude=40, level=500, time=slice(0,1), squeeze=1)

Using Isolines

Back To Top

If we want an isoline rather than the default boxfill, we will need to create the associated isoline graphic rendering object using vcs' createisoline method.

In [9]:
isoline = vcs.createisoline()
data = f("ta")
canvas.plot(data, isoline)

Cross Sections

Back To Top

Now, let's plot a cross section of the data.

In [10]:
data = f["ta"](longitude=-90, latitude=(-90,90), level=(1000,100), time=slice(0,1), squeeze=1)
canvas.plot(data, isoline)

We can also colorize the isolines, if we want, and turn on labels.

In [11]:
levels = vcs.mkscale(*vcs.minmax(data))
colors = vcs.getcolors(levels)
isoline.levels = levels
isoline.linecolors = colors
isoline.label = True
isoline.textcolors= colors
canvas.plot(data, isoline)

Easy Plot Tweaks

Back To Top

We can also control the dimensions' "range" on the plot via the isoline graphic rendering object,

In [12]:
data = f["ta"](longitude=-90, time=slice(0,1), squeeze=1)
# The next two lines put the North Pole on the left of the plot
isoline.datawc_x1 = 90
isoline.datawc_x2 = -90
isoline.datawc_y1 = 3  # log10(1000)
isoline.datawc_y2 = 2.3  # log10(200)

Or tweak the axes' scale (turn the y axis of the plot (not the data) - the atmospheric pressure levels - into a base 10 logarithmic scale),

In [13]:
isoline.yaxisconvert = "log10"

Or change the default labels.

In [14]:
isoline.yticlabels1 = {1000:"1000", 800:"800", 500: "500", 20:"20"}
canvas.plot(data, isoline)


Back To Top

Now let's overlay another variable from a different NetCDF file: the u component of the wind from the file. Since u has different values than the previous ta variable, we will create a separate isoline graphic rendering object, iso2.

In [15]:
f2 ="")
u = f2("u", longitude=-90., time=slice(0,1), squeeze=1)  # First time slice
iso2 = vcs.createisoline(source=isoline)  # Essentially makes a copy
levels = vcs.mkscale(*vcs.minmax(u))
iso2.levels = levels
colors = ["blue",]  # Blue lines
iso2.linecolors = colors
iso2.textcolors = colors
canvas.plot(data, isoline)
canvas.plot(u, iso2)