1 /*
2  * Copyright 2012 AndroidPlot.com
3  *
4  *    Licensed under the Apache License, Version 2.0 (the "License");
5  *    you may not use this file except in compliance with the License.
6  *    You may obtain a copy of the License at
7  *
8  *        http://www.apache.org/licenses/LICENSE-2.0
9  *
10  *    Unless required by applicable law or agreed to in writing, software
11  *    distributed under the License is distributed on an "AS IS" BASIS,
12  *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  *    See the License for the specific language governing permissions and
14  *    limitations under the License.
15  */
16 
17 package com.androidplot.xy;
18 
19 import java.util.ArrayList;
20 import java.util.Collections;
21 import java.util.Comparator;
22 import java.util.List;
23 import java.util.Map.Entry;
24 import java.util.TreeMap;
25 
26 import android.graphics.Canvas;
27 import android.graphics.RectF;
28 
29 import com.androidplot.exception.PlotRenderException;
30 import com.androidplot.util.ValPixConverter;
31 
32 /**
33  * Renders a point as a Bar
34  */
35 public class BarRenderer<T extends BarFormatter> extends XYSeriesRenderer<T> {
36 
37     private BarRenderStyle renderStyle = BarRenderStyle.OVERLAID;  // default Render Style
38 	private BarWidthStyle widthStyle = BarWidthStyle.FIXED_WIDTH;  // default Width Style
39     private float barWidth = 5;
40     private float barGap = 1;
41 
42     public enum BarRenderStyle {
43         OVERLAID,           // bars are overlaid in descending y-val order (largest val in back)
44         STACKED,            // bars are drawn stacked vertically on top of each other
45         SIDE_BY_SIDE        // bars are drawn horizontally next to each-other
46     }
47 
48     public enum BarWidthStyle {
49         FIXED_WIDTH,        // bar width is always barWidth
50         VARIABLE_WIDTH      // bar width is calculated so that there is only barGap between each bar
51     }
52 
BarRenderer(XYPlot plot)53     public BarRenderer(XYPlot plot) {
54         super(plot);
55     }
56 
57     /**
58      * Sets the width of the bars when using the FIXED_WIDTH render style
59      * @param barWidth
60      */
setBarWidth(float barWidth)61     public void setBarWidth(float barWidth) {
62         this.barWidth = barWidth;
63     }
64 
65     /**
66      * Sets the size of the gap between the bar (or bar groups) when using the VARIABLE_WIDTH render style
67      * @param barGap
68      */
setBarGap(float barGap)69     public void setBarGap(float barGap) {
70         this.barGap = barGap;
71     }
72 
setBarRenderStyle(BarRenderStyle renderStyle)73     public void setBarRenderStyle(BarRenderStyle renderStyle) {
74         this.renderStyle = renderStyle;
75     }
76 
setBarWidthStyle(BarWidthStyle widthStyle)77     public void setBarWidthStyle(BarWidthStyle widthStyle) {
78         this.widthStyle = widthStyle;
79     }
80 
setBarWidthStyle(BarWidthStyle style, float value)81     public void setBarWidthStyle(BarWidthStyle style, float value) {
82     	setBarWidthStyle(style);
83         switch (style) {
84         	case FIXED_WIDTH:
85         		setBarWidth(value);
86                 break;
87         	case VARIABLE_WIDTH:
88         		setBarGap(value);
89         		break;
90 		default:
91 			break;
92         }
93     }
94 
95     @Override
doDrawLegendIcon(Canvas canvas, RectF rect, BarFormatter formatter)96     public void doDrawLegendIcon(Canvas canvas, RectF rect, BarFormatter formatter) {
97         canvas.drawRect(rect, formatter.getFillPaint());
98         canvas.drawRect(rect, formatter.getBorderPaint());
99     }
100 
101     /**
102      * Retrieves the BarFormatter instance that corresponds with the series passed in.
103      * Can be overridden to return other BarFormatters as a result of touch events etc.
104      * @param index index of the point being rendered.
105      * @param series XYSeries to which the point being rendered belongs.
106      * @return
107      */
108     @SuppressWarnings("UnusedParameters")
getFormatter(int index, XYSeries series)109     protected T getFormatter(int index, XYSeries series) {
110         return getFormatter(series);
111     }
112 
onRender(Canvas canvas, RectF plotArea)113     public void onRender(Canvas canvas, RectF plotArea) throws PlotRenderException {
114 
115     	List<XYSeries> sl = getPlot().getSeriesListForRenderer(this.getClass());
116 
117     	TreeMap<Number, BarGroup> axisMap = new TreeMap<Number, BarGroup>();
118 
119         // dont try to render anything if there's nothing to render.
120         if(sl == null) return;
121 
122         /*
123          * Build the axisMap (yVal,BarGroup)... a TreeMap of BarGroups
124          * BarGroups represent a point on the X axis where a single or group of bars need to be drawn.
125          */
126 
127         // For each Series
128         for(XYSeries series : sl) {
129         	BarGroup barGroup;
130 
131             // For each value in the series
132             for(int i = 0; i < series.size(); i++) {
133 
134                	if (series.getX(i) != null) {
135 
136             		// get a new bar object
137             		Bar b = new Bar(series,i,plotArea);
138 
139             		// Find or create the barGroup
140 	            	if (axisMap.containsKey(b.intX)) {
141 	            		barGroup = axisMap.get(b.intX);
142 	            	} else {
143 	            		barGroup = new BarGroup(b.intX,plotArea);
144 	            		axisMap.put(b.intX, barGroup);
145 	            	}
146 	            	barGroup.addBar(b);
147             	}
148 
149             }
150         }
151 
152 		// Loop through the axisMap linking up prev pointers
153 		BarGroup prev, current;
154 		prev = null;
155 		for(Entry<Number, BarGroup> mapEntry : axisMap.entrySet()) {
156 			current = mapEntry.getValue();
157     		current.prev = prev;
158     		prev = current;
159 		}
160 
161 
162 		// The default gap between each bar section
163 		int gap  = (int) barGap;
164 
165 		// Determine roughly how wide (rough_width) this bar should be. This is then used as a default width
166 		// when there are gaps in the data or for the first/last bars.
167 		float f_rough_width = ((plotArea.width() - ((axisMap.size() - 1) * gap)) / (axisMap.size() - 1));
168 		int rough_width = (int) f_rough_width;
169 		if (rough_width < 0) rough_width = 0;
170 		if (gap > rough_width) {
171 			gap = rough_width / 2;
172 		}
173 
174 		//Log.d("PARAMTER","PLOT_WIDTH=" + plotArea.width());
175 		//Log.d("PARAMTER","BAR_GROUPS=" + axisMap.size());
176 		//Log.d("PARAMTER","ROUGH_WIDTH=" + rough_width);
177 		//Log.d("PARAMTER","GAP=" + gap);
178 
179 		/*
180 		 * Calculate the dimensions of each barGroup and then draw each bar within it according to
181 		 * the Render Style and Width Style.
182 		 */
183 
184 		for(Number key : axisMap.keySet()) {
185 
186 			BarGroup barGroup = axisMap.get(key);
187 
188 			// Determine the exact left and right X for the Bar Group
189 			switch (widthStyle) {
190 			case FIXED_WIDTH:
191     			// use intX and go halfwidth either side.
192     			barGroup.leftX = barGroup.intX - (int) (barWidth / 2);
193     			barGroup.width = (int) barWidth;
194     			barGroup.rightX = barGroup.leftX + barGroup.width;
195 				break;
196 			case VARIABLE_WIDTH:
197 	    		if (barGroup.prev != null) {
198 	    			if (barGroup.intX - barGroup.prev.intX - gap - 1 > (int)(rough_width * 1.5)) {
199 	    				// use intX and go halfwidth either side.
200 	        			barGroup.leftX = barGroup.intX - (rough_width / 2);
201 	        			barGroup.width = rough_width;
202 	        			barGroup.rightX = barGroup.leftX + barGroup.width;
203 	    			} else {
204 	    				// base left off prev right to get the gap correct.
205 	    				barGroup.leftX = barGroup.prev.rightX + gap + 1;
206 	    				if (barGroup.leftX > barGroup.intX) barGroup.leftX = barGroup.intX;
207 	    				// base right off intX + halfwidth.
208 	    				barGroup.rightX = barGroup.intX + (rough_width / 2);
209 	    				// calculate the width
210 	    				barGroup.width = barGroup.rightX - barGroup.leftX;
211 	    			}
212 	    		} else {
213 	    			// use intX and go halfwidth either side.
214 	    			barGroup.leftX = barGroup.intX - (rough_width / 2);
215 	    			barGroup.width = rough_width;
216 	    			barGroup.rightX = barGroup.leftX + barGroup.width;
217 	    		}
218 				break;
219 			default:
220 				break;
221 			}
222 
223     		//Log.d("BAR_GROUP", "rough_width=" + rough_width + " width=" + barGroup.width + " <" + barGroup.leftX + "|" + barGroup.intX + "|" + barGroup.rightX + ">");
224 
225     		/*
226     		 * Draw the bars within the barGroup area.
227     		 */
228 			switch (renderStyle) {
229 			case OVERLAID:
230 				Collections.sort(barGroup.bars, new BarComparator());
231 				for (Bar b : barGroup.bars) {
232 					BarFormatter formatter = b.formatter();
233 			        PointLabelFormatter plf = formatter.getPointLabelFormatter();
234 			        PointLabeler pointLabeler = null;
235                 	if (formatter != null) {
236                 		pointLabeler = formatter.getPointLabeler();
237                 	}
238 	        		//Log.d("BAR", b.series.getTitle() + " <" + b.barGroup.leftX + "|" + b.barGroup.intX + "|" + b.barGroup.rightX + "> " + b.intY);
239 	    			if (b.barGroup.width >= 2) {
240 	        			canvas.drawRect(b.barGroup.leftX, b.intY, b.barGroup.rightX, b.barGroup.plotArea.bottom, formatter.getFillPaint());
241 	        		}
242 	        		canvas.drawRect(b.barGroup.leftX, b.intY, b.barGroup.rightX, b.barGroup.plotArea.bottom, formatter.getBorderPaint());
243 	        		if(plf != null && pointLabeler != null) {
244 	                    canvas.drawText(pointLabeler.getLabel(b.series, b.seriesIndex), b.intX + plf.hOffset, b.intY + plf.vOffset, plf.getTextPaint());
245 	                }
246 	        	}
247 				break;
248 			case SIDE_BY_SIDE:
249 				int width = (int) barGroup.width / barGroup.bars.size();
250 				int leftX = barGroup.leftX;
251 				Collections.sort(barGroup.bars, new BarComparator());
252 				for (Bar b : barGroup.bars) {
253 					BarFormatter formatter = b.formatter();
254 			        PointLabelFormatter plf = formatter.getPointLabelFormatter();
255 			        PointLabeler pointLabeler = null;
256                 	if (formatter != null) {
257                 		pointLabeler = formatter.getPointLabeler();
258                 	}
259 	        		//Log.d("BAR", "width=" + width + " <" + leftX + "|" + b.intX + "|" + (leftX + width) + "> " + b.intY);
260 	        		if (b.barGroup.width >= 2) {
261 	        			canvas.drawRect(leftX, b.intY, leftX + width, b.barGroup.plotArea.bottom, formatter.getFillPaint());
262 	        		}
263 	        		canvas.drawRect(leftX, b.intY, leftX + width, b.barGroup.plotArea.bottom, formatter.getBorderPaint());
264 	        		if(plf != null && pointLabeler != null) {
265 	                    canvas.drawText(pointLabeler.getLabel(b.series, b.seriesIndex), leftX + width/2 + plf.hOffset, b.intY + plf.vOffset, plf.getTextPaint());
266 	                }
267 	        		leftX = leftX + width;
268 	        	}
269 				break;
270 			case STACKED:
271 				int bottom = (int) barGroup.plotArea.bottom;
272 				Collections.sort(barGroup.bars, new BarComparator());
273 				for (Bar b : barGroup.bars) {
274 					BarFormatter formatter = b.formatter();
275 			        PointLabelFormatter plf = formatter.getPointLabelFormatter();
276 			        PointLabeler pointLabeler = null;
277                 	if (formatter != null) {
278                 		pointLabeler = formatter.getPointLabeler();
279                 	}
280 	        		int height = (int) b.barGroup.plotArea.bottom - b.intY;
281 	        		int top = bottom - height;
282 	        		//Log.d("BAR", "top=" + top + " bottom=" + bottom + " height=" + height);
283 	    			if (b.barGroup.width >= 2) {
284 	        			canvas.drawRect(b.barGroup.leftX, top, b.barGroup.rightX, bottom, formatter.getFillPaint());
285 	        		}
286 	        		canvas.drawRect(b.barGroup.leftX, top, b.barGroup.rightX, bottom, formatter.getBorderPaint());
287 	        		if(plf != null && pointLabeler != null) {
288 	                    canvas.drawText(pointLabeler.getLabel(b.series, b.seriesIndex), b.intX + plf.hOffset, b.intY + plf.vOffset, plf.getTextPaint());
289 	                }
290 		        	bottom = top;
291 	        	}
292 				break;
293 			default:
294 				break;
295 			}
296 
297 		}
298 
299     }
300 
301     private class Bar {
302 		public XYSeries series;
303 		public int seriesIndex;
304 		public double yVal, xVal;
305 		public int intX, intY;
306 		public float pixX, pixY;
307 		public BarGroup barGroup;
308 
Bar(XYSeries series, int seriesIndex, RectF plotArea)309     	public Bar(XYSeries series, int seriesIndex, RectF plotArea) {
310 			this.series = series;
311 			this.seriesIndex = seriesIndex;
312 
313 			this.xVal = series.getX(seriesIndex).doubleValue();
314 			this.pixX = ValPixConverter.valToPix(xVal, getPlot().getCalculatedMinX().doubleValue(), getPlot().getCalculatedMaxX().doubleValue(), plotArea.width(), false) + (plotArea.left);
315 			this.intX = (int) pixX;
316 
317 			if (series.getY(seriesIndex) != null) {
318 				this.yVal = series.getY(seriesIndex).doubleValue();
319 				this.pixY = ValPixConverter.valToPix(yVal, getPlot().getCalculatedMinY().doubleValue(), getPlot().getCalculatedMaxY().doubleValue(), plotArea.height(), true) + plotArea.top;
320 				this.intY = (int) pixY;
321 			} else {
322 				this.yVal = 0;
323 				this.pixY = plotArea.bottom;
324 				this.intY = (int) pixY;
325 			}
326 		}
formatter()327     	public BarFormatter formatter() {
328     		return getFormatter(seriesIndex, series);
329     	}
330     }
331 
332     private class BarGroup {
333     	public ArrayList<Bar> bars;
334     	public int intX;
335     	public int width, leftX, rightX;
336     	public RectF plotArea;
337     	public BarGroup prev;
338 
BarGroup(int intX, RectF plotArea)339     	public BarGroup(int intX, RectF plotArea) {
340     		// Setup the TreeMap with the required comparator
341    			this.bars = new ArrayList<Bar>(); // create a comparator that compares series title given the index.
342     		this.intX = intX;
343 			this.plotArea = plotArea;
344 		}
345 
addBar(Bar bar)346     	public void addBar(Bar bar) {
347     		bar.barGroup = this;
348    			this.bars.add(bar);
349     	}
350     }
351 
352     @SuppressWarnings("WeakerAccess")
353     public class BarComparator implements Comparator<Bar>{
354         @Override
compare(Bar bar1, Bar bar2)355         public int compare(Bar bar1, Bar bar2) {
356 			switch (renderStyle) {
357 			case OVERLAID:
358 				return Integer.valueOf(bar1.intY).compareTo(bar2.intY);
359 			case SIDE_BY_SIDE:
360 				return bar1.series.getTitle().compareToIgnoreCase(bar2.series.getTitle());
361 			case STACKED:
362 				return bar1.series.getTitle().compareToIgnoreCase(bar2.series.getTitle());
363 			default:
364 	            return 0;
365 			}
366         }
367     }
368 
369 }
370