You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@streampipes.apache.org by ri...@apache.org on 2023/08/28 21:03:20 UTC

[streampipes] branch improve-data-explorer-widgets created (now 08a0b2844)

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

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


      at 08a0b2844 Migrate heatmap widget to echarts renderer

This branch includes the following new commits:

     new 789bc67a2 Improve rendering of echarts widgets in data explorer
     new 08a0b2844 Migrate heatmap widget to echarts renderer

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[streampipes] 02/02: Migrate heatmap widget to echarts renderer

Posted by ri...@apache.org.
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 08a0b28444d2025ca073ed5cc55efd0783f22050
Author: Dominik Riemer <do...@gmail.com>
AuthorDate: Mon Aug 28 23:02:26 2023 +0200

    Migrate heatmap widget to echarts renderer
---
 .../base-data-explorer-echarts-widget.directive.ts |  15 ++
 .../distribution-chart-widget.component.ts         |  14 --
 .../widgets/heatmap/heatmap-widget.component.html  |  53 -----
 .../widgets/heatmap/heatmap-widget.component.scss  |  32 ---
 .../widgets/heatmap/heatmap-widget.component.ts    | 265 +--------------------
 .../echarts-renderer/sp-heatmap-renderer.ts        | 104 +++++++-
 6 files changed, 127 insertions(+), 356 deletions(-)

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
index 7bf1e1dcb..64b440ed4 100644
--- 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
@@ -18,6 +18,7 @@
 
 import { Directive, OnInit } from '@angular/core';
 import {
+    DataExplorerField,
     DataExplorerWidgetModel,
     SpQueryResult,
 } from '@streampipes/platform-services';
@@ -97,6 +98,20 @@ export abstract class BaseDataExplorerEchartsWidgetDirective<
         };
     }
 
+    triggerFieldUpdate(
+        selected: DataExplorerField,
+        addedFields: DataExplorerField[],
+        removedFields: DataExplorerField[],
+    ): DataExplorerField {
+        return this.updateSingleField(
+            selected,
+            this.fieldProvider.numericFields,
+            addedFields,
+            removedFields,
+            field => field.fieldCharacteristics.numeric,
+        );
+    }
+
     refreshView() {
         this.renderSubject.next();
     }
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 98183b228..8e4ce4423 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
@@ -45,20 +45,6 @@ export class DistributionChartWidgetComponent
             );
     }
 
-    triggerFieldUpdate(
-        selected: DataExplorerField,
-        addedFields: DataExplorerField[],
-        removedFields: DataExplorerField[],
-    ): DataExplorerField {
-        return this.updateSingleField(
-            selected,
-            this.fieldProvider.numericFields,
-            addedFields,
-            removedFields,
-            field => field.fieldCharacteristics.numeric,
-        );
-    }
-
     getRenderer(): SpEchartsRenderer<DistributionChartWidgetModel> {
         const widgetCategory = DataExplorerWidgetRegistry.getWidgetTemplate(
             WidgetType.DistributionChart,
diff --git a/ui/src/app/data-explorer/components/widgets/heatmap/heatmap-widget.component.html b/ui/src/app/data-explorer/components/widgets/heatmap/heatmap-widget.component.html
deleted file mode 100644
index cc1a8228d..000000000
--- a/ui/src/app/data-explorer/components/widgets/heatmap/heatmap-widget.component.html
+++ /dev/null
@@ -1,53 +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
-    fxFlex="100"
-    fxLayoutAlign="center center"
-    fxLayout="column"
-    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 class="value-panel">
-        <div
-            echarts
-            [options]="option"
-            [ngStyle]="{ width: currentWidth, height: '100%' }"
-            (chartInit)="onChartInit($event)"
-            [merge]="dynamic"
-            [ngStyle]="
-                showData ? { visibility: 'visible' } : { visibility: 'hidden' }
-            "
-            *ngIf="configReady"
-        ></div>
-    </div>
-</div>
diff --git a/ui/src/app/data-explorer/components/widgets/heatmap/heatmap-widget.component.scss b/ui/src/app/data-explorer/components/widgets/heatmap/heatmap-widget.component.scss
deleted file mode 100644
index c4239506e..000000000
--- a/ui/src/app/data-explorer/components/widgets/heatmap/heatmap-widget.component.scss
+++ /dev/null
@@ -1,32 +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.
- *
- */
-
-.plotly-container {
-    width: 100%;
-    align-self: start;
-    height: 100%;
-}
-
-.main-panel {
-    width: 100%;
-    height: 100%;
-    text-align: center;
-    left: 50%;
-    display: inline-grid;
-    align-content: start;
-}
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 f6188d9db..5039553db 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
@@ -17,16 +17,12 @@
  */
 
 import { Component, OnInit } from '@angular/core';
-import {
-    DataExplorerField,
-    SpQueryResult,
-} from '@streampipes/platform-services';
+import { DataExplorerField } from '@streampipes/platform-services';
 
 import { HeatmapWidgetModel } from './model/heatmap-widget.model';
-
-import { EChartsOption } from 'echarts';
 import { BaseDataExplorerEchartsWidgetDirective } from '../base/base-data-explorer-echarts-widget.directive';
 import { SpEchartsRenderer } from '../../../models/dataview-dashboard.model';
+import { SpHeatmapRenderer } from '../../../echarts-renderer/sp-heatmap-renderer';
 
 @Component({
     selector: 'sp-data-explorer-heatmap-widget',
@@ -37,259 +33,24 @@ export class HeatmapWidgetComponent
     extends BaseDataExplorerEchartsWidgetDirective<HeatmapWidgetModel>
     implements OnInit
 {
-    dynamic: EChartsOption;
-
     ngOnInit(): void {
         super.ngOnInit();
     }
 
-    onDataReceived(spQueryResult: SpQueryResult[]) {
-        this.setShownComponents(false, true, false, false);
-        const dataBundle = this.convertData(spQueryResult);
-        if (Object.keys(this.option).length > 0) {
-            this.setOptions(
-                dataBundle[0],
-                dataBundle[1],
-                dataBundle[2],
-                dataBundle[3],
-                dataBundle[4],
-            );
-        }
-    }
-
-    convertData(spQueryResult: SpQueryResult[]) {
-        let min = 1000000;
-        let max = -100000;
-
-        const result = spQueryResult[0].allDataSeries;
-        // const xAxisData = this.transform(result[0].rows, 0);
-
-        const aggregatedXData = [];
-        result.forEach(x => {
-            const localXAxisData = this.transform(x.rows, 0);
-            aggregatedXData.push(...localXAxisData);
-        });
-
-        const xAxisData = aggregatedXData.sort();
-
-        const convXAxisData = [];
-        xAxisData.forEach(x => {
-            const date = new Date(x);
-            const size = 2;
-            const year = date.getFullYear();
-            const month = this.pad(date.getMonth() + 1, size);
-            const day = date.getDate();
-            const hours = this.pad(date.getHours(), size);
-            const minutes = this.pad(date.getMinutes(), size);
-            const seconds = this.pad(date.getSeconds(), size);
-            const milli = this.pad(date.getMilliseconds(), 3);
-
-            const strDate =
-                year +
-                '-' +
-                month +
-                '-' +
-                day +
-                ' ' +
-                hours +
-                ':' +
-                minutes +
-                ':' +
-                seconds +
-                '.' +
-                milli;
-            convXAxisData.push(strDate);
-        });
-
-        const heatIndex = this.getColumnIndex(
-            this.dataExplorerWidget.visualizationConfig.selectedHeatProperty,
-            spQueryResult[0],
-        );
-
-        const yAxisData = [];
-        const contentData = [];
-
-        result.map((groupedList, index) => {
-            let groupedVal = '';
-
-            if (groupedList['tags'] != null) {
-                Object.entries(groupedList['tags']).forEach(([key, value]) => {
-                    groupedVal = value;
-                });
-            }
-
-            yAxisData.push(groupedVal);
-
-            const contentDataPure = this.transform(
-                result[index].rows,
-                heatIndex,
-            );
-            const localMax = Math.max.apply(Math, contentDataPure);
-            const localMin = Math.min.apply(Math, contentDataPure);
-
-            max = localMax > max ? localMax : max;
-            min = localMin < min ? localMin : min;
-
-            const localXAxisData = this.transform(groupedList.rows, 0);
-
-            contentDataPure.map((cnt, colIndex) => {
-                const currentX = localXAxisData[colIndex];
-                const searchedIndex = aggregatedXData.indexOf(currentX);
-                contentData.push([searchedIndex, index, cnt]);
-            });
-        });
-
-        const timeNames = convXAxisData;
-        const groupNames = yAxisData;
-
-        this.option['tooltip'] = {
-            formatter(params) {
-                const timeIndex = params.value[0];
-                const groupNameIndex = params.value[1];
-
-                const value = params.value[2];
-                const time = timeNames[timeIndex];
-                const groupName = groupNames[groupNameIndex];
-
-                let formattedTip =
-                    '<style>' +
-                    'ul {margin: 0px; padding: 0px; list-style-type: none; text-align: left}' +
-                    '</style>' +
-                    '<ul>' +
-                    '<li><b>' +
-                    'Time: ' +
-                    '</b>' +
-                    time +
-                    '</li>';
-
-                if (groupName !== '') {
-                    formattedTip =
-                        formattedTip +
-                        '<li><b>' +
-                        'Group: ' +
-                        '</b>' +
-                        groupName +
-                        '</li>';
-                }
-
-                formattedTip =
-                    formattedTip +
-                    '<li><b>' +
-                    'Value: ' +
-                    '</b>' +
-                    value +
-                    '</li>' +
-                    '</ul>';
-
-                return formattedTip;
-            },
-            position: 'top',
-        };
-
-        if (groupNames.length === 1) {
-            this.option['tooltip']['position'] = (
-                point,
-                params,
-                dom,
-                rect,
-                size,
-            ) => {
-                return [point[0], '10%'];
-            };
-        }
-
-        return [contentData, convXAxisData, yAxisData, min, max];
-    }
-
-    initOptions() {
-        this.option = {
-            tooltip: {},
-            grid: {
-                height: '80%',
-                top: '7%',
-            },
-            xAxis: {
-                type: 'category',
-                data: [],
-                splitArea: {
-                    show: true,
-                },
-            },
-            yAxis: {
-                type: 'category',
-                data: [],
-                splitArea: {
-                    show: true,
-                },
-            },
-            visualMap: {
-                min: 0,
-                max: 10,
-                calculable: true,
-                orient: 'vertical',
-                right: '5%',
-                top: '7%',
-            },
-            series: [
-                {
-                    name: '',
-                    type: 'heatmap',
-                    data: [],
-                    label: {
-                        show: true,
-                    },
-                    emphasis: {
-                        itemStyle: {
-                            shadowBlur: 10,
-                            shadowColor: 'rgba(0, 0, 0, 0.5)',
-                        },
-                    },
-                },
-            ],
-        };
-    }
-
-    setOptions(
-        contentData: any,
-        xAxisData: any,
-        yAxisData: any,
-        min: any,
-        max: any,
-    ) {
-        this.dynamic = this.option;
-        this.dynamic.series[0].data = contentData;
-        this.dynamic.series[0].label.show =
-            this.dataExplorerWidget.visualizationConfig.showLabelsProperty;
-        this.dynamic['xAxis']['data'] = xAxisData;
-        this.dynamic['yAxis']['data'] = yAxisData;
-        this.dynamic['visualMap']['min'] = min;
-        this.dynamic['visualMap']['max'] = max;
-        if (this.eChartsInstance) {
-            this.eChartsInstance.setOption(this.dynamic as EChartsOption);
-        }
-        this.option = this.dynamic;
-    }
-
-    transform(rows, index: number): any[] {
-        return rows.map(row => row[index]);
-    }
-
-    pad(num, size) {
-        num = num.toString();
-        while (num.length < size) {
-            num = '0' + num;
-        }
-        return num;
-    }
-
     getRenderer(): SpEchartsRenderer<HeatmapWidgetModel> {
-        return undefined;
+        return new SpHeatmapRenderer();
     }
 
-    protected handleUpdatedFields(
+    handleUpdatedFields(
         addedFields: DataExplorerField[],
         removedFields: DataExplorerField[],
-    ) {}
-
-    refreshView() {}
+    ) {
+        this.dataExplorerWidget.visualizationConfig.selectedHeatProperty =
+            this.triggerFieldUpdate(
+                this.dataExplorerWidget.visualizationConfig
+                    .selectedHeatProperty,
+                addedFields,
+                removedFields,
+            );
+    }
 }
diff --git a/ui/src/app/data-explorer/echarts-renderer/sp-heatmap-renderer.ts b/ui/src/app/data-explorer/echarts-renderer/sp-heatmap-renderer.ts
index d069d63f5..5a5e05f17 100644
--- a/ui/src/app/data-explorer/echarts-renderer/sp-heatmap-renderer.ts
+++ b/ui/src/app/data-explorer/echarts-renderer/sp-heatmap-renderer.ts
@@ -18,20 +18,114 @@
 
 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 { GeneratedDataset, TagValue } from '../models/dataset.model';
 import { EChartsOption } from 'echarts';
+import {
+    DimensionDefinitionLoose,
+    OptionDataValue,
+    OptionSourceDataArrayRows,
+} from 'echarts/types/src/util/types';
 
 export class SpHeatmapRenderer extends SpBaseEchartsRenderer<HeatmapWidgetModel> {
+    getType(): string {
+        return 'heatmap';
+    }
+
     applyOptions(
         datasets: GeneratedDataset,
         options: EChartsOption,
         widgetConfig: HeatmapWidgetModel,
-        widgetSize: WidgetSize,
     ): void {
-        // TODO
+        this.basicOptions(options);
+
+        const field = widgetConfig.visualizationConfig.selectedHeatProperty;
+        const sourceIndex = field.sourceIndex;
+
+        const rawDataset = datasets.dataset[sourceIndex];
+        const rawDatasetSource: OptionSourceDataArrayRows =
+            rawDataset.source as OptionSourceDataArrayRows;
+        const tags = datasets.tagValues[sourceIndex];
+        const heatIndex = rawDataset.dimensions.indexOf(field.fullDbName);
+        rawDatasetSource.shift();
+        rawDatasetSource.sort((a, b) => {
+            const dateA = new Date(a[0]);
+            const dateB = new Date(b[0]);
+            return dateA.getTime() - dateB.getTime();
+        });
+        const transformedDataset = (
+            rawDataset.source as OptionSourceDataArrayRows
+        ).map((row, index) => {
+            return [
+                index,
+                this.makeTag(rawDataset.dimensions, tags, row),
+                row[heatIndex],
+            ];
+        });
+
+        options.dataset = { source: transformedDataset };
+        (options.xAxis as any).data = rawDatasetSource.map(s => {
+            return new Date(s[0]).toLocaleString();
+        });
+        options.series = [
+            {
+                name: '',
+                type: 'heatmap',
+                datasetIndex: 0,
+                encode: {
+                    itemId: 0,
+                    value: heatIndex,
+                },
+                label: {
+                    show: widgetConfig.visualizationConfig.showLabelsProperty,
+                },
+                emphasis: {
+                    itemStyle: {
+                        shadowBlur: 10,
+                        shadowColor: 'rgba(0, 0, 0, 0.5)',
+                    },
+                },
+            },
+        ];
     }
 
-    getType(): string {
-        return 'heatmap';
+    basicOptions(options: EChartsOption): void {
+        options.tooltip = {};
+        options.grid = {
+            height: '80%',
+            top: '80',
+        };
+        options.xAxis = {
+            type: 'category',
+            splitArea: {
+                show: true,
+            },
+        };
+        options.yAxis = {
+            type: 'category',
+            splitArea: {
+                show: true,
+            },
+        };
+        options.visualMap = {
+            calculable: true,
+            orient: 'horizontal',
+            right: '5%',
+            top: '20',
+        };
+    }
+
+    private makeTag(
+        dimensions: DimensionDefinitionLoose[],
+        tags: TagValue[],
+        row: Array<OptionDataValue>,
+    ) {
+        if (tags.length > 0) {
+            const rowValues = [];
+            tags[0].tagKeys.forEach(key => {
+                const index = dimensions.indexOf(key);
+                rowValues.push(row[index]);
+            });
+            return rowValues.toString();
+        }
     }
 }


[streampipes] 01/02: Improve rendering of echarts widgets in data explorer

Posted by ri...@apache.org.
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 <do...@gmail.com>
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(),
+            ],
         },
     ],
 ]);