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    * 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.setImage(ViewProperties.getHdfIcon());
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}