Refactored loading of channels in (XY-)chartviewer

Change-Id: I8989a9be91a1b1d441c010e0328c2fa8b293f299
diff --git a/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/chartviewer/chart-viewer.component.ts b/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/chartviewer/chart-viewer.component.ts
index 0cb2299..48dd678 100644
--- a/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/chartviewer/chart-viewer.component.ts
+++ b/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/chartviewer/chart-viewer.component.ts
@@ -22,6 +22,9 @@
 import { Node } from '../../../navigator/node';
 import { ChartViewerDataService, getDataArray } from '../../services/chart-viewer-data.service';
 import { MeasuredValues } from '../../model/chartviewer.model';
+import { ChannelGroup } from '../../model/types/channelgroup.class';
+import { Measurement } from '../../model/types/measurement.class';
+import { Channel } from '../../model/types/channel.class';
 
 @Component({
   selector: 'mdm5-chartViewer',
@@ -31,11 +34,7 @@
 export class ChartViewerComponent implements OnChanges {
 
   @Input()
-  channelGroup = new Node();
-  @Input()
-  channels = [new Node()];
-  @Input()
-  channelGroupMap: Map<Node, Node[]> = new Map();
+  measurement: Measurement;
 
   @ViewChild('lineChart')
   chart: UIChart;
@@ -91,10 +90,9 @@
     this.data = this.getEmptyData();
     this.tmpData = this.getEmptyData();
     for (const propName in changes) {
-      if (this.channelGroupMap !== undefined
-        && (propName === 'channelGroupMap' || propName === 'channelGroup' || propName === 'channels')) {
-        this.channelGroupMap.forEach((channels, group) => {
-          this.numberOfRows += this.chartService.getNumberOfRows(group);
+      if (this.measurement !== undefined && (propName === 'measurement')) {
+        this.measurement.channelGroups.forEach(group => {
+          this.numberOfRows += group.numberOfRows;
           if (this.numberOfRows < this.numberOfChunks) {
             this.previewEnabled = false;
           }
@@ -105,16 +103,6 @@
         });
 
         break;
-      } else if (this.channelGroup !== undefined && (propName === 'channelGroup' || propName === 'channels')) {
-        this.numberOfRows += this.chartService.getNumberOfRows(this.channelGroup);
-        if (this.numberOfRows < this.numberOfChunks) {
-          this.previewEnabled = false;
-        }
-        this.requestedValues = Math.min(this.numberOfRows, this.maxRequestedValues);
-
-        this.updateRange();
-        this.chartChannel(undefined);
-        break;
       }
     }
   }
@@ -140,17 +128,18 @@
     this.chartChannel(undefined);
   }
 
-  chartChannel(channelGroup: Node) {
+  chartChannel(channelGroup: ChannelGroup) {
+    let group: ChannelGroup;
     if (channelGroup) {
-      this.chartMeasuredValues(this.chartService.loadValues(
-        channelGroup, this.channelGroupMap.get(channelGroup),
-        this.rangeValues[0] - 1, this.rangeValues[1], this.previewEnabled ? this.numberOfChunks : 0));
+      group = channelGroup;
     } else {
       // either the selected channel or all channels from the map
-      this.chartMeasuredValues(this.chartService.loadValues(
-        this.channelGroup, this.channels.length === 1 ? this.channels : this.channelGroupMap.get(this.channelGroup),
-        this.rangeValues[0] - 1, this.rangeValues[1], this.previewEnabled ? this.numberOfChunks : 0));
+      group = this.measurement.getFirstChannelGroup();
     }
+
+    this.chartMeasuredValues(this.chartService.loadValues(
+      channelGroup, this.measurement.findChannels(channelGroup),
+      this.rangeValues[0] - 1, this.rangeValues[1], this.previewEnabled ? this.numberOfChunks : 0));
   }
 
   chartMeasuredValues(measuredValues: Observable<MeasuredValues[]>) {
@@ -163,7 +152,6 @@
       }),
       catchError(this.httpErrorHandler.handleError)
     ).subscribe(d => {
-       console.log(d);
       if (this.tmpData.labels.length === 0) {
         this.tmpData = d.data;
       } else {
diff --git a/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/chatviewer-nav-card/chart-viewer-nav-card.component.html b/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/chatviewer-nav-card/chart-viewer-nav-card.component.html
index c110d72..86c5175 100644
--- a/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/chatviewer-nav-card/chart-viewer-nav-card.component.html
+++ b/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/chatviewer-nav-card/chart-viewer-nav-card.component.html
@@ -12,11 +12,11 @@
  *
  ********************************************************************************-->
 
-<div *ngIf="channels === undefined">
+<div *ngIf="measurement === undefined">
         No Channel selected.
 </div>
 
-<div *ngIf="channels !== undefined">
+<div *ngIf="measurement !== undefined">
   <div class="p-grid toolbar">
     <div class="thin">
       <p-selectButton [options]="modes" [(ngModel)]="selectedMode"></p-selectButton>
@@ -27,8 +27,8 @@
     </div>
   </div>
   <div [ngSwitch]="selectedMode">
-    <mdm5-chartViewer *ngSwitchCase="'chart'" [channelGroup]="channelGroup" [channels]="channels" [channelGroupMap]="channelGroupMap"></mdm5-chartViewer>
-    <mdm5-dataTable *ngSwitchCase="'table'" [channels]="channels" [channelGroupMap]="channelGroupMap"></mdm5-dataTable>
+    <mdm5-chartViewer *ngSwitchCase="'chart'" [measurement]="measurement"></mdm5-chartViewer>
+    <mdm5-dataTable *ngSwitchCase="'table'" [measurement]="measurement"></mdm5-dataTable>
     <div *ngSwitchDefault></div>
   </div>
 </div>
diff --git a/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/chatviewer-nav-card/chart-viewer-nav-card.component.ts b/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/chatviewer-nav-card/chart-viewer-nav-card.component.ts
index c03be10..dd7979c 100644
--- a/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/chatviewer-nav-card/chart-viewer-nav-card.component.ts
+++ b/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/chatviewer-nav-card/chart-viewer-nav-card.component.ts
@@ -25,6 +25,8 @@
 import { ChartViewerDataService } from '../../services/chart-viewer-data.service';
 import { Query, QueryService, Row, SearchResult } from '../../../tableview/query.service';
 import { map, switchMap } from 'rxjs/operators';
+import { Measurement } from '../../model/types/measurement.class';
+import { TYPE_CHANNEL, TYPE_CHANNELGROUP, TYPE_MEASUREMENT } from '../../model/constants';
 
 @Component({
   selector: 'mdm5-chartViewerNavCard',
@@ -33,23 +35,15 @@
   providers: []
 })
 export class ChartViewerNavCardComponent implements OnInit, OnDestroy {
-  private _contextUrl: string;
-
   subscription: any;
   modes: SelectItem[] = [];
   selectedMode = 'chart';
 
   // public data for sub-components
   selectedNode: Node;
-  channelGroup: Node;
-  channels: Node[];
-  channelGroupMap: Map<Node, Node[]>;
+  cachedMeasurement: Measurement;
+  measurement: Measurement;
 
-  // temporary data for dynamic reloading
-  tmpChannelGroupMap: Map<Node, Node[]>;
-  foundChannels: Node[];
-
-  cachedChannelGroups: Map<Node, Node[]>;
   totalCachedChannels = 0;
 
   // dynamic reloading
@@ -57,7 +51,7 @@
   offset = 0;
   loadInterval = 10;
   allLoaded = false;
-  openChannels = { amount: '' };
+  openChannels = { limit: 0, offset: 0, total: 0 };
 
   constructor(private navigatorService: NavigatorService,
     private notificationService: MDMNotificationService,
@@ -66,9 +60,7 @@
     private _prop: PropertyService,
     private preferenceService: PreferenceService,
     private chartService: ChartViewerDataService,
-    private queryService: QueryService) {
-    this._contextUrl = _prop.getUrl('mdm/environments');
-  }
+    private queryService: QueryService) {}
 
   ngOnInit() {
     this.modes = [
@@ -100,233 +92,54 @@
     if (!node) {
       return;
     }
-    this.selectedNode = node;
-
-    this.cleanData();
-    this.loadData(node, this.limit, false);
+    if (node.type === TYPE_MEASUREMENT || node.type === TYPE_CHANNELGROUP || node.type === TYPE_CHANNEL) {
+      this.selectedNode = node;
+      this.cleanData();
+      this.loadData(node, this.limit, false);
+    } else {
+      this.selectedNode = undefined;
+      this.cleanData();
+    }
   }
 
   cleanData() {
-    this.channels = undefined;
-    this.channelGroup = undefined;
-    this.channelGroupMap = new Map();
-    this.tmpChannelGroupMap = new Map();
-    this.cachedChannelGroups = new Map();
+    this.cachedMeasurement = new Measurement();
     this.totalCachedChannels = 0;
-    this.foundChannels = [];
+    this.measurement = undefined;
     this.offset = 0;
-    this.openChannels = { amount: '' };
+    this.openChannels = { limit: 0, offset: 0, total: 0 };
     this.allLoaded = false;
   }
 
   loadData(node: Node, limit: number, join: boolean) {
-    if (join) {
-      this.tmpChannelGroupMap = new Map();
-    }
-    if (node && node.type === 'Channel') {
-      this.loadChannelGroup(node)
-        .subscribe(channelGroup => {
-          if (channelGroup) {
-            const tmp = new Node();
-            tmp.sourceName = node.sourceName;
-            tmp.type = 'ChannelGroup';
-            tmp.sourceType = 'SubMatrix';
-            tmp.id = channelGroup.id;
-            const attr : Attribute = new Attribute();
-            attr.name = 'SubMatrixNoRows';
-            attr.value = channelGroup.numberOfRows;
-            tmp.attributes = [attr];
-            
-            this.foundChannels = undefined;
-            this.tmpChannelGroupMap.set(tmp, [node]);
-            this.channelGroupMap = new Map(this.tmpChannelGroupMap);
-            this.channelGroup = tmp;
-            this.channels = [node];
-          }
-        });
-    } else if (node && node.type === 'ChannelGroup') {
-      if (!join) {
-        // initially load all channels and store them locally
-        // add the first batch to the limit
-        this.chartService.loadChannelsForGroup(node)
-          .subscribe(searchResult => {
-            // process the results and transform to nodes
-            const tmpChannels = [];
-            for (let i = 0; i < searchResult.rows.length; i++) {
-              const tmp = new Node();
-              tmp.sourceName = node.sourceName;
-              tmp.type = searchResult.rows[i].type;
-              for (let j = 0; j < searchResult.rows[i].columns.length; j++) {
-                if (searchResult.rows[i].columns[j].attribute === 'Id' && searchResult.rows[i].columns[j].type === 'Channel') {
-                  tmp.id = searchResult.rows[i].columns[j].value;
-                }
-                if (searchResult.rows[i].columns[j].attribute === 'Name' && searchResult.rows[i].columns[j].type === 'Channel') {
-                  tmp.name = searchResult.rows[i].columns[j].value;
-                }
-              }
-              tmpChannels.push(tmp);
-            }
-            this.totalCachedChannels += tmpChannels.length;
-            this.cachedChannelGroups.set(node, tmpChannels);
-            this.displayNextChannels();
-          });
-      } else {
-        this.displayNextChannels();
-      }
-    } else if (node && node.type === 'Measurement') {
-      if (!join) {
-        this.loadSubsequentChannelGroups(node).subscribe((channelGroups) => {
-          this.processMeasurement(channelGroups);
-        });
-      } else {
-        this.displayNextChannels();
-      }
-    }
+    this.chartService.loadMeasurement(node).subscribe(measurement => {
+      this.cachedMeasurement = measurement;
+      this.displayNextChannels();
+    });
   }
 
   displayNextChannels() {
-    const keys = this.cachedChannelGroups.keys();
-    let key: IteratorResult<Node> = keys.next();
+    let m = new Measurement(this.cachedMeasurement);
 
-    let processed = 0;
-    let loaded = 0;
-    let dynamicOffset = this.offset;
-    const tmpChannels: Node[] = [];
-
-    while (key !== null && key.value !== null && !key.done) {
-      // the internal cache
-      const channels = this.cachedChannelGroups.get(key.value);
-      if (channels) {
-
-        let start: number = dynamicOffset;
-        if (processed > 0 && start > processed) {
-          start -= processed;
-        }
-
-        let end: number = this.limit + dynamicOffset;
-        if (processed > 0 && end > processed) {
-          end -= processed;
-        }
-        if (end > channels.length) {
-          end = channels.length;
-        }
-
-        // loaded is only affected on a pagination between multiple groups
-        start -= loaded;
-        end -= loaded;
-
-        if (start < 0) {
-          start = 0;
-        }
-        if (end < start) {
-          end = start;
-        }
-
-        if (start > 0) {
-          if (start > channels.length) {
-            this.tmpChannelGroupMap.set(key.value, []);
-          } else {
-            loaded += end - start;
-            let tmpChnls = this.channelGroupMap.get(key.value);
-            const tmpChnls2 = channels.slice(start, end);
-            if (!tmpChnls) {
-              tmpChnls = [];
-            }
-            for (let j = 0; j < tmpChnls2.length; j++) {
-              tmpChnls.push(tmpChnls2[j]);
-            }
-            this.tmpChannelGroupMap.set(key.value, tmpChnls);
-          }
-        // follow-up iterations start always at zero if the offset is lower than the channel size
-        if (channels.length > dynamicOffset) {
-            dynamicOffset = 0;
-          }
-        } else {
-          // no offset, just limit
-          loaded += (end - start);
-          this.tmpChannelGroupMap.set(key.value, channels.slice(start, end));
-        }
-        processed += (channels.length);
-      }
-
-      key = keys.next();
-
-      if (loaded >= this.loadInterval) {
-        // load interval reached for all groups, cancel processing
-        key = null;
+    if (this.measurement) {
+      // Added already selected channels
+      for (let channel of this.measurement.allChannels()) {
+        m.put(this.measurement.findChannelGroupByChannel(channel), channel);
       }
     }
-    this.channelGroupMap = new Map(this.tmpChannelGroupMap);
-    this.channels = tmpChannels;
-    this.offset += loaded;
+
+    let channels = Array.from(this.cachedMeasurement.allChannels());
+    let loadedChannels = channels.slice(this.offset, this.offset + this.limit);
+    loadedChannels.forEach(c => m.put(this.cachedMeasurement.findChannelGroupByChannel(c), c));
+
+    this.totalCachedChannels = channels.length;
+    this.offset += loadedChannels.length;
     this.allLoaded = this.offset >= this.totalCachedChannels;
-    this.openChannels = { amount: (this.loadInterval).toString() + '/' + (this.totalCachedChannels).toString() };
-  }
-
-  processMeasurement(channelGroups: Node[]) {
-    for (let i = 0; i < channelGroups.length; i++) {
-      const node = channelGroups[i];
-      this.chartService.loadChannelsForGroup(node)
-        .subscribe(searchResult => {
-          // process the results and transform to nodes
-          const tmpChannels = [];
-          for (let j = 0; j < searchResult.rows.length; j++) {
-            const tmp = new Node();
-            tmp.sourceName = node.sourceName;
-            tmp.type = searchResult.rows[j].type;
-            for (let k = 0; k < searchResult.rows[j].columns.length; k++) {
-              if (searchResult.rows[j].columns[k].attribute === 'Id' && searchResult.rows[j].columns[k].type === 'Channel') {
-                tmp.id = searchResult.rows[j].columns[k].value;
-              }
-              if (searchResult.rows[j].columns[k].attribute === 'Name' && searchResult.rows[j].columns[k].type === 'Channel') {
-                tmp.name = searchResult.rows[j].columns[k].value;
-              }
-            }
-            tmpChannels.push(tmp);
-          }
-          this.totalCachedChannels += tmpChannels.length;
-          this.cachedChannelGroups.set(node, tmpChannels);
-          if (i + 1 === channelGroups.length) {
-            this.displayNextChannels();
-          }
-        });
-    }
+    this.openChannels = { limit: this.loadInterval, offset: this.offset, total: this.totalCachedChannels };
+    this.measurement = m;
   }
 
   loadMissingChannels(event) {
-    this.loadData(this.selectedNode, this.limit, true);
+    this.displayNextChannels();
   }
-
-  loadSubsequentChannelGroups(measurement: Node) {
-    const url = this._contextUrl + '/' + measurement.sourceName + '/channelgroups?filter=Measurement.Id eq "' + measurement.id + '"';
-    return this.nodeService.getNode(url);
-  }
-
-  loadChannelGroup(channel: Node): Observable<{id: string, numberOfRows: string}> {
-    const query = new Query();
-    query.addFilter(channel.sourceName, 'Channel.Id eq ' + channel.id);
-    query.columns = ['ChannelGroup.Id', 'ChannelGroup.SubMatrixNoRows'];
-    query.resultType = 'ChannelGroup';
-
-    return this.queryService.query(query).pipe(
-      map(sr => sr.rows.map(r => { return { id: r.id, numberOfRows: r.getColumn('ChannelGroup.SubMatrixNoRows') }; })),
-      switchMap(e => (e.length === 1) ? of(e[0]) : throwError('Channel ' + channel.id + ' must be in exactly 1 ChannelGroup.'))
-    );
-  }
-
-  loadChannels(channelgroup: Node, limit?: number, offset?: number): Observable<Node[]> {
-    let url = this._contextUrl + '/' + channelgroup.sourceName + '/channels?filter=ChannelGroup.Id eq "' + channelgroup.id + '"';
-    if (limit) {
-      url = url + '&limit=' + limit;
-    }
-    if (offset) {
-      url = url + '&offset=' + offset;
-    }
-    return this.nodeService.getNode(url);
-  }
-
-  queryRowsForGroup(channelGroup: Node): Observable<SearchResult> {
-    return this.chartService.loadChannelsForGroup(channelGroup);
-  }
-
 }
diff --git a/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/datatable/data-table.component.ts b/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/datatable/data-table.component.ts
index e73cc55..df8dec9 100644
--- a/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/datatable/data-table.component.ts
+++ b/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/datatable/data-table.component.ts
@@ -20,6 +20,7 @@
 import { Node } from '../../../navigator/node';
 import { ChartViewerDataService, getDataArray } from '../../services/chart-viewer-data.service';
 import { MeasuredValues } from '../../model/chartviewer.model';
+import { Measurement } from '../../model/types/measurement.class';
 
 @Component({
   selector: 'mdm5-dataTable',
@@ -31,15 +32,8 @@
   @ViewChild('datatable')
   private table: Table;
 
-  // xy-chart-viewer or single channel node
   @Input()
-  channels = [new Node()];
-  // xy-chart-viewer
-  @Input()
-  channelGroups = [];
-  // chart-viewer
-  @Input()
-  channelGroupMap: Map<Node, Node[]>;
+  measurement: Measurement;
 
   tableLoading = false;
   cols: Column[] = [];
@@ -52,7 +46,7 @@
 
   ngOnChanges(changes: SimpleChanges) {
     for (const propName in changes) {
-      if (propName === 'channelGroup' || propName === 'channels' || propName === 'channelGroupMap') {
+      if (propName === 'measurement') {
         this.table.reset();
         this.cols = [];
         this.datapoints = [];
@@ -75,41 +69,23 @@
   }
 
   loadLazy(event: LazyLoadEvent) {
-    const hasChannelGroups = this.channelGroups !== undefined && this.channelGroups.length > 0;
-    const hasMap = this.channelGroupMap !== undefined && this.channelGroupMap.size > 0;
-    if (!hasChannelGroups && !hasMap) {
+    if (!this.measurement) {
       return;
     }
 
-    let tmpChannelGroups = this.channelGroups;
-
-    if (!hasChannelGroups && hasMap) {
-      tmpChannelGroups = [];
-      this.channels = [];
-      this.channelGroupMap.forEach((values, key) => {
-        tmpChannelGroups.push(key);
-        for (let j = 0; j < values.length; j++) {
-          this.channels.push(values[j]);
-      }
-      });
-    }
-
     setTimeout(() => {
       this.tableLoading = true;
-      for (const chGrp in tmpChannelGroups) {
-        if (tmpChannelGroups.hasOwnProperty(chGrp)) {
-          const tmpChannelGroup = tmpChannelGroups[chGrp];
-          const tmpChannels = hasMap && this.channels.length > 1 || this.channels.length === 1 && !this.channels[0].id
-            ? this.channelGroupMap.get(tmpChannelGroup) : this.channels;
 
-          this.totalRecords = this.chartviewerService.getNumberOfRows(tmpChannelGroup);
-          this.chartviewerService.loadMeasuredValues(tmpChannelGroup, tmpChannels, event.first, event.rows)
-          .subscribe(mv => {
-            this.load(mv);
-            this.tableLoading = false;
-          },
-            () => this.tableLoading = false);
-        }
+      for (const channelGroup of this.measurement.channelGroups) {
+        const channels = this.measurement.findChannels(channelGroup);
+
+        this.totalRecords = channelGroup.numberOfRows;
+        this.chartviewerService.loadMeasuredValues(channelGroup, channels, event.first, event.rows)
+        .subscribe(mv => {
+          this.load(mv);
+          this.tableLoading = false;
+        },
+          () => this.tableLoading = false);
       }
     }, 0);
   }
diff --git a/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/request-options/request-options.component.ts b/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/request-options/request-options.component.ts
index ddbf822..556cdac 100644
--- a/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/request-options/request-options.component.ts
+++ b/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/request-options/request-options.component.ts
@@ -16,6 +16,7 @@
 import { ChartViewerDataService } from '../../services/chart-viewer-data.service';
 import { Node } from '../../../navigator/node';
 import { RequestOptions } from '../../model/chartviewer.model';
+import { Measurement } from '../../model/types/measurement.class';
 
 @Component({
   selector: 'mdm5-request-options',
@@ -38,7 +39,7 @@
   public previewEnabled = true;
 
   @Input()
-  public channelGroups: Node[];
+  public measurement: Measurement;
 
   @Output()
   public onRequestOptionsChanged = new EventEmitter<RequestOptions>();
@@ -52,15 +53,15 @@
   }
 
   ngOnChanges(changes: SimpleChanges) {
-    if (changes['channelGroups']) {
+    if (changes['measurement']) {
       this.reload();
       this.emitRequestOptionsChanged();
     }
   }
 
   private reload() {
-    if (this.channelGroups != undefined && this.channelGroups.length > 0) {
-      this.numberOfRows = this.chartService.getNumberOfRows(this.channelGroups[0]);
+    if (this.measurement != undefined && this.measurement.channelGroups.length > 0) {
+      this.numberOfRows = this.measurement.getFirstChannelGroup().numberOfRows;
       this.step = Math.min(1000, Math.max(Math.floor(this.numberOfRows / 100), 1));
       if (this.numberOfRows < this.numberOfChunks) {
         this.previewEnabled = false;
diff --git a/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/xy-chart-data-selection-panel/xy-chart-data-selection-panel.component.html b/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/xy-chart-data-selection-panel/xy-chart-data-selection-panel.component.html
index c2cfc55..d91ae6e 100644
--- a/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/xy-chart-data-selection-panel/xy-chart-data-selection-panel.component.html
+++ b/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/xy-chart-data-selection-panel/xy-chart-data-selection-panel.component.html
@@ -15,7 +15,7 @@
 <div class="p-grid">
   <div class="p-col-12" style="margin-top: 4px">
     <p-listbox
-      [options]="channelGroups"
+      [options]="measurement.channelGroups"
       [(ngModel)]="selectedChannelGroups"
       multiple="multiple"
       checkbox="checkbox"
diff --git a/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/xy-chart-data-selection-panel/xy-chart-data-selection-panel.component.ts b/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/xy-chart-data-selection-panel/xy-chart-data-selection-panel.component.ts
index 669a029..31fe6da 100644
--- a/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/xy-chart-data-selection-panel/xy-chart-data-selection-panel.component.ts
+++ b/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/xy-chart-data-selection-panel/xy-chart-data-selection-panel.component.ts
@@ -20,11 +20,13 @@
 import { ArrayUtilService } from 'src/app/core/services/array-util.service';
 import { MDMNotificationService } from 'src/app/core/mdm-notification.service';
 import { TYPE_MEASUREMENT, TYPE_CHANNELGROUP, TYPE_CHANNEL } from '../../model/constants';
-import { map, tap } from 'rxjs/operators';
+import { tap } from 'rxjs/operators';
 import { Node } from '../../../navigator/node';
 import {ConfirmationService} from 'primeng/api';
 import { TranslateService } from '@ngx-translate/core';
-import { AxisInfo } from '../../model/types/axis-info.class';
+import { Measurement } from '../../model/types/measurement.class';
+import { ChannelGroup } from '../../model/types/channelgroup.class';
+import { Channel } from '../../model/types/channel.class';
 
 @Component({
   selector: 'mdm5-xy-chart-data-selection-panel',
@@ -34,9 +36,7 @@
 export class XyChartDataSelectionPanelComponent implements OnInit, OnDestroy, OnChanges {
 
   @Input()
-  public channelGroups: Node[];
-  @Input()
-  public channels: Map<string, Node[]>;
+  public measurement: Measurement;
 
   @Input()
   public xyMode: boolean;
@@ -45,20 +45,19 @@
   public onSelectionChanged = new EventEmitter<ChannelSelectionRow[]>();
 
   // select options for channels
-  public yChannelOptions: Node[] = [];
-  public xChannelOptions: {[key: string]: Node[]} = {};
+  public yChannelOptions: Channel[] = [];
+  public xChannelOptions: {[key: string]: Channel[]} = {};
 
   // channel(group) selection for chart
   // !! change via setter to update cache for workarround !!
-  public selectedChannelGroups: Node[] = [];
-  public selectedYChannels: Node[] = [];
+  public selectedChannelGroups: ChannelGroup[] = [];
+  public selectedYChannels: Channel[] = [];
 
-  private xYcache = new Map<string, AxisInfo[]>();
   private selectionChanged = false;
 
   // listbox workarround to determine toggled item(s)
-  private lastChannelGroupSelection: Node[] = [];
-  private lastYChannelSelection: Node[] = [];
+  private lastChannelGroupSelection: ChannelGroup[] = [];
+  private lastYChannelSelection: Channel[] = [];
 
   public selectedChannelRows: ChannelSelectionRow[] = [];
 
@@ -67,7 +66,7 @@
   public hiddenYChannels = false;
 
   // for x-channel default value
-  private independentChannels = new Map<string, Node>();
+  private independentChannels = new Map<string, Channel>();
 
   /********** subscriptions */
   private chartViewerSub: Subscription;
@@ -92,12 +91,11 @@
 
   ngOnChanges(changes: SimpleChanges) {
     if (changes['xyMode']) {
-      this.reloadAxisSelectOptions().subscribe(() => {
-        if (this.selectionChanged) {
-          this.fireSelectionChanged();
-          this.selectionChanged = false;
-        }
-      });
+      this.reloadAxisSelectOptions();
+      if (this.selectionChanged) {
+        this.fireSelectionChanged();
+        this.selectionChanged = false;
+      }
     }
   }
 
@@ -128,7 +126,7 @@
 
   // y-axis
   public onSelectedYChannelsChanged(event: any) {
-    let channel: Node;
+    let channel: Channel;
     // Deselect
     if (this.selectedYChannels.length < this.lastYChannelSelection.length) {
       channel = this.lastYChannelSelection.find(yChannel => !this.selectedYChannels.some(c => yChannel.id === c.id));
@@ -137,7 +135,7 @@
     } else {
       channel = this.selectedYChannels.find(group => !this.lastYChannelSelection.some(g => group.id === g.id));
     }
-    this.addOrRemoveRow(this.findChannelGroup(channel), channel);
+    this.addOrRemoveRow(this.measurement.findChannelGroupByChannel(channel), channel);
     this.lastYChannelSelection = this.selectedYChannels;
     this.fireSelectionChanged();
   }
@@ -171,65 +169,56 @@
     this.hiddenYChannels = !this.hiddenYChannels;
   }
 
-  private handleDeselectChannelGroup(channelGroup: Node) {
+  private handleDeselectChannelGroup(channelGroup: ChannelGroup) {
     // remove rows
     if (this.arrayUtil.isNotEmpty(this.selectedChannelRows)) {
       this.selectedChannelRows = this.selectedChannelRows.filter(row => row.channelGroup.id !== channelGroup.id);
     }
     // remove selection
     if (this.arrayUtil.isNotEmpty(this.selectedYChannels)) {
-      this.setSelectedYChannels(this.selectedYChannels.filter(channel =>
-        this.channels.get(channelGroup.id).findIndex(c => channel.id === c.id) === -1));
+      this.setSelectedYChannels(this.selectedYChannels.filter(channel => this.measurement.hasChannel(channel.id)));
+      // this.channels.get(channelGroup.id).findIndex(c => channel.id === c.id) === -1));
     }
-    this.reloadAxisSelectOptions().subscribe(() => this.fireSelectionChanged());
+    this.reloadAxisSelectOptions();
+    this.fireSelectionChanged();
   }
 
     /**
    * Handle channelGroup selection via multiselect in html template
    * @param channelGroup
    */
-  private handleSelectChannelGroup(channelGroup: Node) {
-    const cached = this.xYcache.get(channelGroup.id);
-    if (cached != undefined) {
-      this.setChannelOptions(cached, channelGroup);
-      this.fireSelectionChanged();
-    } else {
-      this.chartviewerDataService.loadAxisInformation(channelGroup)
-        .subscribe(ai => {
-          this.setChannelOptions(ai, channelGroup);
-          this.fireSelectionChanged();
-        });
-    }
+  private handleSelectChannelGroup(channelGroup: ChannelGroup) {
+    this.setChannelOptions(channelGroup);
+    this.fireSelectionChanged();
   }
 
   private handleNodeSelectionInNavTree(node: Node) {
     if (node != undefined) {
       switch (node.type) {
         case TYPE_MEASUREMENT:
-          this.xYcache.clear();
           this.resetChannelData();
           this.fireSelectionChanged();
           break;
         case TYPE_CHANNELGROUP:
           this.cleanOldState();
-          this.addChannelGroupToSelection(node.id, node);
+          this.addChannelGroupToSelection(node.id, this.measurement.findChannelGroup(node.id));
           break;
         case TYPE_CHANNEL:
           this.cleanOldState();
+          const channel = this.measurement.findChannel(node.id);
+          const channelGroup = this.measurement.findChannelGroupByChannel(channel);
           // add related channel group to selection
-          if (this.arrayUtil.isNotEmpty(this.channelGroups) && this.channels != undefined) {
-            const channelGroup = this.findChannelGroup(node);
+          if (channel !== undefined && channelGroup !== undefined) {
+            const c = this.measurement.findChannel(node.id);
+            const cg = this.measurement.findChannelGroupByChannel(c);
             // this.addChannelGroupToSelection(channelGroupId, this.channelGroups.find(cg => cg.id === channelGroupId));
-            if (!this.isNodeIn(channelGroup, this.selectedChannelGroups)) {
-              this.setSelectedChannelGroups(this.selectedChannelGroups.concat([channelGroup]));
-              this.chartviewerDataService.loadAxisInformation(channelGroup)
-              .subscribe(mvls => {
-                this.setChannelOptions(mvls, channelGroup);
-                this.addChannelToSelection(node, channelGroup);
-              });
+            if (!this.isChannelGroupIn(cg, this.selectedChannelGroups)) {
+              this.setSelectedChannelGroups(this.selectedChannelGroups.concat([cg]));
+              this.setChannelOptions(cg);
+              this.addChannelToSelection(c, cg);
             } else {
               // if channel is in axis type y, set as selected, if not selected already
-              this.addChannelToSelection(node, channelGroup);
+              this.addChannelToSelection(c, cg);
             }
           }
         break;
@@ -246,8 +235,7 @@
    */
   private cleanOldState() {
     if (this.arrayUtil.isNotEmpty(this.selectedChannelGroups)
-      && this.selectedChannelGroups.some(selected => !this.isNodeIn(selected, this.channelGroups))) {
-      this.xYcache.clear();
+      && this.selectedChannelGroups.some(selected => this.measurement.findChannelGroup(selected.id) === undefined)) {
       this.resetChannelData();
     }
   }
@@ -257,9 +245,9 @@
    * @param channel
    * @param channelGroup
    */
-  private addChannelToSelection(channel: Node, channelGroup: Node) {
+  private addChannelToSelection(channel: Channel, channelGroup: ChannelGroup) {
     // adds channel to selected y-channels if possible and creates row in table
-    if (this.isNodeIn(channel, this.yChannelOptions)) {
+    if (this.isChannelIn(channel, this.yChannelOptions)) {
       const index = this.selectedYChannels.findIndex(c => c.id === channel.id);
       if (index === -1) {
         this.setSelectedYChannels(this.selectedYChannels.concat([channel]));
@@ -271,7 +259,7 @@
      * suggestion: set it as x-channel for all selected y-channels of that channelgroup.
      */
     // if channel is of axis type x, set ... ?
-    } else if (this.isNodeIn(channel, this.xChannelOptions[channelGroup.id])) {
+    } else if (this.isChannelIn(channel, this.xChannelOptions[channelGroup.id])) {
       this.notificationService.notifyWarn('Selected X-Channel', 'Selected channel is of type x-axis. Change x/y mode.');
     } else {
       this.notificationService.notifyError('Unknown channel option', 'Channel ' + channel.name + ' has no axis.');
@@ -283,11 +271,11 @@
    * @param id
    * @param channelGroup
    */
-  private addChannelGroupToSelection(id: string, channelGroup: Node) {
+  private addChannelGroupToSelection(id: string, channelGroup: ChannelGroup) {
     if (this.selectedChannelGroups == undefined) {
       this.setSelectedChannelGroups([]);
     }
-    if (!this.isNodeIn(channelGroup, this.selectedChannelGroups)) {
+    if (!this.isChannelGroupIn(channelGroup, this.selectedChannelGroups)) {
       this.setSelectedChannelGroups(this.selectedChannelGroups.concat([channelGroup]));
       this.handleSelectChannelGroup(channelGroup);
     }
@@ -299,38 +287,32 @@
    * @param axisInfos
    * @param channelGroup
    */
-  private setChannelOptions(axisInfos: AxisInfo[], channelGroup: Node) {
+  private setChannelOptions(channelGroup: ChannelGroup) {
     if (channelGroup != undefined) {
-      // cache axisInfos to avoid reloading
-      this.xYcache.set(channelGroup.id, axisInfos);
-
       // create empty array if property not exists
       this.xChannelOptions[channelGroup.id] = this.xChannelOptions[channelGroup.id] || [];
 
       if (this.xyMode) {
-        axisInfos.forEach(ai => {
-          const tmpChannel = this.channels.get(channelGroup.id).filter(c => c.name === ai.name)[0];
-          if (ai.axisType === 'X_AXIS' || ai.axisType === 'XY_AXIS' || ( ai.axisType == undefined && ai.independent)) {
-            this.xChannelOptions[channelGroup.id] = this.xChannelOptions[channelGroup.id].concat(tmpChannel);
-            // cache independent channels for default x-axis.
-            if (ai.independent) {
-              this.independentChannels.set(channelGroup.id, tmpChannel);
+        this.measurement.findChannels(channelGroup).forEach(channel => {
+          if (channel.axisType === 'X_AXIS' || channel.axisType === 'XY_AXIS' || (channel.axisType == undefined && channel.isIndependent)) {
+            this.xChannelOptions[channelGroup.id] = this.xChannelOptions[channelGroup.id].concat(channel);
+            if (channel.isIndependent) {
+              this.independentChannels.set(channelGroup.id, channel);
             }
-          } else if (ai.axisType === 'Y_AXIS' || ai.axisType === 'XY_AXIS') {
-            this.yChannelOptions = this.yChannelOptions.concat(tmpChannel);
+          } else if (channel.axisType === 'Y_AXIS' || channel.axisType === 'XY_AXIS') {
+            this.yChannelOptions = this.yChannelOptions.concat(channel);
           }
         });
       } else {
-        // cache independent channel for default x-axis.
-        const value = axisInfos.find(ai => ai.independent);
-        if (value != undefined) {
-          const tmpChannel = this.channels.get(channelGroup.id).filter(c => c.name === value.name)[0];
-          this.independentChannels.set(channelGroup.id, tmpChannel);
+        let channel = this.measurement.getIndependentChannel(channelGroup);
+        if (channel) {
+          this.independentChannels.set(channelGroup.id, channel);
         }
 
-        const tmpChannels = this.channels.get(channelGroup.id);
-        this.xChannelOptions[channelGroup.id] = this.xChannelOptions[channelGroup.id].concat(tmpChannels);
-        this.yChannelOptions = this.yChannelOptions.concat(tmpChannels);
+        let channels = this.measurement.findChannels(channelGroup);
+
+        this.xChannelOptions[channelGroup.id] = this.xChannelOptions[channelGroup.id].concat(channels);
+        this.yChannelOptions = this.yChannelOptions.concat(channels);
       }
     }
   }
@@ -342,7 +324,6 @@
     this.setSelectedChannelGroups([]);
     this.selectedChannelRows = [];
     this.setSelectedYChannels([]);
-    this.channelGroups = [].concat(this.channelGroups);
     this.xChannelOptions = {};
     this.yChannelOptions = [];
   }
@@ -354,18 +335,19 @@
     this.xChannelOptions = {};
     this.yChannelOptions = [];
     if (this.arrayUtil.isNotEmpty(this.selectedChannelGroups)) {
-      const cached = this.selectedChannelGroups.filter(g => this.xYcache.get(g.id) !== undefined);
-      return of(cached.map((group, index) => this.setChannelOptions(this.xYcache.get(group.id), cached[index])))
-              .pipe(tap(() => this.removeSelectionsNotMatchingXYMode()));
+
+      this.selectedChannelGroups.forEach(cg => {
+        this.setChannelOptions(cg);
+      });
+      this.removeSelectionsNotMatchingXYMode();
     }
-    return of();
   }
 
   private removeSelectionsNotMatchingXYMode() {
     if (this.selectedYChannels.length > 0) {
       const l = this.selectedChannelRows.length;
-      this.selectedChannelRows = this.selectedChannelRows.filter(row => this.isNodeIn(row.yChannel, this.yChannelOptions));
-      this.setSelectedYChannels(this.selectedYChannels.filter(channel => this.isNodeIn(channel, this.yChannelOptions)));
+      this.selectedChannelRows = this.selectedChannelRows.filter(row => this.isChannelIn(row.yChannel, this.yChannelOptions));
+      this.setSelectedYChannels(this.selectedYChannels.filter(channel => this.isChannelIn(channel, this.yChannelOptions)));
       this.selectionChanged = l !== this.selectedChannelRows.length;
     }
   }
@@ -376,7 +358,7 @@
    * @param channelGroup
    * @param yChannel
    */
-  private addOrRemoveRow(channelGroup: Node, yChannel: Node) {
+  private addOrRemoveRow(channelGroup: ChannelGroup, yChannel: Channel) {
     const index = this.selectedChannelRows.findIndex(row => row.yChannel.id === yChannel.id);
     if (index > -1) {
       this.selectedChannelRows.splice(index, 1);
@@ -391,7 +373,7 @@
    * @param channelGroup
    * @param yChannel
    */
-  private addRow(channelGroup: Node, yChannel: Node) {
+  private addRow(channelGroup: ChannelGroup, yChannel: Channel) {
     const row = new ChannelSelectionRow(channelGroup, yChannel, this.getDefaultXChannel(channelGroup));
     this.selectedChannelRows.push(row);
   }
@@ -401,7 +383,9 @@
    * if non is found the first channel with axistype X-Axis or XY-Axis is returned.
    * @param channelGroup
    */
-  private getDefaultXChannel(channelGroup: Node) {
+  private getDefaultXChannel(channelGroup: ChannelGroup) {
+    this.measurement.getIndependentChannel(channelGroup);
+
     let defaultXChannel = this.independentChannels.get(channelGroup.id);
     if (defaultXChannel == undefined && this.xChannelOptions[channelGroup.id].length > 0) {
       defaultXChannel = this.xChannelOptions[channelGroup.id][0];
@@ -415,7 +399,7 @@
    * @param array the array
    * @param id the id
    */
-  private isNodeIn(node: Node, array: Node[]) {
+  private isChannelIn(node: Channel, array: Channel[]) {
     let response = false;
     if (node != undefined && this.arrayUtil.isNotEmpty(array)) {
       response = array.findIndex(n => n.id === node.id) > -1;
@@ -424,26 +408,29 @@
   }
 
   /**
-   * Looks up parent channelGroup for channel via the channel map.
+   * Returns true if node with given id is in the given array.
    *
-   * @param channel
+   * @param array the array
+   * @param id the id
    */
-  private findChannelGroup(channel: Node) {
-    const channelGroupId = Array.from(this.channels.keys())
-            .find(key => this.isNodeIn(channel, this.channels.get(key)));
-    return this.channelGroups.find(cg => cg.id === channelGroupId);
+  private isChannelGroupIn(node: ChannelGroup, array: ChannelGroup[]) {
+    let response = false;
+    if (node != undefined && this.arrayUtil.isNotEmpty(array)) {
+      response = array.findIndex(n => n.id === node.id) > -1;
+    }
+    return response;
   }
 
   private fireSelectionChanged() {
     this.onSelectionChanged.emit(this.selectedChannelRows);
   }
 
-  private setSelectedYChannels(channels: Node[]) {
+  private setSelectedYChannels(channels: Channel[]) {
     this.selectedYChannels = channels;
     this.lastYChannelSelection = channels;
   }
 
-  private setSelectedChannelGroups(channelGroups: Node[]) {
+  private setSelectedChannelGroups(channelGroups: ChannelGroup[]) {
     this.selectedChannelGroups = channelGroups;
     this.lastChannelGroupSelection = channelGroups;
   }
diff --git a/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/xy-chart-viewer-nav-card/xy-chart-viewer-nav-card.component.html b/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/xy-chart-viewer-nav-card/xy-chart-viewer-nav-card.component.html
index 01b2142..3dd144b 100644
--- a/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/xy-chart-viewer-nav-card/xy-chart-viewer-nav-card.component.html
+++ b/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/xy-chart-viewer-nav-card/xy-chart-viewer-nav-card.component.html
@@ -12,12 +12,12 @@
  *
  ********************************************************************************-->
 
-<div *ngIf="measurement === undefined || channelGroups === undefined || channels === undefined">
+<div *ngIf="measurement === undefined">
   <div class="alert alert-info" style="margin: 0;">
     <strong>{{'chartviewer.xy-chart-viewer-nav-card.no-node-selected' | translate}}</strong>
   </div>
 </div>
 
-<div *ngIf="measurement !== undefined && channelGroups !== undefined && channels !== undefined">
-  <app-xy-chart-viewer [channelGroups]="channelGroups" [channels]="channels"></app-xy-chart-viewer>
+<div *ngIf="measurement !== undefined">
+  <app-xy-chart-viewer [measurement]="measurement"></app-xy-chart-viewer>
 </div>
\ No newline at end of file
diff --git a/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/xy-chart-viewer-nav-card/xy-chart-viewer-nav-card.component.ts b/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/xy-chart-viewer-nav-card/xy-chart-viewer-nav-card.component.ts
index af4d82d..ae565e8 100644
--- a/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/xy-chart-viewer-nav-card/xy-chart-viewer-nav-card.component.ts
+++ b/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/xy-chart-viewer-nav-card/xy-chart-viewer-nav-card.component.ts
@@ -19,9 +19,15 @@
 import { TYPE_CHANNEL, TYPE_CHANNELGROUP, TYPE_MEASUREMENT } from '../../model/constants';
 import { Subscription, forkJoin } from 'rxjs';
 import { NodeService } from 'src/app/navigator/node.service';
-import { map, tap } from 'rxjs/operators';
+import { groupBy, map, tap } from 'rxjs/operators';
 import { MDMItem } from 'src/app/core/mdm-item';
 import { ChartViewerService } from '../../services/chart-viewer.service';
+import { ChannelGroup } from '../../model/types/channelgroup.class';
+import { Query, QueryService, Row } from 'src/app/tableview/query.service';
+import { Channel } from '../../model/types/channel.class';
+import { Measurement } from '../../model/types/measurement.class';
+import { nodeChildrenAsMap } from '@angular/router/src/utils/tree';
+import { ChartViewerDataService } from '../../services/chart-viewer-data.service';
 
 @Component({
   selector: 'app-xy-chart-viewer-nav-card',
@@ -29,9 +35,7 @@
 })
 export class XyChartViewerNavCardComponent implements OnInit, OnDestroy {
 
-  public channels = new Map<string, Node[]>();
-  public channelGroups: Node[];
-  public measurement: Node;
+  public measurement: Measurement;
 
   private selectedNode: Node;
 
@@ -42,7 +46,9 @@
     private navigatorService: NavigatorService,
     private notificationService: MDMNotificationService,
     private nodeService: NodeService,
-    private chartViewerService: ChartViewerService) {}
+    private chartViewerService: ChartViewerService,
+    private chartViewerDataService: ChartViewerDataService,
+    private queryService: QueryService) {}
 
   ngOnInit() {
     this.selectedNodeChanged(this.navigatorService.getSelectedNode());
@@ -83,96 +89,55 @@
         case TYPE_CHANNEL:
           this.selectedChannel(node);
           break;
+        default:
+          this.selectedNode = undefined;
+          this.measurement = undefined;
+          this.chartViewerService.sendNodeMeta(this.selectedNode);
       }
     }
   }
 
   // reload all, if channel is not present yet.
-  private selectedChannel(node: Node) {
-    if (!this.isChannelPresent(node)) {
-      this.loadMeasurement(node);
+  private selectedChannel(channel: Node) {
+    if (!this.isChannelPresent(channel)) {
+      this.loadMeasurement(TYPE_CHANNEL, channel);
     } else {
       this.chartViewerService.sendNodeMeta(this.selectedNode);
     }
   }
 
-  private selectedChannelGroup(node: Node) {
-    if (!this.isChannelGroupPresent(node)) {
-      this.loadMeasurement(node);
+  private selectedChannelGroup(channelGroup: Node) {
+    if (!this.isChannelGroupPresent(channelGroup)) {
+      this.loadMeasurement(TYPE_CHANNELGROUP, channelGroup);
     } else {
       this.chartViewerService.sendNodeMeta(this.selectedNode);
     }
   }
 
-  private selectedMeasurement(node: Node) {
-    if (this.measurement == undefined || this.measurement.id !== node.id) {
-      this.measurement = node;
-      this.loadChannelGroups(node);
+  private selectedMeasurement(measurement: Node) {
+    if (this.measurement == undefined || this.measurement.id !== measurement.id) {
+      this.loadMeasurement(TYPE_MEASUREMENT, measurement);
     } else {
       this.chartViewerService.sendNodeMeta(this.selectedNode);
     }
   }
 
+  private isChannelGroupPresent(node: Node) {
+    if (this.measurement) {
+     return this.measurement.findChannelGroup(node.id) !== undefined;
+    }
+  }
+
   private isChannelPresent(node: Node) {
-    if (this.channels != undefined) {
-      /**
-       * @TODO more readable code, but got the impression that performance is bad. Read up on performance topic in depth.
-       */
-      // Array.from(this.channels.values())
-      //       .reduce((a,b) => a.concat(b), [])
-      //       .findIndex(c => c.id === node.id);
-      let index = -1;
-      const iterator = this.channels.values();
-      let res = iterator.next();
-      while (index === -1 && !res.done) {
-        index = res.value.findIndex(c => c.id === node.id);
-        res = iterator.next();
-      }
-      return index > -1;
+    if (this.measurement) {
+      return this.measurement.findChannel(node.id) !== undefined;
     }
-    return false;
   }
 
-  private isChannelGroupPresent(node: Node) {
-    if (this.channelGroups != undefined) {
-      return this.channelGroups.findIndex(channelGroup => channelGroup.id === node.id) > -1;
-    }
-    return false;
+  private loadMeasurement(type: string, node: Node) {
+    this.chartViewerDataService.loadMeasurement(node).subscribe(mea => {
+      this.measurement = mea;
+      this.chartViewerService.sendNodeMeta(this.selectedNode);
+    });
   }
-
-  private loadMeasurement(node: Node) {
-    this.searchNodes(node, TYPE_MEASUREMENT)
-      .pipe(
-        /** @TODO in general true? */
-        // parent measurement must be distinct
-        map(measurements => measurements[0]),
-      ).subscribe(measurement => {
-        this.measurement = measurement;
-        this.loadChannelGroups(this.measurement);
-      });
-  }
-
-  private loadChannelGroups(measurement: Node) {
-    this.searchNodes(measurement, TYPE_CHANNELGROUP)
-        .subscribe(channelGroups => {
-          this.channelGroups = channelGroups;
-          if (this.channelGroups != undefined) {
-            this.loadChannels(channelGroups);
-          }
-        });
-  }
-
-  private loadChannels(channelGroups: Node[]) {
-    this.channels.clear();
-    let obsArray = channelGroups.map(channelGroup => this.searchNodes(channelGroup, TYPE_CHANNEL));
-    forkJoin(obsArray).pipe(
-        tap(array => array.forEach((channels, index) => this.channels.set(channelGroups[index].id, channels)))
-      ).subscribe(datasets => this.chartViewerService.sendNodeMeta(this.selectedNode));
-  }
-
-  private searchNodes(node: Node, targetType: string) {
-    const filter = 'filter=' + node.type + '.Id eq \"' + node.id + '\"';
-    return this.nodeService.searchNodes(filter, node.sourceName, targetType);
-  }
-
 }
diff --git a/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/xy-chart-viewer/xy-chart-viewer.component.html b/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/xy-chart-viewer/xy-chart-viewer.component.html
index 9fa9fbe..ac12ce8 100644
--- a/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/xy-chart-viewer/xy-chart-viewer.component.html
+++ b/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/xy-chart-viewer/xy-chart-viewer.component.html
@@ -26,8 +26,7 @@
   <!-- Axis selection -->
   <div [ngClass]="showSelection ? 'p-col-3' : 'hidden'">
     <mdm5-xy-chart-data-selection-panel
-      [channelGroups]="channelGroups"
-      [channels]="channels"
+      [measurement]="measurement"
       [xyMode]="xyMode"
       (onSelectionChanged)="onDataSelectionChanged($event)"
     ></mdm5-xy-chart-data-selection-panel>
@@ -36,14 +35,14 @@
   <div [ngClass]="showSelection ? 'p-col-9' : 'p-col-12'">
     <div [ngSwitch]="selectedMode">
       <p-chart #xyChart *ngSwitchCase="'chart'" type="scatter" [data]="data"></p-chart>
-      <mdm5-dataTable *ngSwitchCase="'table'" [channelGroupMap]="channelGroupMap"></mdm5-dataTable>
+      <mdm5-dataTable *ngSwitchCase="'table'" [measurement]="measurement"></mdm5-dataTable>
       <div *ngSwitchDefault></div>
     </div>
   </div>
 </div>
 <div class="p-grid">
   <div class="p-col-12">
-    <mdm5-request-options [channelGroups]="channelGroups" (onRequestOptionsChanged)="onRequestOptionsChanged($event)"></mdm5-request-options>
+    <mdm5-request-options [measurement]="measurement" (onRequestOptionsChanged)="onRequestOptionsChanged($event)"></mdm5-request-options>
   </div>
 </div>
 
diff --git a/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/xy-chart-viewer/xy-chart-viewer.component.ts b/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/xy-chart-viewer/xy-chart-viewer.component.ts
index 68447e6..0af5b88 100644
--- a/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/xy-chart-viewer/xy-chart-viewer.component.ts
+++ b/nucleus/webclient/src/main/webapp/src/app/chartviewer/components/xy-chart-viewer/xy-chart-viewer.component.ts
@@ -28,6 +28,8 @@
 import { MDMNotificationService } from 'src/app/core/mdm-notification.service';
 import { SelectItem } from 'primeng/api';
 import { TranslateService } from '@ngx-translate/core';
+import { Measurement } from '../../model/types/measurement.class';
+import { Channel } from '../../model/types/channel.class';
 
 @Component({
   selector: 'app-xy-chart-viewer',
@@ -40,11 +42,7 @@
   public chart: UIChart;
 
   @Input()
-  public channelGroups: Node[];
-  @Input()
-  public channels: Map<string, Node[]>;
-  // the channelgroup map for the data-table display
-  channelGroupMap: Map<Node, Node[]>;
+  public measurement: Measurement;
 
   // the chart data
   public data: ChartData;
@@ -87,15 +85,7 @@
    */
   public onDataSelectionChanged(rows: ChannelSelectionRow[]) {
     this.selectedChannelRows = rows;
-    this.channelGroupMap = new Map();
-    rows.forEach(row => {
-      let channels = this.channelGroupMap.get(row.channelGroup);
-      if (!channels) {
-        channels = [];
-      }
-      channels.push(row.yChannel);
-      this.channelGroupMap.set(row.channelGroup, channels);
-    });
+
     this.doChart();
   }
 
@@ -191,7 +181,7 @@
    * Loads measured values for y-channels, merges them with given values for x
    * @param xValues
    */
-  private loadYData(xValues: {yChannel: Node; measuredValues: MeasuredValues}[]) {
+  private loadYData(xValues: {yChannel: Channel; measuredValues: MeasuredValues}[]) {
     if (xValues != undefined) {
       const yChannels = this.groupYChannelsByChannelGroup(this.selectedChannelRows);
       // const channelGroups = this.selectedChannelGroups.filter(cg => this.arrayUtil.isNotEmpty(yChannels[cg.id]));
@@ -214,7 +204,7 @@
     }
   }
 
-  private extractXMeasuredValues(xValues: {yChannel: Node; measuredValues: MeasuredValues}[], channelId: string) {
+  private extractXMeasuredValues(xValues: {yChannel: Channel; measuredValues: MeasuredValues}[], channelId: string) {
     const val = xValues.find(v => v.yChannel.id === channelId);
     if (val != undefined ) {
       return val.measuredValues;
diff --git a/nucleus/webclient/src/main/webapp/src/app/chartviewer/model/types/channel-selection-row.ts b/nucleus/webclient/src/main/webapp/src/app/chartviewer/model/types/channel-selection-row.ts
index 23f0072..055ca38 100644
--- a/nucleus/webclient/src/main/webapp/src/app/chartviewer/model/types/channel-selection-row.ts
+++ b/nucleus/webclient/src/main/webapp/src/app/chartviewer/model/types/channel-selection-row.ts
@@ -12,13 +12,15 @@
  *

  ********************************************************************************/

 import {Node} from '../../../navigator/node';

+import { Channel } from './channel.class';

+import { ChannelGroup } from './channelgroup.class';

 

 export class ChannelSelectionRow {

-  channelGroup: Node;

-  xChannel: Node;

-  yChannel: Node;

+  channelGroup: ChannelGroup;

+  xChannel: Channel;

+  yChannel: Channel;

 

-  constructor(channelGroup: Node, yChannel: Node, xChannel?: Node) {

+  constructor(channelGroup: ChannelGroup, yChannel: Channel, xChannel?: Channel) {

     this.channelGroup = channelGroup;

     this.yChannel = yChannel;

     this.xChannel = xChannel;

diff --git a/nucleus/webclient/src/main/webapp/src/app/chartviewer/model/types/axis-info.class.ts b/nucleus/webclient/src/main/webapp/src/app/chartviewer/model/types/channel.class.ts
similarity index 66%
rename from nucleus/webclient/src/main/webapp/src/app/chartviewer/model/types/axis-info.class.ts
rename to nucleus/webclient/src/main/webapp/src/app/chartviewer/model/types/channel.class.ts
index ba252be..1760da8 100644
--- a/nucleus/webclient/src/main/webapp/src/app/chartviewer/model/types/axis-info.class.ts
+++ b/nucleus/webclient/src/main/webapp/src/app/chartviewer/model/types/channel.class.ts
@@ -1,5 +1,5 @@
 /********************************************************************************

- * Copyright (c) 2015-2018 Contributors to the Eclipse Foundation

+ * Copyright (c) 2015-2020 Contributors to the Eclipse Foundation

  *

  * See the NOTICE file(s) distributed with this work for additional

  * information regarding copyright ownership.

@@ -12,8 +12,18 @@
  *

  ********************************************************************************/

 

-export class AxisInfo { 

+import { TYPE_CHANNEL } from '../constants';

+

+export class Channel {

+    source: string;

+    type = TYPE_CHANNEL;

+    id: string;

+

     name: string;

+    channelGroupId: string;

+    isIndependent: boolean;

     axisType: string;

-    independent: boolean;

-}
\ No newline at end of file
+

+    constructor () {

+    }

+}

diff --git a/nucleus/webclient/src/main/webapp/src/app/chartviewer/model/types/channelgroup.class.ts b/nucleus/webclient/src/main/webapp/src/app/chartviewer/model/types/channelgroup.class.ts
new file mode 100644
index 0000000..bfe9aa8
--- /dev/null
+++ b/nucleus/webclient/src/main/webapp/src/app/chartviewer/model/types/channelgroup.class.ts
@@ -0,0 +1,29 @@
+/********************************************************************************

+ * Copyright (c) 2015-2020 Contributors to the Eclipse Foundation

+ *

+ * See the NOTICE file(s) distributed with this work for additional

+ * information regarding copyright ownership.

+ *

+ * This program and the accompanying materials are made available under the

+ * terms of the Eclipse Public License v. 2.0 which is available at

+ * http://www.eclipse.org/legal/epl-2.0.

+ *

+ * SPDX-License-Identifier: EPL-2.0

+ *

+ ********************************************************************************/

+

+import { TYPE_CHANNELGROUP } from '../constants';

+

+export class ChannelGroup {

+    source: string;

+    type = TYPE_CHANNELGROUP;

+    id: string;

+    name: string;

+    numberOfRows: number;

+

+    constructor (source: string, id: string, numberOfRows: number) {

+        this.source = source;

+        this.id = id;

+        this.numberOfRows = numberOfRows;

+    }

+}

diff --git a/nucleus/webclient/src/main/webapp/src/app/chartviewer/model/types/measurement.class.ts b/nucleus/webclient/src/main/webapp/src/app/chartviewer/model/types/measurement.class.ts
new file mode 100644
index 0000000..03dde20
--- /dev/null
+++ b/nucleus/webclient/src/main/webapp/src/app/chartviewer/model/types/measurement.class.ts
@@ -0,0 +1,101 @@
+/********************************************************************************

+ * Copyright (c) 2015-2020 Contributors to the Eclipse Foundation

+ *

+ * See the NOTICE file(s) distributed with this work for additional

+ * information regarding copyright ownership.

+ *

+ * This program and the accompanying materials are made available under the

+ * terms of the Eclipse Public License v. 2.0 which is available at

+ * http://www.eclipse.org/legal/epl-2.0.

+ *

+ * SPDX-License-Identifier: EPL-2.0

+ *

+ ********************************************************************************/

+

+import { TYPE_MEASUREMENT } from '../constants';

+import { Channel } from './channel.class';

+import { ChannelGroup } from './channelgroup.class';

+

+export class Measurement {

+

+    source: string;

+    type = TYPE_MEASUREMENT;

+    id: string;

+    name: string;

+    channelGroups: ChannelGroup[] = [];

+    channels: Map<string, Channel[]> = new Map();

+

+    constructor (m?: Measurement) {

+        this.source = m && m.source || '';

+        this.type = m && m.type || '';

+        this.id = m && m.id || '';

+        this.name = m && m.name || '';

+    }

+

+    put(channelGroup: ChannelGroup, channel: Channel) {

+        const cg = this.findChannelGroup(channelGroup.id);

+        if (cg === undefined) {

+            this.channelGroups.push(channelGroup);

+            this.channels.set(channelGroup.id, [channel]);

+        } else {

+            this.channels.get(channelGroup.id).push(channel);

+        }

+    }

+

+    getFirstChannelGroup() {

+        return this.channelGroups[0];

+    }

+

+    getIndependentChannel(channelGroup: ChannelGroup) {

+        let independentChannels = (this.findChannels(channelGroup) || []).filter(c => c.isIndependent);

+        if (independentChannels.length > 0) {

+            return independentChannels[0];

+        } else {

+            return undefined;

+        }

+    }

+

+    hasChannelGroup(channelGroupId: string) {

+        return this.findChannelGroup(channelGroupId) !== undefined;

+    }

+

+    hasChannel(channelId: string): unknown {

+        return this.findChannel(channelId) !== undefined;

+    }

+

+    findChannelGroup(channelGroupId: string) {

+        return this.channelGroups.find(cg => cg.id === channelGroupId);

+    }

+

+    findChannelGroupByChannel(channel: Channel) {

+        for (let entry of Array.from(this.channels.entries())) {

+            let c = entry[1].find(chan => chan.id === channel.id);

+            if (c !== undefined) {

+                return this.findChannelGroup(entry[0]);

+            }

+          }

+        return undefined;

+    }

+

+    findChannel(channelId: string) {

+        for (let entry of Array.from(this.channels.entries())) {

+            let c = entry[1].find(channel => channel.id === channelId);

+            if (c !== undefined) {

+                return c;

+            }

+          }

+        return undefined;

+    }

+    findChannels(channelGroup: ChannelGroup) {

+        return this.channels.get(channelGroup.id);

+    }

+

+    allChannels() {

+        let channels: Channel[] = [];

+

+        for (let entry of Array.from(this.channels.values())) {

+            channels.push(...entry);

+          }

+        return channels;

+    }

+}

diff --git a/nucleus/webclient/src/main/webapp/src/app/chartviewer/services/chart-viewer-data.service.ts b/nucleus/webclient/src/main/webapp/src/app/chartviewer/services/chart-viewer-data.service.ts
index ccd3732..7bf05df 100644
--- a/nucleus/webclient/src/main/webapp/src/app/chartviewer/services/chart-viewer-data.service.ts
+++ b/nucleus/webclient/src/main/webapp/src/app/chartviewer/services/chart-viewer-data.service.ts
@@ -16,15 +16,18 @@
 import { Injectable } from '@angular/core';
 
 import { plainToClass } from 'class-transformer';
-import { map, tap, catchError } from 'rxjs/operators';
+import { map, catchError } from 'rxjs/operators';
 
 import { HttpErrorHandler } from '../../core/http-error-handler';
 import { PropertyService } from '../../core/property.service';
 import { Node } from '../../navigator/node';
 import { MeasuredValuesResponse, MeasuredValues, PreviewValueList } from '../model/chartviewer.model';
 import { QueryService, Query, SearchResult, Row } from '../../tableview/query.service';
-import { Observable } from 'rxjs';
-import { AxisInfo } from '../model/types/axis-info.class';
+import { throwError } from 'rxjs';
+import { ChannelGroup } from '../model/types/channelgroup.class';
+import { Channel } from '../model/types/channel.class';
+import { Measurement } from '../model/types/measurement.class';
+import { TYPE_CHANNEL, TYPE_CHANNELGROUP, TYPE_MEASUREMENT } from '../model/constants';
 
 export function getDataArray(m: MeasuredValues) {
   if (m.scalarType === 'INTEGER') {
@@ -73,7 +76,7 @@
    * @param requestSize number of requested values
    * @param chunks preview chunks. If 0, preview is disabled and original values are loaded
    */
-  loadValues(channelGroup: Node, channels: Node[], startIndex = 0, requestSize = 0, chunks = 0) {
+  loadValues(channelGroup: ChannelGroup, channels: Channel[], startIndex = 0, requestSize = 0, chunks = 0) {
     if (chunks > 0) {
       return this.loadPreviewValues(channelGroup, channels, chunks, startIndex, requestSize);
     } else {
@@ -88,7 +91,7 @@
    * @param startIndex start index to load the data
    * @param requestSize number of requested values
    */
-  loadMeasuredValues(channelGroup: Node, channels: Node[], startIndex = 0, requestSize = 0) {
+  loadMeasuredValues(channelGroup: ChannelGroup, channels: Channel[], startIndex = 0, requestSize = 0) {
     let readRequest = {
       'channelGroupId': channelGroup.id,
       'channelIds': channels !== undefined ? channels.filter(channel => channel.type === 'Channel').map(channel => channel.id) : [],
@@ -97,12 +100,11 @@
       'valuesMode': 'CALCULATED'
     };
 
-    return this.http.post<MeasuredValuesResponse>(this._contextUrl + '/' + channelGroup.sourceName + '/values/read',
+    return this.http.post<MeasuredValuesResponse>(this._contextUrl + '/' + channelGroup.source + '/values/read',
       readRequest, this.httpOptions)
       .pipe(
         map(res => plainToClass(MeasuredValues, res.values)),
         map(measurementValues => measurementValues.sort((a, b) => a.name.localeCompare(b.name))),
-        // tap(r => console.log(r)),
         catchError(this.httpErrorHandler.handleError)
       );
   }
@@ -115,7 +117,7 @@
    * @param startIndex start index to load the data
    * @param requestSize number of requested values
    */
-  loadPreviewValues(channelGroup: Node, channels: Node[], chunks: number, startIndex = 0, requestSize = 0) {
+  loadPreviewValues(channelGroup: ChannelGroup, channels: Channel[], chunks: number, startIndex = 0, requestSize = 0) {
     let readRequest = {
       'channelGroupId': channelGroup.id,
       'channelIds': channels !== undefined ? channels.filter(channel => channel.type === 'Channel').map(channel => channel.id) : [],
@@ -128,7 +130,7 @@
       'readRequest': readRequest
     };
 
-    return this.http.post<PreviewValueList>(this._contextUrl + '/' + channelGroup.sourceName + '/values/preview',
+    return this.http.post<PreviewValueList>(this._contextUrl + '/' + channelGroup.source + '/values/preview',
       previewRequest, this.httpOptions)
       .pipe(
         map(res => plainToClass(MeasuredValues, res.avg)),
@@ -136,39 +138,47 @@
       );
   }
 
-  loadAxisInformation(channelGroup: Node) : Observable<AxisInfo[]>{
+  loadMeasurement(node: Node) {
+    if (node.type !== TYPE_MEASUREMENT && node.type !== TYPE_CHANNELGROUP && node.type !== TYPE_CHANNEL) {
+      return throwError('Node must be of type: ' + TYPE_MEASUREMENT + ' or ' + TYPE_CHANNELGROUP + ' or ' + TYPE_CHANNEL);
+    }
     const query = new Query();
-    query.addFilter(channelGroup.sourceName, 'ChannelGroup.Id eq ' + channelGroup.id);
-    query.columns = ['Channel.Name', 'LocalColumn.axistype', 'LocalColumn.IndependentFlag'];
+    query.addFilter(node.sourceName, node.type + '.Id eq ' + node.id);
+    query.columns = ['Measurement.Id', 'ChannelGroup.Id', 'ChannelGroup.Name', 'ChannelGroup.SubMatrixNoRows',
+      'Channel.Id', 'Channel.Name', 'LocalColumn.axistype', 'LocalColumn.IndependentFlag'];
     query.resultType = 'Channel';
+
     return this.queryService.query(query).pipe(
-      map(res => plainToClass(SearchResult, res)),
-      map(r => r.rows.map(row => { return { 
-        name: row.getColumn('Channel.Name'), 
-        axisType: row.getColumn('LocalColumn.axistype'), 
-        independent: row.getColumn('LocalColumn.IndependentFlag') == '1'
-      }})),
-      map(ai => ai.sort((a, b) => a.name.localeCompare(b.name))),
+      map(sr => sr.rows.map(row => this.mapRow(row))),
+      map(pairs => {
+        let mea = new Measurement();
+        mea.type = TYPE_MEASUREMENT;
+
+        pairs.forEach(entry => {
+          mea.put(entry.channelGroup, entry.channel);
+          mea.source = entry.channelGroup.source;
+          mea.id = entry.meaId;
+        });
+
+        return mea;
+      }),
     );
   }
 
-  getNumberOfRows(channelGroup: Node) {
-    let attr = channelGroup !== undefined && channelGroup.attributes !== undefined ?
-      channelGroup.attributes.find(a => a.name === 'SubMatrixNoRows') : undefined;
-    if (attr === undefined) {
-      // TODO
-      console.log('Could not find attribute SubMatrixNoRows on ChannelGroup with id ' +
-        (channelGroup !== undefined ? channelGroup.id : 'undefined'));
-      return;
-    }
-    return parseInt(<string> attr.value, 10);
-  }
+  private mapRow(row: Row) {
+    let channel = new Channel();
+    channel.id = row.id;
+    channel.name = row.getColumn('Channel.Name');
+    channel.channelGroupId = row.getColumn('ChannelGroup.SubMatrixNoRows');
+    channel.axisType = row.getColumn('LocalColumn.axistype'),
+    channel.isIndependent = row.getColumn('LocalColumn.IndependentFlag') === '1';
 
-  loadChannelsForGroup(channelGroup: Node) {
-    const query = new Query();
-    query.addFilter(channelGroup.sourceName, 'ChannelGroup.Id eq ' + channelGroup.id);
-    query.columns = ['Channel.Id', 'Channel.Name', 'ChannelGroup.Id'];
-    query.resultType = 'Channel';
-    return this.queryService.query(query);
+    let channelGroup = new ChannelGroup(
+      row.source,
+      row.getColumn('ChannelGroup.Id'),
+      parseInt(row.getColumn('ChannelGroup.SubMatrixNoRows'), 10));
+
+    channelGroup.name = row.getColumn('ChannelGroup.Name');
+    return { meaId: row.getColumn('Measurement.Id'), channelGroup: channelGroup, channel: channel };
   }
 }
diff --git a/nucleus/webclient/src/main/webapp/src/assets/i18n/de.json b/nucleus/webclient/src/main/webapp/src/assets/i18n/de.json
index 21691a0..0917129 100644
--- a/nucleus/webclient/src/main/webapp/src/assets/i18n/de.json
+++ b/nucleus/webclient/src/main/webapp/src/assets/i18n/de.json
@@ -440,7 +440,7 @@
       "apply": "Übernehmen"
     },
     "chart-viewer-nav-card": {
-      "load-missing-channels": "{{amount}} Kanäle nachladen"
+      "load-missing-channels": "{{limit}} Kanäle nachladen ({{offset}}/{{total}})"
     },
     "xy-chart-data-selection-panel": {
       "select-channel-placeholder": "Kanal wählen",
diff --git a/nucleus/webclient/src/main/webapp/src/assets/i18n/en.json b/nucleus/webclient/src/main/webapp/src/assets/i18n/en.json
index 125bf15..7f59191 100644
--- a/nucleus/webclient/src/main/webapp/src/assets/i18n/en.json
+++ b/nucleus/webclient/src/main/webapp/src/assets/i18n/en.json
@@ -440,7 +440,7 @@
       "apply": "Apply"
     },
     "chart-viewer-nav-card": {
-      "load-missing-channels": "Load {{amount}} channels"
+      "load-missing-channels": "Load {{limit}} channels ({{offset}}/{{total}})"
     },
     "xy-chart-data-selection-panel": {
       "select-channel-placeholder": "Select Channel",