Problem with spacetree panning the canvas too far (off the screen) when collapsing nodes

591 views
Skip to first unread message

Edward Groenendaal

unread,
Mar 13, 2012, 12:01:41 AM3/13/12
to javascript-information...@googlegroups.com
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.

Edward Groenendaal

unread,
Mar 13, 2012, 12:06:02 AM3/13/12
to javascript-information...@googlegroups.com

Note that I have tried using just the rectangle node, and I have tried different node sizes and not overriding the label size, made no difference.

Cheers, Ed.
Reply all
Reply to author
Forward
0 new messages