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
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%
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.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
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.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
- Edit
helloViz.js
and render what you need. In this example, use the Circle Pack code fromD3.js
, and openhelloViz.js
in your favorite editor. -
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 thedatamodelshapes,
line. - Add
data,
after thedatamodelshapes,
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) { ...
- Add
-
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);
- Remove the
-
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; }
-
Stop and restart Oracle Data Visualization, open the HelloViz visualization, and choose the Sample Order Lines data set to create your project.
Add the # of Customers and Product Category data elements to the canvas.
Your results should look similar to the following:
Description of this image -
In the
helloViz.js
file, add in your own data by replacing thevar oData = ...
variable with the following:var oData = this._generateData(oDataLayout); if(!oData) { return; }
-
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; }
-
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. -
Stop and restart Oracle Data Visualization, and open the HelloViz plugin.
Your results should look similar to the following:
Description of this image -
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); };
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)
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); };
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);
Change this code:
var oSVG = d3.select(elContainer).append("svg")
Use the following:
var oSVG = d3.select(elVizContainer).append("svg")
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; }
-
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:
Description of this image
Refining the Explore Panel
The column drop targets in the Explore
panel
are controlled by plugin.xml
.
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", ...
-
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:
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.
Adding an interaction service using
'obitech-reportservices/interactionservice'
to import the interaction service and theinteractions
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) {
-
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.
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) ... ...
Add in the D3 brushing code, and change
fOnBrushEnd
to invoke marking. Immediately after theaCircles.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 theselectionID
object usingvar oSelection = d.selectionID;
.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(); });
Generate the
selectionID
in the data. Find the_generateData
function, and locate the two occurrences of the following code:if (isLastLayer()) oNode.size = nValue;
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.-
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); } };
-
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:
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:Description of this image -
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 themarking
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) {
-
Edit the
HelloViz
constructor and add the following immediately afterHelloViz.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 = []; };
-
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);
-
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(); });
-
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); };
-
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); };
-
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) };
-
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.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
-
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); } });
-
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.
-
Refresh the browser to update the visualization. Select and drag Office Supplies to the visualization. The action should look similar to the following:
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.
-
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) {
-
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"] } },
-
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);
-
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); };
-
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; };
In your code, call the
_renderLegend
function. At the beginning of the existingrender
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); };
-
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(); };
-
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); };
-
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; };
-
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();
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); };
-
In Oracle Data Visualization, open HelloViz, and add the data element, Customer Segment to Color. The result should look like the following:
Description of this image -
Replace Customer Segment with Profit. The results should look like the following:
Description of this image The legend should now work correctly.
-
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:
Description of this image -
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(); });
-
You must pass additional properties to
_generateData
before adding thecolor
property to data generation. Instead of passing theoDataLayout
object to_generateData
, passoTransientRenderingContext
. Your code should look like the following:var oData = this._generateData(oTransientRenderingContext);
-
Change the
_generateData
function to look like the following by paying attention tofColorNode
as this is where the various properties are processed and added to thecolor
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; };
-
Close and restart Oracle Data Visualization, and open HelloViz. The last configuration with Product Category, # of Customers, and Profit should look like the following:
Description of this image -
Replace Profit with Product Category, and add Product Sub Category to Rows. The results should look similar to the following:
Description of this image -
Add coloring options to the context (right-click) menu to support data point and series coloring. Within
_addVizSpecificMenuOptions
, addthis._addColorMenuOption
and ensure that theoTransientRendering
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); } } };
-
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:
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:
Description of this image