This documentation targets the API shipped with Pentaho 8.0. Click here for the updated version shipped with Pentaho 8.3.
Step 3 - Creating the view
Skeleton view code
Create a file named view-d3.js
and place the following code in it:
define(["module", "d3"], function(module, d3) {
"use strict";
return [
"pentaho/visual/base/view",
"./model",
function(BaseView, Model) {
// Create the Bar View subclass
var BarView = BaseView.extend({
$type: {
id: module.id,
props: [
// Specialize the inherited model property to the Bar model type
{
name: "model",
valueType: Model
}
]
},
_updateAll: function() {
d3.select(this.domContainer).text("Hello World!");
}
});
return BarView;
}
];
});
Remarks:
- Defines a visualization view whose id is the file’s AMD module identifier
(depending on how AMD is configured, it can be, for example:
pentaho-visual-samples-bar-d3/view-d3
). - Inherits directly from the base visualization view, pentaho/visual/base/view.
- The inherited model property is overridden so that its valueType is the Bar model you previously created.
- The _updateAll
method is where the code that fully renders the visualization must go,
and, for now, it simply uses D3 to output
"Hello World!"
in the view’s DOM element,domContainer
.
Installing D3
Execute the following:
# Add and install the D3 dependency
# (also set it as a bundled dependency)
npm install d3 --save --save-bundle
Adapting the HTML sandbox
Edit the sandbox.html
file and place the following code in it:
<!doctype html>
<html>
<head>
<style>
.pentaho-visual-base-model {
border: solid 1px #005da6;
}
</style>
<!-- load requirejs -->
<script type="text/javascript" src="node_modules/requirejs/require.js"></script>
<!-- load the VizAPI dev bootstrap helper -->
<script type="text/javascript" src="node_modules/@pentaho/viz-api/dev-bootstrap.js"></script>
<!-- configure AMD for the sample visualization -->
<script>
require([
"vizapi-dev-init",
"json!./package.json"
], function (devInit, package) {
devInit(package);
require.config({
paths: {
"d3": "./node_modules/d3/build/d3"
}
});
require([
"pentaho/type/Context",
"pentaho/data/Table",
"json!./sandbox-data.json"
], function (Context, Table, dataSpec) {
// Setup up a VizAPI context.
Context.createAsync({ application: "viz-api-sandbox" })
.then(function (context) {
// Get the model and base view types
return context.getDependencyAsync({
BarModel: "pentaho-visual-samples-bar-d3/model",
BaseView: "pentaho/visual/base/view"
});
})
.then(function (types) {
// Create the visualization model.
var modelSpec = {
"data": new Table(dataSpec),
"category": { attributes: ["productFamily"] },
"measure": { attributes: ["sales"] }
};
var model = new types.BarModel(modelSpec);
// Create the visualization view
var viewSpec = {
width: 700,
height: 300,
domContainer: document.getElementById("viz_div"),
model: model
};
// These are responsibilities of the visualization container application:
// 1. Mark the container with the model's CSS classes, for styling purposes.
viewSpec.domContainer.className = model.$type.inheritedStyleClasses.join(" ");
// 2. Set the DOM container dimensions.
viewSpec.domContainer.style.width = viewSpec.width + "px";
viewSpec.domContainer.style.height = viewSpec.height + "px";
// Create the visualization view.
return types.BaseView.createAsync(viewSpec);
})
.then(function (view) {
// Handle the execute action.
view.on("pentaho/visual/action/execute", {
"do": function (action) {
alert("Executed " + action.dataFilter.contentKey);
}
});
// Handle the select action.
view.on("pentaho/visual/action/select", {
"finally": function (action) {
document.getElementById("messages_div").innerText = "Selected: " + view.selectionFilter.contentKey;
}
});
// Render the visualization.
return view.update();
});
});
});
</script>
</head>
<body>
<!-- div that will contain the visualization -->
<div id="viz_div"></div>
<!-- div that will display messages -->
<div id="messages_div"></div>
</body>
</html>
Remarks:
- A script block was added with the AMD/RequireJS configuration of the D3 path. This step is only needed in a sandbox environment. When inside the Pentaho platform, these configurations are provided automatically, built from the web package information.
- The used visualization model is now
pentaho-visual-samples-bar-d3/model
(or other, if you choose a different package name on step 1). - The model now contains visual role mappings for the
category
andmeasure
visual roles. - The dimensions of the visualization were increased.
Now, refresh the sandbox.html
page in the browser, and you should read Hello World!
.
Implementing the render code
Let’s now finally implement the rendering of a Bar chart in D3. To make it easy, we’ll adapt the code of following D3 block: https://bl.ocks.org/mbostock/3885304.
We’ll now go through the view’s _updateAll code, piece by piece.
Method _updateAll
, part 1
In view-d3.js
, replace the code of the _updateAll
method with the following:
// view-d3.js
// _updateAll:
function() {
// Part 1
var model = this.model;
var dataTable = model.data;
var categoryAttribute = model.category.attributes.at(0).name;
var measureAttribute = model.measure.attributes.at(0).name;
var categoryColumn = dataTable.getColumnIndexByAttribute(categoryAttribute);
var measureColumn = dataTable.getColumnIndexByAttribute(measureAttribute);
var scenes = this.__buildScenes(dataTable, categoryColumn, measureColumn);
var container = d3.select(this.domContainer);
// ...
}
Remarks:
- this.model gives you access to the visualization model object.
- Both the visual roles are required, so it is safe to directly read the first mapped attribute.
- Most data table methods accept column indexes, so attribute names are converted into column indexes.
- The data in the data table needs to be converted into an “array of plain objects” form, so that then it can be directly consumed by D3.
- this.domContainer gives you access to the DIV where rendering should take place.
Now, you’ll make a small detour to create that new __buildScenes
method.
Method __buildScenes
Add a property __buildScenes
, after _updateAll
, and give it the following code:
// view-d3.js
// __buildScenes:
function(dataTable, categoryColumn, measureColumn) {
var scenes = [];
for(var i = 0, R = dataTable.getNumberOfRows(); i < R; i++) {
scenes.push({
category: dataTable.getValue(i, categoryColumn),
categoryLabel: dataTable.getFormattedValue(i, categoryColumn),
measure: dataTable.getValue(i, measureColumn),
rowIndex: i
});
}
return scenes;
}
Remarks:
- Note the data table methods which can be used to traverse it: getNumberOfRows, getValue and getFormattedValue.
- In the X axis, you’ll be displaying the value of
categoryLabel
, but the value ofcategory
(and ofrowIndex
) will be useful, later, for adding interactivity to the visualization.
Method _updateAll
, part 2
Having prepared the data for rendering, you’ll now add the adapted D3 code:
// view-d3.js
// _updateAll:
function() {
// Part 1
// ...
// Part 2
container.selectAll("*").remove();
var margin = {top: 50, right: 20, bottom: 30, left: 75};
var width = this.width - margin.left - margin.right;
var height = this.height - margin.top - margin.bottom;
var x = d3.scaleBand().rangeRound([0, width]).padding(0.1);
var y = d3.scaleLinear().rangeRound([height, 0]);
x.domain(scenes.map(function(d) { return d.categoryLabel; }));
y.domain([0, d3.max(scenes, function(d) { return d.measure; })]);
var svg = container.append("svg")
.attr("width", this.width)
.attr("height", this.height);
// Title
var title = dataTable.getColumnLabel(measureColumn) +
" per " +
dataTable.getColumnLabel(categoryColumn);
svg.append("text")
.attr("class", "title")
.attr("y", margin.top / 2)
.attr("x", this.width / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(title);
// Content
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// X axis
g.append("g")
.attr("class", "axis axis-x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Y axis
g.append("g")
.attr("class", "axis axis-y")
.call(d3.axisLeft(y).ticks(10));
// Bars
var bandWidth = x.bandwidth();
var barWidth = Math.min(model.barSize, bandWidth);
var barOffset = bandWidth / 2 - barWidth / 2 + 0.5;
var selectColor = function(d) {
return model.palette.colors.at(d.rowIndex % model.palette.colors.count).value;
};
var bar = g.selectAll(".bar")
.data(scenes)
.enter().append("rect")
.attr("class", "bar")
.attr("fill", selectColor)
.attr("stroke", selectColor)
.attr("x", function(d) { return x(d.categoryLabel) + barOffset; })
.attr("y", function(d) { return y(d.measure); })
.attr("width", barWidth)
.attr("height", function(d) { return height - y(d.measure); });
}
Remarks:
- The view dimensions are available through this.width and this.height.
- The chart title is build with the labels of the mapped attributes, by calling getColumnLabel.
- The Bar model’s
barSize
property is being used to limit the width of bars. - The rowIndex is being used to cycle through and select the bar color from the
palette
property.
Now, refresh the sandbox.html
page in the browser, and you should finally see a Bar chart!
Continue to Styling the view.