001/** 002 * Copyright (c) 2011, The University of Southampton and the individual contributors. 003 * All rights reserved. 004 * 005 * Redistribution and use in source and binary forms, with or without modification, 006 * are permitted provided that the following conditions are met: 007 * 008 * * Redistributions of source code must retain the above copyright notice, 009 * this list of conditions and the following disclaimer. 010 * 011 * * Redistributions in binary form must reproduce the above copyright notice, 012 * this list of conditions and the following disclaimer in the documentation 013 * and/or other materials provided with the distribution. 014 * 015 * * Neither the name of the University of Southampton nor the names of its 016 * contributors may be used to endorse or promote products derived from this 017 * software without specific prior written permission. 018 * 019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 029 */ 030/** 031 * 032 */ 033package org.openimaj.vis.general; 034 035import org.openimaj.image.MBFImage; 036import org.openimaj.image.colour.ColourMap; 037import org.openimaj.image.typography.hershey.HersheyFont; 038import org.openimaj.math.geometry.point.Point2d; 039import org.openimaj.math.geometry.point.Point2dImpl; 040import org.openimaj.math.geometry.shape.Rectangle; 041import org.openimaj.util.array.ArrayUtils; 042import org.openimaj.vis.VisualisationImpl; 043 044/** 045 * The basic bar visualisation that can be used to draw a bar graph of 046 * any data set to an RGBA MBFImage. This basic bar graph does not provide 047 * any axes or controlled rendering. If you want that, use the {@link BarVisualisation} 048 * which is more controllable. This one might be quicker as there's less render 049 * hierarchy. 050 * 051 * @author David Dupplaw (dpd@ecs.soton.ac.uk) 052 * @created 26 Jul 2012 053 * @version $Author$, $Revision$, $Date$ 054 */ 055public class BarVisualisationBasic extends VisualisationImpl<double[]> 056{ 057 /** */ 058 private static final long serialVersionUID = 1L; 059 060 /** The colour of the background */ 061 private Float[] backgroundColour = new Float[] 062 { 0f, 0f, 0f, 1f }; 063 064 /** The colour of the bar */ 065 private Float[] barColour = new Float[] 066 { 1f, 0f, 0f, 1f }; 067 068 /** The colour to stroke the bar */ 069 private Float[] strokeColour = new Float[] 070 { 0f, 0f, 0f, 1f }; 071 072 /** The colour of the text to draw */ 073 private Float[] textColour = new Float[] 074 { 1f, 1f, 1f, 1f }; 075 076 /** The colour to stroke any text */ 077 private Float[] textStrokeColour = new Float[] 078 { 0f, 0f, 0f, 1f }; 079 080 /** Colour of the axis */ 081 private Float[] axisColour = new Float[] 082 { 1f, 1f, 1f, 1f }; 083 084 /** Width of the axis line */ 085 private int axisWidth = 1; 086 087 /** Number of pixels to pad the base of the text */ 088 private final int textBasePad = 4; 089 090 /** Whether to auto scale the vertical axis */ 091 private boolean autoScale = true; 092 093 /** The maximum value of the scale (if autoScale is false) */ 094 private double maxValue = 1d; 095 096 /** The minimum value of the scale (if autoScale if false) */ 097 private double minValue = 0d; 098 099 /** Whether to draw the value of the bar in each bar */ 100 private boolean drawValue = false; 101 102 /** Whether to use individual colours for each bar */ 103 private boolean useIndividualBarColours = false; 104 105 /** The colours of the bars is useIndividualBarColours is true */ 106 private Float[][] barColours = null; 107 108 /** Whether to draw the main axis */ 109 private final boolean drawAxis = true; 110 111 /** Whether or not to fix the axis */ 112 private boolean fixAxis = false; 113 114 /** The location of the fixed axis, if it is to be fixed */ 115 private double axisLocation = 100; 116 117 /** 118 * If the minimum value > 0 (or the max < 0), then whether the make the axis 119 * visible 120 */ 121 private boolean axisAlwaysVisible = true; 122 123 /** Whether to outline the text used to draw the values */ 124 private boolean outlineText = false; 125 126 /** The size of the text to draw */ 127 private int textSize = 12; 128 129 /** Whether to use a colour map or not */ 130 private boolean useColourMap = true; 131 132 /** The colour map to use if useColourMap == true */ 133 private ColourMap colourMap = ColourMap.Autumn; 134 135 /** The scalar being used to plot the data */ 136 private double yscale = 0; 137 138 /** The range of the data being viewed */ 139 private double axisRangeY = 0; 140 141 private double dataRange; 142 143 private StrokeColourProvider<Float[]> strokeColourProvider = new StrokeColourProvider<Float[]>() { 144 145 @Override 146 public Float[] getStrokeColour(final int row) { 147 return BarVisualisationBasic.this.strokeColour; 148 } 149 }; 150 151 /** 152 * Create a bar visualisation of the given size 153 * 154 * @param width 155 * The width of the image 156 * @param height 157 * The height of the image 158 */ 159 public BarVisualisationBasic(final int width, final int height) 160 { 161 super(width, height); 162 } 163 164 /** 165 * Create a bar visualisation that will draw to the given image. 166 * 167 * @param imageToDrawTo 168 * The image to draw to. 169 */ 170 public BarVisualisationBasic(final MBFImage imageToDrawTo) 171 { 172 this.visImage = imageToDrawTo; 173 } 174 175 /** 176 * Overlay a bar visualisation on the given vis 177 * 178 * @param v 179 * The visualisation to overlay 180 */ 181 public BarVisualisationBasic(final VisualisationImpl<?> v) 182 { 183 super(v); 184 } 185 186 /** 187 * Creates the given visualisation with the given data 188 * 189 * @param width 190 * The width of the image 191 * @param height 192 * The height of the image 193 * @param data 194 * The data to visualise 195 */ 196 public BarVisualisationBasic(final int width, final int height, final double[] data) 197 { 198 super(width, height); 199 this.setData(data); 200 } 201 202 /** 203 * Plot the given data to the given image. 204 * 205 * @param image 206 * The image to plot to 207 * @param data 208 * The data to plot 209 */ 210 public static void plotBars(final MBFImage image, final double[] data) 211 { 212 new BarVisualisationBasic(image).plotBars(data); 213 } 214 215 /** 216 * Plot the given data to the bar image. 217 * 218 * @param data 219 * The data to plot. 220 */ 221 public void plotBars(final double[] data) 222 { 223 // Set the background 224 this.visImage.fill(this.getBackgroundColour()); 225 226 final int w = this.visImage.getWidth(); 227 final int h = this.visImage.getHeight(); 228 229 synchronized (data) 230 { 231 // Find min and max values from the data 232 double max = this.maxValue; 233 if (this.autoScale) 234 max = ArrayUtils.maxValue(data); 235 double min = this.minValue; 236 if (this.autoScale) 237 min = ArrayUtils.minValue(data); 238 239 // Find the maximum value that occurs on one or t'other 240 // side of the main axis 241 final double largestAxisValue = Math.max(Math.abs(max), Math.abs(min)); 242 243 // The range displayed on the axis. 244 this.dataRange = max - min; 245 246 // Work out the scalars for the values to fit within the window - in 247 // pixels per unit 248 this.yscale = h / this.dataRange; 249 if (this.fixAxis) 250 { 251 // Recalculate the yscale to fit the fixed axis 252 this.yscale = Math.min((h - this.axisLocation) / Math.abs(min), this.axisLocation / Math.abs(max)); 253 } 254 // Position of the axis - if it's fixed we need to alter 255 // the yscale or we calculate where it fits. 256 else 257 { 258 this.axisLocation = h + min * this.yscale; 259 260 if (this.axisAlwaysVisible && this.axisLocation < 0) 261 { 262 this.axisLocation = 0; 263 this.yscale = h / Math.abs(min); 264 } 265 else if (this.axisAlwaysVisible && this.axisLocation > h) 266 { 267 this.axisLocation = h; 268 this.yscale = h / max; 269 } 270 } 271 272 // Calculate the visible axis range 273 this.axisRangeY = this.getValueAt(0, 0).getY() - this.getValueAt(0, h).getY(); 274 275 // The width of each of the bars 276 final double barWidth = w / (double) data.length; 277 278 // Now draw the bars 279 synchronized (this.visImage) 280 { 281 for (int i = 0; i < data.length; i++) 282 { 283 // Position on the x-axis 284 final int x = (int) (i * barWidth); 285 286 // The size of the bar (negative as we're drawing from the 287 // bottom of the window) 288 double barHeight = -data[i] * this.yscale; 289 290 // This is used to ensure we draw the rectangle from its 291 // top-left each time. 292 double offset = 0; 293 294 // Get the bar colour. We'll get the colour map colour if 295 // we're 296 // doing that, 297 // or if we've fixed bar colours use those. 298 Float[] c = this.getBarColour(); 299 if (this.useColourMap) 300 c = this.colourMap.apply((float) (Math.abs(data[i]) / largestAxisValue)); 301 if (this.useIndividualBarColours) 302 c = this.barColours[i % this.barColours.length]; 303 304 // If we need to draw the rectangle above the axis (a 305 // positive 306 // value 307 // makes barHeight negative), we need to draw from above the 308 // axis, 309 // down to the axis. 310 if (barHeight < 0) 311 barHeight = offset = -barHeight; 312 313 // Create the shape for the bar 314 final int rectPosition = (int) (this.axisLocation - offset); 315 final Rectangle barRect = new Rectangle(x, rectPosition, (int) barWidth, (int) barHeight); 316 317 // Draw the filled rectangle, and then stroke it. 318 this.visImage.drawShapeFilled(barRect, c); 319 320 if (barWidth > 3) 321 this.visImage.drawShape(barRect, this.getStrokeColour(i)); 322 323 // If we're to draw the bar's value, do that here. 324 if (this.drawValue) 325 { 326 // We'll draw the bar's value 327 final String text = "" + data[i]; 328 329 // Find the width and height of the text to draw 330 final HersheyFont f = HersheyFont.TIMES_BOLD; 331 final Rectangle r = f.createStyle(this.visImage.createRenderer()).getRenderer( 332 this.visImage.createRenderer()) 333 .getSize(text, f.createStyle(this.visImage.createRenderer())); 334 335 // Work out where to put the text 336 int tx = (int) (x + barWidth / 2 - r.width / 2); 337 final int ty = (int) (this.axisLocation - offset) - this.textBasePad; 338 339 // Make sure the text will be drawn within the bounds of 340 // the 341 // image. 342 if (tx < 0) 343 tx = 0; 344 if (tx + r.width > this.getWidth()) 345 tx = this.getWidth() - (int) r.width; 346 347 // Stroke the text, if necessary 348 if (this.isOutlineText()) 349 { 350 this.visImage.drawText(text, tx - 1, ty - 1, f, this.textSize, this.getTextStrokeColour()); 351 this.visImage.drawText(text, tx + 1, ty - 1, f, this.textSize, this.getTextStrokeColour()); 352 this.visImage.drawText(text, tx - 1, ty + 1, f, this.textSize, this.getTextStrokeColour()); 353 this.visImage.drawText(text, tx + 1, ty + 1, f, this.textSize, this.getTextStrokeColour()); 354 } 355 356 // Fill the text 357 this.visImage.drawText(text, tx, ty, f, this.textSize, this.getTextColour()); 358 } 359 } 360 361 // Finally, draw the axis on top of everything. 362 if (this.drawAxis) 363 { 364 this.visImage.drawLine(0, (int) (h + this.axisLocation), this.getWidth(), 365 (int) (h + this.axisLocation), this.axisWidth, this.getAxisColour()); 366 } 367 } 368 } 369 } 370 371 /** 372 * {@inheritDoc} 373 * 374 * @see org.openimaj.vis.VisualisationImpl#update() 375 */ 376 @Override 377 public void update() 378 { 379 if (this.data != null) 380 this.plotBars(this.data); 381 } 382 383 /** 384 * Set the colours to use for each bar. 385 * 386 * @param colours 387 * The colours to use. 388 */ 389 public void setInvidiualBarColours(final Float[][] colours) 390 { 391 this.barColours = colours; 392 this.useIndividualBarColours = true; 393 } 394 395 /** 396 * Sets whether values are drawn to the image. 397 * 398 * @param tf 399 * TRUE to draw values 400 */ 401 public void setDrawValues(final boolean tf) 402 { 403 this.drawValue = tf; 404 } 405 406 /** 407 * Set the data from a float array. 408 * 409 * @param data 410 * The data to set 411 */ 412 public void setData(final float[] data) 413 { 414 super.setData(ArrayUtils.convertToDouble(data)); 415 } 416 417 /** 418 * Set the data from a long array. 419 * 420 * @param data 421 * The data to set 422 */ 423 public void setData(final long[] data) 424 { 425 super.setData(ArrayUtils.convertToDouble(data)); 426 } 427 428 /** 429 * Fix the x-axis to the given position in pixels. Note that the position is 430 * given from the bottom of the visualisation window. 431 * 432 * @param position 433 * The position in pixels 434 */ 435 public void fixAxis(final int position) 436 { 437 this.axisLocation = -position; 438 this.fixAxis = true; 439 } 440 441 /** 442 * Allow the x-axis to move as best to fit the data 443 */ 444 public void floatAxis() 445 { 446 this.fixAxis = false; 447 } 448 449 /** 450 * @return the outlineText 451 */ 452 public boolean isOutlineText() 453 { 454 return this.outlineText; 455 } 456 457 /** 458 * @param outlineText 459 * the outlineText to set 460 */ 461 public void setOutlineText(final boolean outlineText) 462 { 463 this.outlineText = outlineText; 464 } 465 466 /** 467 * @return the textSize 468 */ 469 public int getTextSize() 470 { 471 return this.textSize; 472 } 473 474 /** 475 * @param textSize 476 * the textSize to set 477 */ 478 public void setTextSize(final int textSize) 479 { 480 this.textSize = textSize; 481 } 482 483 /** 484 * Whether to use a colour map and which one. 485 * 486 * @param cp 487 * The colour map to use 488 */ 489 public void useColourMap(final ColourMap cp) 490 { 491 this.colourMap = cp; 492 this.useColourMap = true; 493 } 494 495 /** 496 * Revert back to using a static colour rather than a colour map; 497 */ 498 public void useStaticColour() 499 { 500 this.useColourMap = false; 501 } 502 503 /** 504 * @return the barColour 505 */ 506 public Float[] getBarColour() 507 { 508 return this.barColour; 509 } 510 511 /** 512 * @param row the row 513 * @return the strokeColour 514 */ 515 public Float[] getStrokeColour(final int row) 516 { 517 return this.strokeColourProvider.getStrokeColour(row); 518 } 519 520 /** 521 * @param prov 522 */ 523 public void setStrokeProvider(final StrokeColourProvider<Float[]> prov){ 524 this.strokeColourProvider = prov; 525 } 526 527 /** 528 * @return the textColour 529 */ 530 public Float[] getTextColour() 531 { 532 return this.textColour; 533 } 534 535 /** 536 * @return the textStrokeColour 537 */ 538 public Float[] getTextStrokeColour() 539 { 540 return this.textStrokeColour; 541 } 542 543 /** 544 * @return the backgroundColour 545 */ 546 public Float[] getBackgroundColour() 547 { 548 return this.backgroundColour; 549 } 550 551 /** 552 * @param backgroundColour 553 * the backgroundColour to set 554 */ 555 public void setBackgroundColour(final Float[] backgroundColour) 556 { 557 this.backgroundColour = backgroundColour; 558 } 559 560 /** 561 * @param barColour 562 * the barColour to set 563 */ 564 public void setBarColour(final Float[] barColour) 565 { 566 this.barColour = barColour; 567 this.useColourMap = false; 568 } 569 570 /** 571 * @param strokeColour 572 * the strokeColour to set 573 */ 574 public void setStrokeColour(final Float[] strokeColour) 575 { 576 this.strokeColour = strokeColour; 577 } 578 579 /** 580 * @param textColour 581 * the textColour to set 582 */ 583 public void setTextColour(final Float[] textColour) 584 { 585 this.textColour = textColour; 586 } 587 588 /** 589 * @param textStrokeColour 590 * the textStrokeColour to set 591 */ 592 public void setTextStrokeColour(final Float[] textStrokeColour) 593 { 594 this.textStrokeColour = textStrokeColour; 595 } 596 597 /** 598 * @return the axisColour 599 */ 600 public Float[] getAxisColour() 601 { 602 return this.axisColour; 603 } 604 605 /** 606 * @param axisColour 607 * the axisColour to set 608 */ 609 public void setAxisColour(final Float[] axisColour) 610 { 611 this.axisColour = axisColour; 612 } 613 614 /** 615 * Get the width of the axis being drawn 616 * 617 * @return The axis width 618 */ 619 public int getAxisWidth() 620 { 621 return this.axisWidth; 622 } 623 624 /** 625 * Set the axis width 626 * 627 * @param axisWidth 628 * The new axis width 629 */ 630 public void setAxisWidth(final int axisWidth) 631 { 632 this.axisWidth = axisWidth; 633 } 634 635 /** 636 * Returns whether the bars are auto scaling 637 * 638 * @return TRUE if auto scaling 639 */ 640 public boolean isAutoScale() 641 { 642 return this.autoScale; 643 } 644 645 /** 646 * Set whether the bars should auto scale to fit all values within the vis. 647 * 648 * @param autoScale 649 * TRUE to auto scale the values 650 */ 651 public void setAutoScale(final boolean autoScale) 652 { 653 this.autoScale = autoScale; 654 } 655 656 /** 657 * Get the maximum value for the scaling 658 * 659 * @return The maximum value 660 */ 661 public double getMaxValue() 662 { 663 return this.maxValue; 664 } 665 666 /** 667 * Set the maximum value (in units) for the bars. Automatically sets the 668 * autoScaling to FALSE. 669 * 670 * @param maxValue 671 * Set the maximum value to use 672 */ 673 public void setMaxValue(final double maxValue) 674 { 675 this.maxValue = maxValue; 676 this.autoScale = false; 677 } 678 679 /** 680 * Get the minimum value in use. 681 * 682 * @return The minimum value 683 */ 684 public double getMinValue() 685 { 686 return this.minValue; 687 } 688 689 /** 690 * Set the minimum value (in units) to use to plot the bars. Automatically 691 * sets the auto scaling to FALSE. 692 * 693 * @param minValue 694 * the minimum value 695 */ 696 public void setMinValue(final double minValue) 697 { 698 this.minValue = minValue; 699 this.autoScale = false; 700 } 701 702 /** 703 * Whether the axis is always visible 704 * 705 * @return TRUE if the axis is always visible 706 */ 707 public boolean isAxisAlwaysVisible() 708 { 709 return this.axisAlwaysVisible; 710 } 711 712 /** 713 * Set whether the axis should always be visible. If the minimum value is > 714 * 0 or maximum value < 0, then the axis will be made visible (either at the 715 * bottom or the top of the viewport respectively) if this is TRUE. This has 716 * no effect if the axis is fixed and set to a point outside the viewport. 717 * 718 * @param axisAlwaysVisible 719 * TRUE to make the axis always visible 720 */ 721 public void setAxisAlwaysVisible(final boolean axisAlwaysVisible) 722 { 723 this.axisAlwaysVisible = axisAlwaysVisible; 724 } 725 726 /** 727 * Returns the last calculated axis location 728 * 729 * @return the axisLocation The axis location 730 */ 731 public double getAxisLocation() 732 { 733 return this.axisLocation; 734 } 735 736 /** 737 * Set the axis location. Automatically fixes the axis location 738 * 739 * @param axisLocation 740 * the axisLocation to set 741 */ 742 public void setAxisLocation(final double axisLocation) 743 { 744 this.axisLocation = axisLocation; 745 this.fixAxis = true; 746 } 747 748 /** 749 * Returns whether the axis is fixed or not. 750 * 751 * @return the fixAxis TRUE if the axis is fixed; FALSE otherwise 752 */ 753 public boolean isFixAxis() 754 { 755 return this.fixAxis; 756 } 757 758 /** 759 * Set whether the axis should be fixed. 760 * 761 * @param fixAxis 762 * TRUE to fix the axis; FALSE to allow it to float 763 */ 764 public void setFixAxis(final boolean fixAxis) 765 { 766 this.fixAxis = fixAxis; 767 } 768 769 /** 770 * The y-scale being used to plot the data. 771 * 772 * @return the yscale The y-scale 773 */ 774 public double getYscale() 775 { 776 return this.yscale; 777 } 778 779 /** 780 * The data range being displayed. 781 * 782 * @return the axisRangeY 783 */ 784 public double getAxisRangeY() 785 { 786 return this.axisRangeY; 787 } 788 789 /** 790 * Returns the units value at the given pixel coordinate. 791 * 792 * @param x 793 * The x pixel coordinate 794 * @param y 795 * The y pixel coordinate 796 * @return The cartesian unit coordinate 797 */ 798 public Point2d getValueAt(final int x, final int y) 799 { 800 return new Point2dImpl(x * this.data.length / this.getWidth(), (float) ((this.axisLocation - y) / this.yscale)); 801 } 802 803 /** 804 * Shows a basic bar visualisation. 805 * @param args The bar visualisation. 806 */ 807 public static void main( final String[] args ) 808 { 809 final int nPoints = 10; 810 811 final double[] data = new double[nPoints]; 812 for( int i = 0; i < nPoints; i++ ) 813 data[i] = nPoints*(Math.random()*2-1); 814 815 final BarVisualisationBasic bv = new BarVisualisationBasic( 1000, 600 ); 816 bv.setData( data ); 817 bv.showWindow( "Bar Visualisation Demo" ); 818 } 819}