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}