用JavaFX制作桑基图(Sankey Diagram)的经验与感悟分享

前言

核心

      自己只是大二萌新,第一年接触计算机专业课,学校安排了Java语言,这门功课的老师布置了一个较为大型的作业,其中的核心是用JavaFX制作一个桑基图,老师应该是希望通过这次作业让同学能够熟练运用之前学习的知识,并且有足够的自学能力去学习老师没有教授的一些知识(老师规定了不能使用正则表达式等内容,也不能使用JavaFX自带的比如image等)。

        对于这个桑基图的适用范围,老师规定了数据输入的类型,是一个规定格式的txt文件(如图1所示),这份文件会有标题名在第一行(下文会主要以example2为例),Spending plan作为桑基图的图名,我最后将其呈现在了窗口的标题(或者说是在JavaFX中修改了Stage的标题为第一行的信息);第二行是桑基图左边的主支名,后面是分支名以及对应的数字,这份文件的主支数据需要程序自行计算并呈现在图中,我在数据处理的步骤中完成了主支的计算,从而运用到后续的画图之中。我还设计了一个图形交互界面(后文对应章节会详细阐述),还加了一些额外功能(后文对应章节会详细阐述)。

图1

        因此本文分享的核心是如何用一些朴素的方法在JavaFX里实现一个对应数据类型但是有一定适用性的桑基图

时间轴

        代码还是当时交上去的版本,后续没有更新,我前期其实有很多构思的,但是因为时间或者技术上的原因几乎都被我否决了,周日交的作业,周三看了一下书《Introduction to Java Programming and Data Structures, 12th ed, Y.Daniel Liang》,书的14-16章都是讲JavaFX的,打算每天看一章,然后一天写代码,一天完成作业里其他的部分。

        周三看的时候发现书本上有很多很好的例子,我觉得可以直接搬过来作为额外功能,他们也是一些前期的构思,也被我在后期否决了。周四的时候朋友催促我不开始就来不及了,于是我周四就开始敲,书的14章的示例就可以做到图像随着窗口大小的改变而改变了,是将想要呈现的图像作为新的类继承于Pane创建出来,再将Pane放到Scene和Stage上,我觉得这个方法很好,将所有的图形拆分成一个个Node,再将这些Node放在一个Pane上,这个Pane是新创建出来的,是JavaFX原来Pane的子类,而这个Pane放到Scene上,再将Scene放到Stage上,那现在这个Pane的宽和高就是Scene的宽和高,也就是窗口的数值,里面所有的数据都跟这个宽和高绑定,就可以实现图像随窗口大小变化而变化了。还有一种方法是运用Property,绑定属性或者说运用监视器,我对这个方法不是很了解,所以我的方法主体是前一种方法,当然第二种方法也是可行的。

        周四开始敲发现自己实践起来还是有些困难,于是我在ide里创建了一个新的项目作为一个草稿,我周四花了一天的时间画了一个桑基图,里面的数据都是定值,画了一个基础版,我的思路是先画左边的主支的长方形,再画右边的分支的长方形,最后画中间的分支线,左右两边长方形很简单,中间的线本来打算用两条Arc作为一条线,这个方案画出来中间会突兀,但是这是我认为可以解决这个问题的方法,但是最后发现我写的代码一直有bug,那天这个是最困扰我的,后来决定换路线,使用CubicCurve,这个方法比较高级,学习起来需要花费一定成本,他需要开始点和结束点,关键在于中间的控制点,控制点并不是转弯的点,控制点一般在图形的外面圆弧的外面,那天没熬夜顺利解决了这个基础版。

        周五起床,很快把图像继承好了,但是发现分支的字一直很难对齐,这是那天最困扰我的问题,自己通过查阅资料和多次实践理解了添加文字的具体数值,但是发现一直对不起,最后我通过将我代码里的误差值进行多次修改勉强得出了一个可用的版本,后文到对应的章节会详细阐述。那天是冬至,看朋友圈里有人吃饺子有人吃汤圆,感谢朋友们带我两个都吃上了,那天睡前学到的最后一个知识就是关于这个问题的,南方吃汤圆,北方吃饺子。但这不是我最后想的是,那天加了些额外功能,我敲代码的那两天天天看ide于是有了新的灵感,加了一些新功能,但是几乎每加一个功能就会有bug,睡前一直在想怎么修bug。

        周六修bug,修了一天,后面实在修不好了,不是很恶性的就算了,然后晚上开始写注释写报告,这天熬夜了,周日结果晚上9点就交了作业,其实很多地方还可以改进,但是累了不想改了就这样吧。

        这就是大致的一个时间轴,下面我将按照这个时间顺序阐述我的代码里面的一些算法以及其中的一些问题。完整代码会附在最后,后文中标注的行数即是对应完整代码中的行数。

效果图

部分草稿

数据处理

        这个数据的第一行暂时忽略,剩余的行,都是名字+数字的形式,第二行的可以理解为后面是0,这样我们很容易想到可以用键值对来储存数据,因此我用Map写了一个方法如下。(Line373)

public static Map<String, Integer> getValue(String fileName){
        File file = new File(fileName);
        Map<String, Integer> map = new HashMap<>();

        if (!file.exists()){
            System.out.println("This file does not exist!");
            System.exit(0);
        }

        try{
            Scanner input = new Scanner(file);
            String title = input.nextLine();
            String sumWord = input.nextLine();
            int sum = 0;
            while (input.hasNextLine()){
                String[] line = input.nextLine().split(" ");
                int n = line.length;
                String tempWord = "";
                for (int i = 0; i < n - 1; i++){
                    if (i == n - 2){
                        tempWord += line[i];
                    }
                    else {
                        tempWord += line[i];
                        tempWord += " ";
                    }
                }
                map.put(tempWord, Integer.parseInt(line[n - 1]));
                sum += Integer.parseInt(line[n - 1]);
            }
            map.put(sumWord, sum);

        }
        catch (IOException ioe){
            System.out.println(ioe.getMessage());
        }
        return map;
    }

        在数据处理阶段我就考虑到可能有异常出现,因此先有一个if判断去防止文件报错不存在,另一方面运用try和catch防止IOExcepytion,用Scanner去获取文件信息,第一行是标题所以跳过,title变量没有被使用,第二行是主支名,我们给他定义一个和sum暂时为0,后面的数据处理是统一方式的,所以可以用循环,每次循环的时候我们可以把分支的数字加到sum变量上,这样我们就可以让主支有数据了,最后将主支名和对应的数据存进去,也就是将sumWord和sum放进map里。

        到这里就会有一个问题,光一个map是很难确定里面哪一个key是主支,而且直接读取的话顺序会乱,所以我准备再用一个array去记录这些key的顺序,我认为用两个array也是可以解决这个问题,一个array存key一个array存value。

        这里的方法(Line413)和上面类似这里不过多阐述。

public static String[] getOrder(String fileName, int arrayLength){
        File file = new File(fileName);
        String[] order = new String[arrayLength];

        if (!file.exists()){
            System.out.println("This file does not exist!");
            System.exit(0);
        }

        try {
            Scanner input = new Scanner(file);
            String title = input.nextLine();
            order[0] = input.nextLine();
            while (input.hasNextLine()){
                for (int i = 1; i < arrayLength; i++){
                    String[] line = input.nextLine().split(" ");
                    int n = line.length;
                    String tempWord = "";
                    for (int j = 0; j < n - 1; j++){
                        if (j == n - 2){
                            tempWord += line[j];
                        }
                        else {
                            tempWord += line[j];
                            tempWord += " ";
                        }
                    }
                    order[i] = tempWord;
                }
            }
        }
        catch (IOException ioe){
            System.out.println(ioe.getMessage());
        }
        return order;
    }

        第三个方法是读取文件的标题,我认为第一行以外的数据是需要读取出来画桑基图的,而第一行只需要被用来作为窗口的名字,所以我将他单独用一个方法写出来。(Line451)

    public static String getTitle(String fileName){
        File file = new File(fileName);
        String title = "";

        if (!file.exists()){
            System.out.println("This file does not exist!");
            System.exit(0);
        }

        try{
            Scanner input = new Scanner(file);
            title = input.nextLine();
        }
        catch (IOException ioe){
            System.out.println(ioe.getMessage());
        }
        return title;
    }

桑基图

改变大小

        前面说过桑基图将是一个Pane的子类(Line470),所以我们创建一个子类,然后创建一个方法名为paint,再重写原本Pane里的setWidth(Line761)和setHeight(Line767)两个方法,都是先调用父类里的该方法,再调用刚刚的paint方法,这样当宽度和高度变化后,我们都会有一个新的图,所以在paint方法里要注意,将上次的图清除掉,所以paint方法的第一行是将当前SankeyPane上面的Nodes都清除掉。

    @Override
    public void setWidth(double width){
        super.setWidth(width);
        paint();
    }
//    Override setHeight to invoke the method paint to realize resizing
    @Override
    public void setHeight(double height){
        super.setHeight(height);
        paint();
    }
}

准备工作

        下面是paint方法,我们先确定一下缩放比例等问题,我由于用之前的定值做了一张图,所以我对整个图有了大致的比例认知,最后我将左右两个长方形的宽度都定为总宽度的32分之1,而后一个变量scale也是之前试过的值的比例关系,略微改了一下就变成了这个算式,主要是让这个桑基图的高度和窗口大小呈固定比例。(Line623)

        double midy = getHeight() / 2;
        double rectangleWidth = getWidth() / 32;
        double scale = map.get(array[0]) / getHeight() * 5000 / 2025;

        不仅两个长方形大小的变化要改变,每个分支的间距应该也要随窗口的大小变化而变化,如果分支数量为1那这个间隔为0否则就要计算,我的公式写的比较复杂,其实如果化简的话其实就是关于窗口高度得出来的值,而这个值其实也是之前画图经验得出来的值,再将这个值平均分到每个间隔之中,从而得到间隔的值命名为gap。(Line627)

        double gap = 0;
        //if only one branch gap will be zero
        if (mapSize == 2){
            gap = 0;
        }
        else {
            gap = map.get(array[0]) / (mapSize - 2) / scale;
        }

        观察桑基图可以发现,示例图的分支是6个,但是中间还是一条是直的,因为这条分支包含这个总和中的中间值,如果一个分支加上它之前前面的和小于总和的一半,加上它不满足了,那这条分支就是中间直的那条。确定中点对我的算法重要的原因,因为我将这个数值定为高度的中间值,只有找到了这个数值,才能确定整个桑基图可以位于窗口的中间。从这个值出发得到左边主支长方形的起点x1,y1,以及右边第一个分支的起点x2,y2。(注意JavaFX的Rectangle的起点是长方形的左上角)(Line637)

        int sum = 0;
        int midIndex = 0;
        double midHeight = 0;
        for (int i = 1; i < mapSize; i++){
            sum += map.get(array[i]);
            if (sum >= map.get(array[0]) / 2){
                midIndex = i;
                midHeight = map.get(array[i]) / scale;
                break;
            }
        }

//        The values for the left rectangle
        double x1 = getWidth() / 16 * 5;
        double y1 = midy - midHeight / 2;
        for (int i = 1; i < midIndex; i++){
            y1 -= (double) map.get(array[i]) / scale;
        }

//        The values for the first branch rectangle
        double x2 = getWidth() / 64 * 45;
        double y2 = midy - midHeight / 2;
        for (int i = 1; i < midIndex; i++){
            y2 -= map.get(array[i]) / scale;
            y2 -= gap;
        }

        由于我是先画图形再最后再讲图形对应的文本添加上去的,所以我额外用了一个变量用来记录右边分支的起点的总坐标值,方便我后面添加文本(文本的横坐标是根据窗口的宽度变化而变化的,纵坐标跟长方形的纵坐标的关系更为密切,此处记录右边的,左边的易于得到就没有单独设置变量)。(Line665)字体也提前设置好了一个。(Line621)

        剩下的问题是颜色,在JavaFX中设置颜色的方法有很多,我是先按照例图准备的颜色,为了获得例图的颜色,我用测色仪对上面的rgb值进行了检测,得出来的rgb值用Color类的rgb进行导入,从而获得了对应的颜色。我用示例图有两方面原因,一是示例图我觉得的确很好看,是个很好的颜色组合,第二点是如果直接将rgb的三个值用random函数会得到很多随机的颜色,很容易让桑基图变得不够美观。(Line509)(color1是主支的颜色,后面6个是右边分支长方形的颜色,最后6个是中间曲线的颜色)

        Color color1 = Color.rgb(104, 167, 210);
        Color color2 = Color.rgb(195, 86, 255);
        Color color3 = Color.rgb(193, 215, 236);
        Color color4 = Color.rgb(224, 80, 28);
        Color color5 = Color.rgb(250, 133, 63);
        Color color6 = Color.rgb(252, 167, 103);
        Color color7 = Color.rgb(252, 204, 156);
        Color color8 = Color.rgb(228, 176, 255);
        Color color9 = Color.rgb(205, 217, 225);
        Color color10 = Color.rgb(241, 173, 140);
        Color color11 = Color.rgb(253, 199, 163);
        Color color12 = Color.rgb(253, 215, 183);
        Color color13 = Color.rgb(138, 234, 135);

        这就是基础的准备工作。

画图

        左边的长方形最为简单,准备工作中已经准备了起始点的坐标以及长方形的宽度,长方形的高度通过前面准备的scale也可以计算,将这些代入到JavaFX的Rectangle类中,但是这个长方形它分成了边框和里面的填充,所以长方形的长在创建时-1,将边框和填充都用一个颜色,这样就能让最后整个长方形符合我们想要的位置与大小。(Line689)

        double height1 = map.get(array[0]) / scale;
        Rectangle r1 = new Rectangle(x1, y1, rectangleWidth, height1 - 1);
        r1.setStroke(color1);
        r1.setFill(color1);
        getChildren().add(r1);

        右边的分支较为复杂,方法当然是循环,每个长方形的宽度是一样的,提前准备好了,但有对应的高度,这个每次循环里都要计算,画法与左边一样,需要将高度-1,颜色设置统一,最后每次y2加上对应的长方形的长与空着的间隔,这里虽然最后一次不用加间隔,但是这里最后不影响,所以没有分开处理。写这篇文章的时候发现这里代码有问题,包括有多余的行,原代码由于后面额外功能所以颜色方面有所不同,此处展示代码进行了修改。(Line668)

        for (int i = 1; i < mapSize; i++){
            double height = map.get(array[i]) / scale;
            sum += map.get(array[i]);
            Rectangle r2 = new Rectangle(x2, y2, rectangleWidth,  height - 1);
            r2.setStroke(colorArray[i]);
            r2.setFill(colorArray[i]);
            getChildren().add(r2);
            if (i > 1){
                Line l1 = new Line(x2, y2, x2 + rectangleWidth, y2);
                l1.setStroke(Color.BLACK);
                getChildren().add(l1);
            }
            y2 += height;
            if (i < mapSize - 1){
                Line l1 = new Line(x2, y2, x2 + rectangleWidth, y2);
                l1.setStroke(Color.BLACK);
                getChildren().add(l1);
            }
            y2 += gap;
        }

        我们观察图例还可以发现,这里每个分支中间有黑线以区分,观察能发现第一个分支是下面有黑线,最后一个分支是上面有黑线,中间分支是两边都有黑线,这么想可以那就需要写三个判断,我将这个问题拆成两部分,除了第一个分支都画了长方形下面的线,以及除了最后一个分支都画了长方形上面的线,用这两个判断画线我们直接放在循环里,上黑线的起点是长方形的起点,所以放在y2变化前,当y2只加了长方形的长后就可以作为下黑线的起点。(Line675)

        然后是中间的曲线,前文说过用两条Arc的效果应该不如CubicCurve,而且CubicCurve比较方便,它需要三类参数(起点、终点、控制点),前文说过控制点不是转弯点,在转弯的圆弧靠外的位置,在设置好起始点和终止点之后(x1本来是长方形左上角顶点,所以刚开始需要加长方形宽),我将第一个控制点设置在了和起始点一样的高度,但是在起始点和终止点中靠起始点四分之一的位置,然后第二个控制点设置在了和终止点一样的位置,靠终止点四分之一的位置。虽然中间是图形,但是我将重复多条线,这样就让他们变成了一个图形,但这样也有个问题,循环的间隔是1,所以如果最后右边的长方形的长不是整数,就会导致没法对齐,我曾经试过将循环次数改成0.1或者0.01,效果不是很好,不仅没法很好解决对齐问题,还会导致程序变卡许多,因为循环次数太多了,而之前测试的数据就是很好的比例,才使得最后的效果图有不错的效果。(line709)

        x1 += rectangleWidth;
        y2 = y2Start;
        for (int i = 1; i < mapSize; i++){
            double height = map.get(array[i]) / scale;
            for (int j = 1; j <= height; j++){
                CubicCurve c1 = new CubicCurve();
                c1.setStartX(x1);
                c1.setStartY(y1);
                c1.setEndX(x2);
                c1.setEndY(y2);
                c1.setControlX1(x1 + (x2 - x1) / 4);
                c1.setControlY1(y1);
                c1.setControlX2(x2 - (x2 - x1) / 4);
                c1.setControlY2(y2);
                c1.setFill(Color.TRANSPARENT);
                c1.setStroke(colorArray[i + order.length - 1]);
                getChildren().add(c1);
                y1++;
                y2++;
            }
            y2 += gap;
        }

 加字

        前文说过字体已经准备好了一种字体。(Line621)观察可以发现,右边分支的字都是右对齐,这是第一个问题;第二个问题是要让字和图在放大缩小后都能让字不会和图形失去相应的位置关系。

        对于第一个问题我们可以像之前画图一样,将所有的字分为两个部分去添加,左边的主支和右边的分支,分支的多个字之间的距离就会像画图形时候一样简单,然后每个字都是竖着排下去的,所以我选择用VBox去解决问题。(Line697)(Line733)

        VBox vBox1 = new VBox();
        vBox1.setAlignment(Pos.CENTER_RIGHT);
        double vBox1Width = getWidth() / 64 * 19;
        vBox1.setPrefWidth(vBox1Width);
        Text text1 = new Text(x1 / 3, y1, array[0] + ": " + map.get(array[0]));
        text1.setFill(Color.BLACK);
        text1.setFont(font1);
        vBox1.setMargin(text1, new Insets(height1 / 2 + y1 - 10, 0,0 , 0));
        vBox1.getChildren().add(text1);
        getChildren().add(vBox1);
        y2 = y2Start;
        double vBox2Width = getWidth() / 64 * 43;
        VBox vBox2 = new VBox();
        vBox2.setAlignment(Pos.CENTER_RIGHT);
        vBox2.setPrefWidth(vBox2Width);
        for (int i = 1; i < mapSize; i++){
            double heightNow = map.get(array[i]) / scale;
            double heightPrevious = map.get(array[i - 1]) / scale;
            if (i == 1){
                Text text2 = new Text(array[i] + ": " + map.get(array[i]));
                text2.setFill(Color.BLACK);
                text2.setFont(font1);
                vBox2.setMargin(text2, new Insets(heightNow / 2 + y2 - 10, 0,0 , 0));
                vBox2.getChildren().add(text2);
            }
            else {
                Text text2 = new Text(array[i] + ": " + map.get(array[i]));
                text2.setFill(Color.BLACK);
                text2.setFont(font1);
                vBox2.setMargin(text2, new Insets((heightNow + heightPrevious) / 2 + gap - 25, 0,0 , 0));
                vBox2.getChildren().add(text2);
            }
        }
        getChildren().add(vBox2);

        这里出现了新问题,那就是字也有大小,经过实践测试,比如字体大小的20,是指这个字有20个像素点高,而他的起始点和长方形一样是左上角,因此当设置margin的时候,上边距是整个窗口高的一半还要-10,这样可以让高20的字均匀分布在两侧,从而让字真正在正中心(这里代码写复杂了)(Margin控制的是node和前面的距离,而Spacing控制的是node和后面的距离)。

        这里在右边的加字时出现了新的问题,按照理论计算,每两个字之间的距离应该是新加的一段字对应的分支的长度和上一段分支的长度的和的一半再加上中间的间隔,但是实操发现了无法对齐,最后手动慢慢尝试,选择了-25的效果最好,这里是一个没有解决的问题,我认为问题是对于MarginSpacing的理解不到位,Margin在很多资料中被描述为调整节点和父容器的距离,但是如果这里是定值,所有的字的距离是固定的,而不是所有的字堆在一起,说明该描述存在问题。而没有选择Spacing,因为Spacing前没有选择Margin的话会让第一个字顶在最上面,固当时选择了Margin

        对于第二个问题,我的解决方法是提前设置VBox的宽,将宽限制好,这样右对齐的线就是VBox的边界,从而使图形和文字的相对位置不会改变。

        至此核心桑基图已经制作出来了。

额外功能

交互界面​​​​​​​​​​​​​​

        当解决核心问题桑基图之后,我觉得我需要加一个图形交互界面,这样可以让用户能像正常的app一样使用程序去获得自己想要的桑基图。

        因此我将程序的第一个窗口写成了图形用户界面,而刚刚的桑基图作为一个单独类,在这个用户界面类下有刚刚的数据处理的方法,也有专门的方法以生成一个新的桑基图。这便是我想的一个思路。

        先是这个界面的设计,我本来打算想用户登录界面那样将其设计为两排,但是不同于用户登录界面有用户名和密码两行,该交互界面只需要一个输入的文件名,所以原来的文本框一行和按钮一行并为了一行。但GridPane没改了,其余的Pane几乎都可以使用。试了试距离的数值,然后把一个文本,一个文本框,两个按钮添加进去就好。(Line86)这里在文本框里设置了PrompText,但是需要将FocusTraversable改成false才能生效。

按键确认

        按键确认,因为有了用户界面,所以现在需要一个新的方式呈现刚刚的桑基图,我在刚刚写了的类中加入了方法showSankeyDiagrams(Line333),以一个新的窗口的方式展现桑基图,并在实践中得到自己认为合适的最小窗口的数据,添加了最小窗口的数值(既可以自己多次开关测试,又可以在刚刚setWidth和setHeight方法中加入sout从而得出实时数据)。 

    public  static void showSankeyDiagrams(String filename){
        String fullName = filename + ".txt";
        String title = getTitle(fullName);
        Map<String, Integer> map = getValue(fullName);
        int mapSize = getValue(fullName).size();
        String[] array = getOrder(fullName, mapSize);
        SankeyPane sankeyPane = new SankeyPane(map, array);
        Scene scene = new Scene(sankeyPane, 600, 500);
        Stage sankeyStage = new Stage();
        sankeyStage.setMinWidth(450);
        sankeyStage.setMinHeight(450);
        sankeyStage.setTitle(title);
        sankeyStage.setScene(scene);
        sankeyStage.show();
    }

        然后是添加事件,在每个按钮上添加事件,我在对话框上也添加了回车的快捷事件,然后每个事件里调用刚刚写的方法。(Line219,252,277,310)

错误提示

​​​​​​​        这时候很容易想到一个问题,那就是如果用户输入了错误的文件名怎么办,因此要有这种错误处理。但其实在数据处理一部分中,三个方法都有if判断可以解决该问题。但是会导致程序直接退出,给人以闪退的感觉。能不能让这个错误处理更加优雅呢?

        在对话框里加入一个判断从而将问题拦下,以防止软件闪退的问题出现。在错误的提示上,我设置了两个小动作,一是将用户界面的标题改成“文件名不存在”,二是添加了一个动画。(动画准备工作在Line91,错误处理在Line234,267,292)

错误弹窗

        由于担心错误提示不足够,我单独加了弹窗功能。我构思的是,如果出现了太多次输入错误就会出现弹窗来提示用户输入错误了。那这个计数得在多个方法之外,于是我创建了instance variable errorTime(Line25),再创建了两个方法,一个方法让errorTime自增(Line52),另一个方法判断是3的倍数就会弹窗(Line57),最后将这两个方法放到前面的错误处理中。(Line236,269,294)

    public void errorTimer(){
        errorTime++;
    }
    public void errorAlert(){
        if (errorTime % 3 == 0){
            Alert alert = new Alert(Alert.AlertType.WARNING,
                    "Warning for input error!!!",
                    ButtonType.CANCEL,
                    ButtonType.CLOSE);
            alert.show();
        }
    }

智能删词

        该功能的出现是因为我认为输入example2是我们正常习惯的文件名,但是我们代码里需要加上后缀,防止有人输入的东西带上后缀,我在文本框中加入监视器,如果文本框里的文本有“.txt”就会自动删去该部分。当时测试发现会一直报红,但是程序都能正常运行,在g大神的帮助下将其运行顺序放后,现在看来这串代码有一定问题,问题处理也有不妥之处,这个功能的实用性远不如其他几个功能。(Line206)

                Platform.runLater(() -> {
                    int position = textField1.getCaretPosition();
                    if (t1.indexOf(".txt") != -1){
                        int index = t1.indexOf(".txt");
                        String str = t1.substring(0, index);
                        textField1.setText(str);
                    }
                    textField1.positionCaret(position);
                });

智能提示

        在长期的编程环境下,我非常喜欢ide中的一些功能,比如sout回车就可以完成一行代码,或者一些不是很清楚的命令,可以靠打关键字找到对应的命令。因此我也想让我的程序有类似的功能,当用户输入文本后,系统猜测用户可能想要输入的结果,然后提示给用户,但最后呈现出的结果仍有一些缺点。

        首先用户想输入的东西不仅是符合用户给的关键词,还得符合正确的结果,所以第一步是获得正确结果,因此我想的是专门写一个方法getFileNames去获得正确的文件名信息,由于这些文件名一定名字不同,因此储存在了map当中。(Line32)

    public void getFileNames(){
        File dir = new File(System.getProperty("user.dir"));
        String[] fileNames = dir.list();
        for (String fileName : fileNames){
            if (fileName.endsWith(".txt")){
                int index = fileName.lastIndexOf(".txt");
                String key = fileName.substring(0, index);
                this.fileNames.put(key, 1);
            }
        }
    }

        第二步我认为是要分析可能的情况,最原始的便是我说的ide或者其他搜索功能都有的,如果你输入“ex”,他会给你提示包含ex的东西。我认为还存在用户把单词打反了的情况,然后也能提示。并且还有用户单词里的某个字母一不小心打多或打少的情况。因此我先将正确的结果拿出一个,再将输入的文本拆出来,每个字与拿出的正确结果比对,如果文本的所有字符均出现在正确结果中,那该正确结果就会被添加到Tooltip中,呈现出来。(Line133)(修改版如下)

                String[] keywords = t1.split("");
                int n = t1.length();
                int findTimes;
                for (String x : fileNames.keySet()){
                    findTimes = 0;
                    for (int i = 0; i < n; i++){
                        String keyword = keywords[i];
                        if (x.indexOf(keyword) != -1){
                            findTimes++;
                            if (findTimes == n){
                                str = str + y;
                                str = str + "\n";
                            }
                        }
                    }
                }
                tip.setText(str);

        问题有以下几个,一是原代码先判断是否有符合条件的,再去寻找,看上去可以避免资源浪费,其实造成了浪费,可以去掉这部分代码。二是可能因为set的无序性,导致出现的结果不是按顺序出现的。三是比如在添加完下一个功能后,问题二变得更加严重,甚至出现了重复。

颜色变化

        颜色变化功能,我本来是有所顾忌的,因为如果加随机颜色一般用random函数但是我认为随机出的结果充满不确定性,很容易出现不好看的桑基图。因此在前面的时候我用了示例图的所有颜色,那现在需要随机其他的桑基图或者有更多分支该怎么办呢?

        目前我程序中的颜色一共有13种颜色,第一个为主干颜色,后面12个的前6个为分支颜色,后6个是中间曲线的颜色。我认为图示例的颜色可以作为一组,我再按照这个规律放几组颜色,再通过random函数得出随机使用哪一组进行画图,以这样的方式来避免随机出的颜色会影响美观。因此我分析后将颜色相近的放在一起,一共准备了84个颜色给分支,也就是一共85个颜色的颜色库。(Line509)

        因此我多了一项准备工作,我先用一个变量colorsNeed表示需要多少颜色(Line608),再用一个变量group表示(Line609),有多少组可以选择,从而决定选哪一组,自然如果是默认那就是第一组,如果是第二组那就是加上第一组的数字,其余不变,那就是在第二组中选,因此这里有一个变量k,表示前面的组数(Line614),变量temp表示多余的颜色(Line613)(如果一共有8个颜色,但是每次用3个颜色,所以这个分组会有多余的,即为temp)(randomColor-1是因为主干有一个颜色),但是这里很快有了新问题,我打算引入3种类型,一种是默认,在我程序里我命名为REGULAR,也就是最开始的默认颜色,如果不输入这个类型,默认运行这个结果。然后是RANDOM类,随机挑选一个刚刚的颜色组里的颜色,从而生成桑基图。最后是UNIT类,UNIT类是让右边的分支呈现一个颜色。尤其是后两种用了random函数需要注意的问题是,改变大小其实是清楚了就图像,生成新图像,所以这里的random结果得保持不变。因此我将这个random结果放到了instance variable命名为randomColor(Line483),而这个颜色类型命名为colorType(Line481)。

        对于不同情况如何在最后上色达成预计效果,这里我想的方法易读性较差。为了方便,我先改变了一下颜色的顺序,我将每个颜色相近的摆在了一起,所以我将前面输入的颜色也按这个顺序排好,所以现在颜色的顺序是分支颜色和其曲线颜色在一起,以这样的顺序重新排好了之前的12个颜色。(Line596)对于前两种类型,那就是找出颜色组,然后用这个组里的颜色给图片上色,而其中默认的组是第一组,随机的组是随机组。对于最后一种,则是不从组中找颜色,而是用前面的random值找一个颜色用来画图。因此我将这些结合为了一个算式,我写了以下这个公式。(Line672,673,724)

colorList.get((2 * i + colorsNeed * k) * Math.abs(colorType) + (-1) * (Math.abs(colorType) - 1) * randomColor)

        现在看来这个算式问题多多,我将类型默认确定为1,随机确定为2,统一确定为0,希望借助0从而让前面的数不影响后面的算式,但是又需要我在前面加一串代码保证类型2没有问题。(Line610)

        这里存在的问题还有random函数应该是*85,而非84。

        再单独写新的构造器于SankeyPane类(Line493)中,并且在GUI中也要写新的方法(Line348),而所有GUI的确认以及关键词搜索都需要修改,当然我在GUI界面还加了新的instance variableLine29)以及新方法inputColorTypesline45)。但是这些方法加完后,测试发现如果输入空格可能导致一些问题,比如输入“example2 ”会导致example2(空格)被输入,会被数据处理里的文件不存在判断挡住而导致程序直接结束。又或者直接输入空格回车将没有任何反应。

思考

OOP的优缺点​​​​​​​​​​​​​​

        OOP面向对象编程老师上课只提到了优点,但是我自己这次实践能深刻感受到他有优点的同时也有缺点。

        优点比如很多方法不仅我这个程序可以使用,别的程序也可以直接拿来使用,又或者修改后能使用。不同方法和类让代码结构清晰,方便后期我写博客时候阅读。

        缺点比如由于我的代码是先写好了一个桑基图而后考虑GUI以及其他额外功能的,所以很多地方没有预先思考,而是后加的导致难以修改,尤其是在额外功能的第七点颜色变化上,没有提前思考颜色变化,导致在原来的程序上加东西,降低了原代码的可读性和拓展性,其实可以单独拆成新的类和更多方法其实可能效果更好,也可以让后面有更多颜色类型的时候能够更好修改。又比如在颜色处理问题解决后发现的那个特例bug,虽然能被数据处理的方法解决,但是也是一个没有提前规划最后导致的问题之一。再者智能提醒那里的缺陷也可以在提前构思中解决。

编程习惯

        从上一节的缺点就可以发现,编程如果没有提前想好构思,而边写边想很容易导致最后的成品如草稿纸的反复修改一般,造成一系列问题。因此在程序编程之前应提前构思。使用OOP其实可以缓解一部分这样的问题,但是如果前期工作准备就绪后面的工作就能更顺利。

        时隔数周再次查看我的代码发现里面仍有一些算法上以及累赘的问题,而代码的注释自然也是一个大问题,后期我会努力学习更正,代码中变量的命名方式偏数学化而缺乏可读性。

        因此通过这次作业还是收获丰富的,更能说明实践的重要性。

代码

import java.util.*;
import java.io.*;
import java.lang.Math;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.animation.FadeTransition;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.scene.text.*;
import javafx.stage.Stage;
import javafx.util.Duration;

public class SankeyDiagrams extends Application{
    //errorTime will count how many times you input wrong file names
    int errorTime = 0;
    //fileNames records the file ends with '.txt' in the project folder
    Map<String, Integer> fileNames = new HashMap<>();
    //colorTypes records the color types
    Set<String> colorTypes = new HashSet<>();

//    Get the names of file in the project folder
    public void getFileNames(){
        File dir = new File(System.getProperty("user.dir"));
        String[] fileNames = dir.list();
        for (String fileName : fileNames){
            if (fileName.endsWith(".txt")){
                int index = fileName.lastIndexOf(".txt");
                String key = fileName.substring(0, index);
                this.fileNames.put(key, 1);
            }
        }
    }

//    Add the color types to the colorTypes
    public void inputColorTypes(){
        colorTypes.add("REGULAR");
        colorTypes.add("RANDOM");
        colorTypes.add("UNIT");
    }

//    When user inputs a wrong file name, the errorTime will add one;
    public void errorTimer(){
        errorTime++;
    }

//    If user inputs a wrong file name for three times, an alert will be shown
    public void errorAlert(){
        if (errorTime % 3 == 0){
            Alert alert = new Alert(Alert.AlertType.WARNING,
                    "Warning for input error!!!",
                    ButtonType.CANCEL,
                    ButtonType.CLOSE);
            alert.show();
        }
    }

    /**
     Override start to show a graphical interface with a text box
     The user can input file name to get a Sankey diagram directly
     Or with a color type followed to get a Sankey diagram in different ways
     The default color type is REGULAR(show Sankey diagram in the regular way)
     If the user input wrong file name, it will get an error tip
     If the user input wrong file name for several times, an alert will be shown
     If the user input '.txt', system will delete it automatically
     There will be tips for guessing what the user wants to input
     There will be available to press 'Enter' to enter
     There will be available to click the 'Enter' bottom to enter
     There will be available to click the 'Clear' bottom to clear the text box
     */
    @Override
    public void start(Stage primaryStage){
        getFileNames();
        inputColorTypes();

//        Use gridPane to show a text, a textFiled and two buttons
        GridPane gridPane = new GridPane();
        gridPane.setHgap(10);
        gridPane.setAlignment(Pos.CENTER);

//        Set an animation for user getting an error tip after input incorrectly
        FadeTransition fadeTransition = new FadeTransition();
        fadeTransition.setDuration(Duration.seconds(1));
        fadeTransition.setFromValue(0);
        fadeTransition.setToValue(1);
        fadeTransition.setNode(gridPane);

//        Creates some nodes for this graphical interface
        Label l1 = new Label("Input filename : ");
        l1.setFont(Font.font("Arial", FontWeight.BOLD, 20.0));

        TextField textField1 = new TextField();
        textField1.setFont(Font.font(15));
        Tooltip tip = new Tooltip("Please input correct filename!!!");
        tip.setHideDelay(Duration.INDEFINITE);
        tip.setFont(Font.font("Arial", FontWeight.BOLD, 20.0));
        textField1.setTooltip(tip);
        textField1.setPromptText("Welcome");
        textField1.setFocusTraversable(false);

//        Set the input tip always be shown when the mouse is on the textFiled
        textField1.setOnMouseEntered(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                tip.show(textField1, event.getScreenX(), event.getScreenY() + 10);
            }
        });

//        Set the input tip will be hidden when the mouse is not on the textFiled
        textField1.setOnMouseExited(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                tip.hide();
            }
        });

//        Set the tips for guessing what the user want to input
//        Delete '.txt' automatically
        textField1.textProperty().addListener(new ChangeListener<String>() {
            @Override
            public void changed(ObservableValue<? extends String> observableValue, String s, String t1) {
//                First loop is designed to determine the input has been shown in the fileNames
//                Second loop is designed to show what may be the user want to input
                String[] keywords = t1.split("");
                int n = t1.length();
                int findTimes;
                for (String x : fileNames.keySet()){
                    findTimes = 0;
                    for (int i = 0; i < n; i++){
                        String keyword = keywords[i];
                        if (x.indexOf(keyword) != -1){
                            findTimes++;
                        }
                    }
                    if (findTimes == n){
                        String str = "";
                        for (String y : fileNames.keySet()){
                            findTimes = 0;
                            for (int j = 0; j < n; j++){
                                String keyword = keywords[j];
                                if (y.indexOf(keyword) != -1){
                                    findTimes++;
                                    if (findTimes == n){
                                        str = str + y;
                                        str = str + "\n";
                                    }
                                }
                            }
                        }
                        tip.setText(str);
                    }
                }

//                If the user want to input a color type, it will guess what the user what to input
//                First loop is designed to determine the input has been shown in the colorTypes
//                Second loop is designed to  show what may be the user want to input
                String[] type = t1.split(" ");
                if (type.length == 2){
                    String color = type[1];
                    String str = type[0] + " ";
                    String[] colorKeywords = color.split("");
                    int m = colorKeywords.length;

                    for (String x: colorTypes){
                        findTimes = 0;
                        for (int i = 0; i < m; i++){
                            String keyword = colorKeywords[i];
                            if (x.indexOf(keyword) != -1){
                                findTimes++;
                            }
                        }
                        if (findTimes == m){
                            for (String y : colorTypes){
                                findTimes = 0;
                                for (int j = 0; j < m; j++){
                                    String keyword = colorKeywords[j];
                                    if (y.indexOf(keyword) != -1){
                                        findTimes++;
                                        if (findTimes == m){
                                            str = str + y;
                                            str = str + "\n";
                                        }
                                    }
                                }
                            }
                        }
                        tip.setText(str);
                    }
                }

//                If the user did not input anything, the tip will show the primary message
                if (t1.isEmpty()){
                    tip.setText("Please input correct filename!!!");
                }

//                Delete '.txt' automatically
                Platform.runLater(() -> {
                    int position = textField1.getCaretPosition();
                    if (t1.indexOf(".txt") != -1){
                        int index = t1.indexOf(".txt");
                        String str = t1.substring(0, index);
                        textField1.setText(str);
                    }
                    textField1.positionCaret(position);
                });
            }
        });

//        Be available to press 'Enter' to input
        textField1.setOnKeyPressed(new EventHandler<KeyEvent>() {
            @Override
            public void handle(KeyEvent event) {
                if (event.getCode().getName().equals(KeyCode.ENTER.getName())){
                    String str = textField1.textProperty().getValue();
                    String[] type = str.split(" ");
                    if (fileNames.get(type[0]) != null && fileNames.get(type[0]) == 1){
                        if (type.length == 1){
                            showSankeyDiagrams(str);
                        }
                        else if (type.length == 2){
                            showSankeyDiagrams(type[0], type[1]);
                        }
                    }
                    else {
                        primaryStage.setTitle("Filename does not exist!");
                        fadeTransition.play();
                        errorTimer();
                        errorAlert();
                    }
                }
            }
        });

//        Creat  a button for input
        Button b2 = new Button();
        b2.setText("Enter");
        b2.setPrefHeight(10);
        b2.setPrefWidth(60);
        b2.setFont(Font.font("Arial", 15));
        b2.setStyle("-fx-background-radius: 10;");

//        Be available to click the button to input
        b2.setOnMouseClicked(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                if (event.getClickCount() == 1 && event.getButton().name().equals(MouseButton.PRIMARY.name())){
                    String str = textField1.textProperty().getValue();
                    String[] type = str.split(" ");
                    if (fileNames.get(type[0]) != null && fileNames.get(type[0]) == 1){
                        if (type.length == 1){
                            showSankeyDiagrams(str);
                        }
                        else if (type.length == 2){
                            showSankeyDiagrams(type[0], type[1]);
                        }
                    }
                    else {
                        primaryStage.setTitle("Filename does not exist!");
                        fadeTransition.play();
                        errorTimer();
                        errorAlert();
                    }
                }
            }
        });

//        Be available to press 'Enter' to input
        b2.setOnKeyPressed(new EventHandler<KeyEvent>() {
            @Override
            public void handle(KeyEvent event) {
                if (event.getCode().getName().equals(KeyCode.ENTER.getName())){
                    String str = textField1.textProperty().getValue();
                    String[] type = str.split(" ");
                    if (fileNames.get(type[0]) != null && fileNames.get(type[0]) == 1){
                        if (type.length == 1){
                            showSankeyDiagrams(str);
                        }
                        else if (type.length == 2){
                            showSankeyDiagrams(type[0], type[1]);
                        }
                    }
                    else {
                        primaryStage.setTitle("Filename does not exist!");
                        fadeTransition.play();
                        errorTimer();
                        errorAlert();
                    }
                }
            }
        });

//        Creat  a button for clear the textFiled
        Button b1 = new Button();
        b1.setText("Clear");
        b1.setPrefHeight(10);
        b1.setPrefWidth(60);
        b1.setFont(Font.font("Arial", 15));
        b1.setStyle("-fx-background-radius: 10;");

//        Be available to click the button to clear the textFiled
        b1.setOnMouseClicked(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                if (event.getClickCount() == 1 && event.getButton().name().equals(MouseButton.PRIMARY.name())){
                    textField1.setText("");
                }
            }
        });

        gridPane.add(l1, 0, 0);
        gridPane.add(textField1, 1, 0);
        gridPane.add(b2, 2, 0);
        gridPane.add(b1, 3, 0);

        Scene scene = new Scene(gridPane, 600, 300);
        primaryStage.setTitle("SankeyDiagrams");
        primaryStage.setMinWidth(490);
        primaryStage.setMinHeight(120);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

//    Here are two method with Overload to using class SankeyPane to show Sankey diagram from files
    public  static void showSankeyDiagrams(String filename){
        String fullName = filename + ".txt";
        String title = getTitle(fullName);
        Map<String, Integer> map = getValue(fullName);
        int mapSize = getValue(fullName).size();
        String[] array = getOrder(fullName, mapSize);
        SankeyPane sankeyPane = new SankeyPane(map, array);
        Scene scene = new Scene(sankeyPane, 600, 500);
        Stage sankeyStage = new Stage();
        sankeyStage.setMinWidth(450);
        sankeyStage.setMinHeight(450);
        sankeyStage.setTitle(title);
        sankeyStage.setScene(scene);
        sankeyStage.show();
    }
    public  static void showSankeyDiagrams(String filename, String colorType){
        String fullName = filename + ".txt";
        String title = getTitle(fullName);
        Map<String, Integer> map = getValue(fullName);
        int mapSize = getValue(fullName).size();
        String[] array = getOrder(fullName, mapSize);
        //If the user input wrong color type will be viewed as REGULAR
        int ColorType = 1;
        if (colorType.equals("RANDOM")){
            ColorType = -1;
        }
        if (colorType.equals("UNIT")){
            ColorType = 0;
        }
        SankeyPane sankeyPane = new SankeyPane(map, array, ColorType);
        Scene scene = new Scene(sankeyPane, 600, 500);
        Stage sankeyStage = new Stage();
        sankeyStage.setMinWidth(450);
        sankeyStage.setMinHeight(450);
        sankeyStage.setTitle(title);
        sankeyStage.setScene(scene);
        sankeyStage.show();
    }

//    Get the value of the data for Sankey diagram from the file
    public static Map<String, Integer> getValue(String fileName){
        File file = new File(fileName);
        Map<String, Integer> map = new HashMap<>();

        if (!file.exists()){
            System.out.println("This file does not exist!");
            System.exit(0);
        }

        try{
            Scanner input = new Scanner(file);
            String title = input.nextLine();
            String sumWord = input.nextLine();
            int sum = 0;
            while (input.hasNextLine()){
                String[] line = input.nextLine().split(" ");
                int n = line.length;
                String tempWord = "";
                for (int i = 0; i < n - 1; i++){
                    if (i == n - 2){
                        tempWord += line[i];
                    }
                    else {
                        tempWord += line[i];
                        tempWord += " ";
                    }
                }
                map.put(tempWord, Integer.parseInt(line[n - 1]));
                sum += Integer.parseInt(line[n - 1]);
            }
            map.put(sumWord, sum);

        }
        catch (IOException ioe){
            System.out.println(ioe.getMessage());
        }
        return map;
    }

//    Get the order of the data for Sankey diagram from the file
    public static String[] getOrder(String fileName, int arrayLength){
        File file = new File(fileName);
        String[] order = new String[arrayLength];

        if (!file.exists()){
            System.out.println("This file does not exist!");
            System.exit(0);
        }

        try {
            Scanner input = new Scanner(file);
            String title = input.nextLine();
            order[0] = input.nextLine();
            while (input.hasNextLine()){
                for (int i = 1; i < arrayLength; i++){
                    String[] line = input.nextLine().split(" ");
                    int n = line.length;
                    String tempWord = "";
                    for (int j = 0; j < n - 1; j++){
                        if (j == n - 2){
                            tempWord += line[j];
                        }
                        else {
                            tempWord += line[j];
                            tempWord += " ";
                        }
                    }
                    order[i] = tempWord;
                }
            }
        }
        catch (IOException ioe){
            System.out.println(ioe.getMessage());
        }
        return order;
    }

//    Get the title for Sankey diagram from the file
    public static String getTitle(String fileName){
        File file = new File(fileName);
        String title = "";

        if (!file.exists()){
            System.out.println("This file does not exist!");
            System.exit(0);
        }

        try{
            Scanner input = new Scanner(file);
            title = input.nextLine();
        }
        catch (IOException ioe){
            System.out.println(ioe.getMessage());
        }
        return title;
    }
}
class SankeyPane extends Pane{
    //map gets the value of file
    Map<String, Integer> map;
    //array gets the order of the file
    String[] array;
    //mapSize counts how many key-value pairs in map
    int mapSize;
    //colorType gets the value of colorType
    //value 1 means REGULAR(show Sankey diagram in the regular way)
    //value 2 means RANDOM(show Sankey diagram in random colors)
    //value 0 means UNIT(show Sankey diagram with branches in the same color)
    int colorType;
    //randomColor will random a index value for the colorList
    int randomColor;

//    Here are two constructors
    public SankeyPane(Map<String, Integer> map, String[] array) {
        this.map = map;
        this.array = array;
        mapSize = map.size();
        colorType = 1;
        randomColor = 1 + (int)(Math.random() * 84);
    }
    public SankeyPane(Map<String, Integer> map, String[] array, int colorType) {
        this.map = map;
        this.array = array;
        mapSize = map.size();
        this.colorType = colorType ;
        randomColor = 1 + (int)(Math.random() * 84);
    }

//    The method paint will visualise data in file in a Sankey diagram
//    This method will be invoked in the method getWeight and the method getHeight to realize resizing

    public void paint(){
        getChildren().clear();

//        For beauty, here are some colors chosen to the list named colorList
//        All colors will be chosen in the colorList
        Color color1 = Color.rgb(104, 167, 210);
        Color color2 = Color.rgb(195, 86, 255);
        Color color3 = Color.rgb(193, 215, 236);
        Color color4 = Color.rgb(224, 80, 28);
        Color color5 = Color.rgb(250, 133, 63);
        Color color6 = Color.rgb(252, 167, 103);
        Color color7 = Color.rgb(252, 204, 156);
        Color color8 = Color.rgb(228, 176, 255);
        Color color9 = Color.rgb(205, 217, 225);
        Color color10 = Color.rgb(241, 173, 140);
        Color color11 = Color.rgb(253, 199, 163);
        Color color12 = Color.rgb(253, 215, 183);
        Color color13 = Color.rgb(138, 234, 135);
        Color color14 = Color.web("#A8FEAB");
        Color color15 = Color.web("#C4FE9D");
        Color color16 = Color.web("#E0FD8E");
        Color color17 = Color.web("#FCFC7F");
        Color color18 = Color.web("#2F9969");
        Color color19 = Color.web("#52B785");
        Color color20 = Color.web("#8AD4AC");
        Color color21 = Color.web("#C0F1D3");
        Color color22 = Color.web("#23965D");
        Color color23 = Color.web("#43BB73");
        Color color24 = Color.web("#81D5A2");
        Color color25 = Color.web("#C1E7CD");
        Color color26 = Color.web("#4D982E");
        Color color27 = Color.web("#70B643");
        Color color28 = Color.web("#A2D370");
        Color color29 = Color.web("#C1E7CD");
        Color color30 = Color.web("#617C52");
        Color color31 = Color.web("#889F64");
        Color color32 = Color.web("#B0C277");
        Color color33 = Color.web("#D7E589");
        Color color34 = Color.web("#1B4242");
        Color color35 = Color.web("#5C8374");
        Color color36 = Color.web("#9EC8B9");
        Color color37 = Color.web("#B5E6D4");
        Color color38 = Color.web("#85656D");
        Color color39 = Color.web("#AC8C93");
        Color color40 = Color.web("#B8ADAF");
        Color color41 = Color.web("#D1C5C6");
        Color color42 = Color.web("#A24F47");
        Color color43 = Color.web("#BD968F");
        Color color44 = Color.web("#ECCAB7");
        Color color45 = Color.web("#C9BCB9");
        Color color46 = Color.web("#8C85A9");
        Color color47 = Color.web("#B29EBC");
        Color color48 = Color.web("#D6B9D0");
        Color color49 = Color.web("#F8D6E5");
        Color color50 = Color.web("#91A3BB");
        Color color51 = Color.web("#A0C1D4");
        Color color52 = Color.web("#B4CFE2");
        Color color53 = Color.web("#DAD8E5");
        Color color54 = Color.web("#3C79B4");
        Color color55 = Color.web("#78A3CC");
        Color color56 = Color.web("#B3CDE4");
        Color color57 = Color.web("#EEF7FC");
        Color color58 = Color.web("#3163EB");
        Color color59 = Color.web("#5882F8");
        Color color60 = Color.web("#84A1F9");
        Color color61 = Color.web("#ADBFFB");
        Color color62 = Color.web("#A97C26");
        Color color63 = Color.web("#D29C2F");
        Color color64 = Color.web("#F2BE38");
        Color color65 = Color.web("#F5DF7A");
        Color color66 = Color.web("#C65323");
        Color color67 = Color.web("#E96B3D");
        Color color68 = Color.web("#F9885E");
        Color color69 = Color.web("#FAB590");
        Color color70 = Color.web("#D0241C");
        Color color71 = Color.web("#F54D40");
        Color color72 = Color.web("#F9877D");
        Color color73 = Color.web("#F7B3AC");
        Color color74 = Color.web("#D98777");
        Color color75 = Color.web("#E3A995");
        Color color76 = Color.web("#ECCAB7");
        Color color77 = Color.web("#F7EBDB");
        Color color78 = Color.web("#AE4AD9");
        Color color79 = Color.web("#D664F8");
        Color color80 = Color.web("#E48CF9");
        Color color81 = Color.web("#F0B5FF");
        Color color82 = Color.web("#FF007D");
        Color color83 = Color.web("#FF529A");
        Color color84 = Color.web("#FF7AAD");
        Color color85 = Color.web("#FFA3C3");

        List<Color> colorList = new ArrayList<>();
        colorList.addAll(Arrays.asList(color1, color2, color8, color3, color9, color4, color10, color5, color11, color6, color12, color7, color13,
                color14, color15, color16, color17, color18, color19, color20, color21, color22, color23, color24, color25, color26, color27, color28,
                color29, color30, color31, color32, color33, color34, color35, color36, color37, color38, color39, color40, color41, color42, color43,
                color44, color45, color46, color47, color48, color49, color50, color51, color52, color53, color54, color55, color56, color57, color58,
                color59, color60, color61, color62, color63, color64, color65, color66, color67, color68, color69, color70, color71, color72, color73,
                color74, color75, color76, color77, color78, color79, color80, color81, color82, color83, color84, color85));

//        Here are some preparations for different color types
//        For colorType : RANDOM, will firstly count how many groups can be used for diagram
//        Then random a group to get colors
//        For colorType : REGULAR, will choose the first group
//        For colorType : UNIT, will choose a color in the colorList directly
        int colorsNeed = (mapSize - 1) * 2;
        int group = 84 / colorsNeed;
        while (randomColor > group * colorsNeed){
            randomColor = 1 + (int) (Math.random() * 84);
        }
        int temp = (randomColor - 1) % colorsNeed;
        int k = (randomColor - 1 - temp) / colorsNeed;
        if (colorType == 1){
            k = 0;
        }


        // Initialization a font for beauty
        Font font1 = Font.font("Arial", FontWeight.BOLD, 20.0);

        double midy = getHeight() / 2;
        double rectangleWidth = getWidth() / 32;
        double scale = map.get(array[0]) / getHeight() * 5000 / 2025;
        //gap means the length between each branch
        double gap = 0;
        //if only one branch gap will be zero
        if (mapSize == 2){
            gap = 0;
        }
        else {
            gap = map.get(array[0]) / (mapSize - 2) / scale;
        }

//        Calculate for middle branch to define the values for the left rectangle
        int sum = 0;
        int midIndex = 0;
        double midHeight = 0;
        for (int i = 1; i < mapSize; i++){
            sum += map.get(array[i]);
            if (sum >= map.get(array[0]) / 2){
                midIndex = i;
                midHeight = map.get(array[i]) / scale;
                break;
            }
        }

//        The values for the left rectangle
        double x1 = getWidth() / 16 * 5;
        double y1 = midy - midHeight / 2;
        for (int i = 1; i < midIndex; i++){
            y1 -= (double) map.get(array[i]) / scale;
        }

//        The values for the first branch rectangle
        double x2 = getWidth() / 64 * 45;
        double y2 = midy - midHeight / 2;
        for (int i = 1; i < midIndex; i++){
            y2 -= map.get(array[i]) / scale;
            y2 -= gap;
        }

        //record the first value of y of first branch rectangle for painting branches and texts
        double y2Start = y2;

//        Paint the branch rectangles
        for (int i = 1; i < mapSize; i++){
            double height = map.get(array[i]) / scale;
            sum += map.get(array[i]);
            Rectangle r2 = new Rectangle(x2, y2, rectangleWidth,  height - 1);
            r2.setStroke(colorList.get(((2 * i - 1 + colorsNeed * k) * Math.abs(colorType) + (-1) * (Math.abs(colorType) - 1) * randomColor)));
            r2.setFill(colorList.get(((2 * i - 1 + colorsNeed * k) * Math.abs(colorType) + (-1) * (Math.abs(colorType) - 1) * randomColor)));
            getChildren().add(r2);
            if (i > 1){
                Line l1 = new Line(x2, y2, x2 + rectangleWidth, y2);
                l1.setStroke(Color.BLACK);
                getChildren().add(l1);
            }
            y2 += height;
            if (i < mapSize - 1){
                Line l1 = new Line(x2, y2, x2 + rectangleWidth, y2);
                l1.setStroke(Color.BLACK);
                getChildren().add(l1);
            }
            y2 += gap;
        }

//        Paint the left rectangle
        double height1 = map.get(array[0]) / scale;
        Rectangle r1 = new Rectangle(x1, y1, rectangleWidth, height1 - 1);
        r1.setStroke(color1);
        r1.setFill(color1);
        getChildren().add(r1);

//        Set the text of the left rectangle
        VBox vBox1 = new VBox();
        vBox1.setAlignment(Pos.CENTER_RIGHT);
        double vBox1Width = getWidth() / 64 * 19;
        vBox1.setPrefWidth(vBox1Width);
        Text text1 = new Text(x1 / 3, y1, array[0] + ": " + map.get(array[0]));
        text1.setFill(Color.BLACK);
        text1.setFont(font1);
        vBox1.setMargin(text1, new Insets(height1 / 2 + y1 - 10, 0,0 , 0));
        vBox1.getChildren().add(text1);
        getChildren().add(vBox1);

//        Paint the branches
        x1 += rectangleWidth;
        y2 = y2Start;
        for (int i = 1; i < mapSize; i++){
            double height = map.get(array[i]) / scale;
            for (int j = 1; j <= height; j++){
                CubicCurve c1 = new CubicCurve();
                c1.setStartX(x1);
                c1.setStartY(y1);
                c1.setEndX(x2);
                c1.setEndY(y2);
                c1.setControlX1(x1 + (x2 - x1) / 4);
                c1.setControlY1(y1);
                c1.setControlX2(x2 - (x2 - x1) / 4);
                c1.setControlY2(y2);
                c1.setFill(Color.TRANSPARENT);
                c1.setStroke(colorList.get((2 * i + colorsNeed * k) * Math.abs(colorType) + (-1) * (Math.abs(colorType) - 1) * randomColor));
                getChildren().add(c1);
                y1++;
                y2++;
            }
            y2 += gap;
        }

//        Set texts for the branch rectangles
        y2 = y2Start;
        double vBox2Width = getWidth() / 64 * 43;
        VBox vBox2 = new VBox();
        vBox2.setAlignment(Pos.CENTER_RIGHT);
        vBox2.setPrefWidth(vBox2Width);
        for (int i = 1; i < mapSize; i++){
            double heightNow = map.get(array[i]) / scale;
            double heightPrevious = map.get(array[i - 1]) / scale;
            if (i == 1){
                Text text2 = new Text(array[i] + ": " + map.get(array[i]));
                text2.setFill(Color.BLACK);
                text2.setFont(font1);
                vBox2.setMargin(text2, new Insets(heightNow / 2 + y2 - 10, 0,0 , 0));
                vBox2.getChildren().add(text2);
            }
            else {
                Text text2 = new Text(array[i] + ": " + map.get(array[i]));
                text2.setFill(Color.BLACK);
                text2.setFont(font1);
                vBox2.setMargin(text2, new Insets((heightNow + heightPrevious) / 2 + gap - 25, 0,0 , 0));
                vBox2.getChildren().add(text2);
            }
        }
        getChildren().add(vBox2);
    }

//    Override setWidth to invoke the method paint to realize resizing
    @Override
    public void setWidth(double width){
        super.setWidth(width);
        paint();
    }
//    Override setHeight to invoke the method paint to realize resizing
    @Override
    public void setHeight(double height){
        super.setHeight(height);
        paint();
    }
}

  • 22
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值