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}