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

davsclaus pushed a commit to branch 
CAMEL-23631-route-diagram-highlight-error-path
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 8666c8651e0c45ff76d1d186fb17160bfd92af8b
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed May 27 21:45:45 2026 +0200

    CAMEL-23631: camel-diagram - Add highlight support for route paths
    
    Add the ability to highlight specific node paths in route diagrams,
    supporting both success (green) and fail (red) styles. This enables
    visualizing error paths from ErrorRegistry and trace paths from
    message history.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../camel/diagram/DefaultRouteDiagramDumper.java   |  21 +++-
 .../camel/diagram/RouteDiagramAsciiRenderer.java   |  71 +++++++++++-
 .../apache/camel/diagram/RouteDiagramHelper.java   |  93 +++++++++++++++
 .../camel/diagram/RouteDiagramLayoutEngine.java    |   3 +
 .../apache/camel/diagram/RouteDiagramRenderer.java | 128 ++++++++++++++++++---
 .../camel-jbang-cmd-route-diagram.adoc             |   2 +
 .../META-INF/camel-jbang-commands-metadata.json    |   2 +-
 .../commands/action/CamelRouteDiagramAction.java   |  32 +++++-
 8 files changed, 324 insertions(+), 28 deletions(-)

diff --git 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/DefaultRouteDiagramDumper.java
 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/DefaultRouteDiagramDumper.java
index ba0d83311d38..920588cb328e 100644
--- 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/DefaultRouteDiagramDumper.java
+++ 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/DefaultRouteDiagramDumper.java
@@ -141,7 +141,8 @@ public class DefaultRouteDiagramDumper extends 
ServiceSupport implements CamelCo
 
     private static BufferedImage renderImage(
             List<RouteDiagramLayoutEngine.RouteInfo> routes, String theme, int 
fontSize, int nodeWidth,
-            String nodeLabel, boolean metrics) {
+            String nodeLabel, boolean metrics,
+            Set<String> highlightedNodeIds, RouteDiagramHelper.HighlightStyle 
highlightStyle) {
         RouteDiagramRenderer renderer = new RouteDiagramRenderer(
                 nodeWidth * RouteDiagramLayoutEngine.SCALE, fontSize * 
RouteDiagramLayoutEngine.SCALE, metrics);
         RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine(
@@ -159,11 +160,18 @@ public class DefaultRouteDiagramDumper extends 
ServiceSupport implements CamelCo
         }
         theme = theme.toLowerCase();
         RouteDiagramRenderer.DiagramColors colors = 
RouteDiagramRenderer.DiagramColors.parse(theme);
-        return renderer.renderDiagram(layoutRoutes, currentY, colors);
+        return renderer.renderDiagram(layoutRoutes, currentY, colors, 
highlightedNodeIds, highlightStyle);
+    }
+
+    private static BufferedImage renderImage(
+            List<RouteDiagramLayoutEngine.RouteInfo> routes, String theme, int 
fontSize, int nodeWidth,
+            String nodeLabel, boolean metrics) {
+        return renderImage(routes, theme, fontSize, nodeWidth, nodeLabel, 
metrics, null, null);
     }
 
     private static String renderAscii(
-            List<RouteDiagramLayoutEngine.RouteInfo> routes, int nodeWidth, 
String nodeLabel, boolean unicode) {
+            List<RouteDiagramLayoutEngine.RouteInfo> routes, int nodeWidth, 
String nodeLabel, boolean unicode,
+            Set<String> highlightedNodeIds, RouteDiagramHelper.HighlightStyle 
highlightStyle) {
         RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine(
                 nodeWidth, RouteDiagramLayoutEngine.DEFAULT_FONT_SIZE,
                 
RouteDiagramLayoutEngine.NodeLabelMode.valueOf(nodeLabel.toUpperCase()));
@@ -178,7 +186,12 @@ public class DefaultRouteDiagramDumper extends 
ServiceSupport implements CamelCo
 
         RouteDiagramAsciiRenderer renderer = new RouteDiagramAsciiRenderer(
                 nodeWidth * RouteDiagramLayoutEngine.SCALE, unicode);
-        return renderer.renderDiagram(layoutRoutes, currentY);
+        return renderer.renderDiagram(layoutRoutes, currentY, 
highlightedNodeIds, highlightStyle);
+    }
+
+    private static String renderAscii(
+            List<RouteDiagramLayoutEngine.RouteInfo> routes, int nodeWidth, 
String nodeLabel, boolean unicode) {
+        return renderAscii(routes, nodeWidth, nodeLabel, unicode, null, null);
     }
 
 }
diff --git 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
index 5d0f9bb0173a..150fb77a736a 100644
--- 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
+++ 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
@@ -19,6 +19,7 @@ package org.apache.camel.diagram;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Set;
 
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.Bounds;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutNode;
@@ -62,7 +63,9 @@ public class RouteDiagramAsciiRenderer {
 
     public enum CounterType {
         OK,
-        FAIL
+        FAIL,
+        HIGHLIGHT_SUCCESS,
+        HIGHLIGHT_FAIL
     }
 
     public record CounterPos(int row, int col, int length, CounterType type) {
@@ -91,8 +94,19 @@ public class RouteDiagramAsciiRenderer {
         return counterPositions;
     }
 
+    public String renderDiagramAnsi(
+            List<LayoutRoute> layoutRoutes, int totalHeight,
+            Set<String> highlightedNodeIds, RouteDiagramHelper.HighlightStyle 
highlightStyle) {
+        String plain = renderDiagram(layoutRoutes, totalHeight, 
highlightedNodeIds, highlightStyle);
+        return applyAnsiColors(plain);
+    }
+
     public String renderDiagramAnsi(List<LayoutRoute> layoutRoutes, int 
totalHeight) {
         String plain = renderDiagram(layoutRoutes, totalHeight);
+        return applyAnsiColors(plain);
+    }
+
+    private String applyAnsiColors(String plain) {
         if (counterPositions.isEmpty()) {
             return plain;
         }
@@ -104,7 +118,11 @@ public class RouteDiagramAsciiRenderer {
                     String before = line.substring(0, cp.col);
                     String counter = line.substring(cp.col, cp.col + 
cp.length);
                     String after = line.substring(cp.col + cp.length);
-                    String color = cp.type == CounterType.OK ? "\033[32m" : 
"\033[31m";
+                    String color;
+                    switch (cp.type) {
+                        case OK, HIGHLIGHT_SUCCESS -> color = "\033[32m";
+                        default -> color = "\033[31m";
+                    }
                     lines[cp.row] = before + color + counter + "\033[0m" + 
after;
                 }
             }
@@ -112,6 +130,27 @@ public class RouteDiagramAsciiRenderer {
         return String.join("\n", lines);
     }
 
+    public String renderDiagram(
+            List<LayoutRoute> layoutRoutes, int totalHeight,
+            Set<String> highlightedNodeIds, RouteDiagramHelper.HighlightStyle 
highlightStyle) {
+        counterPositions.clear();
+        int maxPixelX = layoutRoutes.stream()
+                .mapToInt(lr -> lr.maxX).max().orElse(nodeWidth) + PADDING;
+        int gridWidth = toCol(maxPixelX) + boxWidth + 4;
+        int gridHeight = totalHeight / Y_SCALE + 20;
+
+        char[][] grid = new char[gridHeight][gridWidth];
+        for (char[] row : grid) {
+            Arrays.fill(row, ' ');
+        }
+
+        for (LayoutRoute lr : layoutRoutes) {
+            drawRoute(grid, lr, highlightedNodeIds, highlightStyle);
+        }
+
+        return gridToString(grid);
+    }
+
     public String renderDiagram(List<LayoutRoute> layoutRoutes, int 
totalHeight) {
         counterPositions.clear();
         int maxPixelX = layoutRoutes.stream()
@@ -131,7 +170,9 @@ public class RouteDiagramAsciiRenderer {
         return gridToString(grid);
     }
 
-    private void drawRoute(char[][] grid, LayoutRoute lr) {
+    private void drawRoute(
+            char[][] grid, LayoutRoute lr,
+            Set<String> highlightedNodeIds, RouteDiagramHelper.HighlightStyle 
highlightStyle) {
         int labelRow = toRow(lr.labelY);
         String label = lr.routeId;
         if (lr.source != null && !lr.source.isEmpty()) {
@@ -157,9 +198,16 @@ public class RouteDiagramAsciiRenderer {
 
         for (LayoutNode ln : lr.nodes) {
             drawNode(grid, ln);
+            if (highlightedNodeIds != null && ln.id != null && 
highlightedNodeIds.contains(ln.id)) {
+                recordHighlightPositions(grid, ln, highlightStyle);
+            }
         }
     }
 
+    private void drawRoute(char[][] grid, LayoutRoute lr) {
+        drawRoute(grid, lr, null, null);
+    }
+
     private void drawNode(char[][] grid, LayoutNode node) {
         int col = toCol(node.x);
         int row = toRow(node.y);
@@ -203,6 +251,23 @@ public class RouteDiagramAsciiRenderer {
         }
     }
 
+    private void recordHighlightPositions(
+            char[][] grid, LayoutNode node, RouteDiagramHelper.HighlightStyle 
style) {
+        int col = toCol(node.x);
+        int row = toRow(node.y);
+        int innerWidth = boxWidth - 4;
+        List<String> lines = rewrapText(node, innerWidth);
+        int height = 2 + lines.size();
+
+        CounterType ct = style == RouteDiagramHelper.HighlightStyle.FAIL
+                ? CounterType.HIGHLIGHT_FAIL
+                : CounterType.HIGHLIGHT_SUCCESS;
+
+        for (int r = row; r < row + height && r < grid.length; r++) {
+            counterPositions.add(new CounterPos(r, col, boxWidth, ct));
+        }
+    }
+
     private void drawArrow(char[][] grid, LayoutNode from, LayoutNode to) {
         int fromCx = centerCol(from);
         int fromBottom = toRow(from.y) + boxHeight(from);
diff --git 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramHelper.java
 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramHelper.java
index 645110f0ca9b..5ad89d4b11e4 100644
--- 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramHelper.java
+++ 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramHelper.java
@@ -17,7 +17,9 @@
 package org.apache.camel.diagram;
 
 import java.util.ArrayList;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.NodeInfo;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.RouteInfo;
@@ -66,6 +68,7 @@ public final class RouteDiagramHelper {
                 for (JsonObject line : lines) {
                     NodeInfo node = new NodeInfo();
                     node.type = line.getString("type");
+                    node.id = line.getString("id");
                     node.code = Jsoner.unescape(line.getString("code"));
                     node.description = line.getString("description");
                     Integer level = line.getInteger("level");
@@ -109,6 +112,96 @@ public final class RouteDiagramHelper {
         return routes;
     }
 
+    public enum HighlightStyle {
+        SUCCESS,
+        FAIL
+    }
+
+    public static class HighlightInfo {
+        private final Set<String> nodeIds;
+        private final List<String> routeOrder;
+        private final HighlightStyle style;
+
+        public HighlightInfo(Set<String> nodeIds, List<String> routeOrder, 
HighlightStyle style) {
+            this.nodeIds = nodeIds;
+            this.routeOrder = routeOrder;
+            this.style = style;
+        }
+
+        public Set<String> getNodeIds() {
+            return nodeIds;
+        }
+
+        public List<String> getRouteOrder() {
+            return routeOrder;
+        }
+
+        public HighlightStyle getStyle() {
+            return style;
+        }
+    }
+
+    /**
+     * Parses message history entries into a {@link HighlightInfo} containing 
the node IDs to highlight and the route
+     * ordering (by first visit).
+     *
+     * @param  messageHistory array of history entries in the format {@code 
"routeId[nodeId] (elapsed ms)"}
+     * @param  style          the highlight style to use
+     * @return                highlight info with node IDs and route ordering
+     */
+    public static HighlightInfo parseMessageHistory(String[] messageHistory, 
HighlightStyle style) {
+        Set<String> nodeIds = new LinkedHashSet<>();
+        Set<String> routeOrderSet = new LinkedHashSet<>();
+        if (messageHistory != null) {
+            for (String h : messageHistory) {
+                int bracket = h.indexOf('[');
+                int end = h.indexOf(']');
+                if (bracket >= 0 && end > bracket) {
+                    routeOrderSet.add(h.substring(0, bracket));
+                    nodeIds.add(h.substring(bracket + 1, end));
+                }
+            }
+        }
+        return new HighlightInfo(nodeIds, new ArrayList<>(routeOrderSet), 
style);
+    }
+
+    /**
+     * Filters and orders routes based on the highlight info. Only routes that 
contain at least one highlighted node are
+     * included, and they are ordered by the sequence in which the message 
first visited each route.
+     *
+     * @param  routes    the full list of parsed routes
+     * @param  highlight the highlight info containing route ordering
+     * @return           filtered and ordered list of routes
+     */
+    public static List<RouteInfo> filterAndOrderRoutes(List<RouteInfo> routes, 
HighlightInfo highlight) {
+        if (highlight == null || highlight.routeOrder.isEmpty()) {
+            return routes;
+        }
+        Set<String> nodeIds = highlight.nodeIds;
+        List<String> order = highlight.routeOrder;
+
+        List<RouteInfo> result = new ArrayList<>();
+        for (RouteInfo route : routes) {
+            boolean hasHighlightedNode = route.nodes.stream()
+                    .anyMatch(n -> n.id != null && nodeIds.contains(n.id));
+            if (hasHighlightedNode) {
+                result.add(route);
+            }
+        }
+        result.sort((a, b) -> {
+            int ia = order.indexOf(a.routeId);
+            int ib = order.indexOf(b.routeId);
+            if (ia < 0) {
+                ia = Integer.MAX_VALUE;
+            }
+            if (ib < 0) {
+                ib = Integer.MAX_VALUE;
+            }
+            return Integer.compare(ia, ib);
+        });
+        return result;
+    }
+
     /**
      * Extracts a short display name from a source path. For example, {@code 
"file:/path/to/my-route.yaml"} becomes
      * {@code "my-route.yaml"}.
diff --git 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java
 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java
index f052e68126c8..7681aba8e338 100644
--- 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java
+++ 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java
@@ -152,6 +152,7 @@ public class RouteDiagramLayoutEngine {
 
     public static class NodeInfo {
         public String type;
+        public String id;
         public String code;
         public String description;
         public int level;
@@ -179,6 +180,7 @@ public class RouteDiagramLayoutEngine {
 
     public static class LayoutNode {
         public String type;
+        public String id;
         public int x;
         public int y;
         public int height;
@@ -405,6 +407,7 @@ public class RouteDiagramLayoutEngine {
 
         LayoutNode ln = new LayoutNode();
         ln.type = node.info.type;
+        ln.id = node.info.id;
         ln.x = nodeX;
         ln.y = y;
         ln.wrappedLines = resolveLabel(node.info, nodeLabelMode).stream()
diff --git 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramRenderer.java
 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramRenderer.java
index 89f4839c5158..a87b85001636 100644
--- 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramRenderer.java
+++ 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramRenderer.java
@@ -22,6 +22,7 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.Bounds;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutNode;
@@ -48,6 +49,10 @@ public class RouteDiagramRenderer {
     private static final int LABEL_TEXT_BASELINE = 14 * SCALE;
     public static final int MAX_IMAGE_DIMENSION = 16384;
 
+    private static final float HIGHLIGHT_STROKE_WIDTH = 3.0f * SCALE;
+    private static final Color HIGHLIGHT_SUCCESS_COLOR = new Color(0x43a047);
+    private static final Color HIGHLIGHT_FAIL_COLOR = new Color(0xe53935);
+
     private static final float[] DASH_PATTERN = { 10f, 5f }; // 10 pixels on, 
5 pixels off
     private static final Stroke DASHED_STROKE = new BasicStroke(
             STROKE_WIDTH,            // line width
@@ -218,6 +223,41 @@ public class RouteDiagramRenderer {
         }
     }
 
+    public BufferedImage renderDiagram(
+            List<LayoutRoute> layoutRoutes, int totalHeight, DiagramColors 
colors,
+            Set<String> highlightedNodeIds, RouteDiagramHelper.HighlightStyle 
highlightStyle) {
+        int imgWidth = layoutRoutes.stream().mapToInt(lr -> 
lr.maxX).max().orElse(400) + PADDING;
+        int imgHeight = totalHeight + PADDING;
+
+        if (imgWidth > MAX_IMAGE_DIMENSION || imgHeight > MAX_IMAGE_DIMENSION) 
{
+            throw new IllegalStateException(
+                    "Diagram too large (" + imgWidth / SCALE + "x" + imgHeight 
/ SCALE
+                                            + " logical pixels). Use --filter 
to narrow the routes displayed.");
+        }
+
+        int imageType = colors.getBg() == null ? BufferedImage.TYPE_INT_ARGB : 
BufferedImage.TYPE_INT_RGB;
+        BufferedImage image = new BufferedImage(imgWidth, imgHeight, 
imageType);
+        Graphics2D g = image.createGraphics();
+        try {
+            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
RenderingHints.VALUE_ANTIALIAS_ON);
+            g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, 
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+            g.setRenderingHint(RenderingHints.KEY_RENDERING, 
RenderingHints.VALUE_RENDER_QUALITY);
+            g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, 
RenderingHints.VALUE_FRACTIONALMETRICS_ON);
+
+            if (colors.getBg() != null) {
+                g.setColor(colors.getBg());
+                g.fillRect(0, 0, imgWidth, imgHeight);
+            }
+
+            for (LayoutRoute lr : layoutRoutes) {
+                drawRoute(g, lr, colors, highlightedNodeIds, highlightStyle);
+            }
+        } finally {
+            g.dispose();
+        }
+        return image;
+    }
+
     public BufferedImage renderDiagram(List<LayoutRoute> layoutRoutes, int 
totalHeight, DiagramColors colors) {
         int imgWidth = layoutRoutes.stream().mapToInt(lr -> 
lr.maxX).max().orElse(400) + PADDING;
         int imgHeight = totalHeight + PADDING;
@@ -251,7 +291,9 @@ public class RouteDiagramRenderer {
         return image;
     }
 
-    private void drawRoute(Graphics2D g, LayoutRoute lr, DiagramColors colors) 
{
+    private void drawRoute(
+            Graphics2D g, LayoutRoute lr, DiagramColors colors,
+            Set<String> highlightedNodeIds, RouteDiagramHelper.HighlightStyle 
highlightStyle) {
         g.setColor(colors.getRouteLabel());
         g.setFont(new Font("SansSerif", Font.BOLD, fontSizeLabel));
         String label = lr.routeId;
@@ -270,19 +312,36 @@ public class RouteDiagramRenderer {
 
         for (LayoutNode ln : lr.nodes) {
             if (ln.parentNode != null) {
+                boolean highlightArrow = highlightedNodeIds != null
+                        && isHighlighted(ln, highlightedNodeIds)
+                        && isHighlighted(ln.parentNode, highlightedNodeIds);
                 if (ln.connectFromMerge) {
-                    drawArrowFromMerge(g, ln, colors);
+                    drawArrowFromMerge(g, ln, colors, highlightArrow, 
highlightStyle);
                 } else {
-                    drawArrow(g, ln.parentNode, ln, colors);
+                    drawArrow(g, ln.parentNode, ln, colors, highlightArrow, 
highlightStyle);
                 }
             }
         }
 
         for (LayoutNode ln : lr.nodes) {
-            drawNode(g, ln, colors);
+            drawNode(g, ln, colors, highlightedNodeIds, highlightStyle);
         }
     }
 
+    private void drawRoute(Graphics2D g, LayoutRoute lr, DiagramColors colors) 
{
+        drawRoute(g, lr, colors, null, null);
+    }
+
+    private static boolean isHighlighted(LayoutNode node, Set<String> 
highlightedNodeIds) {
+        return node.id != null && highlightedNodeIds.contains(node.id);
+    }
+
+    private static Color highlightColor(RouteDiagramHelper.HighlightStyle 
style) {
+        return style == RouteDiagramHelper.HighlightStyle.FAIL
+                ? HIGHLIGHT_FAIL_COLOR
+                : HIGHLIGHT_SUCCESS_COLOR;
+    }
+
     private void drawScopeBox(Graphics2D g, LayoutNode scopeNode, 
DiagramColors colors) {
         TreeNode tn = scopeNode.treeNode;
         Bounds bounds = new Bounds(
@@ -308,17 +367,24 @@ public class RouteDiagramRenderer {
                 ? node.y - SCOPE_BOX_PAD : node.y;
     }
 
-    private void drawArrowFromMerge(Graphics2D g, LayoutNode to, DiagramColors 
colors) {
+    private void drawArrowFromMerge(
+            Graphics2D g, LayoutNode to, DiagramColors colors,
+            boolean highlighted, RouteDiagramHelper.HighlightStyle 
highlightStyle) {
         var stat = to.treeNode.info.stat;
         long total = stat != null ? stat.exchangesTotal : 0;
         long failed = stat != null ? stat.exchangesFailed : 0;
         long ok = total - failed;
 
-        g.setColor(colors.getArrow());
-        if (!metrics || total > 0) {
-            g.setStroke(new BasicStroke(STROKE_WIDTH));
+        if (highlighted) {
+            g.setColor(highlightColor(highlightStyle));
+            g.setStroke(new BasicStroke(HIGHLIGHT_STROKE_WIDTH));
         } else {
-            g.setStroke(DASHED_STROKE);
+            g.setColor(colors.getArrow());
+            if (!metrics || total > 0) {
+                g.setStroke(new BasicStroke(STROKE_WIDTH));
+            } else {
+                g.setStroke(DASHED_STROKE);
+            }
         }
 
         int toCx = to.x + nodeWidth / 2;
@@ -347,14 +413,26 @@ public class RouteDiagramRenderer {
         }
     }
 
-    private void drawNode(Graphics2D g, LayoutNode node, DiagramColors colors) 
{
+    private void drawArrowFromMerge(Graphics2D g, LayoutNode to, DiagramColors 
colors) {
+        drawArrowFromMerge(g, to, colors, false, null);
+    }
+
+    private void drawNode(
+            Graphics2D g, LayoutNode node, DiagramColors colors,
+            Set<String> highlightedNodeIds, RouteDiagramHelper.HighlightStyle 
highlightStyle) {
         Color color = getNodeColor(node.type, colors);
 
         g.setColor(color);
         g.fillRoundRect(node.x, node.y, nodeWidth, node.height, ARC, ARC);
 
-        g.setColor(color.brighter());
-        g.setStroke(new BasicStroke(BORDER_STROKE_WIDTH));
+        boolean highlighted = highlightedNodeIds != null && 
isHighlighted(node, highlightedNodeIds);
+        if (highlighted) {
+            g.setColor(highlightColor(highlightStyle));
+            g.setStroke(new BasicStroke(HIGHLIGHT_STROKE_WIDTH));
+        } else {
+            g.setColor(color.brighter());
+            g.setStroke(new BasicStroke(BORDER_STROKE_WIDTH));
+        }
         g.drawRoundRect(node.x, node.y, nodeWidth, node.height, ARC, ARC);
 
         g.setColor(colors.getText());
@@ -380,21 +458,31 @@ public class RouteDiagramRenderer {
         }
     }
 
-    private void drawArrow(Graphics2D g, LayoutNode from, LayoutNode to, 
DiagramColors colors) {
+    private void drawNode(Graphics2D g, LayoutNode node, DiagramColors colors) 
{
+        drawNode(g, node, colors, null, null);
+    }
+
+    private void drawArrow(
+            Graphics2D g, LayoutNode from, LayoutNode to, DiagramColors colors,
+            boolean highlighted, RouteDiagramHelper.HighlightStyle 
highlightStyle) {
         var stat = metrics ? to.treeNode.info.stat : null;
         if (metrics && BRANCH_CHILD_TYPES.contains(to.type) && 
!to.treeNode.children.isEmpty()) {
-            // grab stat from first child (for example choice to have counters 
for when/otherwise)
             stat = to.treeNode.children.get(0).info.stat;
         }
         long total = stat != null ? stat.exchangesTotal : 0;
         long failed = stat != null ? stat.exchangesFailed : 0;
         long ok = total - failed;
 
-        g.setColor(colors.getArrow());
-        if (!metrics || total > 0) {
-            g.setStroke(new BasicStroke(STROKE_WIDTH));
+        if (highlighted) {
+            g.setColor(highlightColor(highlightStyle));
+            g.setStroke(new BasicStroke(HIGHLIGHT_STROKE_WIDTH));
         } else {
-            g.setStroke(DASHED_STROKE);
+            g.setColor(colors.getArrow());
+            if (!metrics || total > 0) {
+                g.setStroke(new BasicStroke(STROKE_WIDTH));
+            } else {
+                g.setStroke(DASHED_STROKE);
+            }
         }
 
         int fromCx = from.x + nodeWidth / 2;
@@ -423,6 +511,10 @@ public class RouteDiagramRenderer {
         }
     }
 
+    private void drawArrow(Graphics2D g, LayoutNode from, LayoutNode to, 
DiagramColors colors) {
+        drawArrow(g, from, to, colors, false, null);
+    }
+
     private void drawArrowHead(Graphics2D g, int x, int y) {
         int[] xPoints = { x - ARROW_SIZE, x, x + ARROW_SIZE };
         int[] yPoints = { y - ARROW_SIZE, y, y - ARROW_SIZE };
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc
index 354db16e98f4..eaa8d056e71f 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc
@@ -22,6 +22,8 @@ camel cmd route-diagram [options]
 | `--box-width` | Node box width in logical pixels | 180 | int
 | `--filter` | Filter route by filename or route id |  | String
 | `--font-size` | Font size in logical pixels for node text | 12 | int
+| `--highlight` | Comma-separated node IDs to highlight in the diagram |  | 
String
+| `--highlight-style` | Highlight style: success (green) or fail (red) | 
success | String
 | `--ignore-loading-error` | Whether to ignore route loading and compilation 
errors (use this with care!) | false | boolean
 | `--metric` | Whether to include live metrics (only possible for running 
Camel application) | true | boolean
 | `--node-label` | What text to display in diagram nodes: code, description, 
or both (default) | both | String
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index d0d78976a693..eebf916394be 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -3,7 +3,7 @@
     { "name": "ask", "fullName": "ask", "description": "Ask a question about a 
running Camel application using AI", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names": 
"--api-key", "description": "API key. Also reads ANTHROPIC_API_KEY, 
OPENAI_API_KEY, or LLM_API_KEY env vars", "javaType": "java.lang.String", 
"type": "string" }, { "names": "--api-type", "description": "API type: 
'ollama', 'openai', or 'anthropic'", "javaType": "LlmClient.ApiType", "type" 
[...]
     { "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind 
source and sink Kamelets as a new Camel integration", "deprecated": true, 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options": 
[ { "names": "--error-handler", "description": "Add error handler 
(none|log|sink:<endpoint>). Sink endpoints are expected in the format 
[[apigroup\/]version:]kind:[namespace\/]name, plain Camel URIs or Kamelet 
name.", "javaType": "java.lang.String", "type": "stri [...]
     { "name": "catalog", "fullName": "catalog", "description": "List artifacts 
from Camel Catalog", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [ 
{ "names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"component", "fullName": "catalog component", "description": "List components 
from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...]
-    { "name": "cmd", "fullName": "cmd", "description": "Performs commands in 
the running Camel integrations, such as start\/stop route, or change logging 
levels.", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"browse", "fullName": "cmd browse", "description": "Browse pending messages on 
endpoints [...]
+    { "name": "cmd", "fullName": "cmd", "description": "Performs commands in 
the running Camel integrations, such as start\/stop route, or change logging 
levels.", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"browse", "fullName": "cmd browse", "description": "Browse pending messages on 
endpoints [...]
     { "name": "completion", "fullName": "completion", "description": "Generate 
completion script for bash\/zsh", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ] },
     { "name": "config", "fullName": "config", "description": "Get and set user 
configuration values", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", 
"fullName": "config get", "description": "Display user configuration value", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...]
     { "name": "debug", "fullName": "debug", "description": "Debug local Camel 
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug", 
"options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd 
HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background", "description": "Run in the background", "defaultValue": 
"false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "To  [...]
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
index c29cb7b8df37..fa89ddca2317 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
@@ -22,7 +22,10 @@ import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 
 import javax.imageio.ImageIO;
 
@@ -98,6 +101,14 @@ public class CamelRouteDiagramAction extends 
ActionWatchCommand {
                         description = "Whether to include live metrics (only 
possible for running Camel application)")
     boolean metric;
 
+    @CommandLine.Option(names = { "--highlight" },
+                        description = "Comma-separated node IDs to highlight 
in the diagram")
+    String highlight;
+
+    @CommandLine.Option(names = { "--highlight-style" },
+                        description = "Highlight style: success (green) or 
fail (red)", defaultValue = "success")
+    String highlightStyle;
+
     @CommandLine.Option(names = { "--ignore-loading-error" }, defaultValue = 
"false",
                         description = "Whether to ignore route loading and 
compilation errors (use this with care!)")
     boolean ignoreLoadingError;
@@ -198,6 +209,23 @@ public class CamelRouteDiagramAction extends 
ActionWatchCommand {
                 return 1;
             }
 
+            // parse highlight info
+            Set<String> highlightedNodeIds = null;
+            RouteDiagramHelper.HighlightStyle hlStyle = null;
+            RouteDiagramHelper.HighlightInfo highlightInfo = null;
+            if (highlight != null && !highlight.isBlank()) {
+                highlightedNodeIds = new 
LinkedHashSet<>(Arrays.asList(highlight.split(",")));
+                hlStyle = "fail".equalsIgnoreCase(highlightStyle)
+                        ? RouteDiagramHelper.HighlightStyle.FAIL
+                        : RouteDiagramHelper.HighlightStyle.SUCCESS;
+                highlightInfo = new 
RouteDiagramHelper.HighlightInfo(highlightedNodeIds, List.of(), hlStyle);
+                routes = RouteDiagramHelper.filterAndOrderRoutes(routes, 
highlightInfo);
+                if (routes.isEmpty()) {
+                    printer().println("No routes contain highlighted nodes");
+                    return 1;
+                }
+            }
+
             NodeLabelMode labelMode = parseNodeLabelMode(nodeLabel);
             RouteDiagramLayoutEngine engine = new 
RouteDiagramLayoutEngine(boxWidth, fontSize, labelMode);
 
@@ -212,7 +240,7 @@ public class CamelRouteDiagramAction extends 
ActionWatchCommand {
             if (isTextTheme()) {
                 RouteDiagramAsciiRenderer asciiRenderer
                         = new RouteDiagramAsciiRenderer(engine.getNodeWidth(), 
isUnicodeTheme(), pid > 0 && metric);
-                String ascii = asciiRenderer.renderDiagramAnsi(layoutRoutes, 
currentY);
+                String ascii = asciiRenderer.renderDiagramAnsi(layoutRoutes, 
currentY, highlightedNodeIds, hlStyle);
 
                 if (output != null) {
                     String fileName = output.endsWith(".png")
@@ -237,7 +265,7 @@ public class CamelRouteDiagramAction extends 
ActionWatchCommand {
 
                 BufferedImage image;
                 try {
-                    image = renderer.renderDiagram(layoutRoutes, currentY, 
colors);
+                    image = renderer.renderDiagram(layoutRoutes, currentY, 
colors, highlightedNodeIds, hlStyle);
                 } catch (IllegalStateException e) {
                     printer().println(e.getMessage());
                     return 1;


Reply via email to