Hi,
I've got a Primefaces/JSF page that is using InfoViz (2.0.1) for visualising a tree. The tree data is dynamically loaded from the backing bean using jQuery/JSON. Everything is working great, except for the fact that there appears to be a compounding error in the InfoVis toolkit which means that the recentering isn't working when nodes are collapsed and expanded.
This can be observed by expanding a number of nodes so that we go a off to the right of the page for a while, over time you can see that the new nodes are starting to drift towards the left of the screen. Then pan back to the first line of nodes from the root and open a new node there. The recentering scrolls all the way past the root node, and off into no-mans land off to the left of the root. It's as if InfoVis is not calculating the width of the node correctly.
Has anyone else had this issue? I've been through the forums and Google and nothing was immediately obvious, although some were similar.
I include the full xhtml page here, since some of it may be of interest to others using Primefaces and AJAX loading of data into InfoVis, it's pretty damn cool to be honest.
This is not production ready - it's a work in progress, you can see that it is based on one of the examples, and including bits and bobs that I have found on these forums and elsewhere.
I've highlighted the InfoVis configuration and related javascript in blue.
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
   xmlns:ui="http://java.sun.com/jsf/facelets"
   xmlns:h="http://java.sun.com/jsf/html"
   xmlns:f="http://java.sun.com/jsf/core"
   xmlns:p="http://primefaces.org/ui"  Â
   xmlns:sec="http://www.springframework.org/security/facelets/tags"
   template="/module/template.xhtml">
   <ui:define name="title">Genealogy Tree</ui:define>
  Â
   <ui:define name="contentTitle">Genealogy Tree</ui:define>
   <ui:define name="topScripts">
   <script type="text/javascript" src="#{site.localContentUrl}/js/jit-sp-2.0.1.js"></script>
   <script type="text/javascript">
  Â
   /* <![CDATA[ */
       function loadRoot() {
           $.getJSON('/miaccount/genealogy/genealogyRoot.mvc',
                   function(data)
                   {
                       var newNode = data.childData;
                       init(newNode);
                   });
       };
       // get the root node of the tree, the rest is dynamic on demand.
       $(document).ready(function(){
           loadRoot();
       });
  Â
       var labelType, useGradients, nativeTextSupport, animate;
       (function() {
         var ua = navigator.userAgent,
             iStuff = ua.match(/iPhone/i) || ua.match(/iPad/i),
             typeOfCanvas = typeof HTMLCanvasElement,
             nativeCanvasSupport = (typeOfCanvas == 'object' || typeOfCanvas == 'function'),
             textSupport = nativeCanvasSupport
               && (typeof document.createElement('canvas').getContext('2d').fillText == 'function');
         //I'm setting this based on the fact that ExCanvas provides text support for IE
         //and that as of today iPhone/iPad current text support is lame
         labelType = (!nativeCanvasSupport || (textSupport && !iStuff))? 'Native' : 'HTML';
         nativeTextSupport = labelType == 'Native';
         useGradients = nativeCanvasSupport;
         animate = !(iStuff || !nativeCanvasSupport);
       })();
       var Log = {
         elem: false,
         write: function(text){
           if (!this.elem)
             this.elem = document.getElementById('log');
           this.elem.innerHTML = text;
           this.elem.style.left = (500 - this.elem.offsetWidth / 2) + 'px';
         }
       };
       $jit.ST.Plot.NodeTypes.implement({
           'roundrect': {
             'render': function(node, canvas, animating) {
                     var pos = node.pos.getc(true), nconfig = this.node,data = node.data;
                     var width = nconfig.width, height = nconfig.height;
                     var algnPos = this.getAlignedPos(pos, width,height);
                     var ctx = canvas.getCtx(), ort = this.config.orientation;
                     ctx.beginPath();
                           var r = 10; //corner radius
                           var x = algnPos.x;
                           var y = algnPos.y;
                           var h = height;
                           var w = width;
                           ctx.moveTo(x + r, y);
                           ctx.lineTo(x + w - r, y);
                           ctx.quadraticCurveTo(x + w, y, x + w, y + r);
                           ctx.lineTo(x + w, y + h - r);
                           ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
                           ctx.lineTo(x + r, y + h);
                           ctx.quadraticCurveTo(x, y + h, x, y + h - r);
                           ctx.lineTo(x, y + r);
                           ctx.quadraticCurveTo(x, y, x + r, y);
                           ctx.fill();
             }
           }
       });
       function init(data){
          var json = data;
           //init Spacetree
           //Create a new ST instance
           var st = new $jit.ST({
               'injectInto': 'infovis',
               //set duration for the animation
               duration: 800,
               //set animation transition type
               transition: $jit.Trans.Quart.easeInOut,
               //set distance between node and its children
               levelDistance: 50,
               //set max levels to show. Useful when used with
               //the request method for requesting trees of specific depth
               levelsToShow: 2,
               backgroundColor: "#ffffff",
               //set node and edge styles
               //set overridable=true for styling individual
               //nodes or edges
               Node: {
                   height: 24,
                   width: 140,
                  //use a custom
                   //node rendering function
                   type: 'roundrect',
                   color:'#dadada', // grey
                   lineWidth: 2,
                   align:"center",
                   overridable: true
               },
               Tips: {
                   enable: true,
                   onShow: function(tip, elem) {
                      tip.innerHTML = "<table style='width:100%'><tr><td colSpan='2'><b>" + elem.name + "</b></td></tr><tr><td><b>GV</b></td><td>" + elem.data.gv + "</td></tr><tr><td><b>PV</b></td><td>" + elem.data.pv + "</td></tr><tr><td><b>Expired</b></td><td>" + elem.data.expired + "</td></tr><tr><td><b>On BP</b></td><td>" + elem.data.onBp + "</td></tr><tr><td><b>Rank</b></td><td>" + elem.data.rank + "</td></tr></table>";
                      //tip.innerHTML += "<tr><td colSpan='2'><b>" + elem.name + "</b></td></tr>";
                      //tip.innerHTML += "<tr><td><b>GV</b></td><td>" + elem.data.gv + "</td></tr>";
                      //tip.innerHTML += "<tr><td><b>PV</b></td><td>" + elem.data.pv + "</td></tr>";
                      //tip.innerHTML += "<tr><td><b>Expired</b></td><td>" + elem.data.expired + "</td></tr>";
                      //tip.innerHTML += "<tr><td><b>On BP</b></td><td>" + elem.data.onBp + "</td></tr>";
                      //tip.innerHTML += "<tr><td><b>Rank</b></td><td>" + elem.data.rank + "</td></tr>";
                      //tip.innerHTML += "</table>";
                   }
               },
               Edge: {
                   type: 'bezier',
                   lineWidth: 2,
                   color:'#000000', // black
                   overridable: true
               },
               Navigation: {
                  enable: true,
                  zooming: false,
                  panning: true
               },
               Events: {
                  enable: true,
                  onClick: function(elem){
                     document.getElementById('selectedNodeId').value = elem.id;
                     updateCustomer();
                  }
               },
              Â
               //Add a request method for requesting on-demand json trees.
               //This method gets called when a node
               //is clicked and its subtree has a smaller depth
               //than the one specified by the levelsToShow parameter.
               //In that case a subtree is requested and is added to the dataset.
               //This method is asynchronous, so you can make an Ajax request for that
               //subtree and then handle it to the onComplete callback.
               request: function(nodeId, level, onComplete) {
                  $.getJSON('/miaccount/genealogy/' + nodeId + '/genealogyFrontrow.mvc' ,
                         function(data)
                         {
                               var newNode = data.childData;
                               var ans = {"id" : nodeId, 'children' : newNode};
                               onComplete.onComplete(nodeId, ans);
                         });
               },
              Â
               onBeforeCompute: function(node){
                   Log.write("loading " + node.name);
               },
              Â
               onAfterCompute: function(){
                   Log.write("done");
               },
              Â
               //This method is called on DOM label creation.
               //Use this method to add event handlers and styles to
               //your node.
               onCreateLabel: function(label, node){
                   label.id = node.id;
                   if (node.data.type == "REP" || node.data.type == "RS" || node.data.type == "RT") {
                       if (node.data.expired.toString() == "true") {
                           label.innerHTML = "<span><img style='vertical-align:middle;' width='16' height='16' src='#{site.contentUrl}/shop/images/rank/rank-UR.png'/>";
                       } else {
                           label.innerHTML = "<span><img style='vertical-align:middle;' width='16' height='16' src='#{site.contentUrl}/shop/images/rank/rank-" + node.data.rank + ".png'/>";
                       }
                   } else if (node.data.type == "PC") {
                      label.innerHTML = "<span><img style='vertical-align:middle;' width='16' height='16' src='#{site.contentUrl}/shop/images/type-pc.gif'/>";
                   } else if (node.data.type == "LSC") {
                       label.innerHTML = "<span><img style='vertical-align:middle;' width='16' height='16' src='#{site.contentUrl}/shop/images/type-lifestyle.gif'/>";
                   } else {
                      label.innerHTML = "<span>";
                   }
                  Â
                   if (node.data.onBp.toString() == "true") {
                      label.innerHTML += "<img style='vertical-align:middle;' width='16' height='16' src='#{site.contentUrl}/shop/images/autoship-icon.png' />";
                   }
                   label.innerHTML += "<span> </span>" + node.name + "</span>";
                   label.onclick = function(){
                       st.onClick(node.id);
                   };
                   //set label styles
                   var style = label.style;
                   //style.width = 150 + 'px';
                   style.height = 20 + 'px';
                   style.cursor = 'pointer';
                   //style.color = '#7a7a7a'; // dark grey
                   style.color = '#000000'; // black
                   //style.backgroundColor = '#1a1a1a';
                   style.fontSize = '8pt';
                   style.textAlign= 'left';
                   style.paddingTop = '3px';
                   style.paddingLeft = '10px';
               },
              Â
               //This method is called right before plotting
               //a node. It's useful for changing an individual node
               //style properties before plotting it.
               //The data properties prefixed with a dollar
               //sign will override the global node style properties.
               onBeforePlotNode: function(node){
                   //add some color to the nodes in the path between the
                   //root node and the selected node.
                   if (node.selected) {
                       node.data.$color = "#f8971d"; // orange
                   }
                   else {
                       delete node.data.$color;
                   }
                  Â
                   if (parseInt(node.data.childCount) == 0) {
                      node.setData('type', "rectangle");
                   }
                   if (node.data.sponsored.toString() == "true") {
                      node.data.$color = "#cac5ea"; // slight purple
                   }
               },
              Â
               //This method is called right before plotting
               //an edge. It's useful for changing an individual edge
               //style properties before plotting it.
               //Edge data proprties prefixed with a dollar sign will
               //override the Edge global style properties.
               onBeforePlotLine: function(adj){
                   if (adj.nodeFrom.selected && adj.nodeTo.selected) {
                       adj.data.$color = "#f8971d"; // green
                       adj.data.$lineWidth = 3;
                   }
                   else {
                       delete adj.data.$color;
                       delete adj.data.$lineWidth;
                   }
               }
              Â
           });
           //load the inital json data
           st.loadJSON(json);
           //compute node positions and layout
           st.compute();
           //emulate a click on the root node.
           st.onClick(st.root);
           //end
           //Add event handlers to switch spacetree orientation.
          function get(id) {
             return document.getElementById(id);Â
           };
           var top = get('r-top'),
           left = get('r-left'),
           bottom = get('r-bottom'),
           right = get('r-right');
          Â
           function changeHandler() {
               if(this.checked) {
                   top.disabled = bottom.disabled = right.disabled = left.disabled = true;
                   st.switchPosition(this.value, "animate", {
                       onComplete: function(){
                           top.disabled = bottom.disabled = right.disabled = left.disabled = false;
                       }
                   });
               }
           };
          Â
           top.onchange = left.onchange = bottom.onchange = right.onchange = changeHandler;
           //end
       }
       /* ]]> */
       </script>
   </ui:define>
  Â
   <ui:define name="submenu">
       <div class="submenu">
       <ul>
         <li><a href="/miaccount/genealogy/genealogy.jsf">Genealogy Tools Overview</a></li>
         <li><a href="/miaccount/genealogy/genealogyTableTree.jsf">Genealogy Table Tree</a></li>
         <li><a href="/miaccount/genealogy/genealogyTable.jsf" >Genealogy Table</a></li>
         <li><a href="/miaccount/genealogy/genealogyTree.jsf" >Genealogy Tree</a></li>
         <li><a href="/miaccount/genealogy/genealogyAnimatedTree.jsf" style="color:#333333;">Genealogy Animated Tree</a></li>
         <li class="last"><a href="/miaccount/genealogy/genealogyDownload.jsf" >Genealogy Download</a></li>
       </ul>
   </div>
   </ui:define>
   <ui:define name="contentForm">
          <h:panelGroup id="genPage" layout="block" >
               <h:form id="genForm" prependId="false">
                <p:fieldset styleClass="og-panel">
                   <h:panelGroup id="genPageMain" layout="block" rendered="#{genealogyController.available}">
                        <h:panelGroup id="placementsPanel" layout="block" styleClass="right placementText">
                                   <ui:include src="includes/genealogyPlacements.xhtml"/>
                        </h:panelGroup>
                    <p:fieldset id="genealogyTree" styleClass="og-panel" legend="#{msg['hdr_genRtTree']}" toggleable="false">
                               <table style="width:100%;">
                               <tr>
                               <td style="width:100%;vertical-align:top;">
                                   <p:fieldset>
                                   <div style="height:630px">
                                       <div id="infovis"></div>
                                   </div>
                                   </p:fieldset>
                               </td>
                               <td style="vertical-align:top;">
                               <!-- To refresh the Infovis tree you delete the interior divs, and then reinitialise the tree
                                     Only do this when the ajax change is complete and we have the new tree available. -->
                               <p:selectOneMenu styleClass="animatedTreeType" value="#{genealogyController.treeTypeId}">
                                 <p:ajax update="customerDetails" oncomplete="$('#infovis-canvaswidget').remove();
   $('#infovis-label').remove(); loadRoot();"/>
                                 <f:selectItems value="#{genealogyController.treeTypes}"/>
                               </p:selectOneMenu>
                              Â
                               <p:dataTable id="treeLegend" var="rank" value="#{selectItemOptions.ranks}">
                                       <p:column id="iconHeader">
                                           <f:facet name="header">Â
                                                   iconÂ
                                           </f:facet>
                                           <div style="text-align: center;">
                                           <p:graphicImage width="20" height="20" value="#{site.contentUrl}/shop/images/rank/rank-#{rank.value}.png" />
                                           </div>
                                       </p:column>Â
                                       <p:column id="meaningHeader">
                                           <f:facet name="header">Â
                                                   meaningÂ
                                           </f:facet>Â
                                           <h:outputText value="#{rank.label}" />Â
                                       </p:column>
                                       <p:columnGroup type="footer">
                                           <p:row>
                                               <p:column><f:facet name="footer"><p:graphicImage width="20" height="20" value="#{site.contentUrl}/shop/images/rank/rank-UR.png" /></f:facet></p:column>
                                               <p:column style="text-align:left" footerText="Inactive or Expired"/>
                                           </p:row>
                                           <p:row>
                                               <p:column><f:facet name="footer"><p:graphicImage width="20" height="20" value="#{site.contentUrl}/shop/images/type-pc.gif" /></f:facet></p:column>
                                               <p:column style="text-align:left" footerText="Customer"/>
                                           </p:row>
                                           <p:row>
                                               <p:column><f:facet name="footer"><p:graphicImage width="20" height="20" value="#{site.contentUrl}/shop/images/type-lifestyle.gif" /></f:facet></p:column>
                                               <p:column style="text-align:left" footerText="Lifestyle Member"/>
                                           </p:row>
                                           <p:row>
                                               <p:column><f:facet name="footer"><p:graphicImage width="28" height="28" value="#{site.contentUrl}/shop/images/autoship-icon.png" /></f:facet></p:column>
                                               <p:column style="text-align:left" footerText="Bonus Protection"/>
                                           </p:row>
                                       </p:columnGroup>
                                       </p:dataTable>
                                   <br/>
                                       <p:outputPanel id="customerDetails">
                                       <p:fieldset rendered="#{genealogyController.selectedEntry ne null}">
                                       <table>
                                           <tr>
                                               <td><h:outputLabel style="min-width:55px;" value="user"/></td>
                                               <td style="vertical-align:top;"><h:outputText value="#{genealogyController.selectedEntry.userName}"/></td>
                                           </tr>
                                           <tr>
                                               <td><h:outputLabel style="min-width:55px;" value="name"/></td>
                                               <td style="vertical-align:top;"><h:outputText value="#{genealogyController.selectedEntry.displayName}"/></td>
                                           </tr>
                                           <tr>
                                               <td><h:outputLabel style="min-width:55px;" value="sponsor"/></td>
                                               <td style="vertical-align:top;"><h:outputText value="#{genealogyController.selectedSponsorUserName}"/></td>
                                           </tr>
                                            <tr>
                                               <td><h:outputLabel style="min-width:55px;" value="gv"/></td>
                                               <td style="vertical-align:top;"><h:outputText value="#{genealogyController.selectedEntry.gvMonthCurrent}"/></td>
                                           </tr>
                                           <tr>
                                               <td><h:outputLabel style="min-width:55px;" value="pv"/></td>
                                               <td style="vertical-align:top;"><h:outputText value="#{genealogyController.selectedEntry.pvMonthCurrent}"/></td>
                                           </tr>
                                           <tr>
                                               <td colspan="2"><p:commandButton oncomplete="contactDialog.show()" icon="ui-icon ui-icon-search" value="More Details" title="View" update=":contactForm" process="@this" >Â
                                                   <f:setPropertyActionListener value="#{genealogyController.selectedEntry.userId}" target="#{genealogy.viewCustomerId}" />Â
                                               </p:commandButton></td>
                                           </tr>
                                       </table>
                                   </p:fieldset>
                                   </p:outputPanel>
                                   </td>
                                   </tr>
                                   </table>
                                   <p:toolbar id="bottomEmailToolbar">
                                       <p:toolbarGroup align="left">
                                           <label style="min-width: 30px;" for="r-left">left</label>
                                           <input type="radio" id="r-left" name="orientation" checked="checked" value="left" />
                                           <label style="min-width: 30px;" for="r-top">top</label>
                                           <input type="radio" id="r-top" name="orientation" value="top" />
                                           <label style="min-width: 30px;" for="r-bottom">bottom</label>
                                           <input type="radio" id="r-bottom" name="orientation" value="bottom" />
                                           <label style="min-width: 30px;" for="r-right">right</label>
                                           <input type="radio" id="r-right" name="orientation" value="right" />
                                       </p:toolbarGroup>
                                       <p:toolbarGroup align="right">
                                           <div id="log"></div>
                                       </p:toolbarGroup>
                                   </p:toolbar>
                           <div id="log"></div>
                       </p:fieldset>
                       <!-- Tricky way of passing a paramter from JS to JSF, and doing an update from JS -->
                       <h:inputHidden id="selectedNodeId" value="#{genealogyController.selectedNodeId}"/>
                       <p:remoteCommand name="updateCustomer" update="customerDetails" process="selectedNodeId"/>
                  </h:panelGroup>
            </p:fieldset>
          </h:form>
      </h:panelGroup>
      <p:dialog id="contactOrderDialog" header="#{msg['hdr_contact']}" widgetVar="contactDialog" resizable="false" closable="true" width="800" position="top">
           <h:form id="contactForm" prependId="false" >
               <p:outputPanel rendered="#{not empty genealogy.viewCustomerId}">
                   <ui:include src="includes/genealogyContactDetails.xhtml" />
               </p:outputPanel>
           </h:form>
       </p:dialog>
   </ui:define>
</ui:composition>
Cheers, Ed.