    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 COPYING file, which can be found  *
    009 * at the root of the source code distribution tree,                         *
    010 * or in https://www.hdfgroup.org/licenses.                                  *
    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 or image data. There are two types of
    046 * chart, histogram and line plot.
    047 *
    048 * @author Jordan T. Henderson
    049 * @version 2.4 2/27/2016
    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    * Constructs a new ChartView given data and data ranges.
    117    *
    118    * @param parent
    119    *            the parent of this dialog.
    120    * @param title
    121    *            the title of this dialog.
    122    * @param style
    123    *            the style of the chart. Valid values are: HISTOGRAM and LINE
    124    * @param data
    125    *            the two dimensional data array: data[linenumber][datapoints]
    126    * @param xData
    127    *            the range of the X values, xRange[0]=xmin, xRange[1]=xmax.
    128    * @param yRange
    129    *            the range of the Y values, yRange[0]=ymin, yRange[1]=ymax.
    130    */
    131    public Chart(Shell parent, String title, int style, double[][] data, double[] xData, double[] yRange) {
    132        super(parent, style);
    133
    134        if (data == null)
    135            return;
    136
    137        this.windowTitle = title;
    138
    139        try {
    140            curFont = new Font(
    141                    Display.getCurrent(),
    142                    ViewProperties.getFontType(),
    143                    ViewProperties.getFontSize(),
    144                    SWT.NORMAL);
    145        }
    146        catch (Exception ex) {
    147            curFont = null;
    148        }
    149
    150        format = new java.text.DecimalFormat("0.00E0");
    151        this.chartStyle = style;
    152        this.data = data;
    153
    154        if (style == HISTOGRAM) {
    155            isInteger = true;
    156            barColor = new Color(Display.getDefault(), new RGB(0, 0, 255));
    157        }
    158        else {
    159            isInteger = false;
    160        }
    161
    162        if (xData != null) {
    163            int len = xData.length;
    164            if (len == 2) {
    165                this.xmin = xData[0];
    166                this.xmax = xData[1];
    167            }
    168            else {
    169                this.xData = xData;
    170                xmin = xmax = xData[0];
    171                for (int i = 0; i < len; i++) {
    172                    if (xData[i] < xmin)
    173                        xmin = xData[i];
    174
    175                    if (xData[i] > xmax)
    176                        xmax = xData[i];
    177                }
    178            }
    179        }
    180        else {
    181            this.xmin = 1;
    182            this.xmax = data[0].length;
    183        }
    184
    185        this.numberOfLines = Array.getLength(data);
    186        this.numberOfPoints = Array.getLength(data[0]);
    187        this.lineColors = LINE_COLORS;
    188
    189        if (yRange != null) {
    190            // data range is given
    191            this.ymin = yRange[0];
    192            this.ymax = yRange[1];
    193        }
    194        else {
    195            // search data range from the data
    196            findDataRange();
    197        }
    198
    199        if ((ymax < 0.0001) || (ymax > 100000))
    200            format = new java.text.DecimalFormat("###.####E0#");
    201    }
    202
    203    /** Show the Chart dialog. */
    204    public void open() {
    205        Shell parent = getParent();
    206        shell = new Shell(parent, SWT.SHELL_TRIM);
    207        shell.setFont(curFont);
    208        shell.setText(windowTitle);
    209        shell.setImages(ViewProperties.getHdfIcons());
    210        shell.setLayout(new GridLayout(1, true));
    211
    212        if (chartStyle == HISTOGRAM)
    213            shell.setMenuBar(createMenuBar(shell));
    214
    215        shell.addDisposeListener(new DisposeListener() {
    216            public void widgetDisposed(DisposeEvent e) {
    217                if (curFont != null)
    218                    curFont.dispose();
    219                if (barColor != null)
    220                    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        ymin = ymax = data[0][0];
    326        for (int i = 0; i < numberOfLines; i++) {
    327            for (int j = 0; j < numberOfPoints; j++) {
    328                if (data[i][j] < ymin)
    329                    ymin = data[i][j];
    330
    331                if (data[i][j] > ymax)
    332                    ymax = data[i][j];
    333            }
    334        }
    335    }
    336
    337    /** The canvas that paints the data lines. */
    338    private class ChartCanvas extends Canvas
    339    {
    340        // Value controlling gap between the sides of the canvas
    341        // and the drawn elements
    342        private static final int GAP = 10;
    343
    344        // Values controlling the dimensions of the legend for
    345        // line plots, as well as the gap in between each
    346        // element displayed in the legend
    347        private int legendWidth;
    348        private int legendHeight;
    349
    350        private static final int LEGEND_LINE_WIDTH = 10;
    351        private static final int LEGEND_LINE_GAP = 30;
    352
    353        public ChartCanvas(Composite parent, int style) {
    354            super(parent, style);
    355
    356            // Only draw the legend if the Chart type is a line plot
    357            if ((chartStyle == LINEPLOT) && (lineLabels != null)) {
    358                legendWidth = 60;
    359                legendHeight = (2 * LEGEND_LINE_GAP) + (numberOfLines * LEGEND_LINE_GAP);
    360            }
    361
    362            this.addPaintListener(new PaintListener() {
    363                public void paintControl(PaintEvent e) {
    364                    if (numberOfLines <= 0)
    365                        return;
    366
    367                    // Get the graphics context for this paint event
    368                    GC g = e.gc;
    369
    370                    g.setFont(curFont);
    371
    372                    Rectangle canvasBounds = getClientArea();
    373                    Color c = g.getForeground();
    374
    375                    // Calculate maximum width needed to draw the y-axis labels
    376                    int maxYLabelWidth = g.stringExtent(String.valueOf(ymax)).x;
    377
    378                    // Calculate maximum height needed to draw the x-axis labels
    379                    int maxXLabelHeight = g.stringExtent(String.valueOf(xmax)).y;
    380
    381                    // Make sure legend width scales with font size and large column values
    382                    if (lineLabels != null) {
    383                        for (int i = 0; i < lineLabels.length; i++) {
    384                            int width = g.stringExtent(lineLabels[i]).x;
    385                            if (width > (2 * legendWidth / 3) - 10)
    386                                legendWidth += width;
    387                        }
    388                    }
    389
    390                    int xgap = maxYLabelWidth + GAP;
    391                    int ygap = canvasBounds.height - maxXLabelHeight - GAP - 1;
    392                    int plotHeight = ygap - GAP;
    393                    int plotWidth = canvasBounds.width - legendWidth - (2 * GAP) - xgap;
    394                    int xnpoints = Math.min(10, numberOfPoints - 1);
    395                    int ynpoints = 10;
    396
    397                    // draw the X axis
    398                    g.drawLine(xgap, ygap, xgap + plotWidth, ygap);
    399
    400                    // draw the Y axis
    401                    g.drawLine(xgap, ygap, xgap, GAP);
    402
    403                    // draw x labels
    404                    double xp = 0;
    405                    double x = xmin;
    406                    double dw = (double) plotWidth / (double) xnpoints;
    407                    double dx = (xmax - xmin) / xnpoints;
    408                    boolean gtOne = (dx >= 1);
    409                    for (int i = 0; i <= xnpoints; i++) {
    410                        x = xmin + i * dx;
    411                        xp = xgap + i * dw;
    412
    413                        // Draw a tick mark
    414                        g.drawLine((int) xp, ygap, (int) xp, ygap - 5);
    415
    416                        if (gtOne) {
    417                            String value = String.valueOf((int) x);
    418                            Point numberSize = g.stringExtent(value);
    419                            g.drawString(value, (int) xp - (numberSize.x / 2), canvasBounds.height - numberSize.y);
    420                        }
    421                        else {
    422                            String value = String.valueOf(x);
    423                            Point numberSize = g.stringExtent(value);
    424                            g.drawString(value, (int) xp - (numberSize.x / 2), canvasBounds.height - numberSize.y);
    425                        }
    426                    }
    427
    428                    // draw y labels
    429                    double yp = 0;
    430                    double y = ymin;
    431                    double dh = (double) plotHeight / (double) ynpoints;
    432                    double dy = (ymax - ymin) / (ynpoints);
    433                    if (dy > 1)
    434                        dy = Math.round(dy * 10.0) / 10.0;
    435                    for (int i = 0; i <= ynpoints; i++) {
    436                        yp = i * dh;
    437                        y = i * dy + ymin;
    438
    439                        // Draw a tick mark
    440                        g.drawLine(xgap, ygap - (int) yp, xgap + 5, ygap - (int) yp);
    441
    442                        if (isInteger) {
    443                            String value = String.valueOf((int) y);
    444                            Point numberSize = g.stringExtent(value);
    445                            g.drawString(value, 0, ygap - (int) yp - (numberSize.y / 2));
    446                        }
    447                        else {
    448                            String value = format.format(y);
    449                            Point numberSize = g.stringExtent(value);
    450                            g.drawString(value, 0, ygap - (int) yp - (numberSize.y / 2));
    451                        }
    452                    }
    453
    454                    double x0;
    455                    double y0;
    456                    double x1;
    457                    double y1;
    458                    if (chartStyle == LINEPLOT) {
    459                        dw = (double) plotWidth / (double) (numberOfPoints - 1);
    460
    461                        // use y = a + b* x to calculate pixel positions
    462                        double b = plotHeight / (ymin - ymax);
    463                        double a = -b * ymax + GAP;
    464                        boolean hasXdata = ((xData != null) && (xData.length >= numberOfPoints));
    465                        double xRatio = (1 / (xmax - xmin)) * plotWidth;
    466                        double xD = (xmin / (xmax - xmin)) * plotWidth;
    467
    468                        // draw lines for selected spreadsheet columns
    469                        for (int i = 0; i < numberOfLines; i++) {
    470                            // Display each line with a unique color for clarity
    471                            if ((lineColors != null) && (lineColors.length >= numberOfLines))
    472                                g.setForeground(Display.getCurrent().getSystemColor(lineColors[i]));
    473
    474                            // set up the line data for drawing one line a time
    475                            if (hasXdata)
    476                                x0 = xgap + xData[0] * xRatio - xD;
    477                            else
    478                                x0 = xgap;
    479                            y0 = a + b * data[i][0];
    480
    481                            for (int j = 1; j < numberOfPoints; j++) {
    482                                if (hasXdata)
    483                                    x1 = xgap + xData[j] * xRatio - xD;
    484                                else
    485                                    x1 = xgap + j * dw;
    486
    487                                y1 = a + b * data[i][j];
    488                                g.drawLine((int) x0, (int) y0, (int) x1, (int) y1);
    489
    490                                x0 = x1;
    491                                y0 = y1;
    492                            }
    493
    494                            // draw line legend
    495                            if ((lineLabels != null) && (lineLabels.length >= numberOfLines)) {
    496                                x0 = (canvasBounds.width - GAP - legendWidth) + ((double) legendWidth / 3);
    497                                y0 = GAP + (double) LEGEND_LINE_GAP * (i + 1);
    498                                g.drawLine((int) x0, (int) y0, (int) x0 + LEGEND_LINE_WIDTH, (int) y0);
    499                                g.drawString(lineLabels[i], (int) x0 + 10, (int) y0 + 3);
    500                            }
    501                        }
    502
    503                        // draw a box on the legend
    504                        if ((lineLabels != null) && (lineLabels.length >= numberOfLines)) {
    505                            g.setForeground(Display.getCurrent().getSystemColor(SWT.COLOR_BLACK));
    506                            g.drawRectangle(canvasBounds.width - legendWidth - GAP, GAP, legendWidth, legendHeight);
    507                        }
    508
    509                        g.setForeground(c); // set the color back to its default
    510                    } //  (chartStyle == LINEPLOT)
    511                    else if (chartStyle == HISTOGRAM) {
    512                        // draw histogram for selected image area
    513                        xp = xgap;
    514                        int barHeight = 0;
    515                        g.setBackground(barColor);
    516                        int barWidth = plotWidth / numberOfPoints;
    517                        if (barWidth <= 0)
    518                            barWidth = 1;
    519                        dw = (double) plotWidth / (double) numberOfPoints;
    520                        for (int j = 0; j < numberOfPoints; j++) {
    521                            xp = xgap + j * dw;
    522                            barHeight = (int) (data[0][j] * (plotHeight / (ymax - ymin)));
    523                            g.fillRectangle((int) xp, ygap - barHeight, barWidth, barHeight);
    524                        }
    525
    526                        g.setBackground(c); // set the color back to its default
    527                    } // (chartStyle == HISTOGRAM)
    528                }
    529            });
    530        }
    531    }
    532}