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}