1 2 package com.github.mikephil.charting.charts; 3 4 import android.animation.ObjectAnimator; 5 import android.animation.ValueAnimator; 6 import android.animation.ValueAnimator.AnimatorUpdateListener; 7 import android.annotation.SuppressLint; 8 import android.content.Context; 9 import android.graphics.RectF; 10 import android.util.AttributeSet; 11 import android.util.Log; 12 import android.view.MotionEvent; 13 14 import com.github.mikephil.charting.animation.Easing; 15 import com.github.mikephil.charting.animation.Easing.EasingFunction; 16 import com.github.mikephil.charting.components.Legend; 17 import com.github.mikephil.charting.components.XAxis; 18 import com.github.mikephil.charting.data.ChartData; 19 import com.github.mikephil.charting.data.Entry; 20 import com.github.mikephil.charting.interfaces.datasets.IDataSet; 21 import com.github.mikephil.charting.listener.PieRadarChartTouchListener; 22 import com.github.mikephil.charting.utils.MPPointF; 23 import com.github.mikephil.charting.utils.Utils; 24 25 /** 26 * Baseclass of PieChart and RadarChart. 27 * 28 * @author Philipp Jahoda 29 */ 30 public abstract class PieRadarChartBase<T extends ChartData<? extends IDataSet<? extends Entry>>> 31 extends Chart<T> { 32 33 /** 34 * holds the normalized version of the current rotation angle of the chart 35 */ 36 private float mRotationAngle = 270f; 37 38 /** 39 * holds the raw version of the current rotation angle of the chart 40 */ 41 private float mRawRotationAngle = 270f; 42 43 /** 44 * flag that indicates if rotation is enabled or not 45 */ 46 protected boolean mRotateEnabled = true; 47 48 /** 49 * Sets the minimum offset (padding) around the chart, defaults to 0.f 50 */ 51 protected float mMinOffset = 0.f; 52 PieRadarChartBase(Context context)53 public PieRadarChartBase(Context context) { 54 super(context); 55 } 56 PieRadarChartBase(Context context, AttributeSet attrs)57 public PieRadarChartBase(Context context, AttributeSet attrs) { 58 super(context, attrs); 59 } 60 PieRadarChartBase(Context context, AttributeSet attrs, int defStyle)61 public PieRadarChartBase(Context context, AttributeSet attrs, int defStyle) { 62 super(context, attrs, defStyle); 63 } 64 65 @Override init()66 protected void init() { 67 super.init(); 68 69 mChartTouchListener = new PieRadarChartTouchListener(this); 70 } 71 72 @Override calcMinMax()73 protected void calcMinMax() { 74 //mXAxis.mAxisRange = mData.getXVals().size() - 1; 75 } 76 77 @Override getMaxVisibleCount()78 public int getMaxVisibleCount() { 79 return mData.getEntryCount(); 80 } 81 82 @Override onTouchEvent(MotionEvent event)83 public boolean onTouchEvent(MotionEvent event) { 84 // use the pie- and radarchart listener own listener 85 if (mTouchEnabled && mChartTouchListener != null) 86 return mChartTouchListener.onTouch(this, event); 87 else 88 return super.onTouchEvent(event); 89 } 90 91 @Override computeScroll()92 public void computeScroll() { 93 94 if (mChartTouchListener instanceof PieRadarChartTouchListener) 95 ((PieRadarChartTouchListener) mChartTouchListener).computeScroll(); 96 } 97 98 @Override notifyDataSetChanged()99 public void notifyDataSetChanged() { 100 if (mData == null) 101 return; 102 103 calcMinMax(); 104 105 if (mLegend != null) 106 mLegendRenderer.computeLegend(mData); 107 108 calculateOffsets(); 109 } 110 111 @Override calculateOffsets()112 public void calculateOffsets() { 113 114 float legendLeft = 0f, legendRight = 0f, legendBottom = 0f, legendTop = 0f; 115 116 if (mLegend != null && mLegend.isEnabled() && !mLegend.isDrawInsideEnabled()) { 117 118 float fullLegendWidth = Math.min(mLegend.mNeededWidth, 119 mViewPortHandler.getChartWidth() * mLegend.getMaxSizePercent()); 120 121 switch (mLegend.getOrientation()) { 122 case VERTICAL: { 123 float xLegendOffset = 0.f; 124 125 if (mLegend.getHorizontalAlignment() == Legend.LegendHorizontalAlignment.LEFT 126 || mLegend.getHorizontalAlignment() == Legend.LegendHorizontalAlignment.RIGHT) { 127 if (mLegend.getVerticalAlignment() == Legend.LegendVerticalAlignment.CENTER) { 128 // this is the space between the legend and the chart 129 final float spacing = Utils.convertDpToPixel(13f); 130 131 xLegendOffset = fullLegendWidth + spacing; 132 133 } else { 134 // this is the space between the legend and the chart 135 float spacing = Utils.convertDpToPixel(8f); 136 137 float legendWidth = fullLegendWidth + spacing; 138 float legendHeight = mLegend.mNeededHeight + mLegend.mTextHeightMax; 139 140 MPPointF center = getCenter(); 141 142 float bottomX = mLegend.getHorizontalAlignment() == 143 Legend.LegendHorizontalAlignment.RIGHT 144 ? getWidth() - legendWidth + 15.f 145 : legendWidth - 15.f; 146 float bottomY = legendHeight + 15.f; 147 float distLegend = distanceToCenter(bottomX, bottomY); 148 149 MPPointF reference = getPosition(center, getRadius(), 150 getAngleForPoint(bottomX, bottomY)); 151 152 float distReference = distanceToCenter(reference.x, reference.y); 153 float minOffset = Utils.convertDpToPixel(5f); 154 155 if (bottomY >= center.y && getHeight() - legendWidth > getWidth()) { 156 xLegendOffset = legendWidth; 157 } else if (distLegend < distReference) { 158 159 float diff = distReference - distLegend; 160 xLegendOffset = minOffset + diff; 161 } 162 163 MPPointF.recycleInstance(center); 164 MPPointF.recycleInstance(reference); 165 } 166 } 167 168 switch (mLegend.getHorizontalAlignment()) { 169 case LEFT: 170 legendLeft = xLegendOffset; 171 break; 172 173 case RIGHT: 174 legendRight = xLegendOffset; 175 break; 176 177 case CENTER: 178 switch (mLegend.getVerticalAlignment()) { 179 case TOP: 180 legendTop = Math.min(mLegend.mNeededHeight, 181 mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()); 182 break; 183 case BOTTOM: 184 legendBottom = Math.min(mLegend.mNeededHeight, 185 mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()); 186 break; 187 } 188 break; 189 } 190 } 191 break; 192 193 case HORIZONTAL: 194 float yLegendOffset = 0.f; 195 196 if (mLegend.getVerticalAlignment() == Legend.LegendVerticalAlignment.TOP || 197 mLegend.getVerticalAlignment() == Legend.LegendVerticalAlignment.BOTTOM) { 198 199 // It's possible that we do not need this offset anymore as it 200 // is available through the extraOffsets, but changing it can mean 201 // changing default visibility for existing apps. 202 float yOffset = getRequiredLegendOffset(); 203 204 yLegendOffset = Math.min(mLegend.mNeededHeight + yOffset, 205 mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()); 206 207 switch (mLegend.getVerticalAlignment()) { 208 case TOP: 209 legendTop = yLegendOffset; 210 break; 211 case BOTTOM: 212 legendBottom = yLegendOffset; 213 break; 214 } 215 } 216 break; 217 } 218 219 legendLeft += getRequiredBaseOffset(); 220 legendRight += getRequiredBaseOffset(); 221 legendTop += getRequiredBaseOffset(); 222 legendBottom += getRequiredBaseOffset(); 223 } 224 225 float minOffset = Utils.convertDpToPixel(mMinOffset); 226 227 if (this instanceof RadarChart) { 228 XAxis x = this.getXAxis(); 229 230 if (x.isEnabled() && x.isDrawLabelsEnabled()) { 231 minOffset = Math.max(minOffset, x.mLabelRotatedWidth); 232 } 233 } 234 235 legendTop += getExtraTopOffset(); 236 legendRight += getExtraRightOffset(); 237 legendBottom += getExtraBottomOffset(); 238 legendLeft += getExtraLeftOffset(); 239 240 float offsetLeft = Math.max(minOffset, legendLeft); 241 float offsetTop = Math.max(minOffset, legendTop); 242 float offsetRight = Math.max(minOffset, legendRight); 243 float offsetBottom = Math.max(minOffset, Math.max(getRequiredBaseOffset(), legendBottom)); 244 245 mViewPortHandler.restrainViewPort(offsetLeft, offsetTop, offsetRight, offsetBottom); 246 247 if (mLogEnabled) 248 Log.i(LOG_TAG, "offsetLeft: " + offsetLeft + ", offsetTop: " + offsetTop 249 + ", offsetRight: " + offsetRight + ", offsetBottom: " + offsetBottom); 250 } 251 252 /** 253 * returns the angle relative to the chart center for the given point on the 254 * chart in degrees. The angle is always between 0 and 360°, 0° is NORTH, 255 * 90° is EAST, ... 256 * 257 * @param x 258 * @param y 259 * @return 260 */ getAngleForPoint(float x, float y)261 public float getAngleForPoint(float x, float y) { 262 263 MPPointF c = getCenterOffsets(); 264 265 double tx = x - c.x, ty = y - c.y; 266 double length = Math.sqrt(tx * tx + ty * ty); 267 double r = Math.acos(ty / length); 268 269 float angle = (float) Math.toDegrees(r); 270 271 if (x > c.x) 272 angle = 360f - angle; 273 274 // add 90° because chart starts EAST 275 angle = angle + 90f; 276 277 // neutralize overflow 278 if (angle > 360f) 279 angle = angle - 360f; 280 281 MPPointF.recycleInstance(c); 282 283 return angle; 284 } 285 286 /** 287 * Returns a recyclable MPPointF instance. 288 * Calculates the position around a center point, depending on the distance 289 * from the center, and the angle of the position around the center. 290 * 291 * @param center 292 * @param dist 293 * @param angle in degrees, converted to radians internally 294 * @return 295 */ getPosition(MPPointF center, float dist, float angle)296 public MPPointF getPosition(MPPointF center, float dist, float angle) { 297 298 MPPointF p = MPPointF.getInstance(0, 0); 299 getPosition(center, dist, angle, p); 300 return p; 301 } 302 getPosition(MPPointF center, float dist, float angle, MPPointF outputPoint)303 public void getPosition(MPPointF center, float dist, float angle, MPPointF outputPoint) { 304 outputPoint.x = (float) (center.x + dist * Math.cos(Math.toRadians(angle))); 305 outputPoint.y = (float) (center.y + dist * Math.sin(Math.toRadians(angle))); 306 } 307 308 /** 309 * Returns the distance of a certain point on the chart to the center of the 310 * chart. 311 * 312 * @param x 313 * @param y 314 * @return 315 */ distanceToCenter(float x, float y)316 public float distanceToCenter(float x, float y) { 317 318 MPPointF c = getCenterOffsets(); 319 320 float dist = 0f; 321 322 float xDist = 0f; 323 float yDist = 0f; 324 325 if (x > c.x) { 326 xDist = x - c.x; 327 } else { 328 xDist = c.x - x; 329 } 330 331 if (y > c.y) { 332 yDist = y - c.y; 333 } else { 334 yDist = c.y - y; 335 } 336 337 // pythagoras 338 dist = (float) Math.sqrt(Math.pow(xDist, 2.0) + Math.pow(yDist, 2.0)); 339 340 MPPointF.recycleInstance(c); 341 342 return dist; 343 } 344 345 /** 346 * Returns the xIndex for the given angle around the center of the chart. 347 * Returns -1 if not found / outofbounds. 348 * 349 * @param angle 350 * @return 351 */ getIndexForAngle(float angle)352 public abstract int getIndexForAngle(float angle); 353 354 /** 355 * Set an offset for the rotation of the RadarChart in degrees. Default 270f 356 * --> top (NORTH) 357 * 358 * @param angle 359 */ setRotationAngle(float angle)360 public void setRotationAngle(float angle) { 361 mRawRotationAngle = angle; 362 mRotationAngle = Utils.getNormalizedAngle(mRawRotationAngle); 363 } 364 365 /** 366 * gets the raw version of the current rotation angle of the pie chart the 367 * returned value could be any value, negative or positive, outside of the 368 * 360 degrees. this is used when working with rotation direction, mainly by 369 * gestures and animations. 370 * 371 * @return 372 */ getRawRotationAngle()373 public float getRawRotationAngle() { 374 return mRawRotationAngle; 375 } 376 377 /** 378 * gets a normalized version of the current rotation angle of the pie chart, 379 * which will always be between 0.0 < 360.0 380 * 381 * @return 382 */ getRotationAngle()383 public float getRotationAngle() { 384 return mRotationAngle; 385 } 386 387 /** 388 * Set this to true to enable the rotation / spinning of the chart by touch. 389 * Set it to false to disable it. Default: true 390 * 391 * @param enabled 392 */ setRotationEnabled(boolean enabled)393 public void setRotationEnabled(boolean enabled) { 394 mRotateEnabled = enabled; 395 } 396 397 /** 398 * Returns true if rotation of the chart by touch is enabled, false if not. 399 * 400 * @return 401 */ isRotationEnabled()402 public boolean isRotationEnabled() { 403 return mRotateEnabled; 404 } 405 406 /** 407 * Gets the minimum offset (padding) around the chart, defaults to 0.f 408 */ getMinOffset()409 public float getMinOffset() { 410 return mMinOffset; 411 } 412 413 /** 414 * Sets the minimum offset (padding) around the chart, defaults to 0.f 415 */ setMinOffset(float minOffset)416 public void setMinOffset(float minOffset) { 417 mMinOffset = minOffset; 418 } 419 420 /** 421 * returns the diameter of the pie- or radar-chart 422 * 423 * @return 424 */ getDiameter()425 public float getDiameter() { 426 RectF content = mViewPortHandler.getContentRect(); 427 content.left += getExtraLeftOffset(); 428 content.top += getExtraTopOffset(); 429 content.right -= getExtraRightOffset(); 430 content.bottom -= getExtraBottomOffset(); 431 return Math.min(content.width(), content.height()); 432 } 433 434 /** 435 * Returns the radius of the chart in pixels. 436 * 437 * @return 438 */ getRadius()439 public abstract float getRadius(); 440 441 /** 442 * Returns the required offset for the chart legend. 443 * 444 * @return 445 */ getRequiredLegendOffset()446 protected abstract float getRequiredLegendOffset(); 447 448 /** 449 * Returns the base offset needed for the chart without calculating the 450 * legend size. 451 * 452 * @return 453 */ getRequiredBaseOffset()454 protected abstract float getRequiredBaseOffset(); 455 456 @Override getYChartMax()457 public float getYChartMax() { 458 // TODO Auto-generated method stub 459 return 0; 460 } 461 462 @Override getYChartMin()463 public float getYChartMin() { 464 // TODO Auto-generated method stub 465 return 0; 466 } 467 468 /** 469 * ################ ################ ################ ################ 470 */ 471 /** CODE BELOW THIS RELATED TO ANIMATION */ 472 473 /** 474 * Applys a spin animation to the Chart. 475 * 476 * @param durationmillis 477 * @param fromangle 478 * @param toangle 479 */ 480 @SuppressLint("NewApi") spin(int durationmillis, float fromangle, float toangle, EasingFunction easing)481 public void spin(int durationmillis, float fromangle, float toangle, EasingFunction easing) { 482 483 setRotationAngle(fromangle); 484 485 ObjectAnimator spinAnimator = ObjectAnimator.ofFloat(this, "rotationAngle", fromangle, 486 toangle); 487 spinAnimator.setDuration(durationmillis); 488 spinAnimator.setInterpolator(easing); 489 490 spinAnimator.addUpdateListener(new AnimatorUpdateListener() { 491 492 @Override 493 public void onAnimationUpdate(ValueAnimator animation) { 494 postInvalidate(); 495 } 496 }); 497 spinAnimator.start(); 498 } 499 } 500