Reputation: 4828
I have a custom control that contains a scatter plot chart. When a button is pressed, it opens a modal/dialog control that displays the chart (see image below). The only problem is that the chart is displayed twice! Upon opening the dev tools, it appears that all of the lifecycles are rendered twice. I have no idea why this happens but I need it to render only once.
Here is my main controller:
main.controller.js
jQuery.sap.registerModulePath("vizConcept.ScatterPlot", "controls/ScatterPlot");
jQuery.sap.require("vizConcept.ScatterPlot");
jQuery.sap.registerModulePath("vizConcept.ScatterPlotItem", "controls/ScatterPlot");
jQuery.sap.require("vizConcept.ScatterPlotItem");
sap.ui.define([
'jquery.sap.global',
'vizConcept/controller/BaseController',
'sap/ui/model/json/JSONModel',
'vizConcept/model/viewControls',
'sap/m/Button',
'sap/m/Dialog',
],
function (jQuery, BaseController, JSONModel, viewControls, Button, Dialog) {
"use strict";
var controls;
var mainController = BaseController.extend("vizConcept.controller.Main", {
onInit: function(oEvent) {
// Access/expose the defined model(s) configured in the Component.js or Manifest.json within the controller.
this.getView().setModel(this.getOwnerComponent().getModel("products"), "products");
var oModel = this.getView().getModel("products");
this.getView().setModel(oModel);
//
console.log('onInit');
var sUrl = "#" + this.getOwnerComponent().getRouter().getURL("page2");
//IIFE renders our chart, will need to move this in a modular way
},
onAfterRendering: function () {
this._rebindAll();
},
_rebindAll : function() {
var oModel = new sap.ui.model.json.JSONModel({
"buckets": [
{
"quarter": "Q2 2013",
"values": [
{ "name": "Segment A",
"value": "3228",
"value2": "12"
},
{
"name": "Segment B",
"value": "11752",
"value2": "37"
},
{
"name": "Segment C",
"value": "492",
"value2": "3"
},
{
"name": "Segment D",
"value": "654",
"value2": "6"
},
{
"name": "Segment E",
"value": "39165",
"value2": "167"
},
{
"name": "Segment F",
"value": "4745",
"value2": "9"
}
]
},
{
"quarter": "Q3 2013",
"values": [
{ "name": "Segment A",
"value": "6806",
"value2": "24"
},
{
"name": "Segment B",
"value": "11372",
"value2": "51"
},
{
"name": "Segment C",
"value": "2306",
"value2": "10"
},
{
"name": "Segment D",
"value": "1492",
"value2": "7"
},
{
"name": "Segment E",
"value": "33944",
"value2": "170"
},
{
"name": "Segment F",
"value": "6498",
"value2": "16"
}
]
},
{
"quarter": "Q4 2013",
"values": [
{ "name": "Segment A",
"value": "11228",
"value2": "23"
},
{
"name": "Segment B",
"value": "31324",
"value2": "83"
},
{
"name": "Segment C",
"value": "1291",
"value2": "17"
},
{
"name": "Segment D",
"value": "4350",
"value2": "17"
},
{
"name": "Segment E",
"value": "52265",
"value2": "155"
},
{
"name": "Segment F",
"value": "23785",
"value2": "66"
}
]
},
{
"quarter": "Q1 2014",
"values": [
{ "name": "Segment A",
"value": "3708",
"value2": "18"
},
{
"name": "Segment B",
"value": "20176",
"value2": "74"
},
{
"name": "Segment C",
"value": "5169",
"value2": "61"
},
{
"name": "Segment D",
"value": "31322",
"value2": "76"
},
{
"name": "Segment E",
"value": "49069",
"value2": "191"
},
{
"name": "Segment F",
"value": "8928",
"value2": "19"
}
]
},
{
"quarter": "Q2 2014",
"values": [
{ "name": "Segment A",
"value": "2950",
"value2": "26"
},
{
"name": "Segment B",
"value": "6807",
"value2": "54"
},
{
"name": "Segment C",
"value": "3789",
"value2": "110"
},
{
"name": "Segment D",
"value": "12867",
"value2": "91"
},
{
"name": "Segment E",
"value": "21411",
"value2": "128"
},
{
"name": "Segment F",
"value": "18478",
"value2": "21"
}
]
}
]
});
var oScatterPlotHolder = this.byId("RegionScatterPlotHolder");
var oScatterPlotItem = new vizConcept.ScatterPlotItem({quarter:"{quarter}", values:"{values}"});
/* new chart */
var oScatterPlot = new vizConcept.ScatterPlot({
items: {path : "/buckets", template : oScatterPlotItem}
});
//var oModel = sap.ui.getCore().getModel("growth-regions-scatter");
oScatterPlot.setModel(oModel);
oScatterPlotHolder.addItem(oScatterPlot);
$(function() {
var dlg = new sap.m.Dialog({
id: 'vizModal',
title: 'Scatter Plot Example Viz',
width : "1800px",
height : "600px",
content : [oScatterPlotHolder],
beginButton: new Button({
text: 'Close',
press: function () {
dlg.close();
}
})
});
(new sap.m.Button({
text: 'open',
type: 'Accept',
press: function() {
dlg.open();
oScatterPlotHolder.invalidate();
}
})).placeAt('content');
});
},
onToPage2 : function () {
this.getOwnerComponent().getRouter().navTo("page2");
},
});
return mainController;
});
ScatterPlot.js (custom control)
jQuery.sap.require("sap/ui/thirdparty/d3");
jQuery.sap.declare("vizConcept.ScatterPlot");
sap.ui.core.Element.extend("vizConcept.ScatterPlotItem", { metadata : {
properties : {
"quarter" : {type : "string", group : "Misc", defaultValue : null},
"values" : {type : "object", group : "Misc", defaultValue : null}
}
}});
sap.ui.core.Control.extend("vizConcept.ScatterPlot", {
metadata : {
properties: {
"title": {type : "string", group : "Misc", defaultValue : "ScatterPlot Title"}
},
aggregations : {
"items" : { type: "vizConcept.ScatterPlotItem", multiple : true, singularName : "item"}
},
defaultAggregation : "items",
events: {
"onPress" : {},
"onChange":{}
}
},
init : function() {
console.log("vizConcept.ScatterPlot.init()");
this.sParentId = "";
},
createScatterPlot : function() {
/*
* Called from renderer
*/
console.log("vizConcept.ScatterPlot.createScatterPlot()");
var oScatterPlotLayout = new sap.m.VBox({alignItems:sap.m.FlexAlignItems.Center,justifyContent:sap.m.FlexJustifyContent.Center});
var oScatterPlotFlexBox = new sap.m.FlexBox({height:"auto",alignItems:sap.m.FlexAlignItems.Center});
/* ATTENTION: Important
* This is where the magic happens: we need a handle for our SVG to attach to. We can get this using .getIdForLabel()
* Check this in the 'Elements' section of the Chrome Devtools:
* By creating the layout and the Flexbox, we create elements specific for this control, and SAPUI5 takes care of
* ID naming. With this ID, we can append an SVG tag inside the FlexBox
*/
this.sParentId=oScatterPlotFlexBox.getIdForLabel();
oScatterPlotLayout.addItem(oScatterPlotFlexBox);
return oScatterPlotLayout;
},
/**
* The renderer render calls all the functions which are necessary to create the control,
* then it call the renderer of the vertical layout
* @param oRm {RenderManager}
* @param oControl {Control}
*/
renderer : function(oRm, oControl) {
var layout = oControl.createScatterPlot();
oRm.write("<div");
oRm.writeControlData(layout); // writes the Control ID and enables event handling - important!
oRm.writeClasses(); // there is no class to write, but this enables
// support for ColorBoxContainer.addStyleClass(...)
oRm.write(">");
oRm.renderControl(layout);
oRm.addClass('verticalAlignment');
oRm.write("</div>");
},
onAfterRendering: function(){
console.log("vizConcept.ScatterPlot.onAfterRendering()");
//console.log(this.sParentId);
var cItems = this.getItems();
var data = [];
for (var i=0;i<cItems.length;i++){
var oEntry = {};
for (var j in cItems[i].mProperties) {
oEntry[j]=cItems[i].mProperties[j];
}
data.push(oEntry);
}
//console.log("Data:");
//console.log(data);
/*
* ATTENTION: See .createScatterPlot()
* Here we're picking up a handle to the "parent" FlexBox with the ID we got in .createScatterPlot()
* Now simply .append SVG elements as desired
* EVERYTHING BELOW THIS IS PURE D3.js
*/
var margin = {
top: 15,
right: 15,
bottom: 30,
left: 40
},
width = 600 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
// Our X scale
var x = d3.scale.linear()
.range([0, width-80]);
// Our Y scale
var y = d3.scale.linear()
.range([height, 0]);
// Our color bands
var color = d3.scale.ordinal()
.range(["#004460", "#0070A0", "#008BC6", "#009DE0", "#45B5E5", "8CCDE9", "#DAEBF2"]); //"#00A6ED",
// Use our X scale to set a bottom axis
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
// Smae for our left axis
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickFormat(d3.format(".2s"));
var tip = d3.select("body").append("div")
.attr("class", "sctooltip")
.style("position", "absolute")
.style("text-align", "center")
.style("width", "80px")
.style("height", "42px")
.style("padding", "2px")
.style("font", "11px sans-serif")
.style("background", "#F0F0FF")
.style("border", "0px")
.style("border-radius", "8px")
.style("pointer-events", "none")
.style("opacity", 0);
var vis = d3.select("#" + this.sParentId);
var svg = vis.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.style("background-color","white")
.style("font", "12px sans-serif")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
x.domain([0, d3.max(data, function (d) {
var max = d3.max(d.values, function (dd){
return(+dd.value);
})
return max;
})]);
// Our Y domain is from zero to our highest total
y.domain([0, d3.max(data, function (d) {
var max = d3.max(d.values, function (dd){
return(+dd.value2);
})
return max;
})]);
var totalval = 0;
var totalval2 = 0;
data.forEach(function (d) {
var quarter = d.quarter;
d.values.forEach(function (dd){
dd.quarter = quarter;
totalval += +dd.value;
totalval2 += +dd.value2;
});
});
var average = totalval/totalval2;
var line_data = [{"x": 0, "y": 0},{"x": (y.domain()[1]*average), "y": y.domain()[1]}];
var avgline = d3.svg.line()
.x(function(d){ return x(d.x); })
.y(function(d){ return y(d.y); })
.interpolate("linear");
svg.append("g")
.attr("class", "x axis")
.style("fill", "none")
.style("stroke", "grey")
.style("shape-rendering", "crispEdges")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y-axis")
.style("fill", "none")
.style("stroke", "grey")
.style("shape-rendering", "crispEdges")
.call(yAxis);
//average line
svg.append("path")
.attr("class", "avgline")
.style("stroke", "#000")
.style("stroke-width", "1px")
.style("stroke-dasharray", ("4, 4"))
.attr("d", avgline(line_data));
/*
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("XXXXX");
*/
var plot = svg.selectAll(".quarter")
.data(data)
.enter().append("g");
plot.selectAll("dot")
.data(function (d) {
return d.values;
})
.enter().append("circle")
.attr("class", "dot")
.attr("r", 5)
.attr("cx", function (d){
return x(d.value);
})
.attr("cy", function (d) {
return y(d.value2);
})
.style("stroke", "#004460")
.style("fill", function (d) {
return color(d.name);
})
.style("opacity", .9)
.style("visibility", function(d){
if(+d.value != 0){
return "visible";
}else{
return "hidden";
}
})
.style("pointer-events", "visible")
.on("mouseover", function(d){
tip.transition()
.duration(200)
.style("opacity", .8);
tip.html(d.name + "<br/>" + d.quarter + "<br />" + "Avg. " +(+d.value/+d.value2).toFixed(2))
.style("left", (d3.event.pageX-40) + "px")
.style("top", (d3.event.pageY-50) + "px");
})
.on("mouseout", function(d){
tip.transition()
.duration(500)
.style("opacity", 0);
});;
var legend = svg.selectAll(".legend")
.data(color.domain())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function (d, i) {
return "translate(0," + i * 16 + ")";
});
legend.append("rect")
.attr("x", width - 12)
.attr("width", 12)
.attr("height", 12)
.style("fill", color);
legend.append("text")
.attr("x", width - 24)
.attr("y", 6)
.attr("dy", ".35em")
.style("text-anchor", "end")
.style("font", "11px sans-serif")
.text(function (d) {
return d;
});
var avglabel = svg.append("g")
.attr("transform", "translate(" + (width-40) + ",140)");
avglabel.append("text")
.style("text-anchor", "middle")
.text("Average: " + average.toFixed(2));
}
});
Here is an example of what is the outcome of the double rendering
Edit 1 (Here is my element dom tree)
Upvotes: 0
Views: 3156
Reputation: 361
I have noticed depending on lifecycle events around rendering can be tricky, I think it's up to UI5 to determine when these run. If you only want to run once, then you can use onRoutePatternMatched()
or onBeforeShow()
, these are more consistent for me at least.
If you don't want to do this, you can use a global boolean and set it in onAfterRendering()
, that way next time it runs it won't pass the test. Something like this:
ranOnce : false,
onAfterRendering : function(){
if(ranOnce){ break; }
// do codez
ranOnce = true;
}
Upvotes: 1