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

davsclaus pushed a commit to branch fix/CAMEL-23865
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 0dabf2bbc696845a2d0f86c1309998b004b474d0
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Jul 1 11:34:39 2026 +0200

    CAMEL-23865: TUI sparkline scaling for sub-1.0 throughput rates
    
    Use THROUGHPUT_SCALE (x100) in MetricsCollector so fractional msg/s
    rates are preserved as longs. Apply niceMax (1-2-5 step sequence)
    for Y-axis scaling. Use Locale.US for consistent dot decimal formatting.
    Endpoint charts use showYAxis(false) until tamboui supports yAxisFormatter.
    
    Co-Authored-By: Claude Opus 4.7 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/tui/EndpointsTab.java  | 59 +++++++++++++++++++---
 .../jbang/core/commands/tui/MetricsCollector.java  | 25 +++++++--
 .../dsl/jbang/core/commands/tui/OverviewTab.java   | 59 +++++++++++++++++-----
 3 files changed, 120 insertions(+), 23 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
index e50c0b0acb79..7e9511e6bd00 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
@@ -444,17 +444,19 @@ class EndpointsTab implements MonitorTab {
 
         Line chartTitle = Line.from(
                 Span.styled("▬", 
Style.EMPTY.fg(Color.ansi(AnsiColor.BRIGHT_GREEN))),
-                Span.raw(String.format(" in:%-4d ", curIn)),
+                Span.raw(String.format(" in:%-4s ", formatThroughput(curIn))),
                 Span.styled("▬", Style.EMPTY.fg(Color.CYAN)),
-                Span.raw(String.format(" out:%-4d msg/s", curOut)));
+                Span.raw(String.format(" out:%-4s msg/s", 
formatThroughput(curOut))));
 
         Rect rightArea = hSplit.get(1);
+        long maxEp = niceMax(Math.max(maxOf(inArr), maxOf(outArr)));
         frame.renderWidget(DualSparkline.builder()
                 .topData(inArr)
                 .bottomData(outArr)
+                .max(maxEp)
                 .topStyle(Style.EMPTY.fg(Color.ansi(AnsiColor.BRIGHT_GREEN)))
                 .bottomStyle(Style.EMPTY.fg(Color.CYAN))
-                .showYAxis(true)
+                .showYAxis(false)
                 .xLabels("-" + renderPoints + "s", "-" + (renderPoints * 3 / 
4) + "s",
                         "-" + (renderPoints / 2) + "s", "-" + (renderPoints / 
4) + "s", "now")
                 
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
@@ -555,16 +557,20 @@ class EndpointsTab implements MonitorTab {
                 Span.styled(uriLabel, Style.EMPTY.fg(Color.YELLOW)),
                 Span.raw("] "),
                 Span.styled("▬", 
Style.EMPTY.fg(Color.ansi(AnsiColor.BRIGHT_GREEN))),
-                Span.raw(String.format(" in:%-4d ", curIn)),
+                Span.raw(String.format(" in:%-4s ", formatThroughput(curIn))),
                 Span.styled("▬", Style.EMPTY.fg(Color.CYAN)),
-                Span.raw(String.format(" out:%-4d msg/s", curOut)));
+                Span.raw(String.format(" out:%-4s msg/s", 
formatThroughput(curOut))));
 
+        // TODO: use 
.showYAxis(true).yAxisFormatter(EndpointsTab::formatThroughput) when tamboui 
0.5.0 is released
+        //  see https://github.com/tamboui/tamboui/pull/396
+        long maxEpSingle = niceMax(Math.max(maxOf(inArr), maxOf(outArr)));
         frame.renderWidget(DualSparkline.builder()
                 .topData(inArr)
                 .bottomData(outArr)
+                .max(maxEpSingle)
                 .topStyle(Style.EMPTY.fg(Color.ansi(AnsiColor.BRIGHT_GREEN)))
                 .bottomStyle(Style.EMPTY.fg(Color.CYAN))
-                .showYAxis(true)
+                .showYAxis(false)
                 .xLabels("-" + renderPoints + "s", "-" + (renderPoints * 3 / 
4) + "s",
                         "-" + (renderPoints / 2) + "s", "-" + (renderPoints / 
4) + "s", "now")
                 
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
@@ -770,4 +776,45 @@ class EndpointsTab implements MonitorTab {
         result.put("selectedIndex", sel != null ? sel : -1);
         return result;
     }
+
+    private static long maxOf(long[] arr) {
+        long max = 0;
+        for (long v : arr) {
+            if (v > max) {
+                max = v;
+            }
+        }
+        return max;
+    }
+
+    private static long niceMax(long rawMax) {
+        if (rawMax <= 0) {
+            return MetricsCollector.THROUGHPUT_SCALE;
+        }
+        long scale = MetricsCollector.THROUGHPUT_SCALE;
+        int[] steps = { 1, 2, 5 };
+        long multiplier = scale;
+        while (true) {
+            for (int s : steps) {
+                long candidate = s * multiplier;
+                if (candidate >= rawMax) {
+                    return candidate;
+                }
+            }
+            multiplier *= 10;
+        }
+    }
+
+    private static String formatThroughput(long scaledValue) {
+        double v = scaledValue / (double) MetricsCollector.THROUGHPUT_SCALE;
+        if (v >= 10) {
+            return String.valueOf(Math.round(v));
+        } else if (v >= 1) {
+            return String.format(Locale.US, "%.1f", v);
+        } else if (scaledValue > 0) {
+            return String.format(Locale.US, "%.2f", v);
+        } else {
+            return "0";
+        }
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
index 974af492c85f..e4c0ffe530ee 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
@@ -33,6 +33,8 @@ class MetricsCollector {
     static final int MAX_ENDPOINT_CHART_POINTS = 300;
     static final int MAX_HEAP_HISTORY_POINTS = 120;
     static final long HEAP_SAMPLE_INTERVAL_MS = 5000;
+    // Throughput values are stored scaled by this factor so sub-1.0 msg/s 
rates are preserved as longs
+    static final long THROUGHPUT_SCALE = 100;
 
     // Throughput history per PID (one point per second)
     private final Map<String, LinkedList<Long>> throughputHistory = new 
ConcurrentHashMap<>();
@@ -164,15 +166,28 @@ class MetricsCollector {
             samples.remove(0);
         }
 
+        // Use the EWMA throughput from the status JSON (already smoothed in 
camel-core)
+        // and store scaled by 100 so sub-1.0 rates (e.g. 0.20 msg/s) are 
preserved as integers
+        long tp = 0;
+        if (info.throughput != null) {
+            try {
+                tp = Math.round(Double.parseDouble(info.throughput) * 
THROUGHPUT_SCALE);
+            } catch (NumberFormatException e) {
+                // ignore
+            }
+        }
+
+        // Failed throughput still computed from delta (no EWMA source for 
failed-only)
+        long fp = 0;
         if (samples.size() >= 2) {
             long[] oldest = samples.get(0);
             long[] newest = samples.get(samples.size() - 1);
-            long deltaTotal = newest[1] - oldest[1];
             long deltaFailed = newest[2] - oldest[2];
             long deltaTimeMs = newest[0] - oldest[0];
-            long tp = deltaTimeMs > 0 ? (deltaTotal * 1000) / deltaTimeMs : 0;
-            long fp = deltaTimeMs > 0 ? (deltaFailed * 1000) / deltaTimeMs : 0;
+            fp = deltaTimeMs > 0 ? (deltaFailed * 1000 * THROUGHPUT_SCALE) / 
deltaTimeMs : 0;
+        }
 
+        {
             Long lastTime = previousExchangesTime.get(pid);
             if (lastTime == null || now - lastTime >= 1000) {
                 previousExchangesTime.put(pid, now);
@@ -278,8 +293,8 @@ class MetricsCollector {
             long[] oldest = samples.get(0);
             long[] newest = samples.get(samples.size() - 1);
             long deltaMs = newest[0] - oldest[0];
-            long inRate = deltaMs > 0 ? (newest[1] - oldest[1]) * 1000 / 
deltaMs : 0;
-            long outRate = deltaMs > 0 ? (newest[2] - oldest[2]) * 1000 / 
deltaMs : 0;
+            long inRate = deltaMs > 0 ? (newest[1] - oldest[1]) * 1000 * 
THROUGHPUT_SCALE / deltaMs : 0;
+            long outRate = deltaMs > 0 ? (newest[2] - oldest[2]) * 1000 * 
THROUGHPUT_SCALE / deltaMs : 0;
             Long lastTime = prevTimeMap.get(pid);
             if (lastTime == null || now - lastTime >= 1000) {
                 prevTimeMap.put(pid, now);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
index 09b95d7995cf..23e0e062abc5 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
@@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 
@@ -222,7 +223,7 @@ class OverviewTab implements MonitorTab {
         List<Constraint> constraints = new ArrayList<>();
         constraints.add(Constraint.fill());
         if (hasSparkline) {
-            constraints.add(Constraint.length(14));
+            constraints.add(Constraint.length(17));
         } else if (showInfoPanel) {
             int panelH = countInfraLines(infraSel) + 2;
             constraints.add(Constraint.length(Math.min(panelH, area.height() / 
2)));
@@ -404,7 +405,7 @@ class OverviewTab implements MonitorTab {
                     .split(chartInner);
 
             List<Rect> hChunks = Layout.horizontal()
-                    .constraints(Constraint.length(4), Constraint.fill())
+                    .constraints(Constraint.length(5), Constraint.fill())
                     .split(vChunks.get(0));
 
             Rect barChartArea = hChunks.get(1);
@@ -438,10 +439,16 @@ class OverviewTab implements MonitorTab {
             for (long v : mergedTotal) {
                 maxTp = Math.max(maxTp, v);
             }
+            maxTp = niceMax(maxTp);
             long curTp = mergedTotal[renderPoints - 1];
             long curFailed = mergedFailed[renderPoints - 1];
             long curOk = Math.max(0, curTp - curFailed);
 
+            // Format throughput values unscaled for display
+            String curTpFmt = formatThroughput(curTp);
+            String curOkFmt = formatThroughput(curOk);
+            String curFailFmt = formatThroughput(curFailed);
+
             Line titleLine;
             if (chartMode == CHART_SINGLE && ctx.selectedPid != null) {
                 IntegrationInfo chartSel = ctx.findSelectedIntegration();
@@ -450,18 +457,18 @@ class OverviewTab implements MonitorTab {
                 titleLine = Line.from(
                         Span.raw(" ["),
                         Span.styled(chartName, Style.EMPTY.fg(Color.YELLOW)),
-                        Span.raw(String.format("] Throughput: %d msg/s  ", 
curTp)),
+                        Span.raw(String.format("] Throughput: %s msg/s  ", 
curTpFmt)),
                         Span.styled("■", 
Style.EMPTY.fg(Color.ansi(AnsiColor.BRIGHT_GREEN))),
-                        Span.raw(String.format(" ok:%d  ", curOk)),
+                        Span.raw(String.format(" ok:%s  ", curOkFmt)),
                         Span.styled("■", Style.EMPTY.fg(Color.RED)),
-                        Span.raw(String.format(" fail:%d ", curFailed)));
+                        Span.raw(String.format(" fail:%s ", curFailFmt)));
             } else {
                 titleLine = Line.from(
-                        Span.raw(String.format(" [All] Throughput: %d msg/s  
", curTp)),
+                        Span.raw(String.format(" [All] Throughput: %s msg/s  
", curTpFmt)),
                         Span.styled("■", 
Style.EMPTY.fg(Color.ansi(AnsiColor.BRIGHT_GREEN))),
-                        Span.raw(String.format(" ok:%d  ", curOk)),
+                        Span.raw(String.format(" ok:%s  ", curOkFmt)),
                         Span.styled("■", Style.EMPTY.fg(Color.RED)),
-                        Span.raw(String.format(" fail:%d ", curFailed)));
+                        Span.raw(String.format(" fail:%s ", curFailFmt)));
             }
 
             Block chartBlock = 
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
@@ -480,7 +487,7 @@ class OverviewTab implements MonitorTab {
 
             BarChart barChart = BarChart.builder()
                     .data(groups)
-                    .max(maxTp > 0 ? maxTp + 2 : 2)
+                    .max(maxTp)
                     .barWidth(1)
                     .barGap(0)
                     .groupGap(0)
@@ -493,11 +500,11 @@ class OverviewTab implements MonitorTab {
             Style dimStyle = Style.EMPTY.dim();
             for (int row = 0; row < barRows; row++) {
                 if (row == 0) {
-                    yLines.add(Line.from(Span.styled(String.format("%3d", 
maxTp), dimStyle)));
+                    yLines.add(Line.from(Span.styled(String.format("%4s", 
formatThroughput(maxTp)), dimStyle)));
                 } else if (barRows > 4 && row == barRows / 2) {
-                    yLines.add(Line.from(Span.styled(String.format("%3d", 
maxTp / 2), dimStyle)));
+                    yLines.add(Line.from(Span.styled(String.format("%4s", 
formatThroughput(maxTp / 2)), dimStyle)));
                 } else if (row == barRows - 1) {
-                    yLines.add(Line.from(Span.styled("  0", dimStyle)));
+                    yLines.add(Line.from(Span.styled("   0", dimStyle)));
                 } else {
                     yLines.add(Line.from(""));
                 }
@@ -637,6 +644,34 @@ class OverviewTab implements MonitorTab {
         frame.renderWidget(Paragraph.builder().text(Text.from(lines)).build(), 
inner);
     }
 
+    /**
+     * Snap to a nice Y-axis ceiling using a 1-2-5 sequence (scaled by 
THROUGHPUT_SCALE). For example with scale=100:
+     * 0.20 → 1, 1.5 → 2, 3 → 5, 7 → 10, 15 → 20, 35 → 50, 80 → 100, etc.
+     */
+    private static long niceMax(long rawMax) {
+        long s = MetricsCollector.THROUGHPUT_SCALE;
+        long[] steps = { s, 2 * s, 5 * s, 10 * s, 20 * s, 50 * s, 100 * s, 200 
* s, 500 * s, 1000 * s };
+        for (long step : steps) {
+            if (rawMax <= step) {
+                return step;
+            }
+        }
+        return rawMax;
+    }
+
+    private static String formatThroughput(long scaledValue) {
+        double v = scaledValue / (double) MetricsCollector.THROUGHPUT_SCALE;
+        if (v >= 10) {
+            return String.valueOf(Math.round(v));
+        } else if (v >= 1) {
+            return String.format(Locale.US, "%.1f", v);
+        } else if (scaledValue > 0) {
+            return String.format(Locale.US, "%.2f", v);
+        } else {
+            return "0";
+        }
+    }
+
     private static int countInfraLines(InfraInfo infra) {
         int count = 2; // "Service: ..." + blank line
         for (Map.Entry<String, Object> e : infra.properties.entrySet()) {

Reply via email to