{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Reusable User-Frendly Function for Portrait Plots \n", "\n", "Notebook written by *Jiwoo Lee* (Feb. 2019)\n", "\n", "***\n", "\n", "- **OBJECTIVE**: Provide user-frendly and reusable function to generate portrait plot from given 2D array in type of CDMS Transisent Variable.\n", "- **ADVENTAGE**: Users can adjust image size, colormap, font size, margin, etc., without knowing details of portrait plot generation.\n", "- **USAGE**: Feed CDMS MV2 2-D array with customizing options to the [function](#function) to generate a portrait plot. See [below](#options) for details.\n", "\n", "\n", "The CDAT software was developed by LLNL. This tutorial was written by Charles Doutriaux. This work was performed under the auspices of the U.S. Department of Energy by Lawrence Livermore National Laboratory under Contract DE-AC52-07NA27344.\n", "\n", "[Download the Jupyter Notebook](ReusablePortraitPlot.ipynb)" ] }, { "cell_type": "markdown", "metadata": { "toc": true }, "source": [ "

Table of Contents

\n", "
" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from __future__ import print_function" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Prepare the Notebook\n", "[Back to Top](#top)\n", "\n", "Below is for embedded VCS image plotting in the Jupyter Notebook (Courtesy of *C. Doutriaux*). \n", "\n", "**Import necessary modules**\n", "\n", " * ***Install \"pcmdi_metrics\" with the following in your environment using the command below***\n", "\n", " ```conda install -c pcmdi/label/nightly -c conda-forge pcmdi_metrics```" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#\n", "# Prepare the Notebook\n", "#\n", "import tempfile\n", "import base64\n", "import pcmdi_metrics.graphics.portraits\n", "class PortraitNotebook(pcmdi_metrics.graphics.portraits.Portrait):\n", " def __init__(self,x,*args, **kargs):\n", " super(PortraitNotebook, self).__init__(*args, **kargs)\n", " self.x = x\n", " def _repr_png_(self):\n", " tmp = tempfile.mktemp() + \".png\"\n", " self.x.png(tmp)\n", " f = open(tmp, \"rb\")\n", " st = f.read()\n", " return st" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## User-friendly reuseable function to generate a portrait plot as static image\n", "[Back to Top](#top)\n", "\n", "User provides cdms MV2 2D array to the function with custom options if needed. Below code is basically a wrapper of existing portrait plot generation, but help users easily customize their plots, using various options listed below. Users can adjust image size, colormap, font size, margin, etc., without learning details of portrait plot generation.\n", "\n", "### Input\n", "\n", "- **stat_xy**: *cdms2 MV2 2D array* with proper axes decorated, values to visualize.\n", "- **imgName**: *string*, file name for PNG image file (e.g., `'YOUR_PLOT.png'` or `'YOUR_PLOT'`. If `.png` is not included, it will be added, so no worry).\n", "- **plotTilte**: *string*, text to show above plot as its title (optional)\n", "- **img_length**: *integer*, pixels for image file length. default=800.\n", "- **img_height**: *integer*, pixels for image file height. default=600.\n", "- **colormap**: *string* or actual *VCS colormap*. Default is 'viridis' that is default in VCS.\n", "- **clevels**: *list* of numbers (int or float). Colorbar levels. If not given automatically generated.\n", "- **ccolors**: *list* of colors. If not given automatically generate.\n", "- **xtic_textsize**: *int*, size of text for x-axis tic. If not given automatically generated.\n", "- **ytic_textsize**: *int*, size of text for y-axis tic. If not given automatically generated.\n", "- **parea**: *list* or *tuple* of float numbers between 0 to 1. Plotting area: (x1, x2, y1, y2). If not given automatically placed.\n", "- **missing_color**: *string* or *color code* (tuple or list of R, G, B, alpha). Color for missing data box. Default is 'black'\n", "- **Annotate**: *bool*, default=False. If Annotate, show numbers in individual boxes.\n", "- **stat_xy_annotate**: *cdms2 MV2 2D array* with proper axes decorated. Only needed when number to show as value annotated is not corresponding to the colormap. Not even bother when Annotate=False. For example, color for values those normalized by median, while annotate actual value for metrics.\n", "- **num_box_partitioning**: *integer*. It defines how many partitioning in a box. e.g., 4 indicates 4 triangles in each box. Default=1, should be less equal than 4.\n", "- **stat_xy_2**: *cdms2 MV2 2D array*. Stat for 2nd triangle in box. Default=None\n", "- **stat_xy_3**: *cdms2 MV2 2D array*. Stat for 3rd triangle in box. Default=None\n", "- **stat_xy_4**: *cdms2 MV2 2D array*. Stat for 4th triangle in box. Default=None\n", "- **logo**: *bool*, default=True. If False, CDAT logo turned off \n", "- **GridMeshLine**: *bool*, default=True. If False, no lines for boundary of individual boxes\n", "\n", "### Output\n", "[Back to Top](#top)\n", "\n", "- **PNG image file**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pcmdi_metrics.graphics.portraits\n", "import vcs\n", "import sys\n", "\n", "def plot_portrait(\n", " stat_xy, # array to visualize\n", " imgName='portrait_plot', # file name\n", " plotTitle=None, # title string on top\n", " img_length=800, img_height=600, # image size in pixel\n", " colormap='viridis', clevels=None, ccolors=None, # colormap and levels\n", " xtic_textsize=None, ytic_textsize=None, # font size for axes tic labels\n", " parea=None, # plotting area in ratio, in purpose of margin control\n", " missing_color='black', # color for missing data box\n", " Annotate=False, stat_xy_annotate=None, # annotation (showing value number in each box)\n", " num_box_partitioning=1, stat_xy_2=None, stat_xy_3=None, stat_xy_4=None, # additional triangle\n", " logo=True, GridMeshLine=True, # miscellaneous\n", " ):\n", " \"\"\" \n", " NOTE:\n", " Input\n", " - stat_xy: cdms2 MV2 2D array with proper axes decorated, values to visualize.\n", " - imgName: string, file name for PNG image file (e.g., 'YOUR_PLOT.png' or 'YOUR_PLOT'. If .png is not included, it will be added, so no worry).\n", " - plotTilte: string\n", " - img_length: integer, pixels for image file length. default=800.\n", " - img_height: integer, pixels for image file height. default=600.\n", " - colormap: string or actual VCS colormap. Default is 'viridis' that is default in VCS.\n", " - clevels: list of numbers (int or float). Colorbar levels. If not given automatically generated.\n", " - ccolors: list of colors. If not given automatically generate.\n", " - xtic_textsize: int, size of text for x-axis tic. If not given automatically generated.\n", " - ytic_textsize: int, size of text for y-axis tic. If not given automatically generated.\n", " - parea: list or tuple of float numbers between 0 to 1. Plotting area: (x1, x2, y1, y2). If not given automatically placed.\n", " - missing_color: string or color code (tuple or list of R, G, B, alpha). Color for missing data box. Default is 'black'\n", " - Annotate: bool, default=False. If Annotate, show numbers in individual boxes.\n", " - stat_xy_annotate: cdms2 MV2 2D array with proper axes decorated. Only needed when number to show as value annotated is not corresponding to the colormap. Not even bother when Annotate=False. For example, color for values those normalized by median, while annotate actual value for metrics.\n", " - num_box_partitioning: integer. How many partitioning in a box? e.g., 4: 4 triangles in each box. Default=1, should be less equal than 4.\n", " - stat_xy_2: cdms2 MV2 2D array. Stat for 2nd triangle in box. Default=None\n", " - stat_xy_3: cdms2 MV2 2D array. Stat for 3rd triangle in box. Default=None\n", " - stat_xy_4: cdms2 MV2 2D array. Stat for 4th triangle in box. Default=None\n", " - logo: bool, default=True. If False, CDAT logo turned off \n", " - GridMeshLine: bool, default=True. If False, no lines for boundary of individual boxes\n", " Output\n", " - PNG image file\n", " \"\"\"\n", " \n", " # VCS Canvas\n", " x = vcs.init(bg=True,geometry=(img_length, img_height))\n", " \n", " # CDAT logo control\n", " if not logo:\n", " x.drawlogooff()\n", " \n", " # Set up Portrait Plot\n", " \"\"\"\n", " If you are NOT using JUPYTER NOTEBOOK,\n", " it is okay to DEACTIVATE below line and ACTIVATE second below line,\n", " and skip the \"Prepare the Notebook\" part above.\n", " \"\"\"\n", " P = PortraitNotebook(x)\n", " #P = pcmdi_metrics.graphics.portraits.Portrait()\n", " \n", " #\n", " # Preprocessing step to \"decorate\" the axis\n", " #\n", " axes = stat_xy.getAxisList()\n", " xax = [t+' ' for t in list(axes[1][:])]\n", " yax = [t+' ' for t in list(axes[0][:])]\n", " P.decorate(stat_xy, yax, xax)\n", " #\n", " # Customize\n", " #\n", " SET = P.PLOT_SETTINGS\n", " \n", " # Viewport on the Canvas\n", " if parea is not None:\n", " SET.x1, SET.x2, SET.y1, SET.y2 = parea\n", " \n", " # Both X (horizontal) and y (VERTICAL) ticks\n", " # Text table\n", " SET.tictable = vcs.createtexttable()\n", " SET.tictable.color = \"black\"\n", " # X (bottom) ticks\n", " # Text Orientation\n", " SET.xticorientation = vcs.createtextorientation()\n", " SET.xticorientation.angle = -90\n", " SET.xticorientation.halign = \"right\"\n", " if xtic_textsize:\n", " SET.xticorientation.height = xtic_textsize \n", " # Y (vertical) ticks\n", " SET.yticorientation = vcs.createtextorientation()\n", " SET.yticorientation.angle = 0\n", " SET.yticorientation.halign = \"right\"\n", " if ytic_textsize:\n", " SET.yticorientation.height = ytic_textsize\n", " # We can turn off the \"grid\" if needed\n", " if GridMeshLine:\n", " SET.draw_mesh = \"y\"\n", " else:\n", " SET.draw_mesh = \"n\"\n", " # Color for missing data\n", " SET.missing_color = missing_color\n", " # Timestamp\n", " SET.time_stamp = None\n", " # Colormap\n", " SET.colormap = colormap\n", " if clevels:\n", " SET.levels = clevels \n", " if ccolors:\n", " SET.fillareacolors = ccolors\n", " # Annotated Plot (i.e. show value number in boxes)\n", " if Annotate:\n", " SET.values.show = True\n", " if stat_xy_annotate is None:\n", " SET.values.array = stat_xy\n", " else:\n", " SET.values.array = stat_xy_annotate\n", " # Check before plotting\n", " if num_box_partitioning > 4:\n", " sys.exit('ERROR: num_box_partitioning should be less equal than 4')\n", " #\n", " # Plot\n", " #\n", " P.plot(stat_xy, multiple=pp_multiple(1, num_box_partitioning), x=x)\n", " # Add triangles if needed\n", " # Decorate additional arrays with empty string axes to avoid overwriting same information (if not, font will look ugly)\n", " xax_empty = [' ' for t in stat_xy.getAxis(1)[:]]\n", " yax_empty = [' ' for t in stat_xy.getAxis(0)[:]]\n", " if stat_xy_2 is not None:\n", " P.decorate(stat_xy_2, yax_empty, xax_empty)\n", " P.plot(stat_xy_2, x=x, multiple=pp_multiple(2, num_box_partitioning))\n", " if stat_xy_3 is not None:\n", " P.decorate(stat_xy_3, yax_empty, xax_empty)\n", " P.plot(stat_xy_3, x=x, multiple=pp_multiple(3, num_box_partitioning))\n", " if stat_xy_4 is not None:\n", " P.decorate(stat_xy_4, yax_empty, xax_empty)\n", " P.plot(stat_xy_4, x=x, multiple=pp_multiple(4, num_box_partitioning))\n", " # Plot title\n", " if plotTitle:\n", " plot_title = vcs.createtext()\n", " plot_title.x = .5\n", " plot_title.y = (SET.y2 + 1) / 2.\n", " plot_title.height = 30\n", " plot_title.halign = 'center'\n", " plot_title.valign = 'half'\n", " plot_title.color = 'black'\n", " plot_title.string = plotTitle\n", " x.plot(plot_title)\n", " # Save\n", " if imgName.split('.')[-1] not in ['PNG', 'png']:\n", " imgName = imgName+'.png'\n", " x.png(imgName)\n", " # Preserve original axes\n", " stat_xy.setAxisList(axes)\n", " return P\n", "\n", "\n", "def pp_multiple(a, b):\n", " \"\"\"\n", " Note a, b to a.b\n", " Input\n", " - a, b: integer\n", " Output\n", " - c: float, a.b\n", " \"\"\"\n", " c = float(str(a)+'.'+str(b))\n", " return c" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Let's test it with dummy data\n", "Below we create a dummy array to visualize.\n", "\n", "[Back to Top](#top) " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import cdms2\n", "import genutil\n", "import MV2\n", "import numpy as np" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def normalize_by_median(stat_xy):\n", " \"\"\" \n", " NOTE:\n", " Input\n", " - stat_xy: cdms2 MV2 2D array with proper axes decorated, values to visualize.\n", " Output\n", " - stat_xy: stat_xy after normalized by median of each row\n", " \"\"\" \n", " # Get median\n", " median = genutil.statistics.median(stat_xy, axis=1)[0]\n", " # Match shapes\n", " stat_xy, median = genutil.grower(stat_xy, median)\n", " # Normalize by median value\n", " median = np.array(median)\n", " stat_xy_normalized = MV2.divide(MV2.subtract(stat_xy,median), median)\n", " # Decorate axes\n", " stat_xy_normalized.setAxisList(stat_xy.getAxisList())\n", " stat_xy_normalized.id = stat_xy.id\n", " stat_xy = stat_xy_normalized\n", " return stat_xy" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Dummy data\n", "[Back to Top](#top)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Prepare dummy data -- create random array for testing\n", "random_array = np.random.rand(10,30)\n", "X = cdms2.createAxis(['model_ '+str(r) for r in list(range(0,30))])\n", "Y = cdms2.createAxis(['metric_ '+str(r) for r in list(range(0,10))])\n", "stat_xy = MV2.array(random_array, axes=(Y,X), id='statistics')\n", "\n", "# Plant missing value\n", "stat_xy[5][5] = -1.e20\n", "stat_xy = MV2.masked_where(MV2.equal(stat_xy, -1.e20), stat_xy)\n", "\n", "# Normalize rows by its median\n", "Normalize = True\n", "if Normalize:\n", " # Normalize by median value\n", " stat_xy = normalize_by_median(stat_xy)\n", "\n", "# Additional dummy data for annotate test\n", "stat_xy_annotate = MV2.multiply(stat_xy, 2)\n", "\n", "# Additional dummy data for additional triangles\n", "stat_xy_2 = normalize_by_median(MV2.add(stat_xy, 2))\n", "stat_xy_3 = normalize_by_median(MV2.add(stat_xy, 3))\n", "stat_xy_4 = normalize_by_median(MV2.add(stat_xy, 4))\n", "axes = stat_xy.getAxisList()\n", "stat_xy_2.setAxisList(axes)\n", "stat_xy_3.setAxisList(axes)\n", "stat_xy_4.setAxisList(axes)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Portrait plot generation\n", "[Back to Top](#top)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Example 1\n", "[Back to Top](#top)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Colormap to be used \n", "clevels = [-1.e20, -.5, -.4, -.3, -.2, -.1, 0, .1, .2, .3, .4, .5, 1.e20]\n", "ccolors = vcs.getcolors(clevels, split=0, colors=range(16,240))\n", "# Generate plot\n", "plot_portrait(stat_xy, imgName='pp_example1.png',\n", " plotTitle='Example 1',\n", " clevels=clevels, ccolors=ccolors\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Example 2\n", "[Back to Top](#top)\n", "- Add title\n", "- Adjust margin\n", "- Change missing box color to white\n", "- Add number annotation" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plot_portrait(stat_xy, imgName='pp_example2.png', \n", " plotTitle='Example 2',\n", " clevels=clevels, ccolors=ccolors,\n", " xtic_textsize=15, ytic_textsize=15,\n", " parea=(.05, .88, .25, .9),\n", " missing_color='white',\n", " Annotate=True,\n", " GridMeshLine=True,\n", " stat_xy_annotate=stat_xy_annotate)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Example 3\n", "[Back to Top](#top)\n", "- Add triangels" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plot_portrait(stat_xy, imgName='pp_example3.png',\n", " plotTitle='Example 3',\n", " clevels=clevels, ccolors=ccolors,\n", " num_box_partitioning=2,\n", " stat_xy_2=stat_xy_2,\n", " GridMeshLine=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Example 4\n", "[Back to Top](#top)\n", "- Add more triangles\n", "- Change (increase, in this case) image size\n", "- Change font size for axes tic labels\n", "- Hide grid lines" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plot_portrait(\n", " stat_xy,\n", " imgName='pp_example4.png',\n", " plotTitle='Example 4',\n", " img_length=2400, img_height=1200,\n", " clevels=clevels, ccolors=ccolors,\n", " xtic_textsize=10, ytic_textsize=10,\n", " num_box_partitioning=4,\n", " stat_xy_2=stat_xy_2,\n", " stat_xy_3=stat_xy_3,\n", " stat_xy_4=stat_xy_4,\n", " GridMeshLine=False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Example 5\n", "[Back to Top](#top)\n", "- Change colormap\n", "- Missing box as grey\n", "- Turn off CDAT logo" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "colormap = 'bl_to_darkred'\n", "\n", "plot_portrait(stat_xy, imgName='pp_example5.png', plotTitle='Example 5',\n", " colormap=colormap, clevels=clevels, ccolors=ccolors,\n", " missing_color='grey', logo=False,\n", " num_box_partitioning=4,\n", " stat_xy_2=stat_xy_2,\n", " stat_xy_3=stat_xy_3,\n", " stat_xy_4=stat_xy_4,\n", " GridMeshLine=False)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.3" }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": false, "sideBar": true, "skip_h1_title": false, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": true, "toc_position": {}, "toc_section_display": true, "toc_window_display": false } }, "nbformat": 4, "nbformat_minor": 1 }