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

dominikriemer pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/streampipes.git


The following commit(s) were added to refs/heads/dev by this push:
     new 4e2a24c510 feat: Improve chart query builder (#4557)
4e2a24c510 is described below

commit 4e2a24c5104ea7890ee56ca44727f529f5f9891c
Author: Dominik Riemer <[email protected]>
AuthorDate: Sun Jun 14 19:00:59 2026 +0200

    feat: Improve chart query builder (#4557)
---
 .../influx/DataLakeInfluxQueryBuilder.java         |  13 ++-
 .../influx/DataLakeQueryBuilderTest.java           |  25 +++++
 .../dataexplorer/influx/SelectQueryParamsTest.java |  72 +++++++++++++
 .../utils/ProvidedQueryParameterBuilder.java       |   6 ++
 .../param/ProvidedRestQueryParamConverter.java     |   2 +-
 .../dataexplorer/param/model/FillClauseParams.java |  15 ++-
 .../param/model/InfluxQueryParameterValidator.java |  22 ++++
 .../datalake/param/SupportedRestQueryParams.java   |   2 +
 ui/deployment/i18n/de.json                         |   7 ++
 ui/deployment/i18n/en.json                         |   9 +-
 ui/deployment/i18n/pl.json                         |   7 ++
 .../lib/model/datalake/DatalakeQueryParameters.ts  |   1 +
 .../model/datalake/data-lake-query-config.model.ts |   1 +
 .../src/lib/query/DatalakeQueryParameterBuilder.ts |   6 ++
 .../lib/query/data-view-query-generator.service.ts |   4 +
 .../aggregate-configuration.component.html         |  15 ++-
 .../aggregate-configuration.component.ts           |  10 +-
 .../chart-data-settings.component.html             |   8 ++
 .../data-settings/chart-data-settings.component.ts |   3 +
 .../fill-configuration.component.html              |  68 ++++++++++++
 .../fill-configuration.component.scss              |  21 ----
 .../fill-configuration.component.ts                | 115 +++++++++++++++++++++
 22 files changed, 392 insertions(+), 40 deletions(-)

diff --git 
a/streampipes-data-explorer-influx/src/main/java/org/apache/streampipes/dataexplorer/influx/DataLakeInfluxQueryBuilder.java
 
b/streampipes-data-explorer-influx/src/main/java/org/apache/streampipes/dataexplorer/influx/DataLakeInfluxQueryBuilder.java
index 74ffa2428a..30111765d4 100644
--- 
a/streampipes-data-explorer-influx/src/main/java/org/apache/streampipes/dataexplorer/influx/DataLakeInfluxQueryBuilder.java
+++ 
b/streampipes-data-explorer-influx/src/main/java/org/apache/streampipes/dataexplorer/influx/DataLakeInfluxQueryBuilder.java
@@ -99,14 +99,15 @@ public class DataLakeInfluxQueryBuilder implements 
IDataLakeQueryBuilder<Query>
                                                          AggregationFunction 
aggregationFunction,
                                                          String aliasName) {
 
-    this.selectionQuery.function(aggregationFunction.toDbName(), 
columnName).as(aliasName);
+    this.selectionQuery.function(aggregationFunction.toDbName(), 
escapeIdentifier(columnName))
+        .as(escapeIdentifier(aliasName));
 
     return this;
   }
 
   @Override
   public IDataLakeQueryBuilder<Query> withAggregatedColumn(String columnName, 
AggregationFunction aggregationFunction) {
-    this.selectionQuery.function(aggregationFunction.toDbName(), columnName);
+    this.selectionQuery.function(aggregationFunction.toDbName(), 
escapeIdentifier(columnName));
 
     return this;
   }
@@ -320,4 +321,12 @@ public class DataLakeInfluxQueryBuilder implements 
IDataLakeQueryBuilder<Query>
   private String escapeIndex(String index) {
     return "\"" + index + "\"";
   }
+
+  private String escapeIdentifier(String identifier) {
+    if (identifier.matches("[A-Za-z0-9_]+")) {
+      return identifier;
+    }
+
+    return "\"" + identifier.replace("\"", "\\\"") + "\"";
+  }
 }
diff --git 
a/streampipes-data-explorer-influx/src/test/java/org/apache/streampipes/dataexplorer/influx/DataLakeQueryBuilderTest.java
 
b/streampipes-data-explorer-influx/src/test/java/org/apache/streampipes/dataexplorer/influx/DataLakeQueryBuilderTest.java
index 48caeaa516..524e12c2fa 100644
--- 
a/streampipes-data-explorer-influx/src/test/java/org/apache/streampipes/dataexplorer/influx/DataLakeQueryBuilderTest.java
+++ 
b/streampipes-data-explorer-influx/src/test/java/org/apache/streampipes/dataexplorer/influx/DataLakeQueryBuilderTest.java
@@ -18,6 +18,8 @@
 
 package org.apache.streampipes.dataexplorer.influx;
 
+import org.apache.streampipes.model.datalake.AggregationFunction;
+
 import org.junit.jupiter.api.Test;
 
 import java.util.List;
@@ -37,4 +39,27 @@ public class DataLakeQueryBuilderTest {
     var expected = String.format("SELECT one,two FROM \"%s\";", MEASUREMENT);
     assertEquals(expected , result.getCommand());
   }
+
+  @Test
+  public void withAggregatedColumnEscapesDottedFieldAndAliasTest() {
+    var result = DataLakeInfluxQueryBuilder
+        .create(MEASUREMENT)
+        .withAggregatedColumn("temperature.a", AggregationFunction.MEAN, 
"temperature.a")
+        .build();
+
+    var expected = String.format("SELECT MEAN(\"temperature.a\") AS 
\"temperature.a\" FROM \"%s\";", MEASUREMENT);
+    assertEquals(expected, result.getCommand());
+  }
+
+  @Test
+  public void withGroupByEscapesDottedFieldTest() {
+    var result = DataLakeInfluxQueryBuilder
+        .create(MEASUREMENT)
+        .withSimpleColumn("value")
+        .withGroupBy("temperature.a")
+        .build();
+
+    var expected = String.format("SELECT value FROM \"%s\" GROUP BY 
\"temperature.a\";", MEASUREMENT);
+    assertEquals(expected, result.getCommand());
+  }
 }
diff --git 
a/streampipes-data-explorer-influx/src/test/java/org/apache/streampipes/dataexplorer/influx/SelectQueryParamsTest.java
 
b/streampipes-data-explorer-influx/src/test/java/org/apache/streampipes/dataexplorer/influx/SelectQueryParamsTest.java
index b79f55d98d..6513f2a7c3 100644
--- 
a/streampipes-data-explorer-influx/src/test/java/org/apache/streampipes/dataexplorer/influx/SelectQueryParamsTest.java
+++ 
b/streampipes-data-explorer-influx/src/test/java/org/apache/streampipes/dataexplorer/influx/SelectQueryParamsTest.java
@@ -236,6 +236,66 @@ public class SelectQueryParamsTest {
     assertEquals("SELECT value FROM \"abc\" GROUP BY time(1ms),\"sensorId\" 
fill(none);", query);
   }
 
+  @Test
+  public void testGroupByTimeWithPreviousFill() {
+    var params = ProvidedQueryParameterBuilder.create("abc")
+        .withSimpleColumns(List.of("value"))
+        .withTimeInterval("1h")
+        .withFill("previous")
+        .build();
+
+    SelectQueryParams qp = 
ProvidedRestQueryParamConverter.getSelectQueryParams(params);
+
+    String query = 
qp.toQuery(DataLakeInfluxQueryBuilder.create("abc")).getCommand();
+
+    assertEquals("SELECT value FROM \"abc\" GROUP BY time(1h) 
fill(previous);", query);
+  }
+
+  @Test
+  public void testGroupByTimeWithLinearFill() {
+    var params = ProvidedQueryParameterBuilder.create("abc")
+        .withSimpleColumns(List.of("value"))
+        .withTimeInterval("1h")
+        .withFill("linear")
+        .build();
+
+    SelectQueryParams qp = 
ProvidedRestQueryParamConverter.getSelectQueryParams(params);
+
+    String query = 
qp.toQuery(DataLakeInfluxQueryBuilder.create("abc")).getCommand();
+
+    assertEquals("SELECT value FROM \"abc\" GROUP BY time(1h) fill(linear);", 
query);
+  }
+
+  @Test
+  public void testGroupByTimeWithNullFill() {
+    var params = ProvidedQueryParameterBuilder.create("abc")
+        .withSimpleColumns(List.of("value"))
+        .withTimeInterval("1h")
+        .withFill("null")
+        .build();
+
+    SelectQueryParams qp = 
ProvidedRestQueryParamConverter.getSelectQueryParams(params);
+
+    String query = 
qp.toQuery(DataLakeInfluxQueryBuilder.create("abc")).getCommand();
+
+    assertEquals("SELECT value FROM \"abc\" GROUP BY time(1h) fill(null);", 
query);
+  }
+
+  @Test
+  public void testGroupByTimeWithNumericFill() {
+    var params = ProvidedQueryParameterBuilder.create("abc")
+        .withSimpleColumns(List.of("value"))
+        .withTimeInterval("1h")
+        .withFill("12.5")
+        .build();
+
+    SelectQueryParams qp = 
ProvidedRestQueryParamConverter.getSelectQueryParams(params);
+
+    String query = 
qp.toQuery(DataLakeInfluxQueryBuilder.create("abc")).getCommand();
+
+    assertEquals("SELECT value FROM \"abc\" GROUP BY time(1h) fill(12.5);", 
query);
+  }
+
   @Test
   public void testGroupByTimeRejectsUnsafeInterval() {
     var params = ProvidedQueryParameterBuilder.create("abc")
@@ -247,6 +307,18 @@ public class SelectQueryParamsTest {
         () -> ProvidedRestQueryParamConverter.getSelectQueryParams(params));
   }
 
+  @Test
+  public void testGroupByTimeRejectsUnsafeFill() {
+    var params = ProvidedQueryParameterBuilder.create("abc")
+        .withSimpleColumns(List.of("value"))
+        .withTimeInterval("1h")
+        .withFill("previous); DROP MEASUREMENT foo --")
+        .build();
+
+    assertThrows(IllegalArgumentException.class,
+        () -> ProvidedRestQueryParamConverter.getSelectQueryParams(params));
+  }
+
   @Test
   public void testGroupByRejectsUnsafeIdentifier() {
     var params = ProvidedQueryParameterBuilder.create("abc")
diff --git 
a/streampipes-data-explorer-influx/src/test/java/org/apache/streampipes/dataexplorer/influx/utils/ProvidedQueryParameterBuilder.java
 
b/streampipes-data-explorer-influx/src/test/java/org/apache/streampipes/dataexplorer/influx/utils/ProvidedQueryParameterBuilder.java
index 944b4f6a66..3b28051000 100644
--- 
a/streampipes-data-explorer-influx/src/test/java/org/apache/streampipes/dataexplorer/influx/utils/ProvidedQueryParameterBuilder.java
+++ 
b/streampipes-data-explorer-influx/src/test/java/org/apache/streampipes/dataexplorer/influx/utils/ProvidedQueryParameterBuilder.java
@@ -74,6 +74,12 @@ public class ProvidedQueryParameterBuilder {
     return this;
   }
 
+  public ProvidedQueryParameterBuilder withFill(String fill) {
+    this.queryParams.put(SupportedRestQueryParams.QP_FILL, fill);
+
+    return this;
+  }
+
   public ProvidedQueryParameterBuilder withFilter(String filter) {
     this.queryParams.put(SupportedRestQueryParams.QP_FILTER, filter);
 
diff --git 
a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/ProvidedRestQueryParamConverter.java
 
b/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/ProvidedRestQueryParamConverter.java
index 90569a26d2..e6926308c2 100644
--- 
a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/ProvidedRestQueryParamConverter.java
+++ 
b/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/ProvidedRestQueryParamConverter.java
@@ -78,7 +78,7 @@ public class ProvidedRestQueryParamConverter {
     if (params.has(SupportedRestQueryParams.QP_TIME_INTERVAL)) {
       String timeInterval = 
params.getAsString(SupportedRestQueryParams.QP_TIME_INTERVAL);
       
queryParameters.withGroupByTimeParams(GroupByTimeClauseParams.from(timeInterval));
-      queryParameters.withFillParams(FillClauseParams.from());
+      
queryParameters.withFillParams(FillClauseParams.from(params.getAsString(SupportedRestQueryParams.QP_FILL)));
     }
 
     if (params.has(SupportedRestQueryParams.QP_GROUP_BY)) {
diff --git 
a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/FillClauseParams.java
 
b/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/FillClauseParams.java
index ccece054ba..f4d133d4b9 100644
--- 
a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/FillClauseParams.java
+++ 
b/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/FillClauseParams.java
@@ -22,15 +22,28 @@ import 
org.apache.streampipes.dataexplorer.api.IDataLakeQueryBuilder;
 import org.apache.streampipes.dataexplorer.api.IQueryStatement;
 
 public class FillClauseParams implements IQueryStatement {
-  String fill = "none";
+  private final Object fill;
 
   protected FillClauseParams() {
+    this.fill = "none";
   }
 
   public static FillClauseParams from() {
     return new FillClauseParams();
   }
 
+  protected FillClauseParams(String fill) {
+    this.fill = InfluxQueryParameterValidator.requireValidFill(fill);
+  }
+
+  public static FillClauseParams from(String fill) {
+    if (fill == null || fill.isBlank()) {
+      return from();
+    }
+
+    return new FillClauseParams(fill);
+  }
+
   @Override
   public void buildStatement(IDataLakeQueryBuilder<?> builder) {
     builder.withFill(fill);
diff --git 
a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/InfluxQueryParameterValidator.java
 
b/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/InfluxQueryParameterValidator.java
index 750d1b3ac8..6eaaba1bd1 100644
--- 
a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/InfluxQueryParameterValidator.java
+++ 
b/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/InfluxQueryParameterValidator.java
@@ -26,6 +26,7 @@ final class InfluxQueryParameterValidator {
 
   private static final Pattern SAFE_TIME_INTERVAL = 
Pattern.compile("^\\d+(ms|s|m|h|d|w)$");
   private static final Pattern SAFE_IDENTIFIER = 
Pattern.compile("^[^\\s,;()\"']+$");
+  private static final Pattern NUMERIC_FILL = 
Pattern.compile("^-?\\d+(\\.\\d+)?$");
 
   private InfluxQueryParameterValidator() {
   }
@@ -47,4 +48,25 @@ final class InfluxQueryParameterValidator {
 
     return identifier;
   }
+
+  static Object requireValidFill(String fill) {
+    if (fill == null || fill.isBlank()) {
+      return "none";
+    }
+
+    String normalized = fill.trim().toLowerCase();
+
+    if (normalized.equals("none")
+        || normalized.equals("null")
+        || normalized.equals("previous")
+        || normalized.equals("linear")) {
+      return normalized;
+    }
+
+    if (NUMERIC_FILL.matcher(normalized).matches()) {
+      return Double.parseDouble(normalized);
+    }
+
+    throw new IllegalArgumentException("Invalid fill parameter: " + fill);
+  }
 }
diff --git 
a/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/param/SupportedRestQueryParams.java
 
b/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/param/SupportedRestQueryParams.java
index 667d02291f..2eea5c1877 100644
--- 
a/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/param/SupportedRestQueryParams.java
+++ 
b/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/param/SupportedRestQueryParams.java
@@ -32,6 +32,7 @@ public class SupportedRestQueryParams {
   public static final String QP_ORDER = "order";
   public static final String QP_AGGREGATION_FUNCTION = "aggregationFunction";
   public static final String QP_TIME_INTERVAL = "timeInterval";
+  public static final String QP_FILL = "fill";
   public static final String QP_FORMAT = "format";
   public static final String QP_CSV_DELIMITER = "delimiter";
 
@@ -57,6 +58,7 @@ public class SupportedRestQueryParams {
       QP_ORDER,
       QP_AGGREGATION_FUNCTION,
       QP_TIME_INTERVAL,
+      QP_FILL,
       QP_FORMAT,
       QP_CSV_DELIMITER,
       QP_COUNT_ONLY,
diff --git a/ui/deployment/i18n/de.json b/ui/deployment/i18n/de.json
index 2eea78a5da..e0aada136d 100644
--- a/ui/deployment/i18n/de.json
+++ b/ui/deployment/i18n/de.json
@@ -263,6 +263,7 @@
   "Current year": "Aktuelles Jahr",
   "Currently configured query": "Aktuell konfigurierte Anfrage",
   "Custom": "Benutzerdefiniert",
+  "Custom Value": "Benutzerdefinierter Wert",
   "Custom color mapping": "Benutzerdefiniertes Farb-Mapping",
   "Custom fields": "Benutzerdefinierte Felder",
   "Dark mode": "Dunkel",
@@ -448,6 +449,8 @@
   "Filename": "Dateiname",
   "Files": "Dateien",
   "Filetype": "Dateityp",
+  "Fill": "Auffüllen",
+  "Fill Value": "Auffüllwert",
   "Filter": "Filter",
   "Filter Preview": "Filtervorschau",
   "Filter assets": "Assets filtern",
@@ -495,6 +498,7 @@
   "Grouped": "Gruppiert",
   "Grouped colors": "Farben für Gruppierung",
   "Groups": "Gruppen",
+  "Handle empty values": "Leere Werte behandeln",
   "Header": "Header",
   "Header column name": "Spaltenname",
   "Help": "Hilfe",
@@ -593,6 +597,7 @@
   "Light mode": "Hell",
   "Limit": "Limit",
   "Line": "Linien",
+  "Linear Interpolation": "Lineare Interpolation",
   "Link Type": "Link Typ",
   "Linked Resources": "Verknüpfte Ressourcen",
   "Links": "Links",
@@ -792,6 +797,7 @@
   "Preview is being prepared from the current CSV configuration.": "Die 
Vorschau wird aus der aktuellen CSV-Konfiguration erstellt.",
   "Preview is currently unavailable.": "Die Vorschau ist derzeit nicht 
verfügbar.",
   "Previous": "Zurück",
+  "Previous Value": "Vorheriger Wert",
   "Previous page": "Vorherige Seite",
   "Privileges": "Rechte",
   "Probable cause": "Wahrscheinliche Ursache",
@@ -859,6 +865,7 @@
   "Restore password": "Passwort wiederherstellen",
   "Restrict to service tags": "Auf Service-Tags beschränken",
   "Result": "Ergebnis",
+  "Result Labels": "Ergebnisbeschriftungen",
   "Retention": "Aufbewahrung",
   "Retention Log": "Speicherprotokoll",
   "Right": "Rechts",
diff --git a/ui/deployment/i18n/en.json b/ui/deployment/i18n/en.json
index 0c4193aaa4..c81b30c8a5 100644
--- a/ui/deployment/i18n/en.json
+++ b/ui/deployment/i18n/en.json
@@ -263,6 +263,7 @@
   "Current year": null,
   "Currently configured query": null,
   "Custom": null,
+  "Custom Value": null,
   "Custom color mapping": null,
   "Custom fields": null,
   "Dark mode": null,
@@ -448,6 +449,8 @@
   "Filename": null,
   "Files": null,
   "Filetype": null,
+  "Fill": null,
+  "Fill Value": null,
   "Filter": null,
   "Filter Preview": null,
   "Filter assets": null,
@@ -495,6 +498,7 @@
   "Grouped": null,
   "Grouped colors": null,
   "Groups": null,
+  "Handle empty values": null,
   "Header": null,
   "Header column name": null,
   "Help": null,
@@ -593,6 +597,7 @@
   "Light mode": null,
   "Limit": null,
   "Line": null,
+  "Linear Interpolation": null,
   "Link Type": null,
   "Linked Resources": null,
   "Links": null,
@@ -792,6 +797,7 @@
   "Preview is being prepared from the current CSV configuration.": null,
   "Preview is currently unavailable.": null,
   "Previous": null,
+  "Previous Value": null,
   "Previous page": null,
   "Privileges": null,
   "Probable cause": null,
@@ -859,6 +865,7 @@
   "Restore password": null,
   "Restrict to service tags": null,
   "Result": null,
+  "Result Labels": null,
   "Retention": null,
   "Retention Log": null,
   "Right": null,
@@ -899,8 +906,8 @@
   "Schema fields": null,
   "Script": null,
   "Search": null,
-  "Search datasets": null,
   "Search charts": null,
+  "Search datasets": null,
   "Searching for available extensions, please wait...": null,
   "Second": null,
   "Secret Key": null,
diff --git a/ui/deployment/i18n/pl.json b/ui/deployment/i18n/pl.json
index a53abc89a1..506b51c71c 100644
--- a/ui/deployment/i18n/pl.json
+++ b/ui/deployment/i18n/pl.json
@@ -263,6 +263,7 @@
   "Current year": "Bieżący rok",
   "Currently configured query": "Aktualnie skonfigurowane zapytanie",
   "Custom": "Niestandardowe",
+  "Custom Value": "Wartość niestandardowa",
   "Custom color mapping": "Własne mapowanie kolorów",
   "Custom fields": "Pola niestandardowe",
   "Dark mode": "Ciemny",
@@ -448,6 +449,8 @@
   "Filename": "Nazwa pliku",
   "Files": "Pliki",
   "Filetype": "Typ pliku",
+  "Fill": "Wypełnij",
+  "Fill Value": "Wartość wypełnienia",
   "Filter": "Filtr",
   "Filter Preview": "Podgląd filtra",
   "Filter assets": "Filtruj zasoby",
@@ -495,6 +498,7 @@
   "Grouped": "Zgrupowane",
   "Grouped colors": "Kolory grupowania",
   "Groups": "Grupy",
+  "Handle empty values": "Obsłuż puste wartości",
   "Header": "Nagłówek",
   "Header column name": "Nazwa kolumny nagłówka",
   "Help": "Pomoc",
@@ -593,6 +597,7 @@
   "Light mode": "Jasny",
   "Limit": "Limit",
   "Line": "Liniowy",
+  "Linear Interpolation": "Interpolacja liniowa",
   "Link Type": "Typ linku",
   "Linked Resources": "Powiązane zasoby",
   "Links": "Linki",
@@ -792,6 +797,7 @@
   "Preview is being prepared from the current CSV configuration.": "Podgląd 
jest przygotowywany na podstawie bieżącej konfiguracji CSV.",
   "Preview is currently unavailable.": "Podgląd jest obecnie niedostępny.",
   "Previous": "Wstecz",
+  "Previous Value": "Poprzednia wartość",
   "Previous page": "Poprzednia strona",
   "Privileges": "Uprawnienia",
   "Probable cause": "Prawdopodobna przyczyna",
@@ -859,6 +865,7 @@
   "Restore password": "Przywróć hasło",
   "Restrict to service tags": "Ogranicz do tagów usług",
   "Result": "Wynik",
+  "Result Labels": "Etykiety wyników",
   "Retention": "Retencja",
   "Retention Log": "Dziennik retencji",
   "Right": "Prawa",
diff --git 
a/ui/projects/streampipes/platform-services/src/lib/model/datalake/DatalakeQueryParameters.ts
 
b/ui/projects/streampipes/platform-services/src/lib/model/datalake/DatalakeQueryParameters.ts
index eaf8627e11..e288b47fe5 100644
--- 
a/ui/projects/streampipes/platform-services/src/lib/model/datalake/DatalakeQueryParameters.ts
+++ 
b/ui/projects/streampipes/platform-services/src/lib/model/datalake/DatalakeQueryParameters.ts
@@ -29,6 +29,7 @@ export interface DatalakeQueryParameters {
     order?: string;
     aggregationFunction?: string;
     timeInterval?: string;
+    fill?: string | number;
     countOnly?: boolean;
     autoAggregate?: boolean;
     filter?: string;
diff --git 
a/ui/projects/streampipes/platform-services/src/lib/model/datalake/data-lake-query-config.model.ts
 
b/ui/projects/streampipes/platform-services/src/lib/model/datalake/data-lake-query-config.model.ts
index 6b648c48f8..60a3957286 100644
--- 
a/ui/projects/streampipes/platform-services/src/lib/model/datalake/data-lake-query-config.model.ts
+++ 
b/ui/projects/streampipes/platform-services/src/lib/model/datalake/data-lake-query-config.model.ts
@@ -77,6 +77,7 @@ export interface QueryConfig {
     aggregationValue?: number;
     aggregationTimeUnit?: string;
     aggregationFunction?: string;
+    fill?: string | number;
 }
 
 export interface SourceConfig {
diff --git 
a/ui/projects/streampipes/platform-services/src/lib/query/DatalakeQueryParameterBuilder.ts
 
b/ui/projects/streampipes/platform-services/src/lib/query/DatalakeQueryParameterBuilder.ts
index f8ab65e7c9..1c7d78b2bf 100644
--- 
a/ui/projects/streampipes/platform-services/src/lib/query/DatalakeQueryParameterBuilder.ts
+++ 
b/ui/projects/streampipes/platform-services/src/lib/query/DatalakeQueryParameterBuilder.ts
@@ -80,6 +80,12 @@ export class DatalakeQueryParameterBuilder {
         return this;
     }
 
+    public withFill(fill: string | number): DatalakeQueryParameterBuilder {
+        this.queryParams.fill = fill;
+
+        return this;
+    }
+
     public withGrouping(groupBy: FieldConfig[]): DatalakeQueryParameterBuilder 
{
         const groupByRuntimeNames = groupBy.map(
             property => property.runtimeName,
diff --git 
a/ui/projects/streampipes/platform-services/src/lib/query/data-view-query-generator.service.ts
 
b/ui/projects/streampipes/platform-services/src/lib/query/data-view-query-generator.service.ts
index ccb3e33bcc..ef1c5cf9f4 100644
--- 
a/ui/projects/streampipes/platform-services/src/lib/query/data-view-query-generator.service.ts
+++ 
b/ui/projects/streampipes/platform-services/src/lib/query/data-view-query-generator.service.ts
@@ -138,6 +138,10 @@ export class DataViewQueryGeneratorService {
                     queryConfig.aggregationValue,
                 );
             }
+
+            if (queryConfig.fill !== undefined) {
+                queryBuilder.withFill(queryConfig.fill);
+            }
         }
 
         if (ignoreEventsWithMissingValues) {
diff --git 
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/aggregate-configuration/aggregate-configuration.component.html
 
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/aggregate-configuration/aggregate-configuration.component.html
index 9fd6a3014f..706df8aba1 100644
--- 
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/aggregate-configuration/aggregate-configuration.component.html
+++ 
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/aggregate-configuration/aggregate-configuration.component.html
@@ -16,7 +16,7 @@
 ~
 -->
 
-<div fxLayout="column" fxLayoutAlign="stretch" fxFlex="100">
+<div fxLayout="column" fxFlex="100">
     <div
         fxFlex="100"
         fxLayout="column"
@@ -24,12 +24,7 @@
         class="ml-0 mr-5 form-field-small"
     >
         <sp-form-field [level]="3" [label]="'Unit' | translate" class="w-100">
-            <mat-form-field
-                appearance="outline"
-                fxFlex="100"
-                class="w-100"
-                color="accent"
-            >
+            <mat-form-field appearance="outline" fxFlex="100" class="w-100">
                 <mat-select
                     [(value)]="queryConfig.aggregationTimeUnit"
                     class="w-100"
@@ -43,7 +38,11 @@
                 </mat-select>
             </mat-form-field>
         </sp-form-field>
-        <sp-form-field [level]="3" [label]="'Value' | translate" class="w-100">
+        <sp-form-field
+            [level]="3"
+            [label]="'Value' | translate"
+            class="w-100 mb-10"
+        >
             <mat-form-field
                 appearance="outline"
                 fxFlex="100"
diff --git 
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/aggregate-configuration/aggregate-configuration.component.ts
 
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/aggregate-configuration/aggregate-configuration.component.ts
index 77ee49957f..c8514ddaf3 100644
--- 
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/aggregate-configuration/aggregate-configuration.component.ts
+++ 
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/aggregate-configuration/aggregate-configuration.component.ts
@@ -66,11 +66,9 @@ export class AggregateConfigurationComponent {
     ];
 
     triggerDataRefresh() {
-        if (this.widgetId) {
-            this.widgetConfigService.notify({
-                refreshData: true,
-                refreshView: true,
-            });
-        }
+        this.widgetConfigService.notify({
+            refreshData: true,
+            refreshView: true,
+        });
     }
 }
diff --git 
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.html
 
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.html
index dc988a4145..af89a9f414 100644
--- 
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.html
+++ 
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.html
@@ -351,6 +351,14 @@
                                                         >
                                                         
</sp-aggregate-configuration>
                                                     }
+                                                    <sp-fill-configuration
+                                                        class="mt-10"
+                                                        [queryConfig]="
+                                                            
sourceConfig.queryConfig
+                                                        "
+                                                        [widgetId]="widgetId"
+                                                    >
+                                                    </sp-fill-configuration>
                                                 </div>
                                             }
                                         </div>
diff --git 
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.ts
 
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.ts
index 71477ca0ac..5502cff7be 100644
--- 
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.ts
+++ 
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/chart-data-settings.component.ts
@@ -74,6 +74,7 @@ import { ClassDirective } from 
'@ngbracket/ngx-layout/extended';
 import { MatInput } from '@angular/material/input';
 import { MatCheckbox } from '@angular/material/checkbox';
 import { AggregateConfigurationComponent } from 
'./aggregate-configuration/aggregate-configuration.component';
+import { FillConfigurationComponent } from 
'./fill-configuration/fill-configuration.component';
 import { FilterSelectionPanelComponent } from 
'./filter-selection-panel/filter-selection-panel.component';
 import { OrderSelectionPanelComponent } from 
'./order-selection-panel/order-selection-panel.component';
 import { TranslatePipe } from '@ngx-translate/core';
@@ -116,6 +117,7 @@ import {
         MatAutocomplete,
         MatAutocompleteTrigger,
         AggregateConfigurationComponent,
+        FillConfigurationComponent,
         FieldSelectionPanelComponent,
         FilterSelectionPanelComponent,
         GroupSelectionPanelComponent,
@@ -332,6 +334,7 @@ export class ChartDataSettingsComponent implements OnInit {
                 page: 1,
                 aggregationTimeUnit: 'd',
                 aggregationValue: 1,
+                fill: 'none',
             },
             queryType: 'raw',
         };
diff --git 
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/fill-configuration/fill-configuration.component.html
 
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/fill-configuration/fill-configuration.component.html
new file mode 100644
index 0000000000..43b4750a6f
--- /dev/null
+++ 
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/fill-configuration/fill-configuration.component.html
@@ -0,0 +1,68 @@
+<!--
+~ 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">
+    <div
+        fxFlex="100"
+        fxLayout="column"
+        fxLayoutAlign="start center"
+        class="ml-0 mr-5 form-field-small"
+    >
+        <sp-form-field
+            [level]="3"
+            [label]="'Fill' | translate"
+            class="w-100"
+            [description]="'Handle empty values' | translate"
+        >
+            <mat-form-field appearance="outline" fxFlex="100" class="w-100">
+                <mat-select
+                    [(ngModel)]="fillMode"
+                    class="w-100"
+                    (selectionChange)="updateFillMode(fillMode)"
+                >
+                    @for (option of fillOptions; track option.value) {
+                        <mat-option [value]="option.value">{{
+                            option.label | translate
+                        }}</mat-option>
+                    }
+                </mat-select>
+            </mat-form-field>
+        </sp-form-field>
+        @if (fillMode === 'number') {
+            <sp-form-field
+                [level]="3"
+                [label]="'Fill Value' | translate"
+                class="w-100"
+            >
+                <mat-form-field
+                    appearance="outline"
+                    fxFlex="100"
+                    class="w-100"
+                    color="accent"
+                >
+                    <input
+                        matInput
+                        type="number"
+                        [(ngModel)]="customFillValue"
+                        (ngModelChange)="updateCustomFillValue()"
+                    />
+                </mat-form-field>
+            </sp-form-field>
+        }
+    </div>
+</div>
diff --git 
a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/FillClauseParams.java
 
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/fill-configuration/fill-configuration.component.scss
similarity index 61%
copy from 
streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/FillClauseParams.java
copy to 
ui/src/app/chart/components/chart-view/designer-panel/data-settings/fill-configuration/fill-configuration.component.scss
index ccece054ba..13cbc4aacb 100644
--- 
a/streampipes-data-explorer/src/main/java/org/apache/streampipes/dataexplorer/param/model/FillClauseParams.java
+++ 
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/fill-configuration/fill-configuration.component.scss
@@ -15,24 +15,3 @@
  * limitations under the License.
  *
  */
-
-package org.apache.streampipes.dataexplorer.param.model;
-
-import org.apache.streampipes.dataexplorer.api.IDataLakeQueryBuilder;
-import org.apache.streampipes.dataexplorer.api.IQueryStatement;
-
-public class FillClauseParams implements IQueryStatement {
-  String fill = "none";
-
-  protected FillClauseParams() {
-  }
-
-  public static FillClauseParams from() {
-    return new FillClauseParams();
-  }
-
-  @Override
-  public void buildStatement(IDataLakeQueryBuilder<?> builder) {
-    builder.withFill(fill);
-  }
-}
diff --git 
a/ui/src/app/chart/components/chart-view/designer-panel/data-settings/fill-configuration/fill-configuration.component.ts
 
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/fill-configuration/fill-configuration.component.ts
new file mode 100644
index 0000000000..9b0ab2a203
--- /dev/null
+++ 
b/ui/src/app/chart/components/chart-view/designer-panel/data-settings/fill-configuration/fill-configuration.component.ts
@@ -0,0 +1,115 @@
+/*
+ * 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, OnInit, inject } from '@angular/core';
+import { QueryConfig } from '@streampipes/platform-services';
+import { ChartConfigurationService } from 
'../../../../../../chart-shared/services/chart-configuration.service';
+import { TranslatePipe, TranslateService } from '@ngx-translate/core';
+import {
+    FlexDirective,
+    LayoutAlignDirective,
+    LayoutDirective,
+} from '@ngbracket/ngx-layout/flex';
+import { FormFieldComponent } from '@streampipes/shared-ui';
+import { MatFormField } from '@angular/material/form-field';
+import { MatOption, MatSelect } from '@angular/material/select';
+import { MatInput } from '@angular/material/input';
+import { FormsModule } from '@angular/forms';
+
+type FillMode = 'none' | 'previous' | 'linear' | 'null' | 'number';
+
+@Component({
+    selector: 'sp-fill-configuration',
+    templateUrl: './fill-configuration.component.html',
+    styleUrls: ['./fill-configuration.component.scss'],
+    imports: [
+        LayoutDirective,
+        LayoutAlignDirective,
+        FlexDirective,
+        FormFieldComponent,
+        MatFormField,
+        MatSelect,
+        MatOption,
+        MatInput,
+        FormsModule,
+        TranslatePipe,
+    ],
+})
+export class FillConfigurationComponent implements OnInit {
+    private widgetConfigService = inject(ChartConfigurationService);
+    private translate = inject(TranslateService);
+
+    @Input() queryConfig: QueryConfig;
+    @Input() widgetId: string;
+
+    fillMode: FillMode = 'none';
+    customFillValue = 0;
+
+    fillOptions: Array<{ value: FillMode; label: string }> = [
+        { value: 'none', label: this.translate.instant('None') },
+        { value: 'previous', label: this.translate.instant('Previous Value') },
+        {
+            value: 'linear',
+            label: this.translate.instant('Linear Interpolation'),
+        },
+        { value: 'null', label: 'Null' },
+        { value: 'number', label: this.translate.instant('Custom Value') },
+    ];
+
+    ngOnInit(): void {
+        if (
+            typeof this.queryConfig.fill === 'number' &&
+            !Number.isNaN(this.queryConfig.fill)
+        ) {
+            this.fillMode = 'number';
+            this.customFillValue = this.queryConfig.fill;
+            return;
+        }
+
+        const configuredMode = this.queryConfig.fill;
+        if (
+            configuredMode === 'none' ||
+            configuredMode === 'previous' ||
+            configuredMode === 'linear' ||
+            configuredMode === 'null'
+        ) {
+            this.fillMode = configuredMode;
+            return;
+        }
+
+        this.queryConfig.fill = 'none';
+    }
+
+    updateFillMode(mode: FillMode): void {
+        this.fillMode = mode;
+        this.queryConfig.fill = mode === 'number' ? this.customFillValue : 
mode;
+        this.triggerDataRefresh();
+    }
+
+    updateCustomFillValue(): void {
+        this.queryConfig.fill = this.customFillValue;
+        this.triggerDataRefresh();
+    }
+
+    triggerDataRefresh(): void {
+        this.widgetConfigService.notify({
+            refreshData: true,
+            refreshView: true,
+        });
+    }
+}


Reply via email to