    001/*****************************************************************************
    002 * Copyright by The HDF Group.                                               *
    003 * Copyright by the Board of Trustees of the University of Illinois.         *
    004 * All rights reserved.                                                      *
    005 *                                                                           *
    006 * This file is part of the HDF Java Products distribution.                  *
    007 * The full copyright notice, including terms governing use, modification,   *
    008 * and redistribution, is contained in the files COPYING and Copyright.html. *
    009 * COPYING can be found at the root of the source code distribution tree.    *
    010 * Or, see https://support.hdfgroup.org/products/licenses.html               *
    011 * If you do not have access to either file, you may request a copy from     *
    012 * help@hdfgroup.org.                                                        *
    013 ****************************************************************************/
    014
    015package hdf.view;
    016
    017import java.lang.reflect.Array;
    018
    019import org.eclipse.swt.SWT;
    020import org.eclipse.swt.events.DisposeEvent;
    021import org.eclipse.swt.events.DisposeListener;
    022import org.eclipse.swt.events.PaintEvent;
    023import org.eclipse.swt.events.PaintListener;
    024import org.eclipse.swt.events.SelectionAdapter;
    025import org.eclipse.swt.events.SelectionEvent;
    026import org.eclipse.swt.graphics.Color;
    027import org.eclipse.swt.graphics.Font;
    028import org.eclipse.swt.graphics.GC;
    029import org.eclipse.swt.graphics.Point;
    030import org.eclipse.swt.graphics.RGB;
    031import org.eclipse.swt.graphics.Rectangle;
    032import org.eclipse.swt.layout.GridData;
    033import org.eclipse.swt.layout.GridLayout;
    034import org.eclipse.swt.widgets.Button;
    035import org.eclipse.swt.widgets.Canvas;
    036import org.eclipse.swt.widgets.ColorDialog;
    037import org.eclipse.swt.widgets.Composite;
    038import org.eclipse.swt.widgets.Dialog;
    039import org.eclipse.swt.widgets.Display;
    040import org.eclipse.swt.widgets.Menu;
    041import org.eclipse.swt.widgets.MenuItem;
    042import org.eclipse.swt.widgets.Shell;
    043
    044/**
    045 * ChartView displays a histogram/line chart of selected row/column of table data
    046 * or image data. There are two types of chart, histogram and line plot.
    047 *
    048 * @author Jordan T. Henderson
    049 * @version 2.4 2/27/16
    050 */
    051public class Chart extends Dialog {
    052
    053    private Shell                       shell;
    054
    055    private Font                        curFont;
    056
    057    private String                      windowTitle;
    058
    059    private Color                       barColor;
    060
    061    /** histogram style chart */
    062    public static final int             HISTOGRAM = 0;
    063
    064    /** line style chart */
    065    public static final int             LINEPLOT = 1;
    066
    067    /** The default colors of lines for selected columns */
    068    public static final int[]           LINE_COLORS = { SWT.COLOR_BLACK, SWT.COLOR_RED,
    069            SWT.COLOR_DARK_GREEN, SWT.COLOR_BLUE, SWT.COLOR_MAGENTA, /*Pink*/
    070            SWT.COLOR_YELLOW, /*Orange*/ SWT.COLOR_GRAY, SWT.COLOR_CYAN };
    071
    072    /** the data values of line points or histogram */
    073    protected double                    data[][];
    074
    075    /** Panel that draws plot of data values. */
    076    protected ChartCanvas               chartP;
    077
    078    /** number of data points */
    079    protected int                       numberOfPoints;
    080
    081    /** the style of chart: histogram or line */
    082    private int                         chartStyle;
    083
    084    /** the maximum value of the Y axis */
    085    private double                      ymax;
    086
    087    /** the minimum value of the Y axis */
    088    private double                      ymin;
    089
    090    /** the maximum value of the X axis */
    091    private double                      xmax;
    092
    093    /** the minimum value of the X axis */
    094    private double                      xmin;
    095
    096    /** line labels */
    097    private String[]                    lineLabels;
    098
    099    /** line colors */
    100    private int[]                       lineColors;
    101
    102    /** number of lines */
    103    private int                         numberOfLines;
    104
    105    /** the data to plot against **/
    106    private double[]                    xData = null;
    107
    108    /**
    109    * True if the original data is integer (byte, short, integer, long).
    110    */
    111    private boolean                     isInteger;
    112
    113    private java.text.DecimalFormat     format;
    114
    115
    116    /**
    117    * Constructs a new ChartView given data and data ranges.
    118    *
    119    * @param parent
    120    *            the parent of this dialog.
    121    * @param title
    122    *            the title of this dialog.
    123    * @param style
    124    *            the style of the chart. Valid values are: HISTOGRAM and LINE
    125    * @param data
    126    *            the two dimensional data array: data[linenumber][datapoints]
    127    * @param xData
    128    *            the range of the X values, xRange[0]=xmin, xRange[1]=xmax.
    129    * @param yRange
    130    *            the range of the Y values, yRange[0]=ymin, yRange[1]=ymax.
    131    */
    132    public Chart(Shell parent, String title, int style, double[][] data, double[] xData, double[] yRange) {
    133        super(parent, style);
    134
    135        if (data == null) {
    136            return;
    137        }
    138
    139        this.windowTitle = title;
    140
    141        try {
    142            curFont = new Font(
    143                    Display.getCurrent(),
    144                    ViewProperties.getFontType(),
    145                    ViewProperties.getFontSize(),
    146                    SWT.NORMAL);
    147        } catch (Exception ex) {
    148            curFont = null;
    149        }
    150
    151        format = new java.text.DecimalFormat("0.00E0");
    152        this.chartStyle = style;
    153        this.data = data;
    154
    155        if (style == HISTOGRAM) {
    156            isInteger = true;
    157            barColor = new Color(Display.getDefault(), new RGB(0, 0, 255));
    158        }
    159        else {
    160            isInteger = false;
    161        }
    162
    163        if (xData != null) {
    164            int len = xData.length;
    165            if (len == 2) {
    166                this.xmin = xData[0];
    167                this.xmax = xData[1];
    168            }
    169            else {
    170                this.xData = xData;
    171                xmin = xmax = xData[0];
    172                for (int i = 0; i < len; i++) {
    173                    if (xData[i] < xmin) {
    174                        xmin = xData[i];
    175                    }
    176
    177                    if (xData[i] > xmax) {
    178                        xmax = xData[i];
    179                    }
    180                }
    181            }
    182        }
    183        else {
    184            this.xmin = 1;
    185            this.xmax = data[0].length;
    186        }
    187
    188        this.numberOfLines = Array.getLength(data);
    189        this.numberOfPoints = Array.getLength(data[0]);
    190        this.lineColors = LINE_COLORS;
    191
    192        if (yRange != null) {
    193            // data range is given
    194            this.ymin = yRange[0];
    195            this.ymax = yRange[1];
    196        }
    197        else {
    198            // search data range from the data
    199            findDataRange();
    200        }
    201
    202        if ((ymax < 0.0001) || (ymax > 100000)) {
    203            format = new java.text.DecimalFormat("###.####E0#");
    204        }
    205    }
    206
    207    public void open() {
    208        Shell parent = getParent();
    209        shell = new Shell(parent, SWT.SHELL_TRIM);
    210        shell.setFont(curFont);
    211        shell.setText(windowTitle);
    212        shell.setImage(ViewProperties.getHdfIcon());
    213        shell.setLayout(new GridLayout(1, true));
    214
    215        if (chartStyle == HISTOGRAM) shell.setMenuBar(createMenuBar(shell));
    216
    217        shell.addDisposeListener(new DisposeListener() {
    218            public void widgetDisposed(DisposeEvent e) {
    219                if (curFont != null) curFont.dispose();
    220                if (barColor != null) barColor.dispose();
    221            }
    222        });
    223
    224        chartP = new ChartCanvas(shell, SWT.DOUBLE_BUFFERED | SWT.BORDER);
    225        chartP.setBackground(Display.getCurrent().getSystemColor(SWT.COLOR_WHITE));
    226        chartP.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
    227
    228
    229        // Add close button
    230        Composite buttonComposite = new Composite(shell, SWT.NONE);
    231        buttonComposite.setLayout(new GridLayout(1, true));
    232        buttonComposite.setLayoutData(new GridData(SWT.CENTER, SWT.FILL, true, false));
    233
    234        Button closeButton = new Button(buttonComposite, SWT.PUSH);
    235        closeButton.setFont(curFont);
    236        closeButton.setText("   &Close   ");
    237        closeButton.setLayoutData(new GridData(SWT.CENTER, SWT.FILL, false, false));
    238        closeButton.addSelectionListener(new SelectionAdapter() {
    239            public void widgetSelected(SelectionEvent e) {
    240                shell.dispose();
    241            }
    242        });
    243
    244        shell.pack();
    245
    246        int w = 640 + (ViewProperties.getFontSize() - 12) * 15;
    247        int h = 400 + (ViewProperties.getFontSize() - 12) * 10;
    248
    249        shell.setMinimumSize(w, h);
    250
    251        Rectangle parentBounds = parent.getBounds();
    252        Point shellSize = shell.getSize();
    253        shell.setLocation((parentBounds.x + (parentBounds.width / 2)) - (shellSize.x / 2),
    254                (parentBounds.y + (parentBounds.height / 2)) - (shellSize.y / 2));
    255
    256        shell.open();
    257    }
    258
    259    private Menu createMenuBar(Shell parent) {
    260        Menu menu = new Menu(parent, SWT.BAR);
    261
    262        MenuItem item = new MenuItem(menu, SWT.CASCADE);
    263        item.setText("Histogram");
    264
    265        Menu histogramMenu = new Menu(item);
    266        item.setMenu(histogramMenu);
    267
    268        MenuItem setColor = new MenuItem(histogramMenu, SWT.PUSH);
    269        setColor.setText("Change bar color");
    270        setColor.addSelectionListener(new SelectionAdapter() {
    271            public void widgetSelected(SelectionEvent e) {
    272                ColorDialog dialog = new ColorDialog(shell);
    273                dialog.setRGB(barColor.getRGB());
    274                dialog.setText("Select a bar color");
    275
    276                RGB newColor = dialog.open();
    277
    278                if (newColor != null) {
    279                    barColor.dispose();
    280                    barColor = new Color(Display.getDefault(), newColor);
    281                    chartP.redraw();
    282                }
    283            }
    284        });
    285
    286        new MenuItem(histogramMenu, SWT.SEPARATOR);
    287
    288        MenuItem close = new MenuItem(histogramMenu, SWT.PUSH);
    289        close.setText("Close");
    290        close.addSelectionListener(new SelectionAdapter() {
    291            public void widgetSelected(SelectionEvent e) {
    292                shell.dispose();
    293            }
    294        });
    295
    296        return menu;
    297    }
    298
    299    /** Sets the color of each line of a line plot
    300    *
    301    * @param c the list of colors
    302    */
    303    public void setLineColors(int[] c) {
    304        lineColors = c;
    305    }
    306
    307    /** Sets the labels of each line.
    308    *
    309    * @param l the list of line labels
    310    */
    311    public void setLineLabels(String[] l) {
    312        lineLabels = l;
    313    }
    314
    315    /** Sets the data type of the plot data to be integer. */
    316    public void setTypeToInteger() {
    317        isInteger = true;
    318    }
    319
    320    /** Find and set the minimum and maximum values of the data */
    321    private void findDataRange() {
    322        if (data == null) {
    323            return;
    324        }
    325
    326        ymin = ymax = data[0][0];
    327        for (int i = 0; i < numberOfLines; i++) {
    328            for (int j = 0; j < numberOfPoints; j++) {
    329                if (data[i][j] < ymin) {
    330                    ymin = data[i][j];
    331                }
    332
    333                if (data[i][j] > ymax) {
    334                    ymax = data[i][j];
    335                }
    336            }
    337        }
    338    }
    339
    340    /** The canvas that paints the data lines. */
    341    private class ChartCanvas extends Canvas {
    342        // Value controlling gap between the sides of the canvas
    343        // and the drawn elements
    344        private static final int GAP = 10;
    345
    346        // Values controlling the dimensions of the legend for
    347        // line plots, as well as the gap in between each
    348        // element displayed in the legend
    349        private int legendWidth;
    350        private int legendHeight;
    351
    352        private static final int LEGEND_LINE_WIDTH = 10;
    353        private static final int LEGEND_LINE_GAP = 30;
    354
    355        public ChartCanvas(Composite parent, int style) {
    356            super(parent, style);
    357
    358            // Only draw the legend if the Chart type is a line plot
    359            if ((chartStyle == LINEPLOT) && (lineLabels != null)) {
    360                legendWidth = 60;
    361                legendHeight = (2 * LEGEND_LINE_GAP) + (numberOfLines * LEGEND_LINE_GAP);
    362            }
    363
    364            this.addPaintListener(new PaintListener() {
    365                public void paintControl(PaintEvent e) {
    366                    if (numberOfLines <= 0) return;
    367
    368                    // Get the graphics context for this paint event
    369                    GC g = e.gc;
    370
    371                    g.setFont(curFont);
    372
    373                    Rectangle canvasBounds = getClientArea();
    374                    Color c = g.getForeground();
    375
    376                    // Calculate maximum width needed to draw the y-axis labels
    377                    int maxYLabelWidth = g.stringExtent(String.valueOf(ymax)).x;
    378
    379                    // Calculate maximum height needed to draw the x-axis labels
    380                    int maxXLabelHeight = g.stringExtent(String.valueOf(xmax)).y;
    381
    382                    // Make sure legend width scales with font size and large column values
    383                    if (lineLabels != null) {
    384                        for (int i = 0; i < lineLabels.length; i++) {
    385                            int width = g.stringExtent(lineLabels[i]).x;
    386                            if (width > (2 * legendWidth / 3) - 10)
    387                                legendWidth += width;
    388                        }
    389                    }
    390
    391                    int xgap = maxYLabelWidth + GAP;
    392                    int ygap = canvasBounds.height - maxXLabelHeight - GAP - 1;
    393                    int plotHeight = ygap - GAP;
    394                    int plotWidth = canvasBounds.width - legendWidth - (2 * GAP) - xgap;
    395                    int xnpoints = Math.min(10, numberOfPoints - 1);
    396                    int ynpoints = 10;
    397
    398                    // draw the X axis
    399                    g.drawLine(xgap, ygap, xgap + plotWidth, ygap);
    400
    401                    // draw the Y axis
    402                    g.drawLine(xgap, ygap, xgap, GAP);
    403
    404                    // draw x labels
    405                    double xp = 0;
    406                    double x = xmin;
    407                    double dw = (double) plotWidth / (double) xnpoints;
    408                    double dx = (xmax - xmin) / xnpoints;
    409                    boolean gtOne = (dx >= 1);
    410                    for (int i = 0; i <= xnpoints; i++) {
    411                        x = xmin + i * dx;
    412                        xp = xgap + i * dw;
    413
    414                        // Draw a tick mark
    415                        g.drawLine((int) xp, ygap, (int) xp, ygap - 5);
    416
    417                        if (gtOne) {
    418                            String value = String.valueOf((int) x);
    419                            Point numberSize = g.stringExtent(value);
    420                            g.drawString(value, (int) xp - (numberSize.x / 2), canvasBounds.height - numberSize.y);
    421                        }
    422                        else {
    423                            String value = String.valueOf(x);
    424                            Point numberSize = g.stringExtent(value);
    425                            g.drawString(value, (int) xp - (numberSize.x / 2), canvasBounds.height - numberSize.y);
    426                        }
    427                    }
    428
    429                    // draw y labels
    430                    double yp = 0;
    431                    double y = ymin;
    432                    double dh = (double) plotHeight / (double) ynpoints;
    433                    double dy = (ymax - ymin) / (ynpoints);
    434                    if (dy > 1) {
    435                        dy = Math.round(dy * 10.0) / 10.0;
    436                    }
    437                    for (int i = 0; i <= ynpoints; i++) {
    438                        yp = i * dh;
    439                        y = i * dy + ymin;
    440
    441                        // Draw a tick mark
    442                        g.drawLine(xgap, ygap - (int) yp, xgap + 5, ygap - (int) yp);
    443
    444                        if (isInteger) {
    445                            String value = String.valueOf((int) y);
    446                            Point numberSize = g.stringExtent(value);
    447                            g.drawString(value, 0, ygap - (int) yp - (numberSize.y / 2));
    448                        }
    449                        else {
    450                            String value = format.format(y);
    451                            Point numberSize = g.stringExtent(value);
    452                            g.drawString(value, 0, ygap - (int) yp - (numberSize.y / 2));
    453                        }
    454                    }
    455
    456                    double x0;
    457                    double y0;
    458                    double x1;
    459                    double y1;
    460                    if (chartStyle == LINEPLOT) {
    461                        dw = (double) plotWidth / (double) (numberOfPoints - 1);
    462
    463                        // use y = a + b* x to calculate pixel positions
    464                        double b = plotHeight / (ymin - ymax);
    465                        double a = -b * ymax + GAP;
    466                        boolean hasXdata = ((xData != null) && (xData.length >= numberOfPoints));
    467                        double xRatio = (1 / (xmax - xmin)) * plotWidth;
    468                        double xD = (xmin / (xmax - xmin)) * plotWidth;
    469
    470                        // draw lines for selected spreadsheet columns
    471                        for (int i = 0; i < numberOfLines; i++) {
    472                            // Display each line with a unique color for clarity
    473                            if ((lineColors != null) && (lineColors.length >= numberOfLines)) {
    474                                g.setForeground(Display.getCurrent().getSystemColor(lineColors[i]));
    475                            }
    476
    477                            // set up the line data for drawing one line a time
    478                            if (hasXdata) {
    479                                x0 = xgap + xData[0] * xRatio - xD;
    480                            }
    481                            else {
    482                                x0 = xgap;
    483                            }
    484                            y0 = a + b * data[i][0];
    485
    486                            for (int j = 1; j < numberOfPoints; j++) {
    487                                if (hasXdata) {
    488                                    x1 = xgap + xData[j] * xRatio - xD;
    489                                }
    490                                else {
    491                                    x1 = xgap + j * dw;
    492                                }
    493
    494                                y1 = a + b * data[i][j];
    495                                g.drawLine((int) x0, (int) y0, (int) x1, (int) y1);
    496
    497                                x0 = x1;
    498                                y0 = y1;
    499                            }
    500
    501                            // draw line legend
    502                            if ((lineLabels != null) && (lineLabels.length >= numberOfLines)) {
    503                                x0 = (canvasBounds.width - GAP - legendWidth) + ((double) legendWidth / 3);
    504                                y0 = GAP + (double) LEGEND_LINE_GAP * (i + 1);
    505                                g.drawLine((int) x0, (int) y0, (int) x0 + LEGEND_LINE_WIDTH, (int) y0);
    506                                g.drawString(lineLabels[i], (int) x0 + 10, (int) y0 + 3);
    507                            }
    508                        }
    509
    510                        // draw a box on the legend
    511                        if ((lineLabels != null) && (lineLabels.length >= numberOfLines)) {
    512                            g.setForeground(Display.getCurrent().getSystemColor(SWT.COLOR_BLACK));
    513                            g.drawRectangle(canvasBounds.width - legendWidth - GAP, GAP, legendWidth, legendHeight);
    514                        }
    515
    516                        g.setForeground(c); // set the color back to its default
    517                    } //  (chartStyle == LINEPLOT)
    518                    else if (chartStyle == HISTOGRAM) {
    519                        // draw histogram for selected image area
    520                        xp = xgap;
    521                        int barHeight = 0;
    522                        g.setBackground(barColor);
    523                        int barWidth = plotWidth / numberOfPoints;
    524                        if (barWidth <= 0) {
    525                            barWidth = 1;
    526                        }
    527                        dw = (double) plotWidth / (double) numberOfPoints;
    528                        for (int j = 0; j < numberOfPoints; j++) {
    529                            xp = xgap + j * dw;
    530                            barHeight = (int) (data[0][j] * (plotHeight / (ymax - ymin)));
    531                            g.fillRectangle((int) xp, ygap - barHeight, barWidth, barHeight);
    532                        }
    533
    534                        g.setBackground(c); // set the color back to its default
    535                    } // (chartStyle == HISTOGRAM)
    536                }
    537            });
    538        }
    539    }
    540}