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}