Skip to main content

Data Tree

Comments

3 comments

  • Official comment
    Glyn McKenna

    Hi Scott,

    Here are a couple of links to the old element reference which may assist

    It doesn't give you a step-by-step "how-to" guide but it will help with understanding the various element attributes. I have also created a simple example for you to follow in the report definition XML below.  You can copy this into an empty lgx file under your reports folder, or alternatively, create a new report definition in Logi Studio, go to the "Source" tab delete the existing few lines and then copy and paste the entire XML snippet from below into the Source view instead. Go back to the "Definition" tab and you can follow it through in the visual element view.

    I hope that this helps

    <Report ID="DataTreeExample" SavedBy="LOGIXML\gmcKenna" SavedAt="2021-10-20 11:12:01" EngineVersion="12.8.675-SP1">
    <StyleSheet ID="ssBootstrap-431" StyleSheet="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" Theme="Silver" />
    <Body>
    <Division ID="divContainer" HtmlDiv="True" Class="container-fluid">
    <Division ID="divDataTree" HtmlDiv="True" Class="card m-4">
    <Label Class="card-header" ID="lblCardTitle" Caption="Hierarchical Data Tree Example" HtmlTag="h3" />
    <Division ID="divCardBody" HtmlDiv="True" Class="card-body">
    <DataTree ID="dtProducts">
    <DataTreeBranch Expanded="False" ID="dtbProductGroups" Value="Product group">
    <Label Caption="@Data.product_group~" ID="lblProductGroup" Tooltip="@Data.product_group_desc~" />
    <DataTreeBranch Expanded="False" ID="dtpProductSubgroups" Value="Product subgroup">
    <Label Caption="@Data.product_subgroup~" ID="lblProductSubgroup" />
    <DataTreeBranch ID="dtbProducts">
    <Label Caption="@Data.product~" Tooltip="@Data.product_details~" />
    </DataTreeBranch>
    </DataTreeBranch>
    </DataTreeBranch>
    <DataLayer Type="Static" ID="dlProductGroups">
    <StaticDataRow pgid="1" product_group="Clothing" product_group_desc="Women's, Men's and Children's clothing" />
    <StaticDataRow pgid="2" product_group="Homewares" product_group_desc="All things for the home" />
    <StaticDataRow pgid="3" product_group="Tools" product_group_desc="Power tools and hand tools for trade and DIY" />
    <Note Note="You do not have to use Logi Info join elements if you want to join your data in the query or the view" />
    <Join ID="jnPgPsg" JoinType="LeftOuterJoin">
    <MatchCondition LeftDataColumn="pgid" RightDataColumn="pgid" ID="mcPgId" DataType="Number" />
    <DataLayer Type="Static" ID="dlProductSubgroups" IdeDisplayStatus="Collapsed">
    <StaticDataRow pgid="1" product_subgroup="Ladies wear" psgid="1" />
    <StaticDataRow pgid="1" product_subgroup="Mens wear" psgid="2" />
    <StaticDataRow pgid="1" product_subgroup="Childrens clothing" psgid="3" />
    <StaticDataRow pgid="2" product_subgroup="Kitchen" psgid="4" />
    <StaticDataRow pgid="2" product_subgroup="Bathroom" psgid="5" />
    <StaticDataRow pgid="2" product_subgroup="Dining" psgid="6" />
    <StaticDataRow pgid="3" product_subgroup="Electrical" psgid="7" />
    <StaticDataRow pgid="3" product_subgroup="Plumbing" psgid="8" />
    <StaticDataRow pgid="3" product_subgroup="Construction" psgid="9" />
    <Join ID="jnPsgProds" JoinType="LeftOuterJoin">
    <MatchCondition ID="mcPsgId" LeftDataColumn="psgid" RightDataColumn="psgid" DataType="Number" />
    <DataLayer Type="Static" ID="dlProducts">
    <StaticDataRow prodid="1" psgid="1" product="Red dress" product_details="Red evening dress" />
    <StaticDataRow prodid="2" psgid="1" product="Black dress" product_details="Black cocktail dress" />
    <StaticDataRow prodid="3" psgid="2" product="Black trousers" product_details="Black formal trouser" />
    <StaticDataRow prodid="4" psgid="2" product="Shoes" product_details="Black oxford shoe" />
    <StaticDataRow prodid="5" psgid="3" product="Hat" product_details="Bamboo beanie hat" />
    <StaticDataRow prodid="6" psgid="3" product="Gloves" product_details="Winter woolen gloves" />
    <StaticDataRow prodid="7" psgid="4" product="Kitchen knife" product_details="14 inch chef's knife" />
    <StaticDataRow prodid="8" psgid="4" product="Oven" product_details="Built-in electric oven and grill" />
    <StaticDataRow prodid="9" psgid="5" product="Towel" product_details="Large egyption cotton bath sheet" />
    <StaticDataRow prodid="10" psgid="5" product="Shower curtain" product_details="Plain white shower curtain" />
    <StaticDataRow prodid="11" psgid="6" product="Cutlery" product_details="Villeroy and boch stainless steel 68 pcs cutlery set" />
    <StaticDataRow prodid="12" psgid="6" product="Tableware" product_details="Royal Doulton 16 piece white dinner set" />
    <StaticDataRow prodid="13" psgid="7" product="Multi-meter" product_details="Fluke 117 handheld digital multimeter 10a 600V" />
    <StaticDataRow prodid="14" psgid="7" product="Wire stripper" product_details="Automatic electrical wire strippers" />
    <StaticDataRow prodid="15" psgid="8" product="Tap wrench" product_details="Adjustable tap wrench" />
    <StaticDataRow prodid="16" psgid="8" product="PTFE tape" product_details="12M x 12mm white PTFE tape" />
    <StaticDataRow prodid="17" psgid="9" product="Club hammer" product_details="2.5 pound club hammer" />
    <StaticDataRow prodid="18" psgid="9" product="Saw" product_details="580mm handheld precision mitre saw" />
    </DataLayer>
    </Join>
    </DataLayer>
    </Join>
    <GroupFilter GroupColumn="product_group" DataType="Text" Hierarchical="True" ID="gfPgHierarchy" KeepGroupedRows="True">
    <Note Note="You will need to nest as many group filters as you have grouping/dimension cols in the data hierarchy">
    <Note Note="!Important: remember to set the Hierarchical and Keep Grouped Rows attributes to True on all group filters" />
    </Note>
    <GroupFilter ID="gfPgHierarchy" GroupColumn="product_subgroup" DataType="Text" Hierarchical="True" KeepGroupedRows="True" />
    </GroupFilter>
    </DataLayer>
    </DataTree>
    </Division>
    </Division>
    </Division>
    </Body>
    <ideTestParams />
    </Report>
  • Johnny Stevens

    Thanks for the awesome sample, Glyn McKenna!

    I'll add another idea which Logi helped us implement into our solution for a hierarchical data tree using the FancyTree.js library. There is a lot of customization to this, but the heart of the solution is to gather the data into json and then call the FancyTree.js script to display the data with the plus/minus and selection features that we needed.

    We have 3 JsonData elements so allow for lazy loading - one for the top-level results, another for children, and another with CTE to recurse through all of the levels of the hierarchy. The JsonData queries include columns for 'isSelected', 'isLazy', and 'isFolder' which are used to help in display/loading of the results.

    Again, bear in mind that this is highly customized. Here is a sample of the js in use in a DIV housed in a a sharedElement in our application:

    <Division Class="tree-wrapper" HtmlDiv="True" ID="ServiceFilter">
    <InputHidden DefaultValue="@Request.inpServices~" ID="inpServices" />
    <InputHidden DefaultValue="@Request.inpServicesNames~" ID="inpServicesNames" />
    <IncludeScript ID="InitTree" IncludedScript="document.addEventListener(&quot;DOMContentLoaded&quot;, function() {&#xD;&#xA; InitFancyTree('ServiceFilter','inpServices');&#xD;&#xA; $(&quot;input[name=search]&quot;).focus();&#xD;&#xA; $(&quot;#btnResetSearch&quot;).hide();&#xD;&#xA;});&#xD;&#xA;&#xD;&#xA;const const_rdData = &quot;FancyTreeVer&quot;;&#xD;&#xA;const const_rdDataID = &quot;jsonTreeData&quot;;&#xD;&#xA;&#xD;&#xA;&#xD;&#xA;var RefreshInterval = null,&#xD;&#xA;InitFancyTree = function(TreeTarget, InputTarget) {&#xD;&#xA; document.getElementById(TreeTarget).appendChild(BuildTreeTable(2));&#xD;&#xA; $(&quot;#TblTree&quot;).fancytree({&#xD;&#xA; extensions: [&quot;filter&quot;,&quot;glyph&quot;,&quot;table&quot;],&#xD;&#xA; checkbox: false,&#xD;&#xA; selectMode: 2,&#xD;&#xA; filter: {&#xD;&#xA; autoApply: true, // Re-apply last filter if lazy data is loaded&#xD;&#xA; autoExpand: true, // Expand all branches that contain matches while filtered&#xD;&#xA; counter: false, // Show a badge with number of matching child nodes near parent icons&#xD;&#xA; fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar'&#xD;&#xA; hideExpandedCounter: true, // Hide counter badge if parent is expanded&#xD;&#xA; hideExpanders: true, // Hide expanders if all child nodes are hidden by filter. Use with &quot;mode&quot; flag.&#xD;&#xA; highlight: true, // Highlight matches by wrapping inside &lt;mark&gt; tags&#xD;&#xA; leavesOnly: false, // Match end nodes only&#xD;&#xA; nodata: true, // Display a 'no data' status node if result is empty&#xD;&#xA; mode: &quot;hide&quot; // &quot;dimm&quot; or &quot;hide&quot;. Grayout unmatched nodes (pass &quot;hide&quot; to remove unmatched node instead). Use with &quot;hideExpanders&quot; flag.&#xD;&#xA; },&#xD;&#xA; glyph: {&#xD;&#xA; preset: &quot;bootstrap3&quot;,&#xD;&#xA; map: {&#xD;&#xA; doc: &quot;fa fa-file-o&quot;,&#xD;&#xA; docOpen: &quot;fa fa-file-o&quot;,&#xD;&#xA; checkbox: &quot;fa fa-square-o&quot;,&#xD;&#xA; checkboxSelected: &quot;fa fa-check-square-o&quot;,&#xD;&#xA; checkboxUnknown: &quot;fa fa-square&quot;,&#xD;&#xA; dragHelper: &quot;fa arrow-right&quot;,&#xD;&#xA; dropMarker: &quot;fa long-arrow-right&quot;,&#xD;&#xA; error: &quot;fa fa-warning&quot;,&#xD;&#xA; /*expanderClosed: &quot;fa fa-angle-right&quot;,&#xD;&#xA; expanderLazy: &quot;fa fa-angle-right&quot;,&#xD;&#xA; expanderOpen: &quot;fa fa-angle-down&quot;,*/&#xD;&#xA; expanderClosed: &quot;fa fa-caret-right&quot;,&#xD;&#xA; expanderLazy: &quot;fa fa-caret-right&quot;,&#xD;&#xA; expanderOpen: &quot;fa fa-caret-down&quot;,&#xD;&#xA; folder: &quot;fa fa-folder-o&quot;,&#xD;&#xA; folderOpen: &quot;fa fa-folder-open-o&quot;,&#xD;&#xA; loading: &quot;fa fa-spinner fa-pulse&quot;&#xD;&#xA; }&#xD;&#xA; },&#xD;&#xA; source: function() { &#xD;&#xA; var selectedServices = $(&quot;#&quot; + InputTarget).val().length === 0 ? '' : $(&quot;#&quot; + InputTarget).val()&#xD;&#xA; var TreeData = $.ajax({&#xD;&#xA; url: &quot;rdTemplate/rdData.aspx?rdData=&quot; + const_rdData + &quot;&amp;rdDataID=&quot; + const_rdDataID + &quot;&amp;&quot; + InputTarget + &quot;=&quot; + selectedServices,&#xD;&#xA; dataType:&quot;json&quot;, &#xD;&#xA; async: false,&#xD;&#xA; cache: true&#xD;&#xA; }).responseJSON.data;&#xD;&#xA; return nest(TreeData);&#xD;&#xA; },&#xD;&#xA; table: {&#xD;&#xA; nodeColumnIdx: 0&#xD;&#xA; },&#xD;&#xA; click: function(event, data) {&#xD;&#xA; if($(data.originalEvent.target).hasClass('fancytree-expander') || !$(data.originalEvent.target).hasClass('fancytree-add'))&#xD;&#xA; return;&#xD;&#xA; &#xD;&#xA; ToggleServiceTreeAddIcon(data.node.tr.lastChild.lastChild);&#xD;&#xA; &#xD;&#xA; if($('#TblTree').is(&quot;table&quot;))&#xD;&#xA; data.node.setSelected(!$(data.node.tr).hasClass('fancytree-selected'));&#xD;&#xA; else &#xD;&#xA; data.node.setSelected(!$(data.node.span).hasClass('fancytree-selected'));&#xD;&#xA; },&#xD;&#xA; dblclick: function(event, data) {&#xD;&#xA; if($(data.originalEvent.target).hasClass('fancytree-expander') &amp;&amp; $(data.originalEvent.target).hasClass('fancytree-add'))&#xD;&#xA; return;&#xD;&#xA; &#xD;&#xA; ToggleServiceTreeAddIcon(data.node.tr.lastChild.lastChild);&#xD;&#xA; &#xD;&#xA; if($('#TblTree').is(&quot;table&quot;))&#xD;&#xA; data.node.setSelected(!$(data.node.tr).hasClass('fancytree-selected'));&#xD;&#xA; else &#xD;&#xA; data.node.setSelected(!$(data.node.span).hasClass('fancytree-selected'));&#xD;&#xA; },&#xD;&#xA; select: function(event, data) {&#xD;&#xA; // Display list of selected nodes&#xD;&#xA; var selNodes = data.tree.getSelectedNodes();&#xD;&#xA; var selServiceIDs = $.map(selNodes, function(node){&#xD;&#xA; return node.key;&#xD;&#xA; });&#xD;&#xA; initServicesTags(selNodes);&#xD;&#xA; initServicesNames(selNodes);&#xD;&#xA; &#xD;&#xA; $(&quot;#&quot; + InputTarget).val(selServiceIDs.join(&quot;,&quot;))&#xD;&#xA;/* -if requiring to refresh a Logi Info Div then use below sample */&#xD;&#xA; clearTimeout(RefreshInterval);&#xD;&#xA; RefreshInterval = setTimeout(function() {&#xD;&#xA; var refreshUrl = 'rdAjaxCommand=RefreshElement&amp;rdRefreshElementID=colServiceStatus,divButtons';&#xD;&#xA; refreshUrl += '&amp;rdReport='+urlParam('rdReport')+'&amp;drilldown=0&amp;inpSubmit=0';&#xD;&#xA; refreshUrl += '&amp;'+InputTarget+'='+selServiceIDs.join(&quot;,&quot;);&#xD;&#xA; rdAjaxRequestWithFormVars(refreshUrl,'false','',null,null,null,['','',''],true);&#xD;&#xA; }, 50);&#xD;&#xA;&#xD;&#xA; },&#xD;&#xA; renderColumns: function(event, data) {&#xD;&#xA; var node = data.node,&#xD;&#xA; $tdList = $(node.tr).find(&quot;&gt;td&quot;);&#xD;&#xA; $tdList.eq(1).html(&quot;&lt;i class=\&quot;fa fa-plus fancytree-add\&quot;&gt;&lt;/i&gt;&quot;);&#xD;&#xA; },&#xD;&#xA; init: function(event, data) { &#xD;&#xA; var selNodes = data.tree.getSelectedNodes();&#xD;&#xA; for(var i=0; i&lt;selNodes.length; i++) {&#xD;&#xA; if(selNodes[i].parent!==null)&#xD;&#xA; ExpandParent(selNodes[i].parent);&#xD;&#xA; }&#xD;&#xA; initServicesTags(selNodes);&#xD;&#xA; initServicesNames(selNodes);&#xD;&#xA; }, &#xD;&#xA; }); &#xD;&#xA;},&#xD;&#xA;initServicesTags = function(selNodes) {&#xD;&#xA; var selServicesTags = $.map(selNodes, function(node) {&#xD;&#xA; var tagHtml = &quot;&quot;;&#xD;&#xA; tagHtml = tagHtml + &quot;&lt;li class=\&quot;pills-li flowroot\&quot; onclick=\&quot;selNode(&quot; + node.data.id + &quot;)\&quot;&gt;&quot; + node.title + &quot;&lt;span class=\&quot;close fa fa-minus\&quot;&gt;&lt;/span&gt;&lt;/li&gt;&quot;;&#xD;&#xA; return tagHtml;&#xD;&#xA; });&#xD;&#xA; $('#ServicesTags').html(selServicesTags);&#xD;&#xA;},&#xD;&#xA;initServicesNames = function(selNodes) {&#xD;&#xA; var selServicesNames = $.map(selNodes, function(node) {&#xD;&#xA; var serviceNames = &quot;&quot;;&#xD;&#xA; serviceNames = serviceNames + node.title;&#xD;&#xA; return serviceNames;&#xD;&#xA; });&#xD;&#xA; $('#inpServicesNames').val(selServicesNames);&#xD;&#xA;},&#xD;&#xA;selNode = function(id, select=false) {&#xD;&#xA; $(&quot;#TblTree&quot;).fancytree(&quot;getTree&quot;).getNodeByKey(id).setSelected(select);&#xD;&#xA; ToggleServiceTreeAddIcon($(&quot;#TblTree&quot;).fancytree(&quot;getTree&quot;).getNodeByKey(id).tr.lastChild.lastChild);&#xD;&#xA;},&#xD;&#xA;initSelectedNodes = function(TreeTarget=&quot;TblTree&quot;) {&#xD;&#xA; var tree = $.ui.fancytree.getTree(&quot;#&quot;+TreeTarget);&#xD;&#xA; var selNodes = tree.getSelectedNodes();&#xD;&#xA; for(var i=0; i&lt;selNodes.length; i++) {&#xD;&#xA; if(selNodes[i].parent!==null)&#xD;&#xA; ExpandParent(selNodes[i].parent);&#xD;&#xA; }&#xD;&#xA;},&#xD;&#xA;ExpandParent = function(node) {&#xD;&#xA; node.setExpanded(true);&#xD;&#xA; if(node.parent!==null) {&#xD;&#xA; ExpandParent(node.parent);&#xD;&#xA; }&#xD;&#xA;},&#xD;&#xA;urlParam = function(param) {&#xD;&#xA; param = param.replace(/[\[]/, &quot;\\[&quot;).replace(/[\]]/, &quot;\\]&quot;);&#xD;&#xA; var regex = new RegExp(&quot;[\\?&amp;]&quot; + param + &quot;=([^&amp;#]*)&quot;),&#xD;&#xA; results = regex.exec(location.search);&#xD;&#xA; return results === null ? &quot;&quot; : decodeURIComponent(results[1].replace(/\+/g, &quot; &quot;));&#xD;&#xA;},&#xD;&#xA;BuildTreeTable = function(NumberOfColumns) {&#xD;&#xA; var t = document.createElement('table');&#xD;&#xA; var b = document.createElement('tbody');&#xD;&#xA; t.id = &quot;TblTree&quot;;&#xD;&#xA; t.appendChild(b);&#xD;&#xA; var r = document.createElement('tr');&#xD;&#xA; for (var i=0; i&lt;NumberOfColumns; i++) {&#xD;&#xA; var c = document.createElement('td');&#xD;&#xA; r.appendChild(c);&#xD;&#xA; }&#xD;&#xA; b.appendChild(r);&#xD;&#xA; return t;&#xD;&#xA;},&#xD;&#xA;ToggleServiceTreeAddIcon = function(serviceAddedIcon){&#xD;&#xA; if(serviceAddedIcon.classList.contains('fa-plus')) {&#xD;&#xA; serviceAddedIcon.classList.remove('fa-plus');&#xD;&#xA; serviceAddedIcon.classList.add('fa-minus');&#xD;&#xA; } else {&#xD;&#xA; serviceAddedIcon.classList.add('fa-plus');&#xD;&#xA; serviceAddedIcon.classList.remove('fa-minus');&#xD;&#xA; }&#xD;&#xA;};&#xD;&#xA; &#xD;&#xA;const isNull = (obj) =&gt; obj===null ? '' : obj;&#xD;&#xA;const nest = (items, id = '', link = 'parentId') =&gt; items.filter(item =&gt; isNull(item[link]) === id).map(item =&gt; ({ ...item, children: nest(items, item.id), expanded: item.isSelected.indexOf('True')&gt;-1, selected: item.isSelected.indexOf('True')&gt;-1 })).map(item =&gt; ({ ...item, folder: item.isFolder.indexOf('True')&gt;-1 }));&#xD;&#xA;">
    </IncludeScript>
    </Division>

     

    0
  • Scott Florcsk

    Yes, that helped a lot. Thanks Glyn!

    0

Please sign in to leave a comment.