A DAG Printer in Java

背景

DAG应该是业务中常用的数据结构,笔者也是使用DAG描述数据流。但当试图把DAG打印梳理时,发现没可用工具。于是自己手写了一个,以供参考。

目标

  1. 可以打印DAG中,所有节点依赖关系图
  2. DAG中,可能有多个数据源
  3. 需要根据DAG中不同Node的Name动态调整位置
  4. 需要用箭头指明数据流方向

实现

DAG

使用Java graph工具类。此类开源工具很多,读者尽可使用自己的方式实现。如果已经有了DAG,可以略过此节。

  • pom
xml 复制代码
<dependency>
    <groupId>org.jgrapht</groupId>
    <artifactId>jgrapht-core</artifactId>
    <version>1.0.1</version>
</dependency>
  • vertex类
java 复制代码
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public abstract class Vertex {

    /**
     * Vertex ID
     */
    private String id;

    /**
     * Vertex Name
     */
    private String name;

    /**
     * Get indentifier of vertex
     * @return
     */
    public String getIdentifier() {
        return id;
    }
}
  • Edge

使用igrapht提供默认实现import org.jgrapht.graph.DefaultEdge

  • DAG 包装类

以下列举所用到的主要方法。

java 复制代码
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jgrapht.experimental.dag.DirectedAcyclicGraph;
import org.jgrapht.graph.DefaultEdge;
import org.jgrapht.traverse.DepthFirstIterator;

import java.util.Iterator;
import java.util.Set;
import java.util.stream.Collectors;

@Slf4j
public class DAG<T extends Vertex> {
    @Getter
    private DirectedAcyclicGraph<T, DefaultEdge> dag = new DirectedAcyclicGraph<>(DefaultEdge.class);


    @Override
    public Iterator<T> iterator() {
        return dag.iterator();
    }



    public Set<T> getDirectAncestors(T v) {
        return dag.incomingEdgesOf(v).stream()
                .map(defaultEdge -> dag.getEdgeSource(defaultEdge))
                .collect(Collectors.toSet());
    }


    public Set<T> getDirectDescendants(T v) {
        return dag.outgoingEdgesOf(v).stream()
                .map(defaultEdge -> dag.getEdgeTarget(defaultEdge))
                .collect(Collectors.toSet());
    }

    public Set<T> getAllVertices() {
        return dag.vertexSet();
    }


    public Set<DefaultEdge> getEdges(T t) {
        return dag.edgesOf(t);
    }
}

DAGPrinter

效果

实现方法

处理流程

  1. 使用一个Map,来记录Vertex与对应的坐标关系。(X:从上到下,Y:从做左到右)
  2. 任找一个数据源,将其坐标设置为(0,0)
  3. 计算所有vertex节点坐标
  4. 第三步结算过程中,会出现节点横坐标为负值情况(具体可参考代码),将坐标做下非负处理
  5. 将节点根据横坐标归类
  6. 计算出画板尺寸
  7. 根据画板尺寸、节点Name的长度调整下坐标
  8. 初始化画板
  9. 开始创作
  10. 整理成String输出
java 复制代码
public static String printDAG(DAG<Vertex> dag, Vertex root) {
    //step 1
    Map<Vertex, NamePoint> vertexCoordinate = new HashMap<>();

    //step2: Initialize the root coordinate
    updateCoordinateInfo(0, 0, vertexCoordinate, root);
    //step3
    calculateCoordinatesForDAGFromRootVertex(dag, root, vertexCoordinate);

    //step 4: Adjust the coordinate to make all coordinates positive
    adjustCoordinate(vertexCoordinate);

    //step 5: update other info of NamePoint
    Map<Integer, List<NamePoint>> levelNamePointInfo = getLevelNodeMapInfo(vertexCoordinate);
    //step 6
    int heightOfBoard = (levelNamePointInfo.size() - 1) * (spaceBetweenLines + 1) + 1;
    int widthOfBoard = vertexCoordinate.values().stream().mapToInt(coordinate -> coordinate.getTotalLengthOfAllNamesInCurrentLevel() + (coordinate.getTotalPointNumOfCurrentLevel() - 1) * (spaceBetweenPoints + 1)).max().getAsInt() + 1;

    //step 7
    rearrangeCoordinates(levelNamePointInfo, widthOfBoard);

    //step 8:Initialize the board
    List<List<Character>> board = initBoard(heightOfBoard, widthOfBoard);
    //step 9
    drawTheBoard(dag, vertexCoordinate, board);
    //step 10
    return convertBoardToString(board);
}

完整Printer代码

java 复制代码
package com.shopee.deep.graph;

import lombok.Getter;
import lombok.Setter;
import org.jgrapht.graph.DefaultEdge;

import java.awt.Point;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class DAGPrinter {
    private static final int spaceBetweenLines = 5;
    private static final int spaceBetweenPoints = 20;

    static class NamePoint extends Point {
        private final String name;

        @Getter
        private final int nameWidth;
        @Getter
        @Setter
        private int totalPointNumOfCurrentLevel;
        @Getter
        @Setter
        private int indexOfThisPointInCurrentLevel;
        @Getter
        @Setter
        private int totalLengthOfAllNamesInCurrentLevel;

        public NamePoint(int x, int y, String name) {
            super(x, y);
            this.name = name;
            nameWidth = name.length();
        }
    }

    public static String printDAG(DAG<Vertex> dag, Vertex root) {
        Map<Vertex, NamePoint> vertexCoordinate = new HashMap<>();

        // Initialize the root coordinate
        updateCoordinateInfo(0, 0, vertexCoordinate, root);

        calculateCoordinatesForDAGFromRootVertex(dag, root, vertexCoordinate);

        // Adjust the coordinate to make all coordinates positive
        adjustCoordinate(vertexCoordinate);

        //update other info of NamePoint
        Map<Integer, List<NamePoint>> levelNamePointInfo = getLevelNodeMapInfo(vertexCoordinate);

        int heightOfBoard = (levelNamePointInfo.size() - 1) * (spaceBetweenLines + 1) + 1;
        int widthOfBoard = vertexCoordinate.values().stream().mapToInt(coordinate -> coordinate.getTotalLengthOfAllNamesInCurrentLevel() + (coordinate.getTotalPointNumOfCurrentLevel() - 1) * (spaceBetweenPoints + 1)).max().getAsInt() + 1;

        rearrangeCoordinates(levelNamePointInfo, widthOfBoard);

        // Initialize the board
        List<List<Character>> board = initBoard(heightOfBoard, widthOfBoard);

        drawTheBoard(dag, vertexCoordinate, board);

        return convertBoardToString(board);
    }

    private static String convertBoardToString(List<List<Character>> board) {
        return board.stream()
                .map(row -> row.stream().map(String::valueOf).collect(Collectors.joining()))
                .collect(Collectors.joining("\n"));
    }

    private static void rearrangeCoordinates(Map<Integer, List<NamePoint>> levelNamePointInfo, int widthOfBoard) {
        levelNamePointInfo.forEach((level, namePoints) -> {
            int x = level * (spaceBetweenLines + 1);
            int currentLevelWidth = namePoints.get(0).totalLengthOfAllNamesInCurrentLevel + (namePoints.get(0).getTotalPointNumOfCurrentLevel() - 1) * (spaceBetweenPoints + 1) + 1;
            int startY = 0;
            if (currentLevelWidth < widthOfBoard) {
                startY = (widthOfBoard - currentLevelWidth) / 2;
            }
            for (int i = 0; i < namePoints.size(); i++) {
                NamePoint namePoint = namePoints.get(i);
                int y = startY + (namePoint.nameWidth) / 2;
                namePoint.setLocation(x, y);
                startY += namePoint.nameWidth + spaceBetweenPoints;
            }
        });
    }

    private static Map<Integer, List<NamePoint>> getLevelNodeMapInfo(Map<Vertex, NamePoint> vertexCoordinate) {
        Map<Integer, List<NamePoint>> levelNamePointInfo = vertexCoordinate.values().stream().collect(Collectors.groupingBy(namePoint -> namePoint.x));
        levelNamePointInfo.forEach(
                (level, namePoints) -> {
                    int totalLengthOfAllNamesInCurrentLevel = namePoints.stream().mapToInt(namePoint -> namePoint.nameWidth).sum();
                    namePoints.sort(Comparator.comparingInt(namePoint -> namePoint.y));
                    int totalPointNumOfCurrentLevel = namePoints.size();
                    for (int i = 0; i < namePoints.size(); i++) {
                        NamePoint namePoint = namePoints.get(i);
                        namePoint.setTotalLengthOfAllNamesInCurrentLevel(totalLengthOfAllNamesInCurrentLevel);
                        namePoint.setTotalPointNumOfCurrentLevel(totalPointNumOfCurrentLevel);
                        namePoint.setIndexOfThisPointInCurrentLevel(i);
                    }
                });
        return levelNamePointInfo;
    }

    private static void drawTheBoard(DAG<Vertex> dag, Map<Vertex, NamePoint> vertexCoordinate, List<List<Character>> board) {
        vertexCoordinate.values().forEach(coordinate -> {
            int x = coordinate.x;
            int y = coordinate.y;
            for (int i = 0; i < coordinate.nameWidth; i++) {
                board.get(x).set(y + i - coordinate.nameWidth / 2, coordinate.name.charAt(i));
            }
        });

        Set<DefaultEdge> visitedEdges = new HashSet<>();
        Set<Vertex> vertices = dag.getAllVertices();
        for (Vertex vertex : vertices) {
            Set<DefaultEdge> edges = dag.getEdges(vertex);
            for (DefaultEdge edge : edges) {
                if (visitedEdges.contains(edge)) {
                    continue;
                }
                Vertex source = dag.getDag().getEdgeSource(edge);
                Vertex target = dag.getDag().getEdgeTarget(edge);
                NamePoint sourceCoordinate = vertexCoordinate.get(source);
                int sourceX = sourceCoordinate.x;
                int sourceY = sourceCoordinate.y;
                NamePoint targetCoordinate = vertexCoordinate.get(target);
                int targetX = targetCoordinate.x;
                int targetY = targetCoordinate.y;

                // Calculate the slope
                double slope = (targetY - sourceY) / (targetX - sourceX);
                // Calculate the y-intercept
                double yIntercept = sourceY - slope * sourceX;

                for (int i = sourceX + 1; i < targetX; i++) {
                    int j = (int) (slope * i + yIntercept);
                    char c = getCharBySlope(slope, sourceX, targetX);
                    board.get(i).set(j, c);
                }
                visitedEdges.add(edge);
            }
        }
    }

    private static List<List<Character>> initBoard(int height, int width) {
        return Stream.generate(() -> Stream.generate(() -> ' ').limit(width).collect(Collectors.toList()))
                .limit(height)
                .collect(Collectors.toList());
    }


    private static void updateCoordinateInfo(int x, int y, Map<Vertex, NamePoint> vertexCoordinate, Vertex vertex) {
        NamePoint rootCoordinate = new NamePoint(x, y, vertex.getIdentifier());
        vertexCoordinate.put(vertex, rootCoordinate);
    }

    private static Character getCharBySlope(double slope, int sourceX, int targetX) {
        if (slope == 0) {
            return '\u2193';
        } else if (slope == Double.POSITIVE_INFINITY) {
            return '\u2192';
        } else if (slope == Double.NEGATIVE_INFINITY) {
            return '\u2190';
        } else if (slope > 0 && targetX > sourceX) {
            return '\u2198';
        } else if (slope > 0 && targetX < sourceX) {
            return '\u2197';
        } else if (slope < 0 && targetX > sourceX) {
            return '\u2199';
        } else if (slope < 0 && targetX < sourceX) {
            return '\u2196';
        } else {
            return '\u2193';
        }
    }


    private static void adjustCoordinate(Map<Vertex, NamePoint> vertexCoordinate) {
        int minX = vertexCoordinate.values().stream().mapToInt(coordinate -> coordinate.x).min().getAsInt();
        int minY = vertexCoordinate.values().stream().mapToInt(coordinate -> coordinate.y).min().getAsInt();
        vertexCoordinate.forEach((vertex, coordinate) -> {
            coordinate.x = coordinate.x - minX;
            coordinate.y = coordinate.y - minY;
        });
    }

    private static void calculateAncestorCoordinate(DAG<Vertex> dag, Vertex root, Map<Vertex, NamePoint> vertexCoordinate) {
        NamePoint rootCoordinate = vertexCoordinate.get(root);
        int rootX = rootCoordinate.x;
        int rootY = rootCoordinate.y;

        Set<Vertex> ancestors = dag.getDirectAncestors(root);
        for (Vertex ancestor : ancestors) {
            if (vertexCoordinate.containsKey(ancestor)) {
                continue;
            }

            int ancestorX = rootX - 1;
            int ancestorY = rootY;
            while (hasCoordinate(vertexCoordinate, ancestorX, ancestorY)) {
                ancestorY++;
            }

            updateCoordinateInfo(ancestorX, ancestorY, vertexCoordinate, ancestor);

            calculateAncestorCoordinate(dag, ancestor, vertexCoordinate);
        }
    }

    private static void calculateCoordinatesForDAGFromRootVertex(DAG<Vertex> dag, Vertex root, Map<Vertex, NamePoint> vertexCoordinate) {
        NamePoint rootCoordinate = vertexCoordinate.get(root);
        int rootX = rootCoordinate.x;
        int rootY = rootCoordinate.y;

        Set<Vertex> descendants = dag.getDirectDescendants(root);
        for (Vertex descendant : descendants) {
            if (vertexCoordinate.containsKey(descendant)) {
                throw new IllegalArgumentException("DAG is not a tree");
            }

            int descendantX = rootX + 1;

            int descendantY = rootY;
            while (hasCoordinate(vertexCoordinate, descendantX, descendantY)) {
                descendantY++;
            }

            updateCoordinateInfo(descendantX, descendantY, vertexCoordinate, descendant);

            Set<Vertex> ancestors = dag.getDirectAncestors(descendant);
            if (ancestors.size() > 1) {
                for (Vertex ancestor : ancestors) {
                    if (ancestor != root) {
                        calculateAncestorCoordinate(dag, descendant, vertexCoordinate);
                    }
                }
            }

            calculateCoordinatesForDAGFromRootVertex(dag, descendant, vertexCoordinate);
        }
    }

    private static boolean hasCoordinate(Map<Vertex, NamePoint> vertexCoordinate, int x, int y) {
        return vertexCoordinate.values().stream().anyMatch(coordinate -> coordinate.x == x && coordinate.y == y);
    }

}

后记

目前的实现功能,可以初步满足需求。后续可以根据需求再开发些功能。比如根据图本身特点动态调整节点(目前是人肉指定的)、为节点加框、进一步优化箭头排版等。

相关推荐
凡人的AI工具箱3 小时前
AI教你学Python 第11天 : 局部变量与全局变量
开发语言·人工智能·后端·python
是店小二呀3 小时前
【C++】C++ STL探索:Priority Queue与仿函数的深入解析
开发语言·c++·后端
canonical_entropy3 小时前
金蝶云苍穹的Extension与Nop平台的Delta的区别
后端·低代码·架构
我叫啥都行4 小时前
计算机基础知识复习9.7
运维·服务器·网络·笔记·后端
无名指的等待7124 小时前
SpringBoot中使用ElasticSearch
java·spring boot·后端
.生产的驴5 小时前
SpringBoot 消息队列RabbitMQ 消费者确认机制 失败重试机制
java·spring boot·分布式·后端·rabbitmq·java-rabbitmq
AskHarries6 小时前
Spring Boot利用dag加速Spring beans初始化
java·spring boot·后端
苹果酱05676 小时前
一文读懂SpringCLoud
java·开发语言·spring boot·后端·中间件
掐指一算乀缺钱6 小时前
SpringBoot 数据库表结构文档生成
java·数据库·spring boot·后端·spring
计算机学姐9 小时前
基于python+django+vue的影视推荐系统
开发语言·vue.js·后端·python·mysql·django·intellij-idea