Creating Plug-in Visualizations for Oracle Data Visualization


Options



Before You Begin

Purpose

In this tutorial, you create a plug-in that you can use to customize the visualization types available in Oracle Data Visualization. The steps in this tutorial create a functional circle pack visualization.

Time to Complete

Approximately 30 minutes, not including installation and Oracle Data Visualization set up.

Background

This tutorial provides instructions for building a visualization plug-in using JavaScript libraries and the Oracle Data Visualization SDK.

Oracle Data Visualization includes the SDK. You use the SDK to create a visualization called helloViz. When you create this visualization, a folder containing three files is added to your development environment. You modify these files to create the visualization described in this tutorial.

You can create your visualization using a name other than helloViz. However, your results won't exactly match the image examples in this tutorial.

The three files added to the scripts directory are:

  • helloVizstyles.css is the stylesheet where all styles for the new visualization must be placed.

  • helloViz.js contains the default visualization implementation logic for a functional plug-in that you can customize.

  • helloVizdatamodelhandler contains the logic for mapping the logical data model to a physical data layout.

In this tutorial, you create a circle pack visualization type. A circle pack uses circles that are the same size or varying sizes inside of a larger circle. Imagine circles that do not overlap but touch each other to fill the inside of the larger circle. Circle packs represent hierarchical structures such as budgets allocated to market segments for advertising.

What Do You Need?

You need an understanding of the following:

  • Building plug-ins

  • JavaScript

  • require.js

  • jQuery

  • D3.js library

Before you start this tutorial, download and install Data Visualization on your computer.

Creating a Basic Visualization Plug-in

  1. Set the following environment variables using Windows environment variables or a script:

    set DVDESKTOP_SDK_HOME=<installation_directory>\dvdesktop
    set PLUGIN_DEV_DIR=<installation_directory>\dv-custom-plugins
    REM add tools\bin to path:
    set PATH=%DVDESKTOP_SDK_HOME%\tools\bin;%PATH%
  2. Run the following commands to create the plugin development environment:

    mkdir %PLUGIN_DEV_DIR%
    cd %PLUGIN_DEV_DIR%
    bicreateenv

    The bicreateenv script is located in the <installation directory>\Oracle Data Visualization Desktop\tools\bin directory.

  3. Create a visualization plug-in:

    bicreateplugin viz -id com.company.helloViz -subType dataviz

    The visualization plug-in is fully functional. You can add it to the Oracle Data Visualization canvas to fetch data from the data model.

    The directory structure and files should look similar to the following:

    Directory: %PLUGIN_DEV_DIR%\src\customVIZ\com-company-helloViz
    Files: .\extensions
           .\extensions\oracle.bi.tech.plugin.visualization\
           .\extensions\oracle.bi.tech.plugin.visualization\com.company.helloViz.json
           .\extensions\oracle.bi.tech.plugin.visualizationDatamodelHandler\
           .\extensions\oracle.bi.tech.plugin.visualizationDatamodelHandler\com.company.helloViz.visualizationDatamodelHandler.json
           .\resources\helloViz.png 
           .\resources\nls\messages.js 
           .\resources\nls\root\messages.js 
           .\scripts\helloVizstyles.css 
           .\scripts\helloViz.js 
           .\scripts\helloVizdatamodelhandler.js
  4. If Oracle Data Visualization is currently running, you need to close the application before completing this step.

    You test the visualization plug-in and the helloViz visualization in Oracle Data Visualization.

    If you are using this tutorial behind a corporate proxy, you need to modify the gradle.properties to set the proxy settings.

  5. Run the following command to open Oracle Data Visualization in SDK mode within the browser.

    cd %PLUGIN_DEV_DIR%
    .\gradlew run

Editing the Rendering Logic

  1. Edit helloViz.js and render what you need. In this example, use the Circle Pack code from D3.js, and open helloViz.js in your favorite editor.
  2. Load the following D3 JavaScript library objects:

    • Add 'd3js', after the 'obitech-reportservices/datamodelshapes', line.
    • Add 'obitech-reportservices/data', after the 'obitech-reportservices/datamodelshapes', line.
    • Add d3, after the datamodelshapes, line.
    • Add data, after the datamodelshapes, line.

    The result should look similar to the following:

    define(['jquery',
            'obitech-framework/jsx',
            'obitech-report/datavisualization',
            'obitech-reportservices/datamodelshapes',
            'obitech-reportservices/data',
            'd3js',
            'obitech-reportservices/events',
            'obitech-appservices/logger',
            'ojL10n!com-company-helloViz/nls/messages',
            'obitech-framework/messageformat',
            'css!com-company-helloViz/helloVizstyles'],
            function($,
                     jsx,
                     dataviz,
                     datamodelshapes,
                     data,
                     d3,
                     events,
                     logger,
                     messages) {
        ...
  3. Locate the HelloViz.prototype.render function, responsible for rendering the visualization when the data changes, and update the code as follows:

    • Remove the $(elContainer).htm... line.
    • Paste in the following code:
    // Let's reset our container on render
    $(elContainer).empty();
     
    // Get the width and height of our container
    var nWidth = $(elContainer).width();
    var nHeight = $(elContainer).height();
    // Calculate our margin and diameter
    var nMargin = 15,
       nDiameter = Math.min(nWidth, nHeight);
     
    var fFormat = d3.format(",d");
    var fColor = d3.scale.linear()
       .domain([-1, 5])
       .range(["hsl(198, 84%, 61%)", "hsl(230,31%,42%)"])
       .interpolate(d3.interpolateHcl);
    var oPack = d3.layout.pack()
       .padding(2)
       .size([nDiameter - nMargin, nDiameter - nMargin])
       .value(function(d) { return d.size; });
    var oSVG = d3.select(elContainer).append("svg")
       .attr("width", nDiameter)
       .attr("height", nDiameter)
       .append("g")
       .attr("transform", "translate(" + nDiameter / 2 + "," + nDiameter / 2 + ")");
     
    var oData = {
       "name": "root",
       "children": [
        {
         "name": "Product Lines",
         "children": [
          {
              "name": "Paint Products",
              "children": [
               {"name": "Behr", "size": 3938},
               {"name": "Sherwin-Williams", "size": 3812},
               {"name": "Valspar", "size": 6714},
               {"name": "Ace", "size": 743}
              ]
          },
          {
             "name": "Wood Products",
             "children": [
              {"name": "Redwood", "size": 3938},
              {"name": "Cedar", "size": 3812},
              {"name": "Cherry", "size": 6714},
              {"name": "Walnut", "size": 743},
              {"name": "Bamboo", "size": 4500}
             ]
         }
        ]
        }
        ]
    };
    var fRenderData = function(oData) {
       var oFocus = oData,
       aDataNodes = oPack.nodes(oData),
       oView;
       var aCircles = oSVG.selectAll("circle")
          .data(aDataNodes)
          .enter()
          .append("circle")
          .attr("class", function(d) { return d.parent ? d.children ? "circle_pack_node" : "circle_pack_node circle_pack_node--leaf" : "circle_pack_node circle_pack_node--root"; })
          .style("fill", function(d) { return d.children ? fColor(d.depth) : null; })
          .on("click", function(d) { if (oFocus !== d) fZoom(d), d3.event.stopPropagation(); });
     
       var aTextNodes = oSVG.selectAll("text")
          .data(aDataNodes)
          .enter()
          .append("text")
          .attr("class", "circle_pack_label")
          .style("fill-opacity", function(d) { return d.parent === oData ? 1 : 0; })
          .style("display", function(d) { return d.parent === oData ? "inline" : "none"; })
          .text(function(d) {
             return d.name ? d.name.substring(0, d.r / 3)  : ""; });
     
       var aNodes = oSVG.selectAll("circle,text");
     
       d3.select("body")
          .style("background", fColor(-1))
          .on("click", function() { fZoom(oData); });
        
       aCircles.append("title")
         .text(function(d) {
            return d.name + (d.size ? ": " + fFormat(d.size) : "" ); });
     
       fZoomTo([oData.x, oData.y, oData.r * 2 + nMargin]);
     
       function fZoom(d) {
          var oFocus = d;
     
          var aTransition = oSVG.transition()
             .duration(d3.event.altKey ? 7500 : 750)
             .tween("zoom", function(d) {
                var i = d3.interpolateZoom(oView, [oFocus.x, oFocus.y, oFocus.r * 2 + nMargin]);
                return function(t) { fZoomTo(i(t)); };
             });
     
          aTransition.selectAll("text")
            .filter(function(d) { return d.parent === oFocus || !d.children || this.style.display === "inline"; })
              .style("fill-opacity", function(d) { return (d.parent === oFocus || !oFocus.children) ? 1 : 0; })
              .each("start", function(d) { if (d.parent === oFocus || !oFocus.children) this.style.display = "inline"; })
              .each("end", function(d) { if (d.parent !== oFocus) if(!!d.children) this.style.display = "none"; });
       }
     
       function fZoomTo(v) {
          var k = nDiameter / v[2]; oView = v;
          aNodes.attr("transform", function(d) { return "translate(" + (d.x - v[0]) * k + "," + (d.y - v[1]) * k + ")"; });
          aCircles.attr("r", function(d) { return d.r * k; });
          aTextNodes.text(function(d) { return d.name ? d.name.substring(0, (d.r * k) / 3)  : ""; });
       }
    };
     
    fRenderData(oData);
  4. Edit the empty helloVizstyles.css with the following:

    .circle_pack_node {
       cursor: pointer;
    }
     
    .circle_pack_node:hover {
       stroke: #000;
       stroke-width: 1.5px;
    }
     
    .circle_pack_node--leaf {
       fill: white;
    }
     
    .circle_pack_label {
       font: 11px "Helvetica Neue", Helvetica, Arial, sans-serif;
       text-anchor: middle;
       text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, -1px 0 0 #fff, 0 -1px 0 #fff;
    }
     
    .circle_pack_label,
    .circle_pack_node--root,
    .circle_pack_node--leaf {
       pointer-events: visiblePainted;
    }
  5. Stop and restart Oracle Data Visualization, open the HelloViz visualization, and choose the Sample Order Lines data set to create your project.

  6. Add the # of Customers and Product Category data elements to the canvas.

    Your results should look similar to the following:

    This image is described in surrounding text.
    Description of this image
  7. In the helloViz.js file, add in your own data by replacing the var oData = ... variable with the following:

    var oData = this._generateData(oDataLayout);
    if(!oData) {
       return;
    } 
  8. Add the _generateData function:

    HelloViz.prototype._generateData = function(oDataLayout){
       var oData = {
         "name":     "root",
         "children": []
       };
        
       var oDataModel = this.getRootDataModel();
       if(!oDataModel || !oDataLayout){
          return;
       }
       var aAllMeasures = oDataModel.getColumnIDsIn(datamodelshapes.Physical.DATA);
       var nMeasures = aAllMeasures.length;
        
       var nRows = oDataLayout.getEdgeExtent(datamodelshapes.Physical.ROW);
       var nRowLayerCount = oDataLayout.getLayerCount(datamodelshapes.Physical.ROW);
       var nCols = oDataLayout.getEdgeExtent(datamodelshapes.Physical.COLUMN);
       var nColLayerCount = oDataLayout.getLayerCount(datamodelshapes.Physical.COLUMN);
        
       // Measure labels layer
       var isMeasureLabelsLayer = function (eEdgeType, nLayer) {
          return oDataLayout.getLayerMetadata(eEdgeType, nLayer, data.LayerMetadata.LAYER_ISMEASURE_LABELS);
       };
        
       // In the last layer get the data values and colors from this layer
       var getLastNonMeasureLayer = function (eEdge) {
          var nLayerCount = oDataLayout.getLayerCount(eEdge);
          for (var i = nLayerCount - 1; i >= 0; i--) {
             if (!isMeasureLabelsLayer(eEdge, i))
                return i;
          }
          return -1;
       };
        
       var nLastEdge = datamodelshapes.Physical.COLUMN; // check column edge first
        
       var nLastLayer = getLastNonMeasureLayer(datamodelshapes.Physical.COLUMN);
       if (nLastLayer < 0) { // if not on column edge look on row edge
          nLastEdge = datamodelshapes.Physical.ROW;
          nLastLayer = getLastNonMeasureLayer(datamodelshapes.Physical.ROW);
       }
        
       var hasCategoryOrColor = function () {
          return nLastLayer >= 0;
       };
        
       function buildTree(oParentNode, eEdgeType, nLayerCount, nLayer, nRowSlice, nColSlice, nParentEndSlice, nTreeLevel) {
          if (nLayer === nLayerCount) {
             // This is a leaf node on row (category), build column (color) next
             if (eEdgeType === datamodelshapes.Physical.ROW) {
                 buildTree(oParentNode, datamodelshapes.Physical.COLUMN, nColLayerCount, 0, nRowSlice, 0, nCols, nTreeLevel);  
             }
             return;
          }
          // Skip measure labels
          if (isMeasureLabelsLayer(eEdgeType, nLayer) && hasCategoryOrColor()) {
             return buildTree(oParentNode, eEdgeType, nLayerCount, nLayer + 1, nRowSlice, nColSlice, nParentEndSlice, nTreeLevel);
          }
          var isLastLayer = function () {
             return (nLastLayer === -1) || (nLastEdge === eEdgeType && nLastLayer === nLayer);
          };
          var isDuplicateLayer = function () {
             return nLastLayer > -1 &&
                    eEdgeType === datamodelshapes.Physical.COLUMN;
          };
           
          var nEndSlice;
          for (var nSlice = (eEdgeType === datamodelshapes.Physical.COLUMN) ? nColSlice : nRowSlice;
                   nSlice < nParentEndSlice;
                   nSlice = nEndSlice) {
             nEndSlice = oDataLayout.getItemEndSlice(eEdgeType, nLayer, nSlice) + 1;
             var nRowIndex = (eEdgeType === datamodelshapes.Physical.COLUMN) ? nRowSlice : nSlice;
             var nColIndex = (eEdgeType === datamodelshapes.Physical.ROW) ? nColSlice : nSlice;
              
             var nValue = null;
             if (isLastLayer()) {
                var val = 1; // default to rendering equally sized boxes when there are no measures
                if (nMeasures > 0) {
                   val = oDataLayout.getValue(datamodelshapes.Physical.DATA, nRowIndex, nColIndex, false);
                   // Skip no data
                   if (typeof val !== 'string' || val.length === 0){
                      continue;
                   }
                }
                nValue = parseFloat(val);
                // Skip negative and zero values for now. 
                if (nValue <= 0)
                   continue;
             }
             var oNode;
             // Ensure that the same layer isn't rendered twice in  row (Detail) and column (Color) edge
             if (!isDuplicateLayer()) {
                // Create new node
                oNode = {};
                oNode.size = 0;
                var sId = oDataLayout.getValue(eEdgeType, nLayer, nSlice, true);
                oNode.id = (oParentNode.id || '') + '.' + sId;
                oNode.name = oDataLayout.getValue(eEdgeType, nLayer, nSlice, false);
                if (isLastLayer())
                   oNode.size = nValue;
                // Append new node to parent node
                oParentNode.children = oParentNode.children || [];
                oParentNode.children.push(oNode);
                buildTree(oNode, eEdgeType, nLayerCount, nLayer + 1, nRowIndex, nColIndex, nEndSlice, nTreeLevel + 1);
                // Aggregate values into parent node
                oParentNode.size = oParentNode.size || 0;
                if (oNode.size)
                   oParentNode.size += oNode.size;
             }
             else {
                oNode = oParentNode;
                if (isLastLayer())
                   oNode.size = nValue;
                buildTree(oNode, eEdgeType, nLayerCount, nLayer + 1, nRowIndex, nColIndex, nEndSlice, nTreeLevel + 1);
             }
          }
          return;
       }
        
       // Build a treemap starting with DETAIL logical edge (see getLogicalMapper)
       buildTree(oData, datamodelshapes.Physical.ROW, nRowLayerCount, 0, 0, 0, nRows, 0);
        
       // There isn't anything on Category, try COLOR logical edge (see getLogicalMapper)
       if (oData.children.length === 0){
          buildTree(oData, datamodelshapes.Physical.COLUMN, nColLayerCount, 0, 0, 0, nCols, 0);  
       }
       return oData;
    } 
  9. Remove var nRows = oDataLayout.getEdgeExtent(datamodelshapes.Physical.ROW); to avoid running into issues when there is no data.

    // Determine the number of records available for rendering on ROW
    // Because Category was placed on ROW in the data model handler,
    // this returns the number of rows for the data in Category.
    var nRows = oDataLayout.getEdgeExtent(datamodelshapes.Physical.ROW);

    There are multiple instances of the code, var nRows = oDataLayout.getEdgeExtent(datamodelshapes.Physical.ROW);. You should only remove the line one time.

  10. Stop and restart Oracle Data Visualization, and open the HelloViz plugin.

    Your results should look similar to the following:

    This image is described in surrounding text.
    Description of this image
  11. The visualization is not sized or centered. If you resize the browser window, the visualization does not render correctly.

    To fix the size and centering, add the following function after the _generateData function:

    /**
     * Resize the visualization
     * @param {Object} oVizDimensions - contains two properties, width and height
     * @param {module:obitech-report/vizcontext#VizContext} oTransientVizContext the viz context
     */
    HelloViz.prototype.resizeVisualization = function(oVizDimensions, oTransientVizContext){
       var oTransientRenderingContext = this.createRenderingContext(oTransientVizContext);
       this._render(oTransientRenderingContext);
    }; 
  12. Rename the render function to _render to maintain the original render function. Make changes in the _render function.

    Your changes should look similar to the following:

    /**
     * Called whenever new data is ready and this visualization needs to update.
     * @param {module:obitech-renderingcontext/renderingcontext.RenderingContext} oTransientRenderingContext
     */
    HelloViz.prototype._render = function(oTransientRenderingContext) {
        // Note: all events are received after initialize and start complete. The results can return other events
        // such as 'resize' before the onDataReady, for example, this might not be the first event.
    
        ... (existing code here) 
  13. Add _render to your code after the _generateData function as follows:

    /**
     * Called whenever new data is ready and this visualization needs to update.
     * @param {module:obitech-renderingcontext/renderingcontext.RenderingContext} oTransientRenderingContext
     */
    HelloViz.prototype.render = function(oTransientRenderingContext) {
       // Note: all events are received after initialize and start complete. The results can return other events
       // such as 'resize' before the onDataReady, for example, this might not be the first event.
        
       this._render(oTransientRenderingContext);
    }; 
  14. Window resizing now works. However, you cannot center the window. Change $(elContainer).empty();, by adding the following inside the _render function:

    $(elContainer).addClass("helloVizRootContainer");
    var sVizContainerId = this.getSubElementIdFromParent(this.getContainerElem(), "hvc");
    $(elContainer).html ("<div id=\"" + sVizContainerId + "\" class=\"helloVizContainer\" />");
    var elVizContainer = document.getElementById(sVizContainerId); 
  15. Change this code:

    var oSVG = d3.select(elContainer).append("svg")

    Use the following:

    var oSVG = d3.select(elVizContainer).append("svg") 
  16. Add the following code to helloVizstyles.css:

    .helloVizRootContainer {
       display: flex;
       justify-content: center;
    }
     
    .helloVizContainer {
       align-self: center;
        
       -moz-user-select: none;
       -khtml-user-select: none;
       -webkit-user-select: none;
       -ms-user-select: none;
       user-select: none;
    }
  17. Refresh the browse to pick up the changes that you made in the code. Rendering, resizing, and centering should now work correctly. The visualization should look similar to the following:

    This image is described in surrounding text.
    Description of this image

Refining the Explore Panel

The column drop targets in the Explore panel are controlled by plugin.xml.

  1. Remove size and color because they are not used in the Explore panel.

    Edit.\extensions\oracle.bi.tech.plugin.visualizationDatamodelHandler\com.company.helloViz.visualizationDatamodelHandler.json by setting size and color to null using:

     },
    "color"  : "none",
    "size" : "none",
    ...
  2. Close Oracle Data Visualization and restart the SDK to update plugin with your changes, and then open HelloViz. You should only see Rows and Values similar to the following:

    This image is described in surrounding text.
    Description of this image

Adding Interactivity

Oracle Data Visualization supports marking, drilling, and sorting in most visualizations. In this section, integrate marking, drilling, and sorting into the HelloViz visualization.

Adding Brushing/Marking Support

Brushing/Marking enables the ability to select a data set in a visualization and view related data in a different visualization. Brushing/Marking supports both directly-related values and correlated values. For example, if you select the data point California in a visualization, and a second visualization also contains the California data point, then that data point is directly related and is selected. However, if you select the Product Ford F-150 in California, and one visualization has Product and the other has State, selecting the Ford F-150 value also selects California because the data points are correlated in the server.

In this section, you add support for Brushing and Marking in HelloViz.

  1. Adding an interaction service using 'obitech-reportservices/interactionservice' to import the interaction service and the interactions module:

    define(['jquery',
        'obitech-framework/jsx',
        'obitech-report/datavisualization',
        'obitech-reportservices/datamodelshapes',
        'obitech-reportservices/data',
        'd3js',
        'obitech-reportservices/events',
        'obitech-reportservices/interactionservice',
        'obitech-appservices/logger',
        'ojL10n!com-company-helloViz/nls/messages',
        'obitech-framework/messageformat',
        'css!com-company-helloViz/helloVizstyles'],
        function($,
                 jsx,
                 dataviz,
                 datamodelshapes,
                 data,
                 d3,
                 events,
                 interactions,
                 logger,
                 messages) { 
  2. Immediately after var elContainer = this.getContainerElem(), add the following code inside the function:

    // Lets track our visualization for function calls where the 'this' context changes
    var oViz = this;

    Before changing the code, moving the mouse created a larger image (zoom). By default in Oracle Data Visualization, mouse events perform marking operations. Zoom is an explicit option. Align the new visualization with this behavior to avoid user confusion when different visualizations behave differently. The D3 JavaScript library supports the concept of marking using a Brush.

  3. Add a Brush by changing the var oSVG = ... lines to the following:

    var oSVG = d3.select(elVizContainer).append("svg");
           
    var oG = oSVG.attr("width", nDiameter)
             .attr("height", nDiameter)
             .append("g")
             .attr("transform", "translate(" + nDiameter / 2 + "," + nDiameter / 2 + ")"); 

    Change the circle and text definitions to look like the following by appending to oG, and leave subsequent lines as they are:

    var aCircles = oG.selectAll("circle")
        .data(aDataNodes)
        ...
    ...
    var aTextNodes = oG.selectAll("text")
        .data(aDataNodes)
        ...
    ... 
  4. Add in the D3 brushing code, and change fOnBrushEnd to invoke marking. Immediately after the aCircles.append("title") .text(function(d) { return d.name + (d.size ? ": " + fFormat(d.size) : "" ); }); line, append the following:

    var oBrush;
     
    var fOnBrushEnd = function(){
       oSVG.selectAll(".circle_pack_brush").call(oBrush.clear());
        
    var oMarkingService = oViz.getMarkingService();
       oMarkingService.clearMarksForDataLayout(oDataLayout);
        
    var aSelected = oG.selectAll(".circle_pack_selected")
          .each(function(d, i){
             var oSelection = d.selectionID;
             if(oSelection){
                var nRow = oSelection.row;
                var nCol = oSelection.col;
                oMarkingService.setMark(oDataLayout, datamodelshapes.Physical.DATA, nRow, nCol);
             }
          });
        
       oViz._publishMarkEvent(oDataLayout);
    };
     
    oBrush = d3.svg.brush()
       .x(d3.scale.identity().domain([0, nDiameter]))
       .y(d3.scale.identity().domain([0, nDiameter]))
       .on("brushend", fOnBrushEnd)
       .on("brush", function() {                 
           var aExtent = d3.event.target.extent();
           aCircles.classed("circle_pack_selected", function(d) {
              var bSelected = aExtent[0][0] <= d.x && d.x < aExtent[1][0]
                 && aExtent[0][1] <= d.y && d.y < aExtent[1][1];
              return bSelected;
           });
        });
     
    oSVG.append("g")
       .attr("class", "circle_pack_brush")
       .call(oBrush);

    In the previous code, you selected all elements that have the .circle_pack_selected CSS class, and you looked for the selectionID object using var oSelection = d.selectionID;.

  5. In this step, the selectionID object is generated.

    Change the var aCircles = ... block to the following:

    var aCircles = oG.selectAll("circle")
       .data(aDataNodes)
       .enter()
       .append("circle")
       .attr("class", function(d) { return d.parent ? d.children ? "circle_pack_node" : "circle_pack_node circle_pack_node--leaf" : "circle_pack_node circle_pack_node--root"; })
       .attr("selectionID", function(d) { return d.selectionID; })
       .style("fill", function(d) { return d.children ? fColor(d.depth) : null; })
       .on("click", function(d) { if (oFocus !== d) fZoom(d), d3.event.stopPropagation(); });
  6. Generate the selectionID in the data. Find the _generateData function, and locate the two occurrences of the following code:

    if (isLastLayer())
       oNode.size = nValue;
  7. Change the entries to the following code:

    if (isLastLayer()) {
       oNode.size = nValue;
       oNode.selectionID = { row: nRowIndex, col: nColIndex };
    }

    The change adds the selectionID object to all leaf-level circles.

  8. Add the marking events for other visualizations to use after the _render function.

    HelloViz.prototype._publishMarkEvent = function (oDataLayout, eMarkContext) {
       try {
          // Create the marking event
          var markingEvent = new interactions.MarkingEvent(this.getID(), this.getViewName(), oDataLayout, null, eMarkContext);
          var eventRouter = this.getEventRouter();
          if (eventRouter) {
             // Publish the event to listeners
             eventRouter.publish(markingEvent);
          }
       }
       catch (e) {
          console.log("Error during mark", e);
       }
    }; 
  9. Edit helloViz.css and add the following code:

     .circle_pack_node--leaf.circle_pack_selected {
      stroke: black;
      stroke-width: 2;
    }
     
    .circle_pack_brush .extent {
      fill-opacity: .1;
      stroke: #fff;
      shape-rendering: crispEdges;
    }

    Refresh the browser to update the visualization. You can select and drag data elements to the canvas. The visualization should look similar to the following:

    This image is described in surrounding text.
    Description of this image

    Add another visualization such as a sunburst or bar chart next to HelloViz.

    After releasing the mouse, you should see HelloViz and the visualization next to the updated HelloViz. The update happens automatically because you published the mark event using _publishMarkEvent. Your results should look similar to the following after selecting values in HelloViz:

    This image is described in surrounding text.
    Description of this image
  10. HelloViz can't accept marks or make marks during the initial rendering. You can add specific items to tag or mark after the visualization is rendered.

    Use 'obitech-reportservices/markingservice' to import the interaction and the marking module, extendable-ui-definitions, and the euidef function. Your code should look similar to the following:

    define(['jquery',
        'obitech-framework/jsx',
        'obitech-report/datavisualization',
        'obitech-reportservices/datamodelshapes',
        'obitech-reportservices/data',
        'd3js',
        'obitech-reportservices/events',
        'obitech-reportservices/interactionservice',
        'obitech-reportservices/markingservice',
        'obitech-appservices/logger',
        'ojL10n!com-company-helloViz/nls/messages',
        'obitech-application/extendable-ui-definitions',
        'obitech-framework/messageformat',
        'css!com-company-helloViz/helloVizstyles'],
        function($,
                 jsx,
                 dataviz,
                 datamodelshapes,
                 data,
                 d3,
                 events,
                 interactions,
                 marking,
                 logger,
                 messages,
                 euidef) { 
  11. Edit the HelloViz constructor and add the following immediately after HelloViz.baseConstructor.call(this, sID, sDisplayName, sOrigin, sVersion);:

    /**
     * @type {Array.<string>} - The array of selected items
     */
    var aSelectedItems = [];
     
    /**
     * @return  {Array.<string>} - The array of selected items
     */
    this.getSelectedItems = function(){
       return aSelectedItems;
    };
     
    /**
     * Clears the current list of selected items
     */
    this.clearSelectedItems = function(){
       aSelectedItems = [];
    }; 
  12. At the end of HelloViz.prototype.render = function(oTransientRenderingContext) { , add the following code:

    // Generate (asynchronously) the list of selected items for this visual
    this._buildSelectedItems(oTransientRenderingContext);
  13. Change the var aCircles = oG(... block to the following, and add "var aSelectedItems =". You are changing how the selection class is applid when rendering with circles.

    var aSelectedItems = oViz.getSelectedItems();
     
    var aCircles = oG.selectAll("circle")
       .data(aDataNodes)
       .enter()
       .append("circle")
       .attr("class", function(d) {
          var sClass = d.parent ? d.children ? "circle_pack_node" : "circle_pack_node circle_pack_node--leaf" : "circle_pack_node circle_pack_node--root";
          if(d.selectionID) {
             var sSelectionKey = d.selectionID.row + ":" + d.selectionID.col;
             if(aSelectedItems.indexOf(sSelectionKey) >= 0)
                sClass += " circle_pack_selected";
          }
          return sClass;
       })
       .attr("selectionID", function(d) { return d.selectionID; })
       .style("fill", function(d) { return d.children ? fColor(d.depth) : null; })
       .on("click", function(d) { if (oFocus !== d) fZoom(d), d3.event.stopPropagation(); });
  14. Add _buildSelectedItems as a top-level function using the following:

    /**
     * Builds the list of selected items
     * @param {module:obitech-renderingcontext/renderingcontext.RenderingContext} oTransientRenderingContext - The rendering context
     */
    HelloViz.prototype._buildSelectedItems = function(oTransientRenderingContext){
       var oViz = this;
       var oDataLayout = oTransientRenderingContext.get(dataviz.DataContextProperty.DATA_LAYOUT);
       var oMarkingService = this.getMarkingService();
        
       function fMarksReadyCallback() {
          oViz.clearSelectedItems();
          var aSelectedItems = oViz.getSelectedItems();
           
          if(!oViz.isStarted()) {
             return;
          }
          oMarkingService.traverseDataEdgeMarks(oDataLayout, function (nRow, nCol) {
             aSelectedItems.push(nRow + ':' + nCol);
          });
           
          oViz._render(oTransientRenderingContext);         
       }
        
       oMarkingService.getUpdatedMarkingSet(oDataLayout, marking.EMarkOperation.MARK_RELATED, fMarksReadyCallback);
    }; 
  15. Add an onHighlight function:

    /**
     * React to marking service highlight events
     */
    HelloViz.prototype.onHighlight = function(){
       var oTransientVizContext = this.assertOrCreateVizContext();
       var oTransientRenderingContext = this.createRenderingContext(oTransientVizContext);
       this._buildSelectedItems(oTransientRenderingContext);
    }; 
  16. Listen for the INTERACTION_HIGHLIGHT event by adding the following function to subscribe to the initialization listener:

    /**
     * Override _doInitializeComponent in order to subscribe to events
     */
    HelloViz.prototype._doInitializeComponent = function() {
       HelloViz.superClass._doInitializeComponent.call(this);
        
       this.subscribeToEvent(events.types.INTERACTION_HIGHLIGHT, this.onHighlight, this.getViewName() + "." + events.types.INTERACTION_HIGHLIGHT)
    }; 
  17. Refresh the browser to update the visualization. The visualization should accept marks from other visualizations and have the ability to publish other marks. In this example, Furniture is selected in the sunburst, and Furniture is automatically selected in HelloViz.

    If you reached this step and are having issues, use helloViz_sectionXX.js file to recover and continue with the tutorial.

    This image is described in surrounding text.
    Description of this image

    HelloViz checks for selected items during rendering, and highlights the items. If highlight events are received from another visualization, HelloViz refreshes the visualization using the new marks. Because marks are calculated asynchronously, onDataReady event invokes _buildSelectedItems, and then renders without waiting for the user to select any items. You can change the scenario so that rendering is blocked by building the selected items first, the result means that the visualization can take a very long time to render as marks are calculated. The marks are calculated by a query that returns at a different time than the visualization data, render the visualization first, and then only respond to marks when they become available.

Adding Drill, Lateral Drill (Drill Anywhere), Keep, and Remove Support

  1. By default, D3 clears the selected items on all mouse-down events. Avoid clearing the brush with oncontextmenu events (right-click) by performing the following actions:

    Change:

    oSVG.append("g")
       .attr("class", "circle_pack_brush")
       .call(oBrush); 

    to:

    var aBrushNode = oSVG.append("g")
       .attr("class", "circle_pack_brush")
       .call(oBrush);
     
    var fD3MouseDown = aBrushNode.on('mousedown.brush');
     
    aBrushNode.on("mousedown.brush", function(){
       var oEvent = d3.event;
        
       // If the right mouse button was clicked, don't respond
       if(oEvent.button === 2){
          d3.event.stopPropagation();
       } else {
          fD3MouseDown.apply(this, arguments);
       }
    });
  2. The oncontextmenu (right-click) event does not clear the selections. Change the context menu to include the interaction events you want to use. Add the following:

    /**
     * Override to add in options to the context menu
     *
     * @param {module:obitech-report/vizcontext#VizContext} oTransientVizContext the viz context
     * @param {string} sMenuType The menu type associated with the context menu being populated
     * @param {Array} The array of resulting menu options
     * @param {module:obitech-appservices/contextmenu} contextmenu The contextmenu namespace object (used to reduce dependencies)
     * @param {object} evtParams The entire 'params' object that is extracted from client evt
     * @param {object} oTransientRenderingContext the current transient rendering context
     */
    HelloViz.prototype._addVizSpecificMenuOptions = function(oTransientVizContext, sMenuType, aResults, contextmenu, evtParams, oTransientRenderingContext){
       HelloViz.superClass._addVizSpecificMenuOptions.call(this, oTransientVizContext, sMenuType, aResults, contextmenu, evtParams, oTransientRenderingContext);
        
       if (sMenuType === euidef.CM_TYPE_VIZ_PROPS) {
          // Set up the column context for the last column in the ROWS bucket
          var oColumnContext = this.getDrillPathColumnContext(oTransientVizContext, datamodelshapes.Logical.ROW);
           
          // Set up events
          if(!this.isViewOnlyLimit()){
             this._addFilterMenuOption(oTransientVizContext, aResults, null, null, oTransientRenderingContext);
             this._addRemoveSelectedMenuOption(oTransientVizContext, aResults, null, null);
             this._addDrillMenuOption(oTransientVizContext, aResults, null, null, oColumnContext);
             this._addLateralDrillMenuOption(oTransientVizContext, aResults);
          }
       }
    }; 

    The visualization should now render with participation in brushing and selection, and fully accept and raise events.

  3. Refresh the browser to update the visualization. Select and drag Office Supplies to the visualization. The action should look similar to the following:

    This image is described in surrounding text.
    Description of this image

Adding Color and Legends

Oracle Data Visualization supports legends using color. In this section, you add coloring support for the legend used in the Circle Pack.

  1. Define the elements needed in the legend. You must add 'obitech-application/gadgets' and 'obitech-legend/legendandvizcontainer' to your definition statement. Your changes should look like the following:

    define(['jquery',
        'obitech-framework/jsx',
        'obitech-application/gadgets',
        'obitech-report/datavisualization',
        'obitech-legend/legendandvizcontainer',
        'obitech-reportservices/datamodelshapes',
        'obitech-reportservices/data',
        'd3js',
        'obitech-reportservices/events',
        'obitech-reportservices/interactionservice',
        'obitech-reportservices/markingservice',
        'obitech-application/extendable-ui-definitions',
        'obitech-appservices/logger',
        'ojL10n!com-company-helloViz/nls/messages',
        'obitech-framework/messageformat',
        'css!com-company-helloViz/helloVizstyles'],
        function($,
                 jsx,
                 gadgets,
                 dataviz,
                 legendandvizcontainer,
                 datamodelshapes,
                 data,
                 d3,
                 events,
                 interactions,
                 marking,
                 euidef,
                 logger,
                 messages) { 
  2. Add color in the Explore panel by opening \extensions\oracle.bi.tech.plugin.visualizationDatamodelHandler\com.company.helloViz.visualizationDatamodelHandler.json, and replacing "color": "none" with the following code:

    "color"  : {
       "contentType": "both",
       "global": {
           "preferredMin": 1,
           "preferredMax": 1,
           "priority": 7
       },
       "measures": {
           "maxCount": 1
       },
       "categorical": {
           "functionalInfo": ["inner", "col", "categoricalType"]
       }
    },
  3. At the end of the HelloViz constructor, add the following code:

    // Create legend options
    var oLegendOptions = {
       sAutoPositionOverride: gadgets.LegendControlsGadgetValueProperties.Positions.BOTTOM,
       bVerticalAlignOnVizBody: true
    };
    // Add legend capabilities
    legendandvizcontainer.addLegendAndVizContainerFunctions(this, oLegendOptions);
  4. Add a new function for rendering the legend using the following:

    /**
     * Render legend for certain pivot/table like Color fill pivot.
     * @param {module:obitech-report/renderingcontext#RenderingContext} oLegendTransientRenderingContext
     *
     * @private
     */
    HelloViz.prototype._renderLegend = function(oLegendTransientRenderingContext)
    {
       //jsx.assertInstanceOf(oLegendTransientRenderingContext, rc.RenderingContext, "obitech-report/renderingcontext#RenderingContext");
       var elLegendContainer = this.getLegendContainer();
       $(elLegendContainer).empty();
      
       var oLegendModel = {
          aOjLegendSections: this.generateOjLegendSections(oLegendTransientRenderingContext),
          aCustomLegendSections: this.generateCustomLegendSectionModels(oLegendTransientRenderingContext)
       };
        
       return this.configureAndRenderLegend(elLegendContainer,
                                        this.getLegendOptions(oLegendTransientRenderingContext),
                                        oLegendModel);
    }; 
  5. Implement generateOjLegendSections using the following:

    /**
         * Returns the color layer info for categorical colors
         * @param {object} oDataLayoutHelper - The data layout helper
         * @param {object} oColorMapper - The color mapper
         * @param {boolean} bIncludeFormattedSeriesValues - Whether to include formatted series values
         * @param {boolean} bCalcDatapoint - Whether to calc data point coloring
         * @return {object} an object with two arrays; valueIds and formattedValues
         * @private
         */
         HelloViz.prototype._getCategoricalColorPropertiesForRowSlices = function (oDataLayoutHelper, oColorMapper, bIncludeFormattedSeriesValues, bCalcDatapoint, bCalcSeries) {
           var oDataLayout = oDataLayoutHelper.getDataLayout();
           var nRowSlices = oDataLayout.getEdgeExtent(datamodelshapes.Physical.ROW) || 1;
           return jsx.Array.range(nRowSlices).map(function (i) {
              return oColorMapper.getColorProperties(i, 0, oDataLayoutHelper, {bIncludeFormattedSeriesValues:bIncludeFormattedSeriesValues,bIncludeMeasuresInCategoricalColorId:false, bCalcDatapoint:bCalcDatapoint, bCalcSeries:bCalcSeries});
           });
        };
        /**
         * Generate a color legend section
         * @param {module:obitech-renderingcontext/renderingcontext.RenderingContext} oTransientRenderingContext - The rendering context
         * @return {Object} - the color legend section
         * @private
         */
         HelloViz.prototype._getColorLegendSection = function (oTransientRenderingContext
         {
           var oDataLayoutHelper = oTransientRenderingContext.get(dataviz.DataContextProperty.DATA_LAYOUT_HELPER);
           var oLogicalDataModel = oTransientRenderingContext.get(dataviz.DataContextProperty.LOGICAL_DATA_MODEL);
           var aColumnsInColor = oLogicalDataModel.getColumnIDsIn(datamodelshapes.Logical.COLOR);
           if (jsx.isNull(aColumnsInColor) || aColumnsInColor.length === 0) {
              return null;
           }
           var oColorLegendItems = this.buildPresetLegendItemsForColor(oDataLayoutHelper, oDataModelColorMapper) || {};
           var bIsEmptyPresetColorLegend = jsx.Map.isEmpty(oColorLegendItems);
           if (bIsEmptyPresetColorLegend) {
           // Fetch all color assignments for the table
              var aColorProps = this._getCategoricalColorPropertiesForRowSlices(oDataLayoutHelper, oDataModelColorMapper, true, false);
              var oDoneMap = {}
           // Remove the duplicate color assignments, so every item in the legend is unique
           for(var i=0;i<aColorProps.length;i++){
              var oColorProp = aColorProps[i].seriesProps;
              var sLabel = oColorProp.formattedValues.join(", ");
              if(jsx.isNull(oDoneMap[oColorProp.valueId])){
                    oDoneMap[oColorProp.valueId] = true;
                    oColorLegendItems[sLabel] = oColorProp.color;
                 }
              }
           } 
           var aItems = [];
           for (var item in oColorLegendItems) {
              aItems.push({
                 text: item,
                 color: oColorLegendItems[item],
                 type: "marker",
                 markerShape: "square"
              });
           }
           if (aItems.length === 0)
              return null;
           var sTitle = this._getDisplayNameFromColumnIDs(oLogicalDataModel.getColumnIDsIn(datamodelshapes.Logical.COLOR));
           var oColorSection = {
              title: sTitle,
              items: aItems
           };
           return oColorSection;
               };
         /**
         * Generates an array of objects containing 'ojSection' values that are used
         * by the legend component.  In our case, this generates a size
         * and a categorical legend.
         * 
         * @param {module:obitech-renderingcontext/renderingcontext.RenderingContext} oTransientRenderingContext - The rendering context
         *  
         * @return {Array.<>}an array of oj legend sections
           */
        HelloViz.prototype.generateOjLegendSections = function (oTransientRenderingContext)
        {
           var aSections = []; 
           var oColorSection = this._getColorLegendSection(oTransientRenderingContext);
                  if (oColorSection) {
              aSections.push({ ojSection: oColorSection });
           }
           return aSections;
        };     
    
  6. In your code, call the _renderLegend function. At the beginning of the existing render function, add a call to render the legend.

    The function should now look like the following:

    /**
     * Called whenever new data is ready and this visualization needs to update.
     * @param {module:obitech-renderingcontext/renderingcontext.RenderingContext} oTransientRenderingContext
     */
    HelloViz.prototype.render = function(oTransientRenderingContext) {
       // Note: all events are received after initialize and start complete.  You might get other events
       // such as 'resize' before the onDataReady, for example, this might not be the first event.
        
       this._renderLegend(oTransientRenderingContext);
       this._render(oTransientRenderingContext);
        
       // Generate (asynchronously) the list of selected items for this visual
       this._buildSelectedItems(oTransientRenderingContext);
    };
  7. Invoke the legend during the initialize _doInitializeComponent using the following:

    /**
     * Override _doInitializeComponent in order to subscribe to events
     */
    HelloViz.prototype._doInitializeComponent = function() {
       HelloViz.superClass._doInitializeComponent.call(this);
        
       this.subscribeToEvent(events.types.DEFAULT_COLOR_SETTINGS_CHANGED, this._onDefaultColorsSettingsChanged, "**");
       this.subscribeToEvent(events.types.INTERACTION_HIGHLIGHT, this.onHighlight, this.getViewName() + "." + events.types.INTERACTION_HIGHLIGHT);
        
       this.initializeLegendAndVizContainer();
    }; 
  8. You must listen for color events. You already added subscribeToEvent in a previous step. Add the following function to call when colors change by re-rendering the visualization:

    /**
     * Re-render the visualization when color changes
     */
    HelloViz.prototype._onDefaultColorsSettingsChanged = function(){
       var oTransientVizContext = this.assertOrCreateVizContext();
       var oTransientRenderingContext = this.createRenderingContext(oTransientVizContext);
       this._render(oTransientRenderingContext);
    }; 
  9. You want the legend to have default options and have a Legend section in the context (right-click) properties menu, use the following to initialize the default options:

    /**
         * @param oTabbedPanelsGadgetInfo
         */
        HelloViz.prototype._addVizSpecificPropsDialog = function(oTabbedPanelsGadgetInfo)
        {
           var options = this.getViewConfig() || {};
           this._fillDefaultOptions(options, null);
           this._addLegendToVizSpecificPropsDialog(options, oTabbedPanelsGadgetInfo);
           HelloViz.superClass._addVizSpecificPropsDialog.call(this, oTabbedPanelsGadgetInfo);
        };
         
        /**
         * @param sGadgetID
         * @param oPropChange
         * @param oViewSettings
         * @param oActionContext
         */
        HelloViz.prototype._handlePropChange = function (sGadgetID, oPropChange, oViewSettings, oActionContext)
        {
           var conf = oViewSettings.getViewConfigJSON(dataviz.SettingsNS.CHART) || {};
           //Allow the super class an attempt to handle the changes
           var bUpdateSettings = HelloViz.superClass._handlePropChange.call(this, sGadgetID, oPropChange, oViewSettings, oActionContext);
           if(this._handleLegendPropChange(conf, sGadgetID, oPropChange, oViewSettings, oActionContext)){
              bUpdateSettings = true;
           }
           return bUpdateSettings;
        };
         
        /**
         * Given an options / config object, configure it with default options for the visualization.
         *
         * @param {object} oOptions the options
         * @param {module:obitech-framework/actioncontext#ActionContext} oActionContext The ActionContext instance associated with this action
         * @protected
         */
        HelloViz.prototype._fillDefaultOptions = function (oOptions/*, oActionContext*/) {
           if (!jsx.isNull(oOptions) && !jsx.isNull(oOptions.legend))
              return;
           // Legend
           oOptions.legend = jsx.defaultParam(oOptions.legend, {});
           oOptions.legend.rendered = jsx.defaultParam(oOptions.legend.rendered, "on");
           oOptions.legend.position = jsx.defaultParam(oOptions.legend.position, "auto");
           this.getSettings().setViewConfigJSON(dataviz.SettingsNS.CHART, oOptions);
        };
      
        /**
         * Override the base visualization and allow marks on the
         * data edge to be processed for color.
         * @returns {Boolean}
         */
        HelloViz.prototype._isOnlyPhysicalRowEdge = function(){
           return false;
        }; 
  10. When the legend renders, it divides the layout into a body container and legend container, and adjusts the size of the containers. Instead of getting the root container, you must get the body container. At the beginning of the _render function, replace:

    var elContainer = this.getContainerElem();

    with:

    // Retrieve the root container for our visualization using the legend functions.
    // The legend should have already been processed, and 
     // the container size should be set appropriately.
    var elContainer = this.getVizBodyContainer();
  11. Verify that the legend can resize by adding the following to the resizeVisualization:

     /**
     * Resize the visualization
     * @param {Object} oVizDimensions - contains two properties, width and height
     * @param {module:obitech-report/vizcontext#VizContext} oTransientVizContext the viz context
     */
    HelloViz.prototype.resizeVisualization = function(oVizDimensions, oTransientVizContext){
       var oTransientRenderingContext = this.createRenderingContext(oTransientVizContext);
       this.configureAndResizeLegend(this.getLegendOptions(oTransientRenderingContext));
       this._render(oTransientRenderingContext);
    };
  12. In Oracle Data Visualization, open HelloViz, and add the data element, Customer Segment to Color. The result should look like the following:

    This image is described in surrounding text.
    Description of this image
  13. Replace Customer Segment with Profit. The results should look like the following:

    This image is described in surrounding text.
    Description of this image

    The legend should now work correctly.

  14. Right-click and select properties to change legend from Auto to Top. The legend should move to the top of the project and look like the following:

    This image is described in surrounding text.
    Description of this image
  15. Color the D3 nodes when you have a column in color. Add color to the child nodes only by changing fill in the aCircles definition so that the entire definition looks like the following:

    var aCircles = oG.selectAll("circle")
       .data(aDataNodes)
       .enter()
       .append("circle")
       .attr("class", function(d) {
          var sClass = d.parent ? d.children ? "circle_pack_node" : "circle_pack_node circle_pack_node--leaf" : "circle_pack_node circle_pack_node--root";
          if(d.selectionID) {
             var sSelectionKey = d.selectionID.row + ":" + d.selectionID.col;
             if(aSelectedItems.indexOf(sSelectionKey) >= 0)
                sClass += " circle_pack_selected";
          }
          return sClass;
       })
       .attr("selectionID", function(d) { return d.selectionID; })
       .style("fill", function(d) {
          return d.children ? fColor(d.depth) : (d.color ? d.color : null);
        })
       .on("click", function(d) { if (oFocus !== d) fZoom(d), d3.event.stopPropagation(); }); 
  16. You must pass additional properties to _generateData before adding the color property to data generation. Instead of passing the oDataLayout object to _generateData, pass oTransientRenderingContext. Your code should look like the following:

    var oData = this._generateData(oTransientRenderingContext); 
  17. Change the _generateData function to look like the following by paying attention to fColorNode as this is where the various properties are processed and added to the color property of a node.

    /**
         * Generates data in the form or parent-child nodes, each node has a name and a children property.
         * @param {module:obitech-renderingcontext/renderingcontext.RenderingContext} oTransientRenderingContext - The rendering context
         * @returns {Object}
         */
        HelloViz.prototype._generateData = function(oTransientRenderingContext){
           var oData = {
             "name":     "root",
             "children": []
           };
            
           // Retrieve the data object for this visualization
           var oDataLayout = oTransientRenderingContext.get(dataviz.DataContextProperty.DATA_LAYOUT);
           var oDataLayoutHelper = oTransientRenderingContext.get(dataviz.DataContextProperty.DATA_LAYOUT_HELPER);
            
           var oDataModel = this.getRootDataModel();
           if(!oDataModel || !oDataLayout){
              return;
           }
            
           var aAllMeasures = oDataModel.getColumnIDsIn(datamodelshapes.Physical.DATA);
           var nMeasures = aAllMeasures.length;
            
           var nRows = oDataLayout.getEdgeExtent(datamodelshapes.Physical.ROW);
           var nRowLayerCount = oDataLayout.getLayerCount(datamodelshapes.Physical.ROW);
           var nCols = oDataLayout.getEdgeExtent(datamodelshapes.Physical.COLUMN);
           var nColLayerCount = oDataLayout.getLayerCount(datamodelshapes.Physical.COLUMN);
            
           var oColorLayersOnPA = oDataLayoutHelper.getLogicalLayerInfosOnPropertyAdditions(datamodelshapes.Logical.COLOR);
           var oLogicalDataModel = oTransientRenderingContext.get(dataviz.DataContextProperty.LOGICAL_DATA_MODEL);
           var aColumnsInColor = oLogicalDataModel.getColumnIDsIn(datamodelshapes.Logical.COLOR);
           var bHasColorInPA = oColorLayersOnPA.length > 0;
            
           // Color
           var oColorMapper = this._getColorMapper(oTransientRenderingContext);
           var bHasColorMapper = nMeasures > 0 && !jsx.isNull(oColorMapper);
           var bColorByMeasure = false;
           var oColorInterpolator;
           if (bHasColorMapper) {
              bColorByMeasure = oColorMapper.isContinuousColoring();
              if (bColorByMeasure) {
                 oColorInterpolator = this._getColorInterpolator(oTransientRenderingContext);
              }
           }
            
           var hasCategoryOrColor = function () {
              return nLastLayer >= 0;
           };
            
           /**
            * If a node needs to be colored.
            * @param {Boolean} bIsLastLayer
            * @return {Boolean}
            */
            var shouldNodeBeColored = function (bIsLastLayer) {
               return bIsLastLayer;
            };
             
            var getColorValue = function (oTransientRenderingContext, nLayer, nRow, nCol) {
               jsx.assertNumber(nLayer);
               jsx.assertNumber(nRow);
               jsx.assertNumber(nCol);
               var oDataLayout = oTransientRenderingContext.get(dataviz.DataContextProperty.DATA_LAYOUT);
               return oDataLayout.getNumberPropertyAddition(datamodelshapes.Physical.DATA, nRow, nCol, datamodelshapes.PropertyAdditionIDConstants.COLOR, 0);
            };
             
           // Measure labels layer
           var isMeasureLabelsLayer = function (eEdgeType, nLayer) {
              return oDataLayout.getLayerMetadata(eEdgeType, nLayer, data.LayerMetadata.LAYER_ISMEASURE_LABELS);
           };
            
           // Last layer: Get the data values and colors from this layer
           var getLastNonMeasureLayer = function (eEdge) {
              var nLayerCount = oDataLayout.getLayerCount(eEdge);
              for (var i = nLayerCount - 1; i >= 0; i--) {
                 if (!isMeasureLabelsLayer(eEdge, i))
                    return i;
              }
              return -1;
           };
            
           var nLastEdge = datamodelshapes.Physical.COLUMN; // check column edge first
            
           var nLastLayer = getLastNonMeasureLayer(datamodelshapes.Physical.COLUMN);
           if (nLastLayer < 0) { // if not on column edge look on row edge
              nLastEdge = datamodelshapes.Physical.ROW;
              nLastLayer = getLastNonMeasureLayer(datamodelshapes.Physical.ROW);
           }
            
           function buildTree(oParentNode, eEdgeType, nLayerCount, nLayer, nRowSlice, nColSlice, nParentEndSlice, nTreeLevel) {
              var oColorContext = {
                 bPromoteSeriesColor: true,
                 bPromoteDatapointColor: true
              };
               
              if (nLayer === nLayerCount) {
                 // This is a leaf node on row (category), build column (color) next
                 if (eEdgeType === datamodelshapes.Physical.ROW) {
                     oColorContext = buildTree(oParentNode, datamodelshapes.Physical.COLUMN, nColLayerCount, 0, nRowSlice, 0, nCols, nTreeLevel);
                 }
                 return oColorContext;
              }
              // Skip measure labels
              if (isMeasureLabelsLayer(eEdgeType, nLayer) && hasCategoryOrColor()) {
                 return buildTree(oParentNode, eEdgeType, nLayerCount, nLayer + 1, nRowSlice, nColSlice, nParentEndSlice, nTreeLevel);
              }
              var isLastLayer = function () {
                 return (nLastLayer === -1) || (nLastEdge === eEdgeType && nLastLayer === nLayer);
              };
              var isDuplicateLayer = function () {
                 return nLastLayer > -1 &&
                        eEdgeType === datamodelshapes.Physical.COLUMN;
              };
               
              var fColorNode = function (nRow, nCol, oNodeToColor) {
                 if (!shouldNodeBeColored(isLastLayer()))
                    return;
                 if (!bColorByMeasure && !isLastLayer())
                    return;
                 var nColorValue;
                 var sDatapointColor;
                 var sSeriesColor;
                 var sColorLabel;
                 var bHasColorLabel = true; //added to distinguish whether the color label happens to be empty string or doesn't exist at all.
                 var sPattern;
                 var bHasDatapointColor = false;
                 function hasDatapointColor(oColorProps){
                    return !jsx.isNull(oColorProps) && !jsx.isNull(oColorProps.color);
                 }
                  
                 // Add a color, if you have a mapper
                 if (bHasColorMapper) {
                    if (bColorByMeasure) { // continuous color by measure
                       // Only calc data point coloring for the last layer
                       var oDataPointColorProps = isLastLayer() ? oColorMapper.getColorProperties(nRow, nCol, oDataLayoutHelper, { bCalcSeries: false, bCalcDatapoint: true }) : null;
                       if (hasDatapointColor(oDataPointColorProps)) {
                          bHasDatapointColor = true;
                          sDatapointColor = oDataPointColorProps.color;
                       } else {
                          nColorValue = getColorValue(oTransientRenderingContext, nLayer, nRow, nCol);
                          var oColorOptions = oColorInterpolator.calcColorProperties(nRow, nCol, nColorValue);
                          sPattern = oColorOptions.pattern;
                          sDatapointColor = oColorOptions.color;
                       }
                    }
                    else if (bHasColorInPA) {
                       var oPAColorOptions = oColorMapper.getColorProperties(nRow, nCol, oDataLayoutHelper, {bCalcDatapoint:isLastLayer(), bIncludeFormattedSeriesValues:true});
                       var oPASeriesProps = oPAColorOptions.seriesProps;
                       if (jsx.isNotNull(oPASeriesProps) && jsx.isNotNull(oPASeriesProps.valueId) ) {
                          sColorLabel = oPASeriesProps.formattedValues.join(messages.CIRCLEPACK_COLUMN_SEPARATOR);
                       }
                       sPattern = oPASeriesProps.pattern;
                       sSeriesColor = oPASeriesProps.color;
                       sDatapointColor = oPAColorOptions.color;
                       if (hasDatapointColor(oPAColorOptions.datapointProps)) {
                          bHasDatapointColor = true;
                       }
                    }
                    else { // discrete color by dimension
                       // Only calc data point coloring for the last layer
                       var oColorProps = oColorMapper.getColorProperties(nRow, nCol, oDataLayoutHelper, { bCalcDatapoint: isLastLayer(), bIncludeFormattedSeriesValues: true });
                       sDatapointColor = oColorProps.color;
                       sColorLabel = oColorProps.seriesProps.formattedValues.join(messages.CIRCLEPACK_COLUMN_SEPARATOR);
                       bHasColorLabel = oColorProps.seriesProps.formattedValues.length > 0;
                       sSeriesColor = oColorProps.seriesProps.color;
                       if (hasDatapointColor(oColorProps.datapointProps)) {
                          bHasDatapointColor = true;
                       }
                    }
         
                 }
                  
                 if(!bHasDatapointColor && aColumnsInColor.length === 0){
                    oNodeToColor.color = 'white';
                 }
                 else if (sDatapointColor) {
                    oNodeToColor.color = sDatapointColor;
                 }
                 else if(!bColorByMeasure){
                    oNodeToColor.color = sSeriesColor;
                 }
                 //if (!jsx.isNull(sPattern) && sPattern !== "auto")
                 //   oNodeToColor.pattern = sPattern;
                  
                 return;
              };
               
              var oChildColorContext, nEndSlice;
              for (var nSlice = (eEdgeType === datamodelshapes.Physical.COLUMN) ? nColSlice : nRowSlice;
                       nSlice < nParentEndSlice;
                       nSlice = nEndSlice) {
                 nEndSlice = oDataLayout.getItemEndSlice(eEdgeType, nLayer, nSlice) + 1;
                 var nRowIndex = (eEdgeType === datamodelshapes.Physical.COLUMN) ? nRowSlice : nSlice;
                 var nColIndex = (eEdgeType === datamodelshapes.Physical.ROW) ? nColSlice : nSlice;
                  
                 var nValue = null;
                 if (isLastLayer()) {
                    var val = 1; // default to rendering equally sized boxes when there are no measures
                    if (nMeasures > 0) {
                       val = oDataLayout.getValue(datamodelshapes.Physical.DATA, nRowIndex, nColIndex, false);
                       // Skip no data
                       if (typeof val !== 'string' || val.length === 0){
                           
                          // Pass in a fake node that is thrown away, instead of a real treenode to populate with color info
                          var oFakeNode = {};
                          fColorNode(nRowIndex, nColIndex, oFakeNode);
                           
                          continue;
                       }
                    }
                    nValue = parseFloat(val);
                    // Skip negative and zero values for now. A design is needed on how to show these elements.
                    if (nValue <= 0)
                       continue;
                 }
                 var oNode;
                 // Don't render the same layer twice in case when it is in both row (Detail) and column (Color) edge
                 if (!isDuplicateLayer()) {
                    // Create new node
                    oNode = {};
                    oNode.size = 0;
                    var sId = oDataLayout.getValue(eEdgeType, nLayer, nSlice, true);
                    oNode.id = (oParentNode.id || '') + '.' + sId;
                    oNode.name = oDataLayout.getValue(eEdgeType, nLayer, nSlice, false);
                    if (isLastLayer()) {
                       oNode.size = nValue;
                       oNode.selectionID = { row: nRowIndex, col: nColIndex };
                    }
                    // Append new node to parent node
                    oParentNode.children = oParentNode.children || [];
                    oParentNode.children.push(oNode);
                    oChildColorContext = buildTree(oNode, eEdgeType, nLayerCount, nLayer + 1, nRowIndex, nColIndex, nEndSlice, nTreeLevel + 1);
                    fColorNode(nRowIndex, nColIndex, oNode);
                     
                    // Aggregate values into parent node
                    oParentNode.size = oParentNode.size || 0;
                    if (oNode.size)
                       oParentNode.size += oNode.size;
                 }
                 else {
                    oNode = oParentNode;
                    if (isLastLayer()) {
                       oNode.size = nValue;
                       oNode.selectionID = { row: nRowIndex, col: nColIndex };
                    }
                    oChildColorContext = buildTree(oNode, eEdgeType, nLayerCount, nLayer + 1, nRowIndex, nColIndex, nEndSlice, nTreeLevel + 1);
                    fColorNode(nRowIndex, nColIndex, oNode);
                 }
              }
              return oColorContext;
           }
            
           // Build a treemap starting with DETAIL logical edge (see getLogicalMapper)
           buildTree(oData, datamodelshapes.Physical.ROW, nRowLayerCount, 0, 0, 0, nRows, 0);
            
           // Nothing was added to Category, try COLOR logical edge (see getLogicalMapper)
           if (oData.children.length === 0){
              buildTree(oData, datamodelshapes.Physical.COLUMN, nColLayerCount, 0, 0, 0, nCols, 0);  
           }
           return oData;
        };
  18. Close and restart Oracle Data Visualization, and open HelloViz. The last configuration with Product Category, # of Customers, and Profit should look like the following:

    This image is described in surrounding text.
    Description of this image
  19. Replace Profit with Product Category, and add Product Sub Category to Rows. The results should look similar to the following:

    This image is described in surrounding text.
    Description of this image
  20. Add coloring options to the context (right-click) menu to support data point and series coloring. Within _addVizSpecificMenuOptions, add this._addColorMenuOption and ensure that the oTransientRendering context is populated. The function should look like the following:

    /**
     * Override to add in options to the context menu
     *
     * @param {module:obitech-report/vizcontext#VizContext} oTransientVizContext the viz context
     * @param {string} sMenuType The menu type associated with the context menu being populated
     * @param {Array} The array of resulting menu options
     * @param {module:obitech-appservices/contextmenu} contextmenu The contextmenu namespace object (used to reduce dependencies)
     * @param {object} evtParams The entire 'params' object that is extracted from client evt
     * @param {object} oTransientRenderingContext the current transient rendering context
     */
    HelloViz.prototype._addVizSpecificMenuOptions = function(oTransientVizContext, sMenuType, aResults, contextmenu, evtParams, oTransientRenderingContext){
       if(!oTransientRenderingContext){
          oTransientRenderingContext = this.createRenderingContext(oTransientVizContext);
       }
        
       HelloViz.superClass._addVizSpecificMenuOptions.call(this, oTransientVizContext, sMenuType, aResults, contextmenu, evtParams, oTransientRenderingContext);
        
       if (sMenuType === euidef.CM_TYPE_VIZ_PROPS) {
          // Set up the column context for the last column in the ROWS bucket
          var oColumnContext = this.getDrillPathColumnContext(oTransientVizContext, datamodelshapes.Logical.ROW);
           
          // Set up events
          if(!this.isViewOnlyLimit()){
             this._addFilterMenuOption(oTransientVizContext, aResults, null, null, oTransientRenderingContext);
             this._addRemoveSelectedMenuOption(oTransientVizContext, aResults, null, null);
             this._addDrillMenuOption(oTransientVizContext, aResults, null, null, oColumnContext);
             this._addLateralDrillMenuOption(oTransientVizContext, aResults);
              
             this._addColorMenuOption(oTransientVizContext, aResults, oTransientRenderingContext);
          }
       }
    };
  21. Refresh your browser to update the visualization. Select and drag a few nodes to the canvas, right-click to choose data point coloring, and then select a new color. Your results should look similar to the following:

    This image is described in surrounding text.
    Description of this image

    You have completed the changes to support the main coloring capabilities in Oracle Data Visualization. You can now change series colors, data point colors, choose palettes from the canvas properties dialog, and color either from any visualization. The following coloring example shows the Binders and Binder Accessories data point. The treemap next to the circle pack is also updated:

    This image is described in surrounding text.
    Description of this image

Want to Learn More?