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
046 * types of 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    private Shell shell;
053
054    private Font curFont;
055
056    private String windowTitle;
057
058    private Color barColor;
059
060    /** histogram style chart */
061    public static final int HISTOGRAM = 0;
062
063    /** line style chart */
064    public static final int LINEPLOT = 1;
065
066    /** The default colors of lines for selected columns */
067    public static final int[] LINE_COLORS = {SWT.COLOR_BLACK,      SWT.COLOR_RED,
068                                             SWT.COLOR_DARK_GREEN, SWT.COLOR_BLUE,
069                                             SWT.COLOR_MAGENTA, /*Pink*/
070                                             SWT.COLOR_YELLOW,     /*Orange*/ SWT.COLOR_GRAY,
071                                             SWT.COLOR_CYAN};
072
073    /** the data values of line points or histogram */
074    protected double data[][];
075
076    /** Panel that draws plot of data values. */
077    protected ChartCanvas chartP;
078
079    /** number of data points */
080    protected int numberOfPoints;
081
082    /** the style of chart: histogram or line */
083    private int chartStyle;
084
085    /** the maximum value of the Y axis */
086    private double ymax;
087
088    /** the minimum value of the Y axis */
089    private double ymin;
090
091    /** the maximum value of the X axis */
092    private double xmax;
093
094    /** the minimum value of the X axis */
095    private double xmin;
096
097    /** line labels */
098    private String[] lineLabels;
099
100    /** line colors */
101    private int[] lineColors;
102
103    /** number of lines */
104    private int numberOfLines;
105
106    /** the data to plot against **/
107    private double[] xData = null;
108
109    /**
110     * True if the original data is integer (byte, short, integer, long).
111     */
112    private boolean isInteger;
113
114    private java.text.DecimalFormat format;
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    {
134        super(parent, style);
135
136        if (data == null)
137            return;
138
139        this.windowTitle = title;
140
141        try {
142            curFont = new Font(Display.getCurrent(), ViewProperties.getFontType(),
143                               ViewProperties.getFontSize(), SWT.NORMAL);
144        }
145        catch (Exception ex) {
146            curFont = null;
147        }
148
149        format          = new java.text.DecimalFormat("0.00E0");
150        this.chartStyle = style;
151        this.data       = data;
152
153        if (style == HISTOGRAM) {
154            isInteger = true;
155            barColor  = new Color(Display.getDefault(), new RGB(0, 0, 255));
156        }
157        else {
158            isInteger = false;
159        }
160
161        if (xData != null) {
162            int len = xData.length;
163            if (len == 2) {
164                this.xmin = xData[0];
165                this.xmax = xData[1];
166            }
167            else {
168                this.xData = xData;
169                xmin = xmax = xData[0];
170                for (int i = 0; i < len; i++) {
171                    if (xData[i] < xmin)
172                        xmin = xData[i];
173
174                    if (xData[i] > xmax)
175                        xmax = xData[i];
176                }
177            }
178        }
179        else {
180            this.xmin = 1;
181            this.xmax = data[0].length;
182        }
183
184        this.numberOfLines  = Array.getLength(data);
185        this.numberOfPoints = Array.getLength(data[0]);
186        this.lineColors     = LINE_COLORS;
187
188        if (yRange != null) {
189            // data range is given
190            this.ymin = yRange[0];
191            this.ymax = yRange[1];
192        }
193        else {
194            // search data range from the data
195            findDataRange();
196        }
197
198        if ((ymax < 0.0001) || (ymax > 100000))
199            format = new java.text.DecimalFormat("###.####E0#");
200    }
201
202    /** Show the Chart dialog. */
203    public void open()
204    {
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            {
218                if (curFont != null)
219                    curFont.dispose();
220                if (barColor != null)
221                    barColor.dispose();
222            }
223        });
224
225        chartP = new ChartCanvas(shell, SWT.DOUBLE_BUFFERED | SWT.BORDER);
226        chartP.setBackground(Display.getCurrent().getSystemColor(SWT.COLOR_WHITE));
227        chartP.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
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) { shell.dispose(); }
240        });
241
242        shell.pack();
243
244        int w = 640 + (ViewProperties.getFontSize() - 12) * 15;
245        int h = 400 + (ViewProperties.getFontSize() - 12) * 10;
246
247        shell.setMinimumSize(w, h);
248
249        Rectangle parentBounds = parent.getBounds();
250        Point shellSize        = shell.getSize();
251        shell.setLocation((parentBounds.x + (parentBounds.width / 2)) - (shellSize.x / 2),
252                          (parentBounds.y + (parentBounds.height / 2)) - (shellSize.y / 2));
253
254        shell.open();
255    }
256
257    private Menu createMenuBar(Shell parent)
258    {
259        Menu menu = new Menu(parent, SWT.BAR);
260
261        MenuItem item = new MenuItem(menu, SWT.CASCADE);
262        item.setText("Histogram");
263
264        Menu histogramMenu = new Menu(item);
265        item.setMenu(histogramMenu);
266
267        MenuItem setColor = new MenuItem(histogramMenu, SWT.PUSH);
268        setColor.setText("Change bar color");
269        setColor.addSelectionListener(new SelectionAdapter() {
270            public void widgetSelected(SelectionEvent e)
271            {
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) { shell.dispose(); }
292        });
293
294        return menu;
295    }
296
297    /**
298     * Sets the color of each line of a line plot
299     *
300     * @param c the list of colors
301     */
302    public void setLineColors(int[] c) { lineColors = c; }
303
304    /**
305     * Sets the labels of each line.
306     *
307     * @param l the list of line labels
308     */
309    public void setLineLabels(String[] l) { lineLabels = l; }
310
311    /** Sets the data type of the plot data to be integer. */
312    public void setTypeToInteger() { isInteger = true; }
313
314    /** Find and set the minimum and maximum values of the data */
315    private void findDataRange()
316    {
317        if (data == null)
318            return;
319
320        ymin = ymax = data[0][0];
321        for (int i = 0; i < numberOfLines; i++) {
322            for (int j = 0; j < numberOfPoints; j++) {
323                if (data[i][j] < ymin)
324                    ymin = data[i][j];
325
326                if (data[i][j] > ymax)
327                    ymax = data[i][j];
328            }
329        }
330    }
331
332    /** The canvas that paints the data lines. */
333    private class ChartCanvas extends Canvas {
334        // Value controlling gap between the sides of the canvas
335        // and the drawn elements
336        private static final int GAP = 10;
337
338        // Values controlling the dimensions of the legend for
339        // line plots, as well as the gap in between each
340        // element displayed in the legend
341        private int legendWidth;
342        private int legendHeight;
343
344        private static final int LEGEND_LINE_WIDTH = 10;
345        private static final int LEGEND_LINE_GAP   = 30;
346
347        public ChartCanvas(Composite parent, int style)
348        {
349            super(parent, style);
350
351            // Only draw the legend if the Chart type is a line plot
352            if ((chartStyle == LINEPLOT) && (lineLabels != null)) {
353                legendWidth  = 60;
354                legendHeight = (2 * LEGEND_LINE_GAP) + (numberOfLines * LEGEND_LINE_GAP);
355            }
356
357            this.addPaintListener(new PaintListener() {
358                public void paintControl(PaintEvent e)
359                {
360                    if (numberOfLines <= 0)
361                        return;
362
363                    // Get the graphics context for this paint event
364                    GC g = e.gc;
365
366                    g.setFont(curFont);
367
368                    Rectangle canvasBounds = getClientArea();
369                    Color c                = g.getForeground();
370
371                    // Calculate maximum width needed to draw the y-axis labels
372                    int maxYLabelWidth = g.stringExtent(String.valueOf(ymax)).x;
373
374                    // Calculate maximum height needed to draw the x-axis labels
375                    int maxXLabelHeight = g.stringExtent(String.valueOf(xmax)).y;
376
377                    // Make sure legend width scales with font size and large column values
378                    if (lineLabels != null) {
379                        for (int i = 0; i < lineLabels.length; i++) {
380                            int width = g.stringExtent(lineLabels[i]).x;
381                            if (width > (2 * legendWidth / 3) - 10)
382                                legendWidth += width;
383                        }
384                    }
385
386                    int xgap       = maxYLabelWidth + GAP;
387                    int ygap       = canvasBounds.height - maxXLabelHeight - GAP - 1;
388                    int plotHeight = ygap - GAP;
389                    int plotWidth  = canvasBounds.width - legendWidth - (2 * GAP) - xgap;
390                    int xnpoints   = Math.min(10, numberOfPoints - 1);
391                    int ynpoints   = 10;
392
393                    // draw the X axis
394                    g.drawLine(xgap, ygap, xgap + plotWidth, ygap);
395
396                    // draw the Y axis
397                    g.drawLine(xgap, ygap, xgap, GAP);
398
399                    // draw x labels
400                    double xp     = 0;
401                    double x      = xmin;
402                    double dw     = (double)plotWidth / (double)xnpoints;
403                    double dx     = (xmax - xmin) / xnpoints;
404                    boolean gtOne = (dx >= 1);
405                    for (int i = 0; i <= xnpoints; i++) {
406                        x  = xmin + i * dx;
407                        xp = xgap + i * dw;
408
409                        // Draw a tick mark
410                        g.drawLine((int)xp, ygap, (int)xp, ygap - 5);
411
412                        if (gtOne) {
413                            String value     = String.valueOf((int)x);
414                            Point numberSize = g.stringExtent(value);
415                            g.drawString(value, (int)xp - (numberSize.x / 2),
416                                         canvasBounds.height - numberSize.y);
417                        }
418                        else {
419                            String value     = String.valueOf(x);
420                            Point numberSize = g.stringExtent(value);
421                            g.drawString(value, (int)xp - (numberSize.x / 2),
422                                         canvasBounds.height - numberSize.y);
423                        }
424                    }
425
426                    // draw y labels
427                    double yp = 0;
428                    double y  = ymin;
429                    double dh = (double)plotHeight / (double)ynpoints;
430                    double dy = (ymax - ymin) / (ynpoints);
431                    if (dy > 1)
432                        dy = Math.round(dy * 10.0) / 10.0;
433                    for (int i = 0; i <= ynpoints; i++) {
434                        yp = i * dh;
435                        y  = i * dy + ymin;
436
437                        // Draw a tick mark
438                        g.drawLine(xgap, ygap - (int)yp, xgap + 5, ygap - (int)yp);
439
440                        if (isInteger) {
441                            String value     = String.valueOf((int)y);
442                            Point numberSize = g.stringExtent(value);
443                            g.drawString(value, 0, ygap - (int)yp - (numberSize.y / 2));
444                        }
445                        else {
446                            String value     = format.format(y);
447                            Point numberSize = g.stringExtent(value);
448                            g.drawString(value, 0, ygap - (int)yp - (numberSize.y / 2));
449                        }
450                    }
451
452                    double x0;
453                    double y0;
454                    double x1;
455                    double y1;
456                    if (chartStyle == LINEPLOT) {
457                        dw = (double)plotWidth / (double)(numberOfPoints - 1);
458
459                        // use y = a + b* x to calculate pixel positions
460                        double b         = plotHeight / (ymin - ymax);
461                        double a         = -b * ymax + GAP;
462                        boolean hasXdata = ((xData != null) && (xData.length >= numberOfPoints));
463                        double xRatio    = (1 / (xmax - xmin)) * plotWidth;
464                        double xD        = (xmin / (xmax - xmin)) * plotWidth;
465
466                        // draw lines for selected spreadsheet columns
467                        for (int i = 0; i < numberOfLines; i++) {
468                            // Display each line with a unique color for clarity
469                            if ((lineColors != null) && (lineColors.length >= numberOfLines))
470                                g.setForeground(Display.getCurrent().getSystemColor(lineColors[i]));
471
472                            // set up the line data for drawing one line a time
473                            if (hasXdata)
474                                x0 = xgap + xData[0] * xRatio - xD;
475                            else
476                                x0 = xgap;
477                            y0 = a + b * data[i][0];
478
479                            for (int j = 1; j < numberOfPoints; j++) {
480                                if (hasXdata)
481                                    x1 = xgap + xData[j] * xRatio - xD;
482                                else
483                                    x1 = xgap + j * dw;
484
485                                y1 = a + b * data[i][j];
486                                g.drawLine((int)x0, (int)y0, (int)x1, (int)y1);
487
488                                x0 = x1;
489                                y0 = y1;
490                            }
491
492                            // draw line legend
493                            if ((lineLabels != null) && (lineLabels.length >= numberOfLines)) {
494                                x0 = (canvasBounds.width - GAP - legendWidth) + ((double)legendWidth / 3);
495                                y0 = GAP + (double)LEGEND_LINE_GAP * (i + 1);
496                                g.drawLine((int)x0, (int)y0, (int)x0 + LEGEND_LINE_WIDTH, (int)y0);
497                                g.drawString(lineLabels[i], (int)x0 + 10, (int)y0 + 3);
498                            }
499                        }
500
501                        // draw a box on the legend
502                        if ((lineLabels != null) && (lineLabels.length >= numberOfLines)) {
503                            g.setForeground(Display.getCurrent().getSystemColor(SWT.COLOR_BLACK));
504                            g.drawRectangle(canvasBounds.width - legendWidth - GAP, GAP, legendWidth,
505                                            legendHeight);
506                        }
507
508                        g.setForeground(c); // set the color back to its default
509                    }                       //  (chartStyle == LINEPLOT)
510                    else if (chartStyle == HISTOGRAM) {
511                        // draw histogram for selected image area
512                        xp            = xgap;
513                        int barHeight = 0;
514                        g.setBackground(barColor);
515                        int barWidth = plotWidth / numberOfPoints;
516                        if (barWidth <= 0)
517                            barWidth = 1;
518                        dw = (double)plotWidth / (double)numberOfPoints;
519                        for (int j = 0; j < numberOfPoints; j++) {
520                            xp        = xgap + j * dw;
521                            barHeight = (int)(data[0][j] * (plotHeight / (ymax - ymin)));
522                            g.fillRectangle((int)xp, ygap - barHeight, barWidth, barHeight);
523                        }
524
525                        g.setBackground(c); // set the color back to its default
526                    }                       // (chartStyle == HISTOGRAM)
527                }
528            });
529        }
530    }
531}