From 6d34b62c2a81710ccbab2079d685f4b7a2a66cc0 Mon Sep 17 00:00:00 2001
From: Xiao Gui <xgui3783@gmail.com>
Date: Tue, 2 Oct 2018 14:28:26 +0200
Subject: [PATCH] feat: cluster flat tree, improve initial load perf

---
 src/components/components.module.ts           |  4 +-
 .../flatTree/appendSiblingFlag.pipe.ts        | 26 ++++---
 src/components/flatTree/flatTree.component.ts | 59 ++++++++++++++-
 src/components/flatTree/flatTree.style.css    | 27 +++++++
 .../flatTree/flatTree.template.html           | 71 +++++++++++--------
 5 files changed, 141 insertions(+), 46 deletions(-)

diff --git a/src/components/components.module.ts b/src/components/components.module.ts
index edefaf871..8e3c9a588 100644
--- a/src/components/components.module.ts
+++ b/src/components/components.module.ts
@@ -22,6 +22,7 @@ import { RenderPipe } from './flatTree/render.pipe';
 import { HighlightPipe } from './flatTree/highlight.pipe';
 import { FitlerRowsByVisibilityPipe } from './flatTree/filterRowsByVisibility.pipe';
 import { AppendSiblingFlagPipe } from './flatTree/appendSiblingFlag.pipe';
+import { ClusteringPipe } from './flatTree/clustering.pipe';
 
 
 @NgModule({
@@ -53,7 +54,8 @@ import { AppendSiblingFlagPipe } from './flatTree/appendSiblingFlag.pipe';
     RenderPipe,
     HighlightPipe,
     FitlerRowsByVisibilityPipe,
-    AppendSiblingFlagPipe
+    AppendSiblingFlagPipe,
+    ClusteringPipe
   ],
   exports : [
     BrowserAnimationsModule,
diff --git a/src/components/flatTree/appendSiblingFlag.pipe.ts b/src/components/flatTree/appendSiblingFlag.pipe.ts
index bcec2058f..b927bbdbf 100644
--- a/src/components/flatTree/appendSiblingFlag.pipe.ts
+++ b/src/components/flatTree/appendSiblingFlag.pipe.ts
@@ -6,19 +6,17 @@ import { Pipe, PipeTransform } from "@angular/core";
 
 export class AppendSiblingFlagPipe implements PipeTransform{
   public transform(objs:any[]):any[]{
-    const idSet = new Set(objs.map(obj => obj.lvlId))
-    return objs.map(obj => 
-      Object.assign({}, obj, {
-        siblingFlags : obj.lvlId
-          .split('_')
-          .reduce((acc,curr) => acc.concat(acc.length === 0 ? curr : acc[acc.length -1].concat(`_${curr}`)), [])
-          .map(prevIds => this.isLast(prevIds,idSet))
-          .slice(1)
-      })
-    )
-  }
-
-  private isLast(id:string, set:Set<string>):boolean{
-    return !set.has(id.split('_').slice(0,-1).concat( (Number(id.split('_').reverse()[0]) + 1).toString() ).join('_'))
+    return objs
+      .reduceRight((acc,curr) =>  ({
+          acc : acc.acc.concat(Object.assign({}, curr, {
+            siblingFlags : curr.lvlId.split('_').map((v, idx) => typeof acc.flags[idx] !== 'undefined'
+                ? acc.flags[idx]
+                : false)
+              .slice(1)
+              .map(v => !v)
+          })),
+          flags: curr.lvlId.split('_').map((_,idx) => acc.flags[idx] ).slice(0, -1).concat(true)
+        }), { acc:[], flags : Array(256).fill(false) })
+      .acc.reverse()   
   }
 }
\ No newline at end of file
diff --git a/src/components/flatTree/flatTree.component.ts b/src/components/flatTree/flatTree.component.ts
index e31656057..5fde9a144 100644
--- a/src/components/flatTree/flatTree.component.ts
+++ b/src/components/flatTree/flatTree.component.ts
@@ -1,4 +1,4 @@
-import { EventEmitter, Component, Input, Output, ChangeDetectionStrategy, ChangeDetectorRef, Optional } from "@angular/core";
+import { EventEmitter, Component, Input, Output, ChangeDetectionStrategy, ElementRef, OnDestroy, ChangeDetectorRef, ViewChildren, QueryList, AfterViewChecked, AfterViewInit } from "@angular/core";
 import { FlattenedTreeInterface } from "./flattener.pipe";
 
 @Component({
@@ -10,7 +10,7 @@ import { FlattenedTreeInterface } from "./flattener.pipe";
   changeDetection:ChangeDetectionStrategy.OnPush
 })
 
-export class FlatTreeComponent{
+export class FlatTreeComponent implements AfterViewChecked, AfterViewInit, OnDestroy{
   @Input() inputItem : any = {
     name : 'Untitled',
     children : []
@@ -27,6 +27,55 @@ export class FlatTreeComponent{
   @Input() findChildren : (item:any)=>any[] = (item)=>item.children ? item.children : [] 
   @Input() searchFilter : (item:any)=>boolean | null = ()=>true
 
+  @Input() flatTreeViewPort : HTMLElement
+
+  @ViewChildren('flatTreeStart',{read : ElementRef}) flatTreeStartCollection : QueryList<ElementRef>
+  @ViewChildren('flatTreeEnd',{read : ElementRef}) flatTreeEndCollection : QueryList<ElementRef>
+
+  intersectionObserver : IntersectionObserver
+
+  constructor(
+    private cdr:ChangeDetectorRef
+  ){
+
+  }
+
+  ngAfterViewChecked(){
+    if(this.intersectionObserver){
+      this.intersectionObserver.disconnect()
+      this.flatTreeStartCollection.forEach(er => this.intersectionObserver.observe(er.nativeElement))
+      this.flatTreeEndCollection.forEach(er => this.intersectionObserver.observe(er.nativeElement))
+    }
+  }
+
+  ngAfterViewInit(){
+
+    if(this.flatTreeViewPort){
+      this.intersectionObserver = new IntersectionObserver(entries => {
+        const currPos = entries
+          .filter(entry => entry.isIntersecting)
+          .filter(entry => Number(entry.target.getAttribute('clusterindex')) !== NaN )
+          .map(entry => Number(entry.target.getAttribute('clusterindex')))
+          .reduce((acc, clusterindex, i, array) => acc + (clusterindex / array.length), 0)
+        
+        if( currPos - this._currentPos >= 1 ){
+          this._currentPos = Math.round(currPos)
+          this.cdr.markForCheck()
+        }
+      },{
+        root: this.flatTreeViewPort,
+        rootMargin : '0px',
+        threshold : 0.1
+      })
+    }
+  }
+
+  ngOnDestroy(){
+    if(this.intersectionObserver){
+      this.intersectionObserver.disconnect()
+    }
+  }
+
   getClass(level:number){
     return [...Array(level+1)].map((v,idx) => `render-node-level-${idx}`).join(' ')
   }
@@ -55,4 +104,10 @@ export class FlatTreeComponent{
         ? true
         : false
   }
+
+  private _currentPos : number = 0
+
+  showCluster(index:number){
+    return index <= this._currentPos + 1
+  }
 }
\ No newline at end of file
diff --git a/src/components/flatTree/flatTree.style.css b/src/components/flatTree/flatTree.style.css
index 15a2cb786..cc5bdf60a 100644
--- a/src/components/flatTree/flatTree.style.css
+++ b/src/components/flatTree/flatTree.style.css
@@ -1,6 +1,33 @@
 :host
 {
   display: block;
+  height:100%;
+}
+
+[clusterContainer]
+{
+  position:relative;
+}
+
+[flatTreeStart]
+{
+  position: absolute;
+  top: 0;
+  height:50%;
+  width:1em;
+  opacity:0;
+  background-color:rgba(200,250,200,0.2);
+}
+
+[flatTreeEnd]
+{
+  position: absolute;
+  top: 0;
+  transform: translateY(100%);
+  width:1em;
+  opacity:0;
+  background-color:rgba(250,200,200,0.2);
+  height:50%;
 }
 
 .render-node-text
diff --git a/src/components/flatTree/flatTree.template.html b/src/components/flatTree/flatTree.template.html
index 33f7b3c95..f8b60d1ac 100644
--- a/src/components/flatTree/flatTree.template.html
+++ b/src/components/flatTree/flatTree.template.html
@@ -1,33 +1,46 @@
-<div 
-  *ngFor = "let flattenedItem of (inputItem | flattenTreePipe : findChildren | filterRowsByVisbilityPipe : findChildren : searchFilter | appendSiblingFlagPipe)"
-  [ngClass] = "getClass(flattenedItem.flattenedTreeLevel)"
-  [attr.flattenedtreelevel] = "flattenedItem.flattenedTreeLevel" 
-  [attr.collapsed] = "flattenedItem.collapsed ? flattenedItem.collapsed : false"
-  [attr.lvlId] = "flattenedItem.lvlId"
-  [hidden] = "collapseRow(flattenedItem) "
-  renderNode>
+<div class="container">
+  <div *ngFor = "let flattenedItems of (inputItem | flattenTreePipe : findChildren | filterRowsByVisbilityPipe : findChildren : searchFilter | appendSiblingFlagPipe | clusteringPipe : 50 ); let index = index" clusterContainer>
 
-  <span class = "padding-block-container">
-    <span
-      [attr.hidemargin] = "block"
-      *ngFor = "let block of flattenedItem.siblingFlags"
-      class = "padding-block">
+    <div [attr.clusterindex] = "index" flatTreeStart #flatTreeStart>
+    </div>
 
-    </span>
-  </span>
-  <span 
-    *ngIf = "findChildren(flattenedItem).length > 0; else noChildren"
-    (click) = "$event.stopPropagation(); toggleCollapse(flattenedItem)" >
-    <i [ngClass] = "isCollapsed(flattenedItem) ? 'glyphicon-chevron-right' : 'glyphicon-chevron-down'" class="glyphicon"></i>
-  </span>
-  <ng-template #noChildren>
-    <i class="glyphicon glyphicon-none">
+    <div *ngIf = "showCluster(index)">
+      <div
+        *ngFor = "let flattenedItem of flattenedItems"
+        [ngClass] = "getClass(flattenedItem.flattenedTreeLevel)"
+        [attr.flattenedtreelevel] = "flattenedItem.flattenedTreeLevel" 
+        [attr.collapsed] = "flattenedItem.collapsed ? flattenedItem.collapsed : false"
+        [attr.lvlId] = "flattenedItem.lvlId"
+        [hidden] = "collapseRow(flattenedItem) "
+        renderNode>
+      
+        <span class = "padding-block-container">
+          <span
+            [attr.hidemargin] = "block"
+            *ngFor = "let block of flattenedItem.siblingFlags"
+            class = "padding-block">
+      
+          </span>
+        </span>
+        <span 
+          *ngIf = "findChildren(flattenedItem).length > 0; else noChildren"
+          (click) = "$event.stopPropagation(); toggleCollapse(flattenedItem)" >
+          <i [ngClass] = "isCollapsed(flattenedItem) ? 'glyphicon-chevron-right' : 'glyphicon-chevron-down'" class="glyphicon"></i>
+        </span>
+        <ng-template #noChildren>
+          <i class="glyphicon glyphicon-none">
+      
+          </i>
+        </ng-template>
+        <span
+          (click) = "treeNodeClick.emit({event:$event,inputItem:flattenedItem})"
+          class = "render-node-text"
+          [innerHtml] = "flattenedItem | renderPipe : renderNode ">
+        </span>
+      </div>
+    </div>
 
-    </i>
-  </ng-template>
-  <span
-    (click) = "treeNodeClick.emit({event:$event,inputItem:flattenedItem})"
-    class = "render-node-text"
-    [innerHtml] = "flattenedItem | renderPipe : renderNode ">
-  </span>
+    <div [attr.clusterindex] = "index" flatTreeEnd #flatTreeEnd>
+    </div>
+  </div>
 </div>
\ No newline at end of file
-- 
GitLab