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 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 */ 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 */ 69 public void setBarGap(float barGap) { 70 this.barGap = barGap; 71 } 72 73 public void setBarRenderStyle(BarRenderStyle renderStyle) { 74 this.renderStyle = renderStyle; 75 } 76 77 public void setBarWidthStyle(BarWidthStyle widthStyle) { 78 this.widthStyle = widthStyle; 79 } 80 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 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") 109 protected T getFormatter(int index, XYSeries series) { 110 return getFormatter(series); 111 } 112 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 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 } 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 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 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 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