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}