This is an automated email from the ASF dual-hosted git repository.

riemer pushed a commit to branch improve-data-explorer-widgets
in repository https://gitbox.apache.org/repos/asf/streampipes.git

commit 789bc67a2278a5602a186be29fc930b226113f4c
Author: Dominik Riemer <[email protected]>
AuthorDate: Mon Aug 28 20:20:23 2023 +0200

    Improve rendering of echarts widgets in data explorer
---
 ui/deployment/appng5.module.mst                    |  13 +
 ui/package-lock.json                               |  41 +++
 ui/package.json                                    |   3 +
 .../echarts-transform/histogram.transform.ts       |  78 +++++
 .../echarts-transform/round-values.transform.ts    |  75 +++++
 .../value-distribution.transform.ts                |  79 +++++
 ...ta-explorer-visualisation-settings.component.ts |   2 +-
 .../base-data-explorer-echarts-widget.directive.ts | 112 +++++++
 .../base/base-data-explorer-widget.directive.ts    |  16 +-
 .../echarts-widget.component.html}                 |  22 +-
 .../widgets/base/echarts-widget.component.scss     |   6 +
 ...distribution-chart-widget-config.component.html |  75 ++++-
 .../distribution-chart-widget-config.component.ts  |  29 +-
 .../distribution-chart-widget.component.html       |  65 -----
 .../distribution-chart-widget.component.ts         | 195 ++-----------
 .../model/distribution-chart-widget.model.ts       |   5 +
 .../value-heatmap/value-heatmap.component.ts       | 188 ------------
 .../widgets/heatmap/heatmap-widget.component.ts    |  65 +----
 .../time-series-chart-widget.component.html        |   2 +
 .../time-series-chart-widget.component.ts          |   4 +
 ui/src/app/data-explorer/data-explorer.module.ts   |   2 -
 .../echarts-renderer/sp-base-echarts-renderer.ts   | 324 +++++++++++++++++++++
 .../sp-base-single-field-echarts-renderer.ts       | 121 ++++++++
 .../sp-heatmap-renderer.ts}                        |  20 ++
 .../echarts-renderer/sp-histogram-renderer.ts      |  78 +++++
 .../echarts-renderer/sp-pie-renderer.ts            | 100 +++++++
 .../echarts-renderer/sp-value-heatmap-renderer.ts  | 101 +++++++
 .../dataset.model.ts}                              |  38 +++
 .../models/dataview-dashboard.model.ts             |  20 +-
 .../registry/data-explorer-widget-registry.ts      |   8 +-
 .../registry/data-explorer-widgets.ts              |  10 +-
 31 files changed, 1393 insertions(+), 504 deletions(-)

diff --git a/ui/deployment/appng5.module.mst b/ui/deployment/appng5.module.mst
index 7751d0c59..ae5630a3b 100644
--- a/ui/deployment/appng5.module.mst
+++ b/ui/deployment/appng5.module.mst
@@ -54,6 +54,19 @@ import { AvailableRolesService } from 
'./services/available-roles.service';
 
 import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
 
+import { ValueDistributionTransform } from 
'./core-ui/echarts-transform/value-distribution.transform';
+import { HistogramTransform } from 
'./core-ui/echarts-transform/histogram.transform';
+import { RoundValuesTransform} from 
'./core-ui/echarts-transform/round-values.transform';
+
+import * as echarts from 'echarts';
+import * as transform from 'echarts-simple-transform';
+
+echarts.registerTransform(transform.aggregate);
+echarts.registerTransform(ValueDistributionTransform);
+echarts.registerTransform(HistogramTransform);
+echarts.registerTransform(RoundValuesTransform);
+
+
 import * as $ from 'jquery';
 
 @NgModule({
diff --git a/ui/package-lock.json b/ui/package-lock.json
index ab368ab59..9131d78c9 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -38,10 +38,13 @@
         "angular2-uuid": "1.1.1",
         "codemirror": "^5.65.11",
         "console-browserify": "^1.2.0",
+        "d3-array": "^3.2.4",
         "dagre": "0.8.5",
         "datatables.net": "^1.13.1",
         "datatables.net-dt": "^1.13.1",
         "echarts": "^5.4.1",
+        "echarts-simple-transform": "^1.0.0",
+        "echarts-stat": "^1.2.0",
         "echarts-wordcloud": "^2.1.0",
         "file-saver": "2.0.5",
         "jquery": "^3.7.0",
@@ -11735,6 +11738,24 @@
         "zrender": "5.4.1"
       }
     },
+    "node_modules/echarts-simple-transform": {
+      "version": "1.0.0",
+      "resolved": 
"https://registry.npmjs.org/echarts-simple-transform/-/echarts-simple-transform-1.0.0.tgz";,
+      "integrity": 
"sha512-zidekQFwV7s2rLUBVvum9d7kbhX2uoVWqDNYWv+MciEbjBzlhdvLDzciSgeBNe9bwEiqVfGC6NP99zjo3JXDIg==",
+      "dependencies": {
+        "tslib": "2.0.3"
+      }
+    },
+    "node_modules/echarts-simple-transform/node_modules/tslib": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz";,
+      "integrity": 
"sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
+    },
+    "node_modules/echarts-stat": {
+      "version": "1.2.0",
+      "resolved": 
"https://registry.npmjs.org/echarts-stat/-/echarts-stat-1.2.0.tgz";,
+      "integrity": 
"sha512-zLd7Kgs+tuTSeaK0VQEMNmnMivEkhvHIk1gpBtLzpRerfcIQ+Bd5XudOMmtwpaTc1WDZbA7d1V//iiBccR46Qg=="
+    },
     "node_modules/echarts-wordcloud": {
       "version": "2.1.0",
       "resolved": 
"https://registry.npmjs.org/echarts-wordcloud/-/echarts-wordcloud-2.1.0.tgz";,
@@ -36354,6 +36375,26 @@
         }
       }
     },
+    "echarts-simple-transform": {
+      "version": "1.0.0",
+      "resolved": 
"https://registry.npmjs.org/echarts-simple-transform/-/echarts-simple-transform-1.0.0.tgz";,
+      "integrity": 
"sha512-zidekQFwV7s2rLUBVvum9d7kbhX2uoVWqDNYWv+MciEbjBzlhdvLDzciSgeBNe9bwEiqVfGC6NP99zjo3JXDIg==",
+      "requires": {
+        "tslib": "2.0.3"
+      },
+      "dependencies": {
+        "tslib": {
+          "version": "2.0.3",
+          "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz";,
+          "integrity": 
"sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
+        }
+      }
+    },
+    "echarts-stat": {
+      "version": "1.2.0",
+      "resolved": 
"https://registry.npmjs.org/echarts-stat/-/echarts-stat-1.2.0.tgz";,
+      "integrity": 
"sha512-zLd7Kgs+tuTSeaK0VQEMNmnMivEkhvHIk1gpBtLzpRerfcIQ+Bd5XudOMmtwpaTc1WDZbA7d1V//iiBccR46Qg=="
+    },
     "echarts-wordcloud": {
       "version": "2.1.0",
       "resolved": 
"https://registry.npmjs.org/echarts-wordcloud/-/echarts-wordcloud-2.1.0.tgz";,
diff --git a/ui/package.json b/ui/package.json
index 2e88b61cb..a5236546c 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -60,11 +60,14 @@
     "angular2-uuid": "1.1.1",
     "codemirror": "^5.65.11",
     "console-browserify": "^1.2.0",
+    "d3-array": "^3.2.4",
     "dagre": "0.8.5",
     "datatables.net": "^1.13.1",
     "datatables.net-dt": "^1.13.1",
     "echarts": "^5.4.1",
     "echarts-wordcloud": "^2.1.0",
+    "echarts-stat": "^1.2.0",
+    "echarts-simple-transform": "^1.0.0",
     "file-saver": "2.0.5",
     "jquery": "^3.7.0",
     "jquery-ui-dist": "1.13.2",
diff --git a/ui/src/app/core-ui/echarts-transform/histogram.transform.ts 
b/ui/src/app/core-ui/echarts-transform/histogram.transform.ts
new file mode 100644
index 000000000..ba5c9f086
--- /dev/null
+++ b/ui/src/app/core-ui/echarts-transform/histogram.transform.ts
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import {
+    DataTransformOption,
+    ExternalDataTransform,
+    ExternalDataTransformResultItem,
+} from 'echarts/types/src/data/helper/transform';
+
+import { bin } from 'd3-array';
+import { OptionSourceDataArrayRows } from 'echarts/types/src/util/types';
+
+export interface HistogramConfig extends DataTransformOption {
+    field: string;
+    autoBin: boolean;
+    numberOfBins: number;
+    autoDomain: boolean;
+    domainMin: number;
+    domainMax: number;
+}
+
+export const HistogramTransform: ExternalDataTransform<HistogramConfig> = {
+    type: 'sp:histogram',
+
+    transform: function (
+        params,
+    ): ExternalDataTransformResultItem | ExternalDataTransformResultItem[] {
+        const upstream = params.upstream;
+        const clonedData = upstream.cloneRawData();
+        const field = params.config['field'];
+        const autoBin = params.config['autoBin'];
+        const numberOfBins = +params.config['numberOfBins'];
+        const autoDomain = params.config['autoDomain'];
+        const domainMin = +params.config['domainMin'];
+        const domainMax = +params.config['domainMax'];
+        const dimension = upstream.getDimensionInfo(field);
+        const values = (clonedData as any).map(row => row[dimension.index]);
+        values.shift();
+
+        let bins = bin();
+        if (!autoBin && numberOfBins) {
+            bins = bins.thresholds(numberOfBins);
+        }
+        if (!autoDomain && domainMin && domainMax) {
+            bins = bins.domain([domainMin, domainMax]);
+        }
+        const d3h = bins(values);
+        const source: OptionSourceDataArrayRows = [];
+        const hist: number[] = d3h.map(item => item.length);
+        const binEdges = d3h.map(item => item.x0);
+        if (d3h.length) {
+            binEdges.push(d3h.at(-1).x1);
+        }
+        hist.forEach((val, index) => {
+            source.push([binEdges[index] + '-' + binEdges[index + 1], val]);
+        });
+
+        return {
+            data: source,
+            dimensions: ['edge', 'hist'],
+        };
+    },
+};
diff --git a/ui/src/app/core-ui/echarts-transform/round-values.transform.ts 
b/ui/src/app/core-ui/echarts-transform/round-values.transform.ts
new file mode 100644
index 000000000..6c9cd3e5f
--- /dev/null
+++ b/ui/src/app/core-ui/echarts-transform/round-values.transform.ts
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import {
+    DataTransformOption,
+    ExternalDataTransform,
+    ExternalDataTransformResultItem,
+} from 'echarts/types/src/data/helper/transform';
+import { DataExplorerField } from '@streampipes/platform-services';
+import { OptionSourceDataArrayRows } from 'echarts/types/src/util/types';
+
+export interface RoundValuesConfig extends DataTransformOption {
+    fields: DataExplorerField[];
+    roundingValue: number;
+}
+
+export const RoundValuesTransform: ExternalDataTransform<RoundValuesConfig> = {
+    type: 'sp:round',
+
+    transform: function (
+        params,
+    ): ExternalDataTransformResultItem | ExternalDataTransformResultItem[] {
+        const fields: string[] = (
+            params.config['fields'] as DataExplorerField[]
+        )
+            .filter(f => f.fieldCharacteristics.numeric)
+            .map(f => f.fullDbName);
+        const roundingValue = +params.config['roundingValue'];
+        const upstream = params.upstream;
+
+        const dimsDef = upstream.cloneAllDimensionInfo();
+
+        const roundColumnIndices = dimsDef
+            .filter(dim => fields.indexOf(dim.name) > -1)
+            .map(dim => dim.index);
+
+        const data = upstream.cloneRawData() as OptionSourceDataArrayRows;
+        const result = data.map((row, index) => {
+            if (index == 0) {
+                return row;
+            } else {
+                return row.map((value, index) => {
+                    if (roundColumnIndices.indexOf(index) > -1) {
+                        return (
+                            Math.round((value as number) / roundingValue) *
+                            roundingValue
+                        );
+                    } else {
+                        return value;
+                    }
+                });
+            }
+        });
+        result.shift();
+        return {
+            dimensions: upstream.cloneAllDimensionInfo(),
+            data: result,
+        };
+    },
+};
diff --git 
a/ui/src/app/core-ui/echarts-transform/value-distribution.transform.ts 
b/ui/src/app/core-ui/echarts-transform/value-distribution.transform.ts
new file mode 100644
index 000000000..bc88eecf5
--- /dev/null
+++ b/ui/src/app/core-ui/echarts-transform/value-distribution.transform.ts
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import {
+    DataTransformOption,
+    ExternalDataTransform,
+    ExternalDataTransformResultItem,
+} from 'echarts/types/src/data/helper/transform';
+
+export interface ValueDistributionConfig extends DataTransformOption {
+    field: string;
+    resolution: number;
+}
+
+export const ValueDistributionTransform: 
ExternalDataTransform<ValueDistributionConfig> =
+    {
+        type: 'sp:value-distribution',
+
+        transform: function (
+            params,
+        ): ExternalDataTransformResultItem | ExternalDataTransformResultItem[] 
{
+            const upstream = params.upstream;
+            const clonedData = upstream.cloneRawData();
+            const field = params.config['field'];
+            const resolution = +params.config['resolution'];
+            const dimension = upstream.getDimensionInfo(field);
+
+            const dataResult = [];
+            const allValues = (clonedData as any).map(
+                row => row[dimension.index],
+            );
+            allValues.shift();
+            const total = allValues.length;
+            let currentCount = 0;
+            allValues.sort((a, b) => a - b);
+            let start = allValues[0];
+            for (let i = 0; i < allValues.length; i++) {
+                const value = allValues[i];
+                if (value < start + +resolution) {
+                    currentCount += 1;
+                }
+                if (value >= start + resolution || i + 1 === allValues.length) 
{
+                    const currentRange =
+                        '>' +
+                        start.toFixed(2) +
+                        (i + 1 < allValues.length
+                            ? '<' + allValues[i + 1].toFixed(2)
+                            : '');
+                    dataResult.push([
+                        currentRange,
+                        0,
+                        (currentCount + 1) / total,
+                    ]);
+                    currentCount = 0;
+                    start = allValues[i + 1];
+                }
+            }
+
+            return {
+                data: dataResult,
+                dimensions: ['category', 'group', field],
+            };
+        },
+    };
diff --git 
a/ui/src/app/data-explorer/components/designer-panel/visualisation-settings/data-explorer-visualisation-settings.component.ts
 
b/ui/src/app/data-explorer/components/designer-panel/visualisation-settings/data-explorer-visualisation-settings.component.ts
index f509b3b97..52e95293c 100644
--- 
a/ui/src/app/data-explorer/components/designer-panel/visualisation-settings/data-explorer-visualisation-settings.component.ts
+++ 
b/ui/src/app/data-explorer/components/designer-panel/visualisation-settings/data-explorer-visualisation-settings.component.ts
@@ -33,7 +33,7 @@ export class DataExplorerVisualisationSettingsComponent 
implements OnInit {
 
     constructor(private widgetTypeService: WidgetTypeService) {}
 
-    availableWidgets: IWidget[];
+    availableWidgets: IWidget<any>[];
 
     ngOnInit(): void {
         this.availableWidgets =
diff --git 
a/ui/src/app/data-explorer/components/widgets/base/base-data-explorer-echarts-widget.directive.ts
 
b/ui/src/app/data-explorer/components/widgets/base/base-data-explorer-echarts-widget.directive.ts
new file mode 100644
index 000000000..7bf1e1dcb
--- /dev/null
+++ 
b/ui/src/app/data-explorer/components/widgets/base/base-data-explorer-echarts-widget.directive.ts
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { Directive, OnInit } from '@angular/core';
+import {
+    DataExplorerWidgetModel,
+    SpQueryResult,
+} from '@streampipes/platform-services';
+import { SpEchartsRenderer } from '../../../models/dataview-dashboard.model';
+import { BaseDataExplorerWidgetDirective } from 
'./base-data-explorer-widget.directive';
+import { ECharts } from 'echarts/core';
+import { EChartsOption } from 'echarts';
+import { Subject, Subscription } from 'rxjs';
+import { debounceTime } from 'rxjs/operators';
+
+@Directive()
+export abstract class BaseDataExplorerEchartsWidgetDirective<
+        T extends DataExplorerWidgetModel,
+    >
+    extends BaseDataExplorerWidgetDirective<T>
+    implements OnInit
+{
+    eChartsInstance: ECharts;
+    currentWidth: number;
+    currentHeight: number;
+
+    option: EChartsOption;
+
+    configReady = false;
+    latestData: SpQueryResult[];
+
+    renderSubject = new Subject<void>();
+    renderSubjectSubscription: Subscription;
+
+    ngOnInit(): void {
+        super.ngOnInit();
+        this.initOptions();
+        this.renderSubjectSubscription = this.renderSubject
+            .pipe(debounceTime(300))
+            .subscribe(() => {
+                this.renderChartOptions(this.latestData);
+            });
+    }
+
+    beforeDataFetched() {}
+
+    onDataReceived(spQueryResult: SpQueryResult[]) {
+        this.renderChartOptions(spQueryResult);
+        this.latestData = spQueryResult;
+        this.setShownComponents(false, true, false, false);
+    }
+
+    onResize(width: number, height: number) {
+        this.currentWidth = width;
+        this.currentHeight = height;
+        this.configReady = true;
+        this.applySize(width, height);
+        if (this.latestData) {
+            this.renderSubject.next();
+        }
+    }
+
+    onChartInit(ec: ECharts) {
+        this.eChartsInstance = ec;
+        this.applySize(this.currentWidth, this.currentHeight);
+        this.initOptions();
+    }
+
+    applySize(width: number, height: number) {
+        if (this.eChartsInstance) {
+            this.eChartsInstance.resize({ width, height });
+        }
+    }
+
+    renderChartOptions(spQueryResult: SpQueryResult[]): void {
+        this.option = {
+            ...this.getRenderer().render(
+                spQueryResult,
+                this.dataExplorerWidget,
+                { width: this.currentWidth, height: this.currentHeight },
+            ),
+        };
+    }
+
+    refreshView() {
+        this.renderSubject.next();
+    }
+
+    abstract getRenderer(): SpEchartsRenderer<T>;
+
+    initOptions() {}
+
+    public cleanupSubscriptions(): void {
+        super.cleanupSubscriptions();
+        this.renderSubjectSubscription.unsubscribe();
+    }
+}
diff --git 
a/ui/src/app/data-explorer/components/widgets/base/base-data-explorer-widget.directive.ts
 
b/ui/src/app/data-explorer/components/widgets/base/base-data-explorer-widget.directive.ts
index cc197ac2c..daef0ac5c 100644
--- 
a/ui/src/app/data-explorer/components/widgets/base/base-data-explorer-widget.directive.ts
+++ 
b/ui/src/app/data-explorer/components/widgets/base/base-data-explorer-widget.directive.ts
@@ -93,6 +93,9 @@ export abstract class BaseDataExplorerWidgetDirective<
     resizeSub: Subscription;
     timeSelectionSub: Subscription;
 
+    widthOffset: number;
+    heightOffset: number;
+
     requestQueue$: Subject<Observable<SpQueryResult>[]> = new Subject<
         Observable<SpQueryResult>[]
     >();
@@ -107,8 +110,8 @@ export abstract class BaseDataExplorerWidgetDirective<
     ) {}
 
     ngOnInit(): void {
-        const heightOffset = this.gridMode ? 70 : 65;
-        const widthOffset = this.gridMode ? 10 : 10;
+        this.heightOffset = this.gridMode ? 70 : 65;
+        this.widthOffset = this.gridMode ? 10 : 10;
         this.showData = true;
         const sourceConfigs = this.dataExplorerWidget.dataConfig.sourceConfigs;
         this.fieldProvider =
@@ -181,8 +184,9 @@ export abstract class BaseDataExplorerWidgetDirective<
                 info => {
                     if (info.gridsterItem.id === this.dataExplorerWidget._id) {
                         this.onResize(
-                            this.gridsterItemComponent.width - widthOffset,
-                            this.gridsterItemComponent.height - heightOffset,
+                            this.gridsterItemComponent.width - 
this.widthOffset,
+                            this.gridsterItemComponent.height -
+                                this.heightOffset,
                         );
                     }
                 },
@@ -197,8 +201,8 @@ export abstract class BaseDataExplorerWidgetDirective<
             );
         this.updateData();
         this.onResize(
-            this.gridsterItemComponent.width - widthOffset,
-            this.gridsterItemComponent.height - heightOffset,
+            this.gridsterItemComponent.width - this.widthOffset,
+            this.gridsterItemComponent.height - this.heightOffset,
         );
     }
 
diff --git 
a/ui/src/app/data-explorer/components/widgets/distribution-chart/value-heatmap/value-heatmap.component.html
 
b/ui/src/app/data-explorer/components/widgets/base/echarts-widget.component.html
similarity index 64%
rename from 
ui/src/app/data-explorer/components/widgets/distribution-chart/value-heatmap/value-heatmap.component.html
rename to 
ui/src/app/data-explorer/components/widgets/base/echarts-widget.component.html
index 4af8d0181..80b5ecc3a 100644
--- 
a/ui/src/app/data-explorer/components/widgets/distribution-chart/value-heatmap/value-heatmap.component.html
+++ 
b/ui/src/app/data-explorer/components/widgets/base/echarts-widget.component.html
@@ -16,13 +16,27 @@
   ~
   -->
 
-<div fxFlex="100">
+<div class="main-panel">
+    <sp-no-data-in-date-range
+        *ngIf="showNoDataInDateRange"
+        [viewDateRange]="timeSettings"
+        class="h-100"
+    >
+    </sp-no-data-in-date-range>
+
+    <sp-too-much-data
+        [amountOfEvents]="amountOfTooMuchEvents"
+        (loadDataWithTooManyEventsEmitter)="loadDataWithTooManyEvents()"
+        *ngIf="showTooMuchData"
+        class="h-100"
+    >
+    </sp-too-much-data>
+
     <div
         echarts
         [options]="option"
-        [ngStyle]="{ width: currentWidth_, height: '100%' }"
+        [ngStyle]="{ width: currentWidth, height: '100%' }"
         (chartInit)="onChartInit($event)"
-        [merge]="dynamic"
-        *ngIf="configReady"
+        *ngIf="option"
     ></div>
 </div>
diff --git 
a/ui/src/app/data-explorer/components/widgets/base/echarts-widget.component.scss
 
b/ui/src/app/data-explorer/components/widgets/base/echarts-widget.component.scss
new file mode 100644
index 000000000..43abc27d7
--- /dev/null
+++ 
b/ui/src/app/data-explorer/components/widgets/base/echarts-widget.component.scss
@@ -0,0 +1,6 @@
+.main-panel {
+    width: 100%;
+    height: 100%;
+    align-content: center;
+    display: block;
+}
diff --git 
a/ui/src/app/data-explorer/components/widgets/distribution-chart/config/distribution-chart-widget-config.component.html
 
b/ui/src/app/data-explorer/components/widgets/distribution-chart/config/distribution-chart-widget-config.component.html
index b6707989c..cbb230145 100644
--- 
a/ui/src/app/data-explorer/components/widgets/distribution-chart/config/distribution-chart-widget-config.component.html
+++ 
b/ui/src/app/data-explorer/components/widgets/distribution-chart/config/distribution-chart-widget-config.component.html
@@ -51,6 +51,75 @@
     >
     </sp-select-property>
 
+    <div
+        fxLayoutGap="10px"
+        fxLayout="column"
+        *ngIf="
+            currentlyConfiguredWidget.visualizationConfig.displayType ===
+            'histogram'
+        "
+    >
+        <mat-checkbox
+            [(ngModel)]="currentlyConfiguredWidget.visualizationConfig.autoBin"
+            (change)="triggerDataRefresh()"
+        >
+            Auto-compute number of bins
+        </mat-checkbox>
+        <mat-form-field
+            fxFlex="100"
+            color="accent"
+            appearance="outline"
+            *ngIf="!currentlyConfiguredWidget.visualizationConfig.autoBin"
+        >
+            <mat-label><span>Number of bins</span></mat-label>
+            <input
+                [(ngModel)]="
+                    currentlyConfiguredWidget.visualizationConfig.numberOfBins
+                "
+                matInput
+                (input)="onNumBinChange($event.target.value)"
+            />
+        </mat-form-field>
+        <mat-checkbox
+            [(ngModel)]="
+                currentlyConfiguredWidget.visualizationConfig.autoDomain
+            "
+            (change)="triggerViewRefresh()"
+        >
+            All values
+        </mat-checkbox>
+        <mat-form-field
+            fxFlex="100"
+            color="accent"
+            appearance="outline"
+            *ngIf="!currentlyConfiguredWidget.visualizationConfig.autoDomain"
+        >
+            <mat-label><span>Minimum value</span></mat-label>
+            <input
+                [(ngModel)]="
+                    currentlyConfiguredWidget.visualizationConfig.domainMin
+                "
+                matInput
+                (input)="onDomainMinChange($event.target.value)"
+            />
+        </mat-form-field>
+        <mat-form-field
+            fxFlex="100"
+            color="accent"
+            appearance="outline"
+            *ngIf="!currentlyConfiguredWidget.visualizationConfig.autoDomain"
+        >
+            <mat-label><span>Maximum value</span></mat-label>
+            <input
+                [(ngModel)]="
+                    currentlyConfiguredWidget.visualizationConfig.domainMax
+                "
+                matInput
+                (input)="onDomainMaxChange($event.target.value)"
+            />
+        </mat-form-field>
+    </div>
+
     <h5
         *ngIf="
             currentlyConfiguredWidget.visualizationConfig.displayType ===
@@ -62,6 +131,7 @@
         Rounding
     </h5>
     <mat-form-field
+        appearance="outline"
         class="marginColorField"
         color="accent"
         fxFlex="35"
@@ -87,13 +157,14 @@
     </mat-form-field>
 
     <div
+        class="mt-10"
         *ngIf="
             currentlyConfiguredWidget.visualizationConfig.displayType ===
             'heatmap'
         "
     >
-        <mat-form-field fxFlex="100" color="accent">
-            <mat-label> <span>Resolution</span></mat-label>
+        <mat-form-field fxFlex="100" color="accent" appearance="outline">
+            <mat-label><span>Resolution</span></mat-label>
             <input
                 [(ngModel)]="
                     currentlyConfiguredWidget.visualizationConfig.resolution
diff --git 
a/ui/src/app/data-explorer/components/widgets/distribution-chart/config/distribution-chart-widget-config.component.ts
 
b/ui/src/app/data-explorer/components/widgets/distribution-chart/config/distribution-chart-widget-config.component.ts
index 3c74771d1..579df93a5 100644
--- 
a/ui/src/app/data-explorer/components/widgets/distribution-chart/config/distribution-chart-widget-config.component.ts
+++ 
b/ui/src/app/data-explorer/components/widgets/distribution-chart/config/distribution-chart-widget-config.component.ts
@@ -44,7 +44,7 @@ export class DistributionWidgetConfigComponent
     setSelectedProperty(field: DataExplorerField) {
         this.currentlyConfiguredWidget.visualizationConfig.selectedProperty =
             field;
-        this.triggerDataRefresh();
+        this.triggerViewRefresh();
     }
 
     updateDisplayType(selectedType: string) {
@@ -61,19 +61,35 @@ export class DistributionWidgetConfigComponent
             
this.currentlyConfiguredWidget.visualizationConfig.selectedProperty =
                 this.fieldProvider.numericFields[0];
         }
-        this.triggerDataRefresh();
+        this.triggerViewRefresh();
     }
 
     updateRoundingValue(selectedType: number) {
         this.currentlyConfiguredWidget.visualizationConfig.roundingValue =
             selectedType;
-        this.triggerDataRefresh();
+        this.triggerViewRefresh();
     }
 
     onResolutionChange(resolution: number): void {
         this.currentlyConfiguredWidget.visualizationConfig.resolution =
             resolution;
-        this.triggerDataRefresh();
+        this.triggerViewRefresh();
+    }
+
+    onNumBinChange(numberOfBins: number): void {
+        this.currentlyConfiguredWidget.visualizationConfig.numberOfBins =
+            numberOfBins;
+        this.triggerViewRefresh();
+    }
+
+    onDomainMinChange(value: number) {
+        this.currentlyConfiguredWidget.visualizationConfig.domainMin = value;
+        this.triggerViewRefresh();
+    }
+
+    onDomainMaxChange(value: number) {
+        this.currentlyConfiguredWidget.visualizationConfig.domainMax = value;
+        this.triggerViewRefresh();
     }
 
     protected getWidgetType(): WidgetType {
@@ -87,6 +103,11 @@ export class DistributionWidgetConfigComponent
             displayType: 'histogram',
             roundingValue: 0.1,
             resolution: 1,
+            autoBin: true,
+            numberOfBins: 10,
+            domainMin: 0,
+            domainMax: 100,
+            autoDomain: true,
         };
     }
 }
diff --git 
a/ui/src/app/data-explorer/components/widgets/distribution-chart/distribution-chart-widget.component.html
 
b/ui/src/app/data-explorer/components/widgets/distribution-chart/distribution-chart-widget.component.html
deleted file mode 100644
index 18f024579..000000000
--- 
a/ui/src/app/data-explorer/components/widgets/distribution-chart/distribution-chart-widget.component.html
+++ /dev/null
@@ -1,65 +0,0 @@
-<!--
-  ~ Licensed to the Apache Software Foundation (ASF) under one or more
-  ~ contributor license agreements.  See the NOTICE file distributed with
-  ~ this work for additional information regarding copyright ownership.
-  ~ The ASF licenses this file to You under the Apache License, Version 2.0
-  ~ (the "License"); you may not use this file except in compliance with
-  ~ the License.  You may obtain a copy of the License at
-  ~
-  ~    http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  ~
-  -->
-
-<div
-    fxLayout="column"
-    fxFlex="100"
-    [ngStyle]="{
-        background: dataExplorerWidget.baseAppearanceConfig.backgroundColor,
-        color: dataExplorerWidget.baseAppearanceConfig.textColor
-    }"
->
-    <sp-no-data-in-date-range
-        *ngIf="showNoDataInDateRange"
-        [viewDateRange]="timeSettings"
-        class="h-100"
-    >
-    </sp-no-data-in-date-range>
-
-    <sp-too-much-data
-        [amountOfEvents]="amountOfTooMuchEvents"
-        (loadDataWithTooManyEventsEmitter)="loadDataWithTooManyEvents()"
-        *ngIf="showTooMuchData"
-        class="h-100"
-    ></sp-too-much-data>
-
-    <plotly-plot
-        *ngIf="
-            showData &&
-            !(dataExplorerWidget.visualizationConfig.displayType === 'heatmap')
-        "
-        [divId]="dataExplorerWidget._id"
-        class="plotly-container"
-        [data]="data"
-        [layout]="graph.layout"
-        [config]="graph.config"
-    >
-    </plotly-plot>
-
-    <sp-data-explorer-value-heatmap-widget
-        *ngIf="
-            showData &&
-            dataExplorerWidget.visualizationConfig.displayType === 'heatmap'
-        "
-        [currentWidth]="currentWidth"
-        [currentHeight]="currentHeight"
-        [widgetConfig]="dataExplorerWidget"
-        [data]="latestData"
-    >
-    </sp-data-explorer-value-heatmap-widget>
-</div>
diff --git 
a/ui/src/app/data-explorer/components/widgets/distribution-chart/distribution-chart-widget.component.ts
 
b/ui/src/app/data-explorer/components/widgets/distribution-chart/distribution-chart-widget.component.ts
index eb9da4cac..98183b228 100644
--- 
a/ui/src/app/data-explorer/components/widgets/distribution-chart/distribution-chart-widget.component.ts
+++ 
b/ui/src/app/data-explorer/components/widgets/distribution-chart/distribution-chart-widget.component.ts
@@ -17,192 +17,22 @@
  */
 
 import { Component, OnInit } from '@angular/core';
-import { BaseDataExplorerWidgetDirective } from 
'../base/base-data-explorer-widget.directive';
 import { DistributionChartWidgetModel } from 
'./model/distribution-chart-widget.model';
-import {
-    DataExplorerField,
-    SpQueryResult,
-} from '@streampipes/platform-services';
+import { DataExplorerField } from '@streampipes/platform-services';
+import { BaseDataExplorerEchartsWidgetDirective } from 
'../base/base-data-explorer-echarts-widget.directive';
+import { SpEchartsRenderer } from '../../../models/dataview-dashboard.model';
+import { DataExplorerWidgetRegistry } from 
'../../../registry/data-explorer-widget-registry';
+import { WidgetType } from '../../../registry/data-explorer-widgets';
 
 @Component({
     selector: 'sp-data-explorer-distribution-chart-widget',
-    templateUrl: './distribution-chart-widget.component.html',
-    styleUrls: ['./distribution-chart-widget.component.scss'],
+    templateUrl: '../base/echarts-widget.component.html',
+    styleUrls: ['../base/echarts-widget.component.scss'],
 })
 export class DistributionChartWidgetComponent
-    extends BaseDataExplorerWidgetDirective<DistributionChartWidgetModel>
+    extends 
BaseDataExplorerEchartsWidgetDirective<DistributionChartWidgetModel>
     implements OnInit
 {
-    data = [];
-    latestData: SpQueryResult[];
-
-    rowNo = 2;
-    colNo = 2;
-    fixedColNo = 2;
-
-    currentWidth: number;
-    currentHeight: number;
-
-    graph = {
-        layout: {
-            grid: {
-                rows: this.rowNo,
-                columns: this.colNo,
-            },
-            font: {
-                color: '#FFF',
-            },
-            autosize: true,
-            plot_bgcolor: '#fff',
-            paper_bgcolor: '#fff',
-        },
-
-        config: {
-            modeBarButtonsToRemove: [
-                'lasso2d',
-                'select2d',
-                'toggleSpikelines',
-                'toImage',
-            ],
-            displaylogo: false,
-            displayModeBar: false,
-            responsive: true,
-        },
-    };
-
-    refreshView() {
-        this.updateAppearance();
-    }
-
-    transform(rows, index: number): any[] {
-        return rows.map(row => row[index]);
-    }
-
-    prepareData(spQueryResult: SpQueryResult[]) {
-        this.data = [];
-
-        const len = spQueryResult[0].allDataSeries.length;
-
-        const even = len % this.colNo === 0;
-
-        this.rowNo = even ? len / this.fixedColNo : (len + 1) / 
this.fixedColNo;
-
-        this.colNo = len === 1 ? 1 : this.fixedColNo;
-
-        let rowCount = 0;
-        let colCount = 0;
-
-        spQueryResult[0].allDataSeries.map((group, gindex) => {
-            const series = group;
-            const finalLabels: string[] = [];
-            const finalValues: number[] = [];
-            const values: Map<string, number> = new Map();
-            const field =
-                this.dataExplorerWidget.visualizationConfig.selectedProperty;
-            const index = this.getColumnIndex(field, spQueryResult[0]);
-            const histoValues: number[] = [];
-
-            let groupName;
-
-            if (group['tags'] != null) {
-                Object.entries(group['tags']).forEach(([key, val]) => {
-                    groupName = val;
-                });
-            }
-
-            groupName = groupName === undefined ? field.fullDbName : groupName;
-
-            if (series.total > 0) {
-                const colValues = this.transform(series.rows, index);
-                colValues.forEach(value => {
-                    histoValues.push(value);
-
-                    if (field.fieldCharacteristics.numeric) {
-                        const roundingValue =
-                            this.dataExplorerWidget.visualizationConfig
-                                .roundingValue;
-                        value =
-                            Math.round(value / roundingValue) * roundingValue;
-                    }
-
-                    if (!values.has(value)) {
-                        values.set(value, 0);
-                    }
-                    const currentVal = values.get(value);
-                    values.set(value, currentVal + 1);
-                });
-            }
-            values.forEach((value, key) => {
-                finalLabels.push(key);
-                finalValues.push(value);
-            });
-
-            let component;
-
-            if (
-                this.dataExplorerWidget.visualizationConfig.displayType ===
-                'pie'
-            ) {
-                component = {
-                    name: groupName,
-                    values: finalValues,
-                    labels: finalLabels,
-                    type: 'pie',
-                    domain: {
-                        row: rowCount,
-                        column: colCount,
-                    },
-                };
-
-                if (colCount === this.colNo - 1) {
-                    colCount = 0;
-                    rowCount += 1;
-                } else {
-                    colCount += 1;
-                }
-            } else {
-                component = {
-                    x: histoValues,
-                    type: 'histogram',
-                };
-            }
-            this.data.push(component);
-        });
-    }
-
-    existsLabel(labels: string[], value: string) {
-        return labels.indexOf(value) > -1;
-    }
-
-    updateAppearance() {
-        this.graph.layout.paper_bgcolor =
-            this.dataExplorerWidget.baseAppearanceConfig.backgroundColor;
-        this.graph.layout.plot_bgcolor =
-            this.dataExplorerWidget.baseAppearanceConfig.backgroundColor;
-        this.graph.layout.font.color =
-            this.dataExplorerWidget.baseAppearanceConfig.textColor;
-        this.graph.layout.grid = {
-            rows: this.rowNo,
-            columns: this.colNo,
-        };
-    }
-
-    onResize(width: number, height: number) {
-        this.currentWidth = width;
-        this.currentHeight = height;
-        this.graph.layout.autosize = false;
-        (this.graph.layout as any).width = width;
-        (this.graph.layout as any).height = height;
-    }
-
-    beforeDataFetched() {}
-
-    onDataReceived(spQueryResult: SpQueryResult[]) {
-        this.prepareData(spQueryResult);
-        this.latestData = spQueryResult;
-        this.setShownComponents(false, true, false, false);
-    }
-
     handleUpdatedFields(
         addedFields: DataExplorerField[],
         removedFields: DataExplorerField[],
@@ -228,4 +58,13 @@ export class DistributionChartWidgetComponent
             field => field.fieldCharacteristics.numeric,
         );
     }
+
+    getRenderer(): SpEchartsRenderer<DistributionChartWidgetModel> {
+        const widgetCategory = DataExplorerWidgetRegistry.getWidgetTemplate(
+            WidgetType.DistributionChart,
+        );
+        const chartType =
+            this.dataExplorerWidget.visualizationConfig.displayType;
+        return widgetCategory.renderers.find(r => r.getType() === chartType);
+    }
 }
diff --git 
a/ui/src/app/data-explorer/components/widgets/distribution-chart/model/distribution-chart-widget.model.ts
 
b/ui/src/app/data-explorer/components/widgets/distribution-chart/model/distribution-chart-widget.model.ts
index e37cbf646..c7c7835bb 100644
--- 
a/ui/src/app/data-explorer/components/widgets/distribution-chart/model/distribution-chart-widget.model.ts
+++ 
b/ui/src/app/data-explorer/components/widgets/distribution-chart/model/distribution-chart-widget.model.ts
@@ -28,6 +28,11 @@ export interface DistributionChartVisConfig extends 
DataExplorerVisConfig {
     displayType: string;
     roundingValue: number;
     resolution: number;
+    autoBin: boolean;
+    numberOfBins: number;
+    autoDomain: boolean;
+    domainMin: number;
+    domainMax: number;
 }
 
 export interface DistributionChartWidgetModel extends DataExplorerWidgetModel {
diff --git 
a/ui/src/app/data-explorer/components/widgets/distribution-chart/value-heatmap/value-heatmap.component.ts
 
b/ui/src/app/data-explorer/components/widgets/distribution-chart/value-heatmap/value-heatmap.component.ts
deleted file mode 100644
index 9dbdc824a..000000000
--- 
a/ui/src/app/data-explorer/components/widgets/distribution-chart/value-heatmap/value-heatmap.component.ts
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-import { Component, Input } from '@angular/core';
-import { EChartsOption } from 'echarts';
-import { ECharts } from 'echarts/core';
-import { SpQueryResult } from '@streampipes/platform-services';
-import { DistributionChartWidgetModel } from 
'../model/distribution-chart-widget.model';
-
-@Component({
-    selector: 'sp-data-explorer-value-heatmap-widget',
-    templateUrl: './value-heatmap.component.html',
-    styleUrls: ['./value-heatmap.component.scss'],
-})
-export class SpValueHeatmapComponent {
-    currentWidth_: number;
-    currentHeight_: number;
-    data_: SpQueryResult[];
-
-    @Input()
-    widgetConfig: DistributionChartWidgetModel;
-
-    configReady = true;
-    option: EChartsOption = {
-        grid: {
-            height: '80%',
-            top: '7%',
-        },
-        tooltip: {},
-        xAxis: {
-            type: 'category',
-            data: [],
-        },
-        yAxis: {
-            type: 'category',
-            data: [],
-        },
-        visualMap: {
-            min: 0,
-            max: 1,
-            calculable: true,
-            realtime: false,
-            top: '10px',
-            left: '10px',
-            inRange: {
-                color: [
-                    '#313695',
-                    '#4575b4',
-                    '#74add1',
-                    '#abd9e9',
-                    '#e0f3f8',
-                    '#ffffbf',
-                    '#fee090',
-                    '#fdae61',
-                    '#f46d43',
-                    '#d73027',
-                    '#a50026',
-                ],
-            },
-        },
-        series: [
-            {
-                name: 'Gaussian',
-                type: 'heatmap',
-                data: [],
-                emphasis: {
-                    itemStyle: {
-                        borderColor: '#333',
-                        borderWidth: 1,
-                    },
-                },
-                animation: false,
-            },
-        ],
-    };
-
-    dynamic: EChartsOption;
-
-    eChartsInstance: ECharts;
-
-    onChartInit(ec: ECharts) {
-        this.eChartsInstance = ec;
-        this.applySize(this.currentWidth_, this.currentHeight_);
-        this.initOptions();
-    }
-
-    initOptions() {
-        if (this.data_) {
-            const dataResult = [];
-            const resolution: number =
-                +this.widgetConfig.visualizationConfig.resolution;
-            const allRows = this.data_[0].allDataSeries[0].rows;
-            const fieldIndex = this.getFieldIndex(
-                this.data_[0].allDataSeries[0].headers,
-            );
-            const allValues = allRows.map(row => row[fieldIndex]);
-            const total = allValues.length;
-            let currentCount = 0;
-            allValues.sort((a, b) => a - b);
-            let start = allValues[0];
-            for (let i = 0; i < allValues.length; i++) {
-                const value = allValues[i];
-                if (value < start + resolution) {
-                    currentCount += 1;
-                }
-                if (value >= start + resolution || i + 1 === allValues.length) 
{
-                    const currentRange =
-                        '>' +
-                        start.toFixed(2) +
-                        (i + 1 < allValues.length
-                            ? '<' + allValues[i + 1].toFixed(2)
-                            : '');
-                    dataResult.push([
-                        currentRange,
-                        0,
-                        (currentCount + 1) / total,
-                    ]);
-                    currentCount = 0;
-                    start = allValues[i + 1];
-                }
-            }
-            this.dynamic = this.option;
-            (this.dynamic.xAxis as any).data = dataResult.map(r => r[0]);
-            this.dynamic.series[0].data = dataResult;
-            this.dynamic.series[0].name = this.data_[0].headers[fieldIndex];
-            if (this.eChartsInstance) {
-                this.eChartsInstance.setOption(this.dynamic);
-            }
-        }
-    }
-
-    getFieldIndex(headers: string[]) {
-        return headers.indexOf(
-            this.widgetConfig.visualizationConfig.selectedProperty.fullDbName,
-        );
-    }
-
-    applySize(width: number, height: number) {
-        if (this.eChartsInstance) {
-            this.eChartsInstance.resize({ width, height });
-        }
-    }
-
-    @Input()
-    set currentWidth(currentWidth: number) {
-        this.currentWidth_ = currentWidth;
-        this.applySize(this.currentWidth_, this.currentHeight_);
-    }
-
-    get currentWidth() {
-        return this.currentWidth_;
-    }
-
-    @Input()
-    set currentHeight(currentHeight: number) {
-        this.currentHeight_ = currentHeight;
-        this.applySize(this.currentWidth_, this.currentHeight_);
-    }
-
-    get currentHeight() {
-        return this.currentHeight_;
-    }
-
-    @Input()
-    set data(data: SpQueryResult[]) {
-        this.data_ = data;
-        this.initOptions();
-    }
-
-    get data() {
-        return this.data_;
-    }
-}
diff --git 
a/ui/src/app/data-explorer/components/widgets/heatmap/heatmap-widget.component.ts
 
b/ui/src/app/data-explorer/components/widgets/heatmap/heatmap-widget.component.ts
index d77497d8d..f6188d9db 100644
--- 
a/ui/src/app/data-explorer/components/widgets/heatmap/heatmap-widget.component.ts
+++ 
b/ui/src/app/data-explorer/components/widgets/heatmap/heatmap-widget.component.ts
@@ -22,52 +22,27 @@ import {
     SpQueryResult,
 } from '@streampipes/platform-services';
 
-import { BaseDataExplorerWidgetDirective } from 
'../base/base-data-explorer-widget.directive';
 import { HeatmapWidgetModel } from './model/heatmap-widget.model';
 
 import { EChartsOption } from 'echarts';
-import { ECharts } from 'echarts/core';
+import { BaseDataExplorerEchartsWidgetDirective } from 
'../base/base-data-explorer-echarts-widget.directive';
+import { SpEchartsRenderer } from '../../../models/dataview-dashboard.model';
 
 @Component({
     selector: 'sp-data-explorer-heatmap-widget',
-    templateUrl: './heatmap-widget.component.html',
-    styleUrls: ['./heatmap-widget.component.scss'],
+    templateUrl: '../base/echarts-widget.component.html',
+    styleUrls: ['../base/echarts-widget.component.scss'],
 })
 export class HeatmapWidgetComponent
-    extends BaseDataExplorerWidgetDirective<HeatmapWidgetModel>
+    extends BaseDataExplorerEchartsWidgetDirective<HeatmapWidgetModel>
     implements OnInit
 {
-    eChartsInstance: ECharts;
-    currentWidth: number;
-    currentHeight: number;
-
-    option = {};
     dynamic: EChartsOption;
 
-    configReady = false;
-
     ngOnInit(): void {
         super.ngOnInit();
-        this.onSizeChanged(
-            this.gridsterItemComponent.width,
-            this.gridsterItemComponent.height,
-        );
-        this.initOptions();
     }
 
-    public refreshView() {}
-
-    onResize(width: number, height: number) {
-        this.onSizeChanged(width, height);
-    }
-
-    handleUpdatedFields(
-        addedFields: DataExplorerField[],
-        removedFields: DataExplorerField[],
-    ) {}
-
-    beforeDataFetched() {}
-
     onDataReceived(spQueryResult: SpQueryResult[]) {
         this.setShownComponents(false, true, false, false);
         const dataBundle = this.convertData(spQueryResult);
@@ -82,25 +57,6 @@ export class HeatmapWidgetComponent
         }
     }
 
-    onChartInit(ec: ECharts) {
-        this.eChartsInstance = ec;
-        this.applySize(this.currentWidth, this.currentHeight);
-        this.initOptions();
-    }
-
-    protected onSizeChanged(width: number, height: number) {
-        this.currentWidth = width;
-        this.currentHeight = height;
-        this.configReady = true;
-        this.applySize(width, height);
-    }
-
-    applySize(width: number, height: number) {
-        if (this.eChartsInstance) {
-            this.eChartsInstance.resize({ width, height });
-        }
-    }
-
     convertData(spQueryResult: SpQueryResult[]) {
         let min = 1000000;
         let max = -100000;
@@ -325,4 +281,15 @@ export class HeatmapWidgetComponent
         }
         return num;
     }
+
+    getRenderer(): SpEchartsRenderer<HeatmapWidgetModel> {
+        return undefined;
+    }
+
+    protected handleUpdatedFields(
+        addedFields: DataExplorerField[],
+        removedFields: DataExplorerField[],
+    ) {}
+
+    refreshView() {}
 }
diff --git 
a/ui/src/app/data-explorer/components/widgets/time-series-chart/time-series-chart-widget.component.html
 
b/ui/src/app/data-explorer/components/widgets/time-series-chart/time-series-chart-widget.component.html
index f4a9b8d5e..69cfdbcd3 100644
--- 
a/ui/src/app/data-explorer/components/widgets/time-series-chart/time-series-chart-widget.component.html
+++ 
b/ui/src/app/data-explorer/components/widgets/time-series-chart/time-series-chart-widget.component.html
@@ -44,6 +44,8 @@
         [divId]="dataExplorerWidget._id"
         class="plotly-container"
         [data]="data"
+        [updateOnlyWithRevision]="true"
+        [revision]="revision"
         [layout]="graph.layout"
         [config]="graph.config"
     >
diff --git 
a/ui/src/app/data-explorer/components/widgets/time-series-chart/time-series-chart-widget.component.ts
 
b/ui/src/app/data-explorer/components/widgets/time-series-chart/time-series-chart-widget.component.ts
index 1b36db696..00e2e95f5 100644
--- 
a/ui/src/app/data-explorer/components/widgets/time-series-chart/time-series-chart-widget.component.ts
+++ 
b/ui/src/app/data-explorer/components/widgets/time-series-chart/time-series-chart-widget.component.ts
@@ -103,6 +103,8 @@ export class TimeSeriesChartWidgetComponent
         },
     };
 
+    revision = 1;
+
     ngOnInit(): void {
         this.updatemenus = [
             {
@@ -505,6 +507,7 @@ export class TimeSeriesChartWidgetComponent
             (this.graph.layout as any).width =
                 width - this.offsetRightLineChart;
             (this.graph.layout as any).height = height;
+            this.revision += 1;
         }, 10);
     }
 
@@ -524,6 +527,7 @@ export class TimeSeriesChartWidgetComponent
         );
 
         this.setShownComponents(false, true, false, false);
+        this.revision += 1;
     }
 
     handleUpdatedFields(
diff --git a/ui/src/app/data-explorer/data-explorer.module.ts 
b/ui/src/app/data-explorer/data-explorer.module.ts
index fe6cfa40f..e902f19a3 100644
--- a/ui/src/app/data-explorer/data-explorer.module.ts
+++ b/ui/src/app/data-explorer/data-explorer.module.ts
@@ -96,7 +96,6 @@ import { WidgetDirective } from 
'./components/widget/widget.directive';
 import { CorrelationWidgetConfigComponent } from 
'./components/widgets/correlation-chart/config/correlation-chart-widget-config.component';
 import { TimeSelectionService } from './services/time-selection.service';
 import { TooMuchDataComponent } from 
'./components/widgets/utils/too-much-data/too-much-data.component';
-import { SpValueHeatmapComponent } from 
'./components/widgets/distribution-chart/value-heatmap/value-heatmap.component';
 import { RouterModule } from '@angular/router';
 import { DataExplorerDashboardSlideViewComponent } from 
'./components/widget-view/slide-view/data-explorer-dashboard-slide-view.component';
 import { SharedUiModule } from '@streampipes/shared-ui';
@@ -210,7 +209,6 @@ export const MY_NATIVE_FORMATS = {
         SelectPropertiesComponent,
         SelectColorPropertiesComponent,
         SelectPropertyComponent,
-        SpValueHeatmapComponent,
         TableWidgetComponent,
         TableWidgetConfigComponent,
         MapWidgetConfigComponent,
diff --git 
a/ui/src/app/data-explorer/echarts-renderer/sp-base-echarts-renderer.ts 
b/ui/src/app/data-explorer/echarts-renderer/sp-base-echarts-renderer.ts
new file mode 100644
index 000000000..f06aca6af
--- /dev/null
+++ b/ui/src/app/data-explorer/echarts-renderer/sp-base-echarts-renderer.ts
@@ -0,0 +1,324 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import {
+    SpEchartsRenderer,
+    WidgetBaseAppearanceConfig,
+} from '../models/dataview-dashboard.model';
+import {
+    DataExplorerWidgetModel,
+    SpQueryResult,
+} from '@streampipes/platform-services';
+import { EChartsOption } from 'echarts';
+import { BoxLayoutOptionMixin } from 'echarts/types/src/util/types';
+import {
+    DatasetOption,
+    GridOption,
+    XAXisOption,
+    YAXisOption,
+} from 'echarts/types/dist/shared';
+import {
+    AxisOptions,
+    GeneratedDataset,
+    GridOptions,
+    TagValue,
+    WidgetSize,
+} from '../models/dataset.model';
+import { DataTransformOption } from 'echarts/types/src/data/helper/transform';
+
+export abstract class SpBaseEchartsRenderer<T extends DataExplorerWidgetModel>
+    implements SpEchartsRenderer<T>
+{
+    minimumChartWidth = 200;
+    minimumChartHeight = 50;
+    gridMargin = 50;
+
+    abstract getType(): string;
+
+    render(
+        spQueryResult: SpQueryResult[],
+        widgetConfig: T,
+        widgetSize: WidgetSize,
+    ): EChartsOption {
+        const options = this.makeBaseConfig();
+        const datasets = this.toDataset(spQueryResult, widgetConfig);
+        this.applyOptions(datasets, options, widgetConfig, widgetSize);
+        return options;
+    }
+
+    makeBaseConfig(): EChartsOption {
+        return {
+            legend: {
+                orient: 'horizontal',
+                top: 'top',
+            },
+            tooltip: {
+                show: true,
+            },
+        };
+    }
+
+    applySeriesPosition(
+        series: BoxLayoutOptionMixin[],
+        gridOptions: GridOption[],
+    ) {
+        series.forEach((s, index) => {
+            const gridOption = gridOptions[index];
+            s.left = gridOption.left;
+            s.top = gridOption.top;
+            s.width = gridOption.width;
+            s.height = gridOption.height;
+        });
+    }
+
+    makeAxisOptions(
+        appearanceConfig: WidgetBaseAppearanceConfig,
+        xAxisType: 'category' | 'value' | 'time' | 'log',
+        yAxisType: 'category' | 'value' | 'time' | 'log',
+        numberOfAxes: number,
+    ): AxisOptions {
+        const xAxisOptions: XAXisOption[] = [];
+        const yAxisOptions: YAXisOption[] = [];
+        for (let i = 0; i < numberOfAxes; i++) {
+            xAxisOptions.push(
+                this.makeAxis(xAxisType, i, appearanceConfig) as XAXisOption,
+            );
+            yAxisOptions.push(
+                this.makeAxis(yAxisType, i, appearanceConfig) as YAXisOption,
+            );
+        }
+
+        return {
+            xAxisOptions: xAxisOptions,
+            yAxisOptions: yAxisOptions,
+        };
+    }
+
+    makeAxis(
+        axisType: 'category' | 'value' | 'time' | 'log',
+        gridIndex: number,
+        appearanceConfig: WidgetBaseAppearanceConfig,
+    ): XAXisOption | YAXisOption {
+        return {
+            type: axisType,
+            gridIndex: gridIndex,
+            nameTextStyle: {
+                color: appearanceConfig.textColor,
+            },
+            axisTick: {
+                lineStyle: {
+                    color: appearanceConfig.textColor,
+                },
+            },
+            axisLabel: {
+                color: appearanceConfig.textColor,
+            },
+            axisLine: {
+                lineStyle: {
+                    color: appearanceConfig.textColor,
+                },
+            },
+        };
+    }
+
+    makeSeriesName(tag: TagValue): string {
+        return tag.tagKeys.toString() + ' ' + tag.values.toString();
+    }
+
+    addSeriesTitles(
+        options: EChartsOption,
+        series: any[],
+        gridOptions: GridOption[],
+    ): void {
+        options.title = series.map((s, index) => {
+            const grid = gridOptions[index];
+            return {
+                text: s.name,
+                textStyle: {
+                    fontSize: 12,
+                    fontWeight: 'normal',
+                },
+                left: grid.left,
+                top: grid.top,
+            };
+        });
+    }
+
+    makeGrid(tagLength: number, widgetSize: WidgetSize): GridOptions {
+        if (tagLength === 0) {
+            return {
+                grid: [
+                    {
+                        height: widgetSize.height - 100,
+                        width: widgetSize.width - 100,
+                        top: this.gridMargin,
+                        left: this.gridMargin,
+                        right: this.gridMargin,
+                        bottom: this.gridMargin,
+                    },
+                ],
+                numberOfColumns: 1,
+                numberOfRows: 1,
+            };
+        } else {
+            const grid: GridOption[] = [];
+            const totalCanvasWidth = widgetSize.width - this.gridMargin * 2;
+            const totalCanvasHeight = widgetSize.height - this.gridMargin * 2;
+            const numberOfColumns = Math.max(
+                Math.floor(totalCanvasWidth / this.minimumChartWidth),
+                1,
+            );
+            const numberOfRows = Math.ceil(tagLength / numberOfColumns);
+
+            const groupItemWidth =
+                (widgetSize.width -
+                    (numberOfColumns * this.gridMargin + this.gridMargin)) /
+                numberOfColumns;
+            const groupItemHeight = Math.max(
+                this.minimumChartHeight,
+                (totalCanvasHeight - (numberOfRows - 1) * this.gridMargin) /
+                    numberOfRows,
+            );
+
+            for (let i = 0; i < numberOfRows; i++) {
+                for (let j = 0; j < numberOfColumns; j++) {
+                    if (i * j < tagLength) {
+                        grid.push({
+                            left: Math.floor(
+                                j * groupItemWidth + (j + 1) * this.gridMargin,
+                            ),
+                            top:
+                                this.gridMargin +
+                                i * (groupItemHeight + this.gridMargin),
+                            width: groupItemWidth,
+                            height: groupItemHeight,
+                            borderWidth: 1,
+                        });
+                    }
+                }
+            }
+            return {
+                grid: grid,
+                numberOfRows,
+                numberOfColumns,
+            };
+        }
+    }
+
+    toDataset(
+        queryResults: SpQueryResult[],
+        widgetConfig: T,
+    ): GeneratedDataset {
+        const datasets: DatasetOption[] = [];
+        const rawDataStartIndices: number[] = [];
+        const rawDataEndIndices: number[] = [];
+        const preparedDataStartIndices: number[] = [];
+        const preparedDataEndIndices: number[] = [];
+        const tagValues: TagValue[][] = [];
+        let initialTransformsCount = 0;
+
+        queryResults.forEach((queryResult, index) => {
+            const currentDatasetSize = datasets.length;
+            const initialTransforms = this.initialTransforms(
+                widgetConfig,
+                index,
+            );
+            const data = [];
+            data.push(queryResult.headers);
+            queryResult.allDataSeries.forEach(series => {
+                data.push(...series.rows);
+            });
+
+            const tags: TagValue[] = queryResult.allDataSeries
+                .map(series => series.tags)
+                .filter(tags => tags !== null)
+                .map(kv => {
+                    return {
+                        tagKeys: Object.keys(kv),
+                        values: Object.values(kv),
+                    };
+                });
+
+            datasets.push({
+                source: data,
+                dimensions: queryResult.headers,
+            });
+
+            if (initialTransforms.length > 0) {
+                initialTransformsCount++;
+                datasets.push({
+                    fromDatasetIndex: currentDatasetSize,
+                    transform: initialTransforms,
+                });
+            }
+            tags.forEach(tag => {
+                const filters = tag.tagKeys.map((key, index) => {
+                    return {
+                        dimension: key,
+                        value: tag.values[index],
+                    };
+                });
+                datasets.push({
+                    fromDatasetIndex:
+                        currentDatasetSize + initialTransforms.length,
+                    transform: {
+                        type: 'filter',
+                        config: {
+                            and: filters,
+                        },
+                    },
+                });
+            });
+            tagValues.push(tags);
+            rawDataStartIndices.push(
+                currentDatasetSize + initialTransforms.length,
+            );
+            rawDataEndIndices.push(
+                currentDatasetSize + initialTransforms.length + tags.length,
+            );
+        });
+
+        return {
+            dataset: datasets,
+            tagValues: tagValues,
+            rawDataStartIndices: rawDataStartIndices,
+            rawDataEndIndices: rawDataEndIndices,
+            preparedDataStartIndices: preparedDataStartIndices,
+            preparedDataEndIndices: preparedDataEndIndices,
+            initialTransformsCount: initialTransformsCount,
+        };
+    }
+
+    makePercent(value: number) {
+        return value + '%';
+    }
+
+    abstract applyOptions(
+        datasets: GeneratedDataset,
+        options: EChartsOption,
+        widgetConfig: T,
+        widgetSize: WidgetSize,
+    ): void;
+
+    initialTransforms(
+        widgetConfig: T,
+        sourceIndex: number,
+    ): DataTransformOption[] {
+        return [];
+    }
+}
diff --git 
a/ui/src/app/data-explorer/echarts-renderer/sp-base-single-field-echarts-renderer.ts
 
b/ui/src/app/data-explorer/echarts-renderer/sp-base-single-field-echarts-renderer.ts
new file mode 100644
index 000000000..94236d83b
--- /dev/null
+++ 
b/ui/src/app/data-explorer/echarts-renderer/sp-base-single-field-echarts-renderer.ts
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import {
+    DataExplorerField,
+    DataExplorerWidgetModel,
+} from '@streampipes/platform-services';
+import { EChartsOption, SeriesOption } from 'echarts';
+import { SpBaseEchartsRenderer } from './sp-base-echarts-renderer';
+import { GeneratedDataset, WidgetSize } from '../models/dataset.model';
+import { DataTransformOption } from 'echarts/types/src/data/helper/transform';
+import { WidgetBaseAppearanceConfig } from 
'../models/dataview-dashboard.model';
+
+export abstract class SpBaseSingleFieldEchartsRenderer<
+    T extends DataExplorerWidgetModel,
+    S extends SeriesOption,
+> extends SpBaseEchartsRenderer<T> {
+    applyOptions(
+        datasets: GeneratedDataset,
+        options: EChartsOption,
+        widgetConfig: T,
+        widgetSize: WidgetSize,
+    ) {
+        const affectedField = this.getAffectedField(widgetConfig);
+        const sourceIndex = affectedField.sourceIndex;
+        const tags = datasets.tagValues[sourceIndex];
+        const rawDatasetStartIndex = datasets.rawDataStartIndices[sourceIndex];
+        const numberOfCharts = tags.length === 0 ? 1 : tags.length;
+        const series = [];
+        const seriesStartIndex = this.computeSeriesStart(datasets);
+
+        for (let i = 0; i < numberOfCharts; i++) {
+            datasets.dataset.push({
+                fromDatasetIndex: rawDatasetStartIndex + i,
+                transform: this.addDatasetTransform(widgetConfig),
+            });
+            const seriesName =
+                tags.length === 0 ? 'Default' : this.makeSeriesName(tags[i]);
+            series.push(
+                this.addSeriesItem(
+                    seriesName,
+                    seriesStartIndex + i,
+                    widgetConfig,
+                    i,
+                ),
+            );
+        }
+        const gridOptions = this.makeGrid(tags.length, widgetSize);
+        options.grid = gridOptions.grid;
+        if (this.showAxes()) {
+            const a = this.makeAxisOptions(
+                widgetConfig.baseAppearanceConfig as 
WidgetBaseAppearanceConfig,
+                this.getXAxisType(),
+                this.getYAxisType(),
+                gridOptions.numberOfRows * gridOptions.numberOfColumns,
+            );
+            options.xAxis = a.xAxisOptions;
+            options.yAxis = a.yAxisOptions;
+        }
+        if (this.shouldApplySeriesPosition()) {
+            this.applySeriesPosition(series, gridOptions.grid);
+        }
+        if (numberOfCharts > 1) {
+            this.addSeriesTitles(options, series, gridOptions.grid);
+        }
+        options.dataset = datasets.dataset;
+        options.series = series;
+        this.addAdditionalConfigs(options);
+    }
+
+    showAxes(): boolean {
+        return true;
+    }
+
+    abstract addDatasetTransform(widgetConfig: T): DataTransformOption;
+
+    abstract addAdditionalConfigs(option: EChartsOption): void;
+
+    abstract addSeriesItem(
+        name: string,
+        datasetIndex: number,
+        widgetConfig: T,
+        index: number,
+    ): S;
+
+    abstract getAffectedField(widgetConfig: T): DataExplorerField;
+
+    private computeSeriesStart(datasets: GeneratedDataset) {
+        return (
+            datasets.rawDataEndIndices[datasets.rawDataEndIndices.length - 1] +
+            1
+        );
+    }
+
+    shouldApplySeriesPosition(): boolean {
+        return false;
+    }
+
+    getXAxisType(): 'category' | 'value' | 'time' | 'log' {
+        return 'category';
+    }
+
+    getYAxisType(): 'category' | 'value' | 'time' | 'log' {
+        return 'category';
+    }
+}
diff --git 
a/ui/src/app/data-explorer/components/widgets/distribution-chart/value-heatmap/value-heatmap.component.scss
 b/ui/src/app/data-explorer/echarts-renderer/sp-heatmap-renderer.ts
similarity index 56%
rename from 
ui/src/app/data-explorer/components/widgets/distribution-chart/value-heatmap/value-heatmap.component.scss
rename to ui/src/app/data-explorer/echarts-renderer/sp-heatmap-renderer.ts
index 13cbc4aac..d069d63f5 100644
--- 
a/ui/src/app/data-explorer/components/widgets/distribution-chart/value-heatmap/value-heatmap.component.scss
+++ b/ui/src/app/data-explorer/echarts-renderer/sp-heatmap-renderer.ts
@@ -15,3 +15,23 @@
  * limitations under the License.
  *
  */
+
+import { SpBaseEchartsRenderer } from './sp-base-echarts-renderer';
+import { HeatmapWidgetModel } from 
'../components/widgets/heatmap/model/heatmap-widget.model';
+import { GeneratedDataset, WidgetSize } from '../models/dataset.model';
+import { EChartsOption } from 'echarts';
+
+export class SpHeatmapRenderer extends 
SpBaseEchartsRenderer<HeatmapWidgetModel> {
+    applyOptions(
+        datasets: GeneratedDataset,
+        options: EChartsOption,
+        widgetConfig: HeatmapWidgetModel,
+        widgetSize: WidgetSize,
+    ): void {
+        // TODO
+    }
+
+    getType(): string {
+        return 'heatmap';
+    }
+}
diff --git a/ui/src/app/data-explorer/echarts-renderer/sp-histogram-renderer.ts 
b/ui/src/app/data-explorer/echarts-renderer/sp-histogram-renderer.ts
new file mode 100644
index 000000000..12310a00d
--- /dev/null
+++ b/ui/src/app/data-explorer/echarts-renderer/sp-histogram-renderer.ts
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { DistributionChartWidgetModel } from 
'../components/widgets/distribution-chart/model/distribution-chart-widget.model';
+import { BarSeriesOption, EChartsOption } from 'echarts';
+import { SpBaseSingleFieldEchartsRenderer } from 
'./sp-base-single-field-echarts-renderer';
+import { DataTransformOption } from 'echarts/types/src/data/helper/transform';
+
+export class SpHistogramRenderer extends SpBaseSingleFieldEchartsRenderer<
+    DistributionChartWidgetModel,
+    BarSeriesOption
+> {
+    getType(): string {
+        return 'histogram';
+    }
+
+    addAdditionalConfigs(option: EChartsOption) {
+        //do nothing
+    }
+
+    addDatasetTransform(
+        widgetConfig: DistributionChartWidgetModel,
+    ): DataTransformOption {
+        return {
+            type: 'sp:histogram',
+            config: {
+                field: widgetConfig.visualizationConfig.selectedProperty
+                    .fullDbName,
+                autoBin: widgetConfig.visualizationConfig.autoBin,
+                numberOfBins: widgetConfig.visualizationConfig.numberOfBins,
+                autoDomain: widgetConfig.visualizationConfig.autoDomain,
+                domainMin: widgetConfig.visualizationConfig.domainMin,
+                domainMax: widgetConfig.visualizationConfig.domainMax,
+            },
+        };
+    }
+
+    addSeriesItem(
+        name: string,
+        datasetIndex: number,
+        widgetConfig: DistributionChartWidgetModel,
+        index: number,
+    ): BarSeriesOption {
+        return {
+            name,
+            type: 'bar',
+            universalTransition: true,
+            datasetIndex: datasetIndex,
+            xAxisIndex: index,
+            yAxisIndex: index,
+            encode: { x: 'edge', y: 'hist' },
+            barWidth: '99.3%',
+        };
+    }
+
+    getAffectedField(widgetConfig: DistributionChartWidgetModel) {
+        return widgetConfig.visualizationConfig.selectedProperty;
+    }
+
+    getYAxisType(): 'value' | 'category' | 'time' | 'log' {
+        return 'value';
+    }
+}
diff --git a/ui/src/app/data-explorer/echarts-renderer/sp-pie-renderer.ts 
b/ui/src/app/data-explorer/echarts-renderer/sp-pie-renderer.ts
new file mode 100644
index 000000000..1bae54ecd
--- /dev/null
+++ b/ui/src/app/data-explorer/echarts-renderer/sp-pie-renderer.ts
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { DistributionChartWidgetModel } from 
'../components/widgets/distribution-chart/model/distribution-chart-widget.model';
+import { EChartsOption, PieSeriesOption } from 'echarts';
+import { DataTransformOption } from 'echarts/types/src/data/helper/transform';
+import { SpBaseSingleFieldEchartsRenderer } from 
'./sp-base-single-field-echarts-renderer';
+
+export class SpPieRenderer extends SpBaseSingleFieldEchartsRenderer<
+    DistributionChartWidgetModel,
+    PieSeriesOption
+> {
+    getType(): string {
+        return 'pie';
+    }
+
+    addDatasetTransform(
+        widgetConfig: DistributionChartWidgetModel,
+    ): DataTransformOption {
+        const field =
+            widgetConfig.visualizationConfig.selectedProperty.fullDbName;
+        return {
+            type: 'ecSimpleTransform:aggregate',
+            config: {
+                resultDimensions: [
+                    { name: 'name', from: field },
+                    { name: 'value', from: 'time', method: 'count' },
+                ],
+                groupBy: field,
+            },
+        };
+    }
+
+    addAdditionalConfigs(option: EChartsOption) {
+        // do nothing
+    }
+
+    addSeriesItem(
+        name: string,
+        datasetIndex: number,
+        widgetConfig: DistributionChartWidgetModel,
+    ): PieSeriesOption {
+        return {
+            name,
+            type: 'pie',
+            universalTransition: true,
+            datasetIndex: datasetIndex,
+            encode: { itemName: 'name', value: 'value' },
+        };
+    }
+
+    initialTransforms(
+        widgetConfig: DistributionChartWidgetModel,
+        sourceIndex: number,
+    ): DataTransformOption[] {
+        const fieldSource = widgetConfig.visualizationConfig.selectedProperty;
+        return fieldSource.sourceIndex === sourceIndex &&
+            fieldSource.fieldCharacteristics.numeric
+            ? [
+                  {
+                      type: 'sp:round',
+                      config: {
+                          fields: [
+                              
widgetConfig.visualizationConfig.selectedProperty,
+                          ],
+                          roundingValue:
+                              widgetConfig.visualizationConfig.roundingValue,
+                      },
+                  },
+              ]
+            : [];
+    }
+
+    getAffectedField(widgetConfig: DistributionChartWidgetModel) {
+        return widgetConfig.visualizationConfig.selectedProperty;
+    }
+
+    showAxes(): boolean {
+        return false;
+    }
+
+    shouldApplySeriesPosition(): boolean {
+        return true;
+    }
+}
diff --git 
a/ui/src/app/data-explorer/echarts-renderer/sp-value-heatmap-renderer.ts 
b/ui/src/app/data-explorer/echarts-renderer/sp-value-heatmap-renderer.ts
new file mode 100644
index 000000000..87d19abd1
--- /dev/null
+++ b/ui/src/app/data-explorer/echarts-renderer/sp-value-heatmap-renderer.ts
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { DistributionChartWidgetModel } from 
'../components/widgets/distribution-chart/model/distribution-chart-widget.model';
+import { EChartsOption, HeatmapSeriesOption } from 'echarts';
+import { SpBaseSingleFieldEchartsRenderer } from 
'./sp-base-single-field-echarts-renderer';
+import { DataTransformOption } from 'echarts/types/src/data/helper/transform';
+
+export class SpValueHeatmapRenderer extends SpBaseSingleFieldEchartsRenderer<
+    DistributionChartWidgetModel,
+    HeatmapSeriesOption
+> {
+    getType(): string {
+        return 'heatmap';
+    }
+
+    addDatasetTransform(
+        widgetConfig: DistributionChartWidgetModel,
+    ): DataTransformOption {
+        const field =
+            widgetConfig.visualizationConfig.selectedProperty.fullDbName;
+        return {
+            type: 'sp:value-distribution',
+            config: {
+                field,
+                resolution: widgetConfig.visualizationConfig.resolution,
+            },
+        };
+    }
+
+    addAdditionalConfigs(options: EChartsOption) {
+        options.legend = { show: false };
+        options.visualMap = {
+            min: 0,
+            max: 1,
+            calculable: true,
+            realtime: false,
+            top: '0px',
+            right: '50px',
+            orient: 'horizontal',
+            inRange: {
+                color: [
+                    '#313695',
+                    '#4575b4',
+                    '#74add1',
+                    '#abd9e9',
+                    '#e0f3f8',
+                    '#ffffbf',
+                    '#fee090',
+                    '#fdae61',
+                    '#f46d43',
+                    '#d73027',
+                    '#a50026',
+                ],
+            },
+        };
+    }
+
+    addSeriesItem(
+        name: string,
+        datasetIndex: number,
+        widgetConfig: DistributionChartWidgetModel,
+        index: number,
+    ): HeatmapSeriesOption {
+        return {
+            universalTransition: true,
+            animation: true,
+            name: name,
+            type: 'heatmap',
+            datasetIndex: datasetIndex,
+            encode: { itemId: 0, value: 2 },
+            xAxisIndex: index,
+            yAxisIndex: index,
+            emphasis: {
+                itemStyle: {
+                    borderColor: '#333',
+                    borderWidth: 1,
+                },
+            },
+        };
+    }
+
+    getAffectedField(widgetConfig: DistributionChartWidgetModel) {
+        return widgetConfig.visualizationConfig.selectedProperty;
+    }
+}
diff --git 
a/ui/src/app/data-explorer/components/widgets/distribution-chart/distribution-chart-widget.component.scss
 b/ui/src/app/data-explorer/models/dataset.model.ts
similarity index 51%
rename from 
ui/src/app/data-explorer/components/widgets/distribution-chart/distribution-chart-widget.component.scss
rename to ui/src/app/data-explorer/models/dataset.model.ts
index 13cbc4aac..494d11e82 100644
--- 
a/ui/src/app/data-explorer/components/widgets/distribution-chart/distribution-chart-widget.component.scss
+++ b/ui/src/app/data-explorer/models/dataset.model.ts
@@ -15,3 +15,41 @@
  * limitations under the License.
  *
  */
+
+import {
+    DatasetOption,
+    GridOption,
+    XAXisOption,
+    YAXisOption,
+} from 'echarts/types/dist/shared';
+
+export interface WidgetSize {
+    width: number;
+    height: number;
+}
+
+export interface TagValue {
+    tagKeys: string[];
+    values: any[];
+}
+
+export interface GeneratedDataset {
+    dataset: DatasetOption[];
+    tagValues: TagValue[][];
+    rawDataStartIndices: number[];
+    rawDataEndIndices: number[];
+    preparedDataStartIndices: number[];
+    preparedDataEndIndices: number[];
+    initialTransformsCount: number;
+}
+
+export interface GridOptions {
+    grid: GridOption[];
+    numberOfRows: number;
+    numberOfColumns: number;
+}
+
+export interface AxisOptions {
+    xAxisOptions: XAXisOption[];
+    yAxisOptions: YAXisOption[];
+}
diff --git a/ui/src/app/data-explorer/models/dataview-dashboard.model.ts 
b/ui/src/app/data-explorer/models/dataview-dashboard.model.ts
index be9dbd075..c3b414f44 100644
--- a/ui/src/app/data-explorer/models/dataview-dashboard.model.ts
+++ b/ui/src/app/data-explorer/models/dataview-dashboard.model.ts
@@ -18,15 +18,31 @@
 
 import { GridsterConfig } from 'angular-gridster2';
 import { WidgetType } from '../registry/data-explorer-widgets';
-import { DataExplorerField } from '@streampipes/platform-services';
+import {
+    DataExplorerField,
+    DataExplorerWidgetModel,
+    SpQueryResult,
+} from '@streampipes/platform-services';
+import { EChartsOption } from 'echarts';
+import { WidgetSize } from './dataset.model';
 
 // eslint-disable-next-line @typescript-eslint/no-empty-interface
 export interface IDataViewDashboardConfig extends GridsterConfig {}
 
-export interface IWidget {
+export interface SpEchartsRenderer<T extends DataExplorerWidgetModel> {
+    getType(): string;
+    render(
+        queryResult: SpQueryResult[],
+        widgetConfig: T,
+        widgetSize: WidgetSize,
+    ): EChartsOption;
+}
+
+export interface IWidget<T extends DataExplorerWidgetModel> {
     id: string;
     label: string;
     componentClass: any;
+    renderers?: SpEchartsRenderer<T>[];
 }
 
 export interface WidgetBaseAppearanceConfig {
diff --git a/ui/src/app/data-explorer/registry/data-explorer-widget-registry.ts 
b/ui/src/app/data-explorer/registry/data-explorer-widget-registry.ts
index 132e894c3..5772bb2cd 100644
--- a/ui/src/app/data-explorer/registry/data-explorer-widget-registry.ts
+++ b/ui/src/app/data-explorer/registry/data-explorer-widget-registry.ts
@@ -17,10 +17,14 @@
  */
 
 import { IWidget } from '../models/dataview-dashboard.model';
-import { WidgetTypeMap } from './data-explorer-widgets';
+import { WidgetType, WidgetTypeMap } from './data-explorer-widgets';
 
 export class DataExplorerWidgetRegistry {
-    static getAvailableWidgetTemplates(): IWidget[] {
+    static getAvailableWidgetTemplates(): IWidget<any>[] {
         return Array.from(WidgetTypeMap.values());
     }
+
+    static getWidgetTemplate(type: WidgetType) {
+        return WidgetTypeMap.get(type);
+    }
 }
diff --git a/ui/src/app/data-explorer/registry/data-explorer-widgets.ts 
b/ui/src/app/data-explorer/registry/data-explorer-widgets.ts
index 91489dc9d..0b27cf31a 100644
--- a/ui/src/app/data-explorer/registry/data-explorer-widgets.ts
+++ b/ui/src/app/data-explorer/registry/data-explorer-widgets.ts
@@ -25,6 +25,9 @@ import { ImageWidgetComponent } from 
'../components/widgets/image/image-widget.c
 import { IndicatorChartWidgetComponent } from 
'../components/widgets/indicator/indicator-chart-widget.component';
 import { CorrelationChartWidgetComponent } from 
'../components/widgets/correlation-chart/correlation-chart-widget.component';
 import { DistributionChartWidgetComponent } from 
'../components/widgets/distribution-chart/distribution-chart-widget.component';
+import { SpPieRenderer } from '../echarts-renderer/sp-pie-renderer';
+import { SpValueHeatmapRenderer } from 
'../echarts-renderer/sp-value-heatmap-renderer';
+import { SpHistogramRenderer } from 
'../echarts-renderer/sp-histogram-renderer';
 
 export enum WidgetType {
     Table,
@@ -37,7 +40,7 @@ export enum WidgetType {
     DistributionChart,
 }
 
-export const WidgetTypeMap = new Map<number, IWidget>([
+export const WidgetTypeMap = new Map<number, IWidget<any>>([
     [
         WidgetType.Table,
         {
@@ -96,6 +99,11 @@ export const WidgetTypeMap = new Map<number, IWidget>([
             id: 'distribution-chart',
             label: 'Distribution',
             componentClass: DistributionChartWidgetComponent,
+            renderers: [
+                new SpHistogramRenderer(),
+                new SpPieRenderer(),
+                new SpValueHeatmapRenderer(),
+            ],
         },
     ],
 ]);


Reply via email to