This commit is contained in:
coco
2026-07-03 15:56:07 +08:00
commit caef23209c
5767 changed files with 1004268 additions and 0 deletions
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="lecho.lib.hellocharts"
android:versionCode="13"
android:versionName="1.5.8" >
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="22" />
</manifest>
@@ -0,0 +1,104 @@
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
apply from: 'https://raw.github.com/chrisbanes/gradle-mvn-push/master/gradle-mvn-push.gradle'
//To upload to maven central
//http://zserge.com/blog/gradle-maven-publish.html
//gradle uploadArchives
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
}
}
dependencies {
//compile fileTree(dir: 'libs', include: '*.jar')
compile 'com.android.support:support-v4:23.4.0'
}
android {
compileSdkVersion Integer.parseInt(project.ANDROID_BUILD_SDK_VERSION)
buildToolsVersion project.ANDROID_BUILD_TOOLS_VERSION
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
defaultConfig {
minSdkVersion Integer.parseInt(project.ANDROID_BUILD_MIN_SDK_VERSION)
targetSdkVersion Integer.parseInt(project.ANDROID_BUILD_TARGET_SDK_VERSION)
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
resources.srcDirs = ['src']
aidl.srcDirs = ['src']
renderscript.srcDirs = ['src']
res.srcDirs = ['res']
assets.srcDirs = ['assets']
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
lintOptions {
abortOnError false
}
}
//Execute "gradle clean jarRelease" to cook jar.
android.libraryVariants.all { variant ->
def name = variant.buildType.name
if (name.equals(com.android.builder.core.BuilderConstants.DEBUG)) {
return; // Skip debug builds.
}
def task = project.tasks.create "jar${name.capitalize()}", Jar
task.dependsOn variant.javaCompile
task.from variant.javaCompile.destinationDir
artifacts.add('archives', task);
}
// To publish to maven local execute "gradle clean build publishToMavenLocal"
// To publish to nexus execute "gradle clean build publish"
android.libraryVariants
publishing {
publications {
maven(MavenPublication) {
artifact "${project.buildDir}/outputs/aar/${project.name}-release.aar"
artifactId = POM_ARTIFACT_ID
groupId = GROUP
version = VERSION_NAME
// Task androidSourcesJar is provided by gradle-mvn-push.gradle
//artifact androidSourcesJar {
// classifier "sources"
//}
}
}
repositories {
maven {
credentials {
username 'nexusUser'
password 'nexusPass'
}
url "http://your-nexus-url/"
}
}
}
@@ -0,0 +1,3 @@
POM_NAME=HelloCharts Library for Android
POM_ARTIFACT_ID=hellocharts-library
POM_PACKAGING=aar
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>lecho.lib.hellocharts</groupId>
<artifactId>hellocharts-library</artifactId>
<name>HelloCharts Android</name>
<version>1.0</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<android.version>4.4</android.version>
<support.version>19</support.version>
</properties>
<dependencies>
<!--Upload android.jar(sdk\platforms\android-19 with version 4.4 in pom.xml) and android-support-v4.jar(sdk\extras\android\support\v4 with version 19 in pom.xml)-->
<!--to your local maven repository to use maven build.-->
<dependency>
<groupId>com.google.android</groupId>
<artifactId>android</artifactId>
<version>${android.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.android.support</groupId>
<artifactId>support-v4</artifactId>
<version>${support.version}</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>src</sourceDirectory>
<plugins>
<plugin>
<groupId>com.jayway.maven.plugins.android.generation2</groupId>
<artifactId>android-maven-plugin</artifactId>
<extensions>true</extensions>
</plugin>
</plugins>
</build>
<licenses>
<license>
<name>Apache License Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.html</url>
<distribution>repo</distribution>
</license>
</licenses>
</project>
@@ -0,0 +1,20 @@
# To enable ProGuard in your project, edit project.properties
# to define the proguard.config property as described in that file.
#
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
@@ -0,0 +1,15 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system edit
# "ant.properties", and override values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-21
android.library=true
@@ -0,0 +1,3 @@
<resources>
</resources>
@@ -0,0 +1,3 @@
<resources>
</resources>
@@ -0,0 +1,3 @@
<resources>
</resources>
@@ -0,0 +1,3 @@
<resources>
</resources>
@@ -0,0 +1,15 @@
package lecho.lib.hellocharts.animation;
import java.util.EventListener;
/**
* Listener used to listen for chart animation start and stop events. Implementations of this interface can be used for
* all types of chart animations(data, viewport, PieChart rotation).
*/
public interface ChartAnimationListener extends EventListener {
public void onAnimationStarted();
public void onAnimationFinished();
}
@@ -0,0 +1,15 @@
package lecho.lib.hellocharts.animation;
public interface ChartDataAnimator {
public static final long DEFAULT_ANIMATION_DURATION = 500;
public void startAnimation(long duration);
public void cancelAnimation();
public boolean isAnimationStarted();
public void setChartAnimationListener(ChartAnimationListener animationListener);
}
@@ -0,0 +1,77 @@
package lecho.lib.hellocharts.animation;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.SuppressLint;
import lecho.lib.hellocharts.view.Chart;
@SuppressLint("NewApi")
public class ChartDataAnimatorV14 implements ChartDataAnimator, AnimatorListener, AnimatorUpdateListener {
private final Chart chart;
private ValueAnimator animator;
private ChartAnimationListener animationListener = new DummyChartAnimationListener();
public ChartDataAnimatorV14(Chart chart) {
this.chart = chart;
animator = ValueAnimator.ofFloat(0.0f, 1.0f);
animator.addListener(this);
animator.addUpdateListener(this);
}
@Override
public void startAnimation(long duration) {
if (duration >= 0) {
animator.setDuration(duration);
} else {
animator.setDuration(DEFAULT_ANIMATION_DURATION);
}
animator.start();
}
@Override
public void cancelAnimation() {
animator.cancel();
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
chart.animationDataUpdate(animation.getAnimatedFraction());
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
chart.animationDataFinished();
animationListener.onAnimationFinished();
}
@Override
public void onAnimationRepeat(Animator animation) {
}
@Override
public void onAnimationStart(Animator animation) {
animationListener.onAnimationStarted();
}
@Override
public boolean isAnimationStarted() {
return animator.isStarted();
}
@Override
public void setChartAnimationListener(ChartAnimationListener animationListener) {
if (null == animationListener) {
this.animationListener = new DummyChartAnimationListener();
} else {
this.animationListener = animationListener;
}
}
}
@@ -0,0 +1,77 @@
package lecho.lib.hellocharts.animation;
import android.os.Handler;
import android.os.SystemClock;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import lecho.lib.hellocharts.view.Chart;
public class ChartDataAnimatorV8 implements ChartDataAnimator {
final Chart chart;
final Handler handler;
final Interpolator interpolator = new AccelerateDecelerateInterpolator();
long start;
boolean isAnimationStarted = false;
long duration;
private final Runnable runnable = new Runnable() {
@Override
public void run() {
long elapsed = SystemClock.uptimeMillis() - start;
if (elapsed > duration) {
isAnimationStarted = false;
handler.removeCallbacks(runnable);
chart.animationDataFinished();
return;
}
float scale = Math.min(interpolator.getInterpolation((float) elapsed / duration), 1);
chart.animationDataUpdate(scale);
handler.postDelayed(this, 16);
}
};
private ChartAnimationListener animationListener = new DummyChartAnimationListener();
public ChartDataAnimatorV8(Chart chart) {
this.chart = chart;
this.handler = new Handler();
}
@Override
public void startAnimation(long duration) {
if (duration >= 0) {
this.duration = duration;
} else {
this.duration = DEFAULT_ANIMATION_DURATION;
}
isAnimationStarted = true;
animationListener.onAnimationStarted();
start = SystemClock.uptimeMillis();
handler.post(runnable);
}
@Override
public void cancelAnimation() {
isAnimationStarted = false;
handler.removeCallbacks(runnable);
chart.animationDataFinished();
animationListener.onAnimationFinished();
}
@Override
public boolean isAnimationStarted() {
return isAnimationStarted;
}
@Override
public void setChartAnimationListener(ChartAnimationListener animationListener) {
if (null == animationListener) {
this.animationListener = new DummyChartAnimationListener();
} else {
this.animationListener = animationListener;
}
}
}
@@ -0,0 +1,19 @@
package lecho.lib.hellocharts.animation;
import lecho.lib.hellocharts.model.Viewport;
public interface ChartViewportAnimator {
public static final int FAST_ANIMATION_DURATION = 300;
public void startAnimation(Viewport startViewport, Viewport targetViewport);
public void startAnimation(Viewport startViewport, Viewport targetViewport, long duration);
public void cancelAnimation();
public boolean isAnimationStarted();
public void setChartAnimationListener(ChartAnimationListener animationListener);
}
@@ -0,0 +1,95 @@
package lecho.lib.hellocharts.animation;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.SuppressLint;
import lecho.lib.hellocharts.model.Viewport;
import lecho.lib.hellocharts.view.Chart;
@SuppressLint("NewApi")
public class ChartViewportAnimatorV14 implements ChartViewportAnimator, AnimatorListener, AnimatorUpdateListener {
private final Chart chart;
private ValueAnimator animator;
private Viewport startViewport = new Viewport();
private Viewport targetViewport = new Viewport();
private Viewport newViewport = new Viewport();
private ChartAnimationListener animationListener = new DummyChartAnimationListener();
public ChartViewportAnimatorV14(Chart chart) {
this.chart = chart;
animator = ValueAnimator.ofFloat(0.0f, 1.0f);
animator.addListener(this);
animator.addUpdateListener(this);
animator.setDuration(FAST_ANIMATION_DURATION);
}
@Override
public void startAnimation(Viewport startViewport, Viewport targetViewport) {
this.startViewport.set(startViewport);
this.targetViewport.set(targetViewport);
animator.setDuration(FAST_ANIMATION_DURATION);
animator.start();
}
@Override
public void startAnimation(Viewport startViewport, Viewport targetViewport, long duration) {
this.startViewport.set(startViewport);
this.targetViewport.set(targetViewport);
animator.setDuration(duration);
animator.start();
}
@Override
public void cancelAnimation() {
animator.cancel();
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float scale = animation.getAnimatedFraction();
float diffLeft = (targetViewport.left - startViewport.left) * scale;
float diffTop = (targetViewport.top - startViewport.top) * scale;
float diffRight = (targetViewport.right - startViewport.right) * scale;
float diffBottom = (targetViewport.bottom - startViewport.bottom) * scale;
newViewport.set(startViewport.left + diffLeft, startViewport.top + diffTop, startViewport.right + diffRight,
startViewport.bottom + diffBottom);
chart.setCurrentViewport(newViewport);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
chart.setCurrentViewport(targetViewport);
animationListener.onAnimationFinished();
}
@Override
public void onAnimationRepeat(Animator animation) {
}
@Override
public void onAnimationStart(Animator animation) {
animationListener.onAnimationStarted();
}
@Override
public boolean isAnimationStarted() {
return animator.isStarted();
}
@Override
public void setChartAnimationListener(ChartAnimationListener animationListener) {
if (null == animationListener) {
this.animationListener = new DummyChartAnimationListener();
} else {
this.animationListener = animationListener;
}
}
}
@@ -0,0 +1,99 @@
package lecho.lib.hellocharts.animation;
import android.os.Handler;
import android.os.SystemClock;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import lecho.lib.hellocharts.model.Viewport;
import lecho.lib.hellocharts.view.Chart;
public class ChartViewportAnimatorV8 implements ChartViewportAnimator {
final Chart chart;
final Handler handler;
final Interpolator interpolator = new AccelerateDecelerateInterpolator();
long start;
boolean isAnimationStarted = false;
private Viewport startViewport = new Viewport();
private Viewport targetViewport = new Viewport();
private Viewport newViewport = new Viewport();
private long duration;
private ChartAnimationListener animationListener = new DummyChartAnimationListener();
private final Runnable runnable = new Runnable() {
@Override
public void run() {
long elapsed = SystemClock.uptimeMillis() - start;
if (elapsed > duration) {
isAnimationStarted = false;
handler.removeCallbacks(runnable);
chart.setCurrentViewport(targetViewport);
animationListener.onAnimationFinished();
return;
}
float scale = Math.min(interpolator.getInterpolation((float) elapsed / duration), 1);
float diffLeft = (targetViewport.left - startViewport.left) * scale;
float diffTop = (targetViewport.top - startViewport.top) * scale;
float diffRight = (targetViewport.right - startViewport.right) * scale;
float diffBottom = (targetViewport.bottom - startViewport.bottom) * scale;
newViewport.set(startViewport.left + diffLeft, startViewport.top + diffTop,
startViewport.right + diffRight, startViewport.bottom + diffBottom);
chart.setCurrentViewport(newViewport);
handler.postDelayed(this, 16);
}
};
public ChartViewportAnimatorV8(Chart chart) {
this.chart = chart;
this.duration = FAST_ANIMATION_DURATION;
this.handler = new Handler();
}
@Override
public void startAnimation(Viewport startViewport, Viewport targetViewport) {
this.startViewport.set(startViewport);
this.targetViewport.set(targetViewport);
duration = FAST_ANIMATION_DURATION;
isAnimationStarted = true;
animationListener.onAnimationStarted();
start = SystemClock.uptimeMillis();
handler.post(runnable);
}
@Override
public void startAnimation(Viewport startViewport, Viewport targetViewport, long duration) {
this.startViewport.set(startViewport);
this.targetViewport.set(targetViewport);
this.duration = duration;
isAnimationStarted = true;
animationListener.onAnimationStarted();
start = SystemClock.uptimeMillis();
handler.post(runnable);
}
@Override
public void cancelAnimation() {
isAnimationStarted = false;
handler.removeCallbacks(runnable);
chart.setCurrentViewport(targetViewport);
animationListener.onAnimationFinished();
}
@Override
public boolean isAnimationStarted() {
return isAnimationStarted;
}
@Override
public void setChartAnimationListener(ChartAnimationListener animationListener) {
if (null == animationListener) {
this.animationListener = new DummyChartAnimationListener();
} else {
this.animationListener = animationListener;
}
}
}
@@ -0,0 +1,17 @@
package lecho.lib.hellocharts.animation;
public class DummyChartAnimationListener implements ChartAnimationListener {
@Override
public void onAnimationStarted() {
// do nothing
}
@Override
public void onAnimationFinished() {
// do nothing
}
}
@@ -0,0 +1,15 @@
package lecho.lib.hellocharts.animation;
public interface PieChartRotationAnimator {
public static final int FAST_ANIMATION_DURATION = 200;
public void startAnimation(float startAngle, float angleToRotate);
public void cancelAnimation();
public boolean isAnimationStarted();
public void setChartAnimationListener(ChartAnimationListener animationListener);
}
@@ -0,0 +1,84 @@
package lecho.lib.hellocharts.animation;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.SuppressLint;
import lecho.lib.hellocharts.view.PieChartView;
@SuppressLint("NewApi")
public class PieChartRotationAnimatorV14 implements PieChartRotationAnimator, AnimatorListener, AnimatorUpdateListener {
private final PieChartView chart;
private ValueAnimator animator;
private float startRotation = 0;
private float targetRotation = 0;
private ChartAnimationListener animationListener = new DummyChartAnimationListener();
public PieChartRotationAnimatorV14(PieChartView chart) {
this(chart, FAST_ANIMATION_DURATION);
}
public PieChartRotationAnimatorV14(PieChartView chart, long duration) {
this.chart = chart;
animator = ValueAnimator.ofFloat(0.0f, 1.0f);
animator.setDuration(duration);
animator.addListener(this);
animator.addUpdateListener(this);
}
@Override
public void startAnimation(float startRotation, float targetRotation) {
this.startRotation = (startRotation % 360 + 360) % 360;
this.targetRotation = (targetRotation % 360 + 360) % 360;
animator.start();
}
@Override
public void cancelAnimation() {
animator.cancel();
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float scale = animation.getAnimatedFraction();
float rotation = startRotation + (targetRotation - startRotation) * scale;
rotation = (rotation % 360 + 360) % 360;
chart.setChartRotation((int) rotation, false);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
chart.setChartRotation((int) targetRotation, false);
animationListener.onAnimationFinished();
}
@Override
public void onAnimationRepeat(Animator animation) {
}
@Override
public void onAnimationStart(Animator animation) {
animationListener.onAnimationStarted();
}
@Override
public boolean isAnimationStarted() {
return animator.isStarted();
}
@Override
public void setChartAnimationListener(ChartAnimationListener animationListener) {
if (null == animationListener) {
this.animationListener = new DummyChartAnimationListener();
} else {
this.animationListener = animationListener;
}
}
}
@@ -0,0 +1,82 @@
package lecho.lib.hellocharts.animation;
import android.os.Handler;
import android.os.SystemClock;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import lecho.lib.hellocharts.view.PieChartView;
public class PieChartRotationAnimatorV8 implements PieChartRotationAnimator {
final PieChartView chart;
final long duration;
final Handler handler;
final Interpolator interpolator = new AccelerateDecelerateInterpolator();
long start;
boolean isAnimationStarted = false;
private float startRotation = 0;
private float targetRotation = 0;
private ChartAnimationListener animationListener = new DummyChartAnimationListener();
private final Runnable runnable = new Runnable() {
@Override
public void run() {
long elapsed = SystemClock.uptimeMillis() - start;
if (elapsed > duration) {
isAnimationStarted = false;
handler.removeCallbacks(runnable);
chart.setChartRotation((int) targetRotation, false);
animationListener.onAnimationFinished();
return;
}
float scale = Math.min(interpolator.getInterpolation((float) elapsed / duration), 1);
float rotation = startRotation + (targetRotation - startRotation) * scale;
rotation = (rotation % 360 + 360) % 360;
chart.setChartRotation((int) rotation, false);
handler.postDelayed(this, 16);
}
};
public PieChartRotationAnimatorV8(PieChartView chart) {
this(chart, FAST_ANIMATION_DURATION);
}
public PieChartRotationAnimatorV8(PieChartView chart, long duration) {
this.chart = chart;
this.duration = duration;
this.handler = new Handler();
}
@Override
public void startAnimation(float startRotation, float targetRotation) {
this.startRotation = (startRotation % 360 + 360) % 360;
this.targetRotation = (targetRotation % 360 + 360) % 360;
isAnimationStarted = true;
animationListener.onAnimationStarted();
start = SystemClock.uptimeMillis();
handler.post(runnable);
}
@Override
public void cancelAnimation() {
isAnimationStarted = false;
handler.removeCallbacks(runnable);
chart.setChartRotation((int) targetRotation, false);
animationListener.onAnimationFinished();
}
@Override
public boolean isAnimationStarted() {
return isAnimationStarted;
}
@Override
public void setChartAnimationListener(ChartAnimationListener animationListener) {
if (null == animationListener) {
this.animationListener = new DummyChartAnimationListener();
} else {
this.animationListener = animationListener;
}
}
}
@@ -0,0 +1,349 @@
package lecho.lib.hellocharts.computator;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import lecho.lib.hellocharts.listener.DummyVieportChangeListener;
import lecho.lib.hellocharts.listener.ViewportChangeListener;
import lecho.lib.hellocharts.model.Viewport;
/**
* Computes raw points coordinates(in pixels), holds content area dimensions and chart viewport.
*/
public class ChartComputator {
/**
* Maximum chart zoom.
*/
protected static final float DEFAULT_MAXIMUM_ZOOM = 20f;
protected float maxZoom = DEFAULT_MAXIMUM_ZOOM;
protected int chartWidth;
protected int chartHeight;
//contentRectMinusAllMargins <= contentRectMinusAxesMargins <= maxContentRect
protected Rect contentRectMinusAllMargins = new Rect();
protected Rect contentRectMinusAxesMargins = new Rect();
protected Rect maxContentRect = new Rect();
/**
* This rectangle represents the currently visible chart values ranges. The currently visible chart X values are
* from this rectangle's left to its right. The currently visible chart Y values are from this rectangle's top to
* its bottom.
*/
protected Viewport currentViewport = new Viewport();
protected Viewport maxViewport = new Viewport();
protected float minViewportWidth;
protected float minViewportHeight;
/**
* Warning! Viewport listener is disabled for all charts beside preview charts to avoid additional method calls
* during animations.
*/
protected ViewportChangeListener viewportChangeListener = new DummyVieportChangeListener();
/**
* Calculates available width and height. Should be called when chart dimensions change. ContentRect is relative to
* chart view not the device's screen.
*/
public void setContentRect(int width, int height, int paddingLeft, int paddingTop, int paddingRight,
int paddingBottom) {
chartWidth = width;
chartHeight = height;
maxContentRect.set(paddingLeft, paddingTop, width - paddingRight, height - paddingBottom);
contentRectMinusAxesMargins.set(maxContentRect);
contentRectMinusAllMargins.set(maxContentRect);
}
public void resetContentRect() {
contentRectMinusAxesMargins.set(maxContentRect);
contentRectMinusAllMargins.set(maxContentRect);
}
public void insetContentRect(int deltaLeft, int deltaTop, int deltaRight, int deltaBottom) {
contentRectMinusAxesMargins.left = contentRectMinusAxesMargins.left + deltaLeft;
contentRectMinusAxesMargins.top = contentRectMinusAxesMargins.top + deltaTop;
contentRectMinusAxesMargins.right = contentRectMinusAxesMargins.right - deltaRight;
contentRectMinusAxesMargins.bottom = contentRectMinusAxesMargins.bottom - deltaBottom;
insetContentRectByInternalMargins(deltaLeft, deltaTop, deltaRight, deltaBottom);
}
public void insetContentRectByInternalMargins(int deltaLeft, int deltaTop, int deltaRight, int deltaBottom) {
contentRectMinusAllMargins.left = contentRectMinusAllMargins.left + deltaLeft;
contentRectMinusAllMargins.top = contentRectMinusAllMargins.top + deltaTop;
contentRectMinusAllMargins.right = contentRectMinusAllMargins.right - deltaRight;
contentRectMinusAllMargins.bottom = contentRectMinusAllMargins.bottom - deltaBottom;
}
/**
* Checks if new viewport doesn't exceed max available viewport.
*/
public void constrainViewport(float left, float top, float right, float bottom) {
if (right - left < minViewportWidth) {
// Minimum width - constrain horizontal zoom!
right = left + minViewportWidth;
if (left < maxViewport.left) {
left = maxViewport.left;
right = left + minViewportWidth;
} else if (right > maxViewport.right) {
right = maxViewport.right;
left = right - minViewportWidth;
}
}
if (top - bottom < minViewportHeight) {
// Minimum height - constrain vertical zoom!
bottom = top - minViewportHeight;
if (top > maxViewport.top) {
top = maxViewport.top;
bottom = top - minViewportHeight;
} else if (bottom < maxViewport.bottom) {
bottom = maxViewport.bottom;
top = bottom + minViewportHeight;
}
}
currentViewport.left = Math.max(maxViewport.left, left);
currentViewport.top = Math.min(maxViewport.top, top);
currentViewport.right = Math.min(maxViewport.right, right);
currentViewport.bottom = Math.max(maxViewport.bottom, bottom);
viewportChangeListener.onViewportChanged(currentViewport);
}
/**
* Sets the current viewport (defined by {@link #currentViewport}) to the given X and Y positions.
*/
public void setViewportTopLeft(float left, float top) {
/**
* Constrains within the scroll range. The scroll range is simply the viewport extremes (AXIS_X_MAX,
* etc.) minus
* the viewport size. For example, if the extrema were 0 and 10, and the viewport size was 2, the scroll range
* would be 0 to 8.
*/
final float curWidth = currentViewport.width();
final float curHeight = currentViewport.height();
left = Math.max(maxViewport.left, Math.min(left, maxViewport.right - curWidth));
top = Math.max(maxViewport.bottom + curHeight, Math.min(top, maxViewport.top));
constrainViewport(left, top, left + curWidth, top - curHeight);
}
/**
* Translates chart value into raw pixel value. Returned value is absolute pixel X coordinate. If this method
* return
* 0 that means left most pixel of the screen.
*/
public float computeRawX(float valueX) {
// TODO: (contentRectMinusAllMargins.width() / currentViewport.width()) can be recalculated only when viewport
// change.
final float pixelOffset = (valueX - currentViewport.left) * (contentRectMinusAllMargins.width() /
currentViewport.width());
return contentRectMinusAllMargins.left + pixelOffset;
}
/**
* Translates chart value into raw pixel value. Returned value is absolute pixel Y coordinate. If this method
* return
* 0 that means top most pixel of the screen.
*/
public float computeRawY(float valueY) {
final float pixelOffset = (valueY - currentViewport.bottom) * (contentRectMinusAllMargins.height() /
currentViewport.height());
return contentRectMinusAllMargins.bottom - pixelOffset;
}
/**
* Translates viewport distance int pixel distance for X coordinates.
*/
public float computeRawDistanceX(float distance) {
return distance * (contentRectMinusAllMargins.width() / currentViewport.width());
}
/**
* Translates viewport distance int pixel distance for X coordinates.
*/
public float computeRawDistanceY(float distance) {
return distance * (contentRectMinusAllMargins.height() / currentViewport.height());
}
/**
* Finds the chart point (i.e. within the chart's domain and range) represented by the given pixel coordinates, if
* that pixel is within the chart region described by {@link #contentRectMinusAllMargins}. If the point is found,
* the "dest"
* argument is set to the point and this function returns true. Otherwise, this function returns false and
* "dest" is
* unchanged.
*/
public boolean rawPixelsToDataPoint(float x, float y, PointF dest) {
if (!contentRectMinusAllMargins.contains((int) x, (int) y)) {
return false;
}
dest.set(currentViewport.left + (x - contentRectMinusAllMargins.left) * currentViewport.width() /
contentRectMinusAllMargins.width(),
currentViewport.bottom + (y - contentRectMinusAllMargins.bottom) * currentViewport.height() /
-contentRectMinusAllMargins.height());
return true;
}
/**
* Computes the current scrollable surface size, in pixels. For example, if the entire chart area is visible, this
* is simply the current size of {@link #contentRectMinusAllMargins}. If the chart is zoomed in 200% in both
* directions, the
* returned size will be twice as large horizontally and vertically.
*/
public void computeScrollSurfaceSize(Point out) {
out.set((int) (maxViewport.width() * contentRectMinusAllMargins.width() / currentViewport.width()),
(int) (maxViewport.height() * contentRectMinusAllMargins.height() / currentViewport.height()));
}
/**
* Check if given coordinates lies inside contentRectMinusAllMargins.
*/
public boolean isWithinContentRect(float x, float y, float precision) {
if (x >= contentRectMinusAllMargins.left - precision && x <= contentRectMinusAllMargins.right + precision) {
if (y <= contentRectMinusAllMargins.bottom + precision && y >= contentRectMinusAllMargins.top -
precision) {
return true;
}
}
return false;
}
/**
* Returns content rectangle in pixels.
*
* @see #setContentRect(int, int, int, int, int, int)
*/
public Rect getContentRectMinusAllMargins() {
return contentRectMinusAllMargins;
}
/**
* Returns content rectangle with chart internal margins, for example for LineChart contentRectMinusAxesMargins is
* bigger
* than contentRectMinusAllMargins by point radius, thanks to that points are not cut on edges.
*
* @see #setContentRect(int, int, int, int, int, int)
*/
public Rect getContentRectMinusAxesMargins() {
return contentRectMinusAxesMargins;
}
/**
* Returns current chart viewport, returned object is mutable but should not be modified.
*
* @return
*/
public Viewport getCurrentViewport() {
return currentViewport;
}
/**
* Set current viewport to the same values as viewport passed in parameter. This method use deep copy so parameter
* can be safely modified later. Current viewport must be equal or smaller than maximum viewport.
*
* @param viewport
*/
public void setCurrentViewport(Viewport viewport) {
constrainViewport(viewport.left, viewport.top, viewport.right, viewport.bottom);
}
/**
* Set new values for curent viewport, that will change what part of chart is visible. Current viewport must be
* equal or smaller than maximum viewport.
*/
public void setCurrentViewport(float left, float top, float right, float bottom) {
constrainViewport(left, top, right, bottom);
}
/**
* Returns maximum viewport - values ranges extremes.
*/
public Viewport getMaximumViewport() {
return maxViewport;
}
/**
* Set maximum viewport to the same values as viewport passed in parameter. This method use deep copy so parameter
* can be safely modified later.
*
* @param maxViewport
*/
public void setMaxViewport(Viewport maxViewport) {
setMaxViewport(maxViewport.left, maxViewport.top, maxViewport.right, maxViewport.bottom);
}
/**
* Set new values for maximum viewport, that will change what part of chart is visible.
*/
public void setMaxViewport(float left, float top, float right, float bottom) {
this.maxViewport.set(left, top, right, bottom);
computeMinimumWidthAndHeight();
}
/**
* Returns viewport for visible part of chart, for most charts it is equal to current viewport.
*
* @return
*/
public Viewport getVisibleViewport() {
return currentViewport;
}
public void setVisibleViewport(Viewport visibleViewport) {
setCurrentViewport(visibleViewport);
}
public float getMinimumViewportWidth() {
return minViewportWidth;
}
public float getMinimumViewportHeight() {
return minViewportHeight;
}
public void setViewportChangeListener(ViewportChangeListener viewportChangeListener) {
if (null == viewportChangeListener) {
this.viewportChangeListener = new DummyVieportChangeListener();
} else {
this.viewportChangeListener = viewportChangeListener;
}
}
public int getChartWidth() {
return chartWidth;
}
public int getChartHeight() {
return chartHeight;
}
public float getMaxZoom() {
return maxZoom;
}
/**
* Set maximum zoom level, default is 20.
*
* @param maxZoom
*/
public void setMaxZoom(float maxZoom) {
if (maxZoom < 1) {
maxZoom = 1;
}
this.maxZoom = maxZoom;
computeMinimumWidthAndHeight();
setCurrentViewport(currentViewport);
}
private void computeMinimumWidthAndHeight() {
minViewportWidth = this.maxViewport.width() / maxZoom;
minViewportHeight = this.maxViewport.height() / maxZoom;
}
}
@@ -0,0 +1,36 @@
package lecho.lib.hellocharts.computator;
import lecho.lib.hellocharts.model.Viewport;
/**
* Version of ChartComputator for preview charts. It always uses maxViewport as visible viewport and currentViewport as
* preview area.
*/
public class PreviewChartComputator extends ChartComputator {
public float computeRawX(float valueX) {
final float pixelOffset = (valueX - maxViewport.left) * (contentRectMinusAllMargins.width() / maxViewport
.width());
return contentRectMinusAllMargins.left + pixelOffset;
}
public float computeRawY(float valueY) {
final float pixelOffset = (valueY - maxViewport.bottom) * (contentRectMinusAllMargins.height() / maxViewport
.height());
return contentRectMinusAllMargins.bottom - pixelOffset;
}
public Viewport getVisibleViewport() {
return maxViewport;
}
public void setVisibleViewport(Viewport visibleViewport) {
setMaxViewport(visibleViewport);
}
public void constrainViewport(float left, float top, float right, float bottom) {
super.constrainViewport(left, top, right, bottom);
viewportChangeListener.onViewportChanged(currentViewport);
}
}
@@ -0,0 +1,23 @@
package lecho.lib.hellocharts.formatter;
import lecho.lib.hellocharts.model.AxisValue;
public interface AxisValueFormatter {
/**
* Formats AxisValue for manual(custom) axis. Result is stored in (output) formattedValue array. Method
* returns number of chars of formatted value. The formatted value starts at index [formattedValue.length -
* charsNumber] and ends at index [formattedValue.length-1].
*/
public int formatValueForManualAxis(char[] formattedValue, AxisValue axisValue);
/**
* Used only for auto-generated axes. If you are not going to use your implementation for aut-generated axes you can
* skip implementation of this method and just return 0. SFormats values with given number of digits after
* decimal separator. Result is stored in given array. Method returns number of chars for formatted value. The
* formatted value starts at index [formattedValue.length - charsNumber] and ends at index [formattedValue
* .length-1].
*/
public int formatValueForAutoGeneratedAxis(char[] formattedValue, float value, int autoDecimalDigits);
}
@@ -0,0 +1,8 @@
package lecho.lib.hellocharts.formatter;
import lecho.lib.hellocharts.model.BubbleValue;
public interface BubbleChartValueFormatter {
public int formatChartValue(char[] formattedValue, BubbleValue value);
}
@@ -0,0 +1,9 @@
package lecho.lib.hellocharts.formatter;
import lecho.lib.hellocharts.model.SubcolumnValue;
public interface ColumnChartValueFormatter {
public int formatChartValue(char[] formattedValue, SubcolumnValue value);
}
@@ -0,0 +1,9 @@
package lecho.lib.hellocharts.formatter;
import lecho.lib.hellocharts.model.PointValue;
public interface LineChartValueFormatter {
public int formatChartValue(char[] formattedValue, PointValue value);
}
@@ -0,0 +1,8 @@
package lecho.lib.hellocharts.formatter;
import lecho.lib.hellocharts.model.SliceValue;
public interface PieChartValueFormatter {
public int formatChartValue(char[] formattedValue, SliceValue value);
}
@@ -0,0 +1,68 @@
package lecho.lib.hellocharts.formatter;
import lecho.lib.hellocharts.model.AxisValue;
public class SimpleAxisValueFormatter implements AxisValueFormatter {
private ValueFormatterHelper valueFormatterHelper = new ValueFormatterHelper();
public SimpleAxisValueFormatter() {
valueFormatterHelper.determineDecimalSeparator();
}
public SimpleAxisValueFormatter(int decimalDigitsNumber) {
this();
valueFormatterHelper.setDecimalDigitsNumber(decimalDigitsNumber);
}
@Override
public int formatValueForManualAxis(char[] formattedValue, AxisValue axisValue) {
final int charsNumber = valueFormatterHelper.formatFloatValueWithPrependedAndAppendedText(formattedValue,
axisValue.getValue(), axisValue.getLabelAsChars());
return charsNumber;
}
@Override
public int formatValueForAutoGeneratedAxis(char[] formattedValue, float value, int autoDecimalDigits) {
final int charsNumber = valueFormatterHelper.formatFloatValueWithPrependedAndAppendedText(formattedValue,
value, autoDecimalDigits);
return charsNumber;
}
public int getDecimalDigitsNumber() {
return valueFormatterHelper.getDecimalDigitsNumber();
}
public SimpleAxisValueFormatter setDecimalDigitsNumber(int decimalDigitsNumber) {
valueFormatterHelper.setDecimalDigitsNumber(decimalDigitsNumber);
return this;
}
public char[] getAppendedText() {
return valueFormatterHelper.getAppendedText();
}
public SimpleAxisValueFormatter setAppendedText(char[] appendedText) {
valueFormatterHelper.setAppendedText(appendedText);
return this;
}
public char[] getPrependedText() {
return valueFormatterHelper.getPrependedText();
}
public SimpleAxisValueFormatter setPrependedText(char[] prependedText) {
valueFormatterHelper.setPrependedText(prependedText);
return this;
}
public char getDecimalSeparator() {
return valueFormatterHelper.getDecimalSeparator();
}
public SimpleAxisValueFormatter setDecimalSeparator(char decimalSeparator) {
valueFormatterHelper.setDecimalSeparator(decimalSeparator);
return this;
}
}
@@ -0,0 +1,60 @@
package lecho.lib.hellocharts.formatter;
import lecho.lib.hellocharts.model.BubbleValue;
public class SimpleBubbleChartValueFormatter implements BubbleChartValueFormatter {
private ValueFormatterHelper valueFormatterHelper = new ValueFormatterHelper();
public SimpleBubbleChartValueFormatter() {
valueFormatterHelper.determineDecimalSeparator();
}
public SimpleBubbleChartValueFormatter(int decimalDigitsNumber) {
this();
valueFormatterHelper.setDecimalDigitsNumber(decimalDigitsNumber);
}
@Override
public int formatChartValue(char[] formattedValue, BubbleValue value) {
return valueFormatterHelper.formatFloatValueWithPrependedAndAppendedText(formattedValue, value.getZ(), value
.getLabelAsChars());
}
public int getDecimalDigitsNumber() {
return valueFormatterHelper.getDecimalDigitsNumber();
}
public SimpleBubbleChartValueFormatter setDecimalDigitsNumber(int decimalDigitsNumber) {
valueFormatterHelper.setDecimalDigitsNumber(decimalDigitsNumber);
return this;
}
public char[] getAppendedText() {
return valueFormatterHelper.getAppendedText();
}
public SimpleBubbleChartValueFormatter setAppendedText(char[] appendedText) {
valueFormatterHelper.setAppendedText(appendedText);
return this;
}
public char[] getPrependedText() {
return valueFormatterHelper.getPrependedText();
}
public SimpleBubbleChartValueFormatter setPrependedText(char[] prependedText) {
valueFormatterHelper.setPrependedText(prependedText);
return this;
}
public char getDecimalSeparator() {
return valueFormatterHelper.getDecimalSeparator();
}
public SimpleBubbleChartValueFormatter setDecimalSeparator(char decimalSeparator) {
valueFormatterHelper.setDecimalSeparator(decimalSeparator);
return this;
}
}
@@ -0,0 +1,59 @@
package lecho.lib.hellocharts.formatter;
import lecho.lib.hellocharts.model.SubcolumnValue;
public class SimpleColumnChartValueFormatter implements ColumnChartValueFormatter {
private ValueFormatterHelper valueFormatterHelper = new ValueFormatterHelper();
public SimpleColumnChartValueFormatter() {
valueFormatterHelper.determineDecimalSeparator();
}
public SimpleColumnChartValueFormatter(int decimalDigitsNumber) {
this();
valueFormatterHelper.setDecimalDigitsNumber(decimalDigitsNumber);
}
@Override
public int formatChartValue(char[] formattedValue, SubcolumnValue value) {
return valueFormatterHelper.formatFloatValueWithPrependedAndAppendedText(formattedValue, value.getValue(),
value.getLabelAsChars());
}
public int getDecimalDigitsNumber() {
return valueFormatterHelper.getDecimalDigitsNumber();
}
public SimpleColumnChartValueFormatter setDecimalDigitsNumber(int decimalDigitsNumber) {
valueFormatterHelper.setDecimalDigitsNumber(decimalDigitsNumber);
return this;
}
public char[] getAppendedText() {
return valueFormatterHelper.getAppendedText();
}
public SimpleColumnChartValueFormatter setAppendedText(char[] appendedText) {
valueFormatterHelper.setAppendedText(appendedText);
return this;
}
public char[] getPrependedText() {
return valueFormatterHelper.getPrependedText();
}
public SimpleColumnChartValueFormatter setPrependedText(char[] prependedText) {
valueFormatterHelper.setPrependedText(prependedText);
return this;
}
public char getDecimalSeparator() {
return valueFormatterHelper.getDecimalSeparator();
}
public SimpleColumnChartValueFormatter setDecimalSeparator(char decimalSeparator) {
valueFormatterHelper.setDecimalSeparator(decimalSeparator);
return this;
}
}
@@ -0,0 +1,59 @@
package lecho.lib.hellocharts.formatter;
import lecho.lib.hellocharts.model.PointValue;
public class SimpleLineChartValueFormatter implements LineChartValueFormatter {
private ValueFormatterHelper valueFormatterHelper = new ValueFormatterHelper();
public SimpleLineChartValueFormatter() {
valueFormatterHelper.determineDecimalSeparator();
}
public SimpleLineChartValueFormatter(int decimalDigitsNumber) {
this();
valueFormatterHelper.setDecimalDigitsNumber(decimalDigitsNumber);
}
@Override
public int formatChartValue(char[] formattedValue, PointValue value) {
return valueFormatterHelper.formatFloatValueWithPrependedAndAppendedText(formattedValue, value.getY(), value
.getLabelAsChars());
}
public int getDecimalDigitsNumber() {
return valueFormatterHelper.getDecimalDigitsNumber();
}
public SimpleLineChartValueFormatter setDecimalDigitsNumber(int decimalDigitsNumber) {
valueFormatterHelper.setDecimalDigitsNumber(decimalDigitsNumber);
return this;
}
public char[] getAppendedText() {
return valueFormatterHelper.getAppendedText();
}
public SimpleLineChartValueFormatter setAppendedText(char[] appendedText) {
valueFormatterHelper.setAppendedText(appendedText);
return this;
}
public char[] getPrependedText() {
return valueFormatterHelper.getPrependedText();
}
public SimpleLineChartValueFormatter setPrependedText(char[] prependedText) {
valueFormatterHelper.setPrependedText(prependedText);
return this;
}
public char getDecimalSeparator() {
return valueFormatterHelper.getDecimalSeparator();
}
public SimpleLineChartValueFormatter setDecimalSeparator(char decimalSeparator) {
valueFormatterHelper.setDecimalSeparator(decimalSeparator);
return this;
}
}
@@ -0,0 +1,60 @@
package lecho.lib.hellocharts.formatter;
import lecho.lib.hellocharts.model.SliceValue;
public class SimplePieChartValueFormatter implements PieChartValueFormatter {
private ValueFormatterHelper valueFormatterHelper = new ValueFormatterHelper();
public SimplePieChartValueFormatter() {
valueFormatterHelper.determineDecimalSeparator();
}
public SimplePieChartValueFormatter(int decimalDigitsNumber) {
this();
valueFormatterHelper.setDecimalDigitsNumber(decimalDigitsNumber);
}
@Override
public int formatChartValue(char[] formattedValue, SliceValue value) {
return valueFormatterHelper.formatFloatValueWithPrependedAndAppendedText(formattedValue, value.getValue(),
value.getLabelAsChars());
}
public int getDecimalDigitsNumber() {
return valueFormatterHelper.getDecimalDigitsNumber();
}
public SimplePieChartValueFormatter setDecimalDigitsNumber(int decimalDigitsNumber) {
valueFormatterHelper.setDecimalDigitsNumber(decimalDigitsNumber);
return this;
}
public char[] getAppendedText() {
return valueFormatterHelper.getAppendedText();
}
public SimplePieChartValueFormatter setAppendedText(char[] appendedText) {
valueFormatterHelper.setAppendedText(appendedText);
return this;
}
public char[] getPrependedText() {
return valueFormatterHelper.getPrependedText();
}
public SimplePieChartValueFormatter setPrependedText(char[] prependedText) {
valueFormatterHelper.setPrependedText(prependedText);
return this;
}
public char getDecimalSeparator() {
return valueFormatterHelper.getDecimalSeparator();
}
public SimplePieChartValueFormatter setDecimalSeparator(char decimalSeparator) {
valueFormatterHelper.setDecimalSeparator(decimalSeparator);
return this;
}
}
@@ -0,0 +1,144 @@
package lecho.lib.hellocharts.formatter;
import android.util.Log;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import lecho.lib.hellocharts.util.FloatUtils;
public class ValueFormatterHelper {
public static final int DEFAULT_DIGITS_NUMBER = 0;
private static final String TAG = "ValueFormatterHelper";
private int decimalDigitsNumber = Integer.MIN_VALUE;
private char[] appendedText = new char[0];
private char[] prependedText = new char[0];
private char decimalSeparator = '.';
public void determineDecimalSeparator() {
NumberFormat numberFormat = NumberFormat.getInstance();
if (numberFormat instanceof DecimalFormat) {
decimalSeparator = ((DecimalFormat) numberFormat).getDecimalFormatSymbols().getDecimalSeparator();
}
}
public int getDecimalDigitsNumber() {
return decimalDigitsNumber;
}
public ValueFormatterHelper setDecimalDigitsNumber(int decimalDigitsNumber) {
this.decimalDigitsNumber = decimalDigitsNumber;
return this;
}
public char[] getAppendedText() {
return appendedText;
}
public ValueFormatterHelper setAppendedText(char[] appendedText) {
if (null != appendedText) {
this.appendedText = appendedText;
}
return this;
}
public char[] getPrependedText() {
return prependedText;
}
public ValueFormatterHelper setPrependedText(char[] prependedText) {
if (null != prependedText) {
this.prependedText = prependedText;
}
return this;
}
public char getDecimalSeparator() {
return decimalSeparator;
}
public ValueFormatterHelper setDecimalSeparator(char decimalSeparator) {
char nullChar = '\0';
if (nullChar != decimalSeparator) {
this.decimalSeparator = decimalSeparator;
}
return this;
}
/**
* Formats float value. Result is stored in (output) formattedValue array. Method
* returns number of chars of formatted value. The formatted value starts at index [formattedValue.length -
* charsNumber] and ends at index [formattedValue.length-1].
* Note: If label is not null it will be used as formattedValue instead of float value.
* Note: Parameter defaultDigitsNumber is used only if you didn't change decimalDigintsNumber value using
* method {@link #setDecimalDigitsNumber(int)}.
*/
public int formatFloatValueWithPrependedAndAppendedText(char[] formattedValue, float value, int
defaultDigitsNumber, char[] label) {
if (null != label) {
// If custom label is not null use only name characters as formatted value.
// Copy label into formatted value array.
int labelLength = label.length;
if (labelLength > formattedValue.length) {
Log.w(TAG, "Label length is larger than buffer size(64chars), some chars will be skipped!");
labelLength = formattedValue.length;
}
System.arraycopy(label, 0, formattedValue, formattedValue.length - labelLength, labelLength);
return labelLength;
}
final int appliedDigitsNumber = getAppliedDecimalDigitsNumber(defaultDigitsNumber);
final int charsNumber = formatFloatValue(formattedValue, value, appliedDigitsNumber);
appendText(formattedValue);
prependText(formattedValue, charsNumber);
return charsNumber + getPrependedText().length + getAppendedText().length;
}
/**
* @see #formatFloatValueWithPrependedAndAppendedText(char[], float, int, char[])
*/
public int formatFloatValueWithPrependedAndAppendedText(char[] formattedValue, float value, char[] label) {
return formatFloatValueWithPrependedAndAppendedText(formattedValue, value, DEFAULT_DIGITS_NUMBER, label);
}
/**
* @see #formatFloatValueWithPrependedAndAppendedText(char[], float, int, char[])
*/
public int formatFloatValueWithPrependedAndAppendedText(char[] formattedValue, float value, int
defaultDigitsNumber) {
return formatFloatValueWithPrependedAndAppendedText(formattedValue, value, defaultDigitsNumber, null);
}
public int formatFloatValue(char[] formattedValue, float value, int decimalDigitsNumber) {
return FloatUtils.formatFloat(formattedValue, value, formattedValue.length - appendedText.length,
decimalDigitsNumber,
decimalSeparator);
}
public void appendText(char[] formattedValue) {
if (appendedText.length > 0) {
System.arraycopy(appendedText, 0, formattedValue, formattedValue.length - appendedText.length,
appendedText.length);
}
}
public void prependText(char[] formattedValue, int charsNumber) {
if (prependedText.length > 0) {
System.arraycopy(prependedText, 0, formattedValue, formattedValue.length - charsNumber - appendedText.length
- prependedText.length, prependedText.length);
}
}
public int getAppliedDecimalDigitsNumber(int defaultDigitsNumber) {
final int appliedDecimalDigitsNumber;
if (decimalDigitsNumber < 0) {
//When decimalDigitsNumber < 0 that means that user didn't set that value and defaultDigitsNumber should
// be used.
appliedDecimalDigitsNumber = defaultDigitsNumber;
} else {
appliedDecimalDigitsNumber = decimalDigitsNumber;
}
return appliedDecimalDigitsNumber;
}
}
@@ -0,0 +1,127 @@
package lecho.lib.hellocharts.gesture;
import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
import android.support.v4.widget.ScrollerCompat;
import lecho.lib.hellocharts.computator.ChartComputator;
import lecho.lib.hellocharts.model.Viewport;
/**
* Encapsulates scrolling functionality.
*/
public class ChartScroller {
private Viewport scrollerStartViewport = new Viewport(); // Used only for zooms and flings
private Point surfaceSizeBuffer = new Point();// Used for scroll and flings
private ScrollerCompat scroller;
public ChartScroller(Context context) {
scroller = ScrollerCompat.create(context);
}
public boolean startScroll(ChartComputator computator) {
scroller.abortAnimation();
scrollerStartViewport.set(computator.getCurrentViewport());
return true;
}
public boolean scroll(ChartComputator computator, float distanceX, float distanceY, ScrollResult scrollResult) {
// Scrolling uses math based on the viewport (as opposed to math using pixels). Pixel offset is the offset in
// screen pixels, while viewport offset is the offset within the current viewport. For additional
// information on
// surface sizes and pixel offsets, see the docs for {@link computeScrollSurfaceSize()}. For additional
// information about the viewport, see the comments for {@link mCurrentViewport}.
final Viewport maxViewport = computator.getMaximumViewport();
final Viewport visibleViewport = computator.getVisibleViewport();
final Viewport currentViewport = computator.getCurrentViewport();
final Rect contentRect = computator.getContentRectMinusAllMargins();
final boolean canScrollLeft = currentViewport.left > maxViewport.left;
final boolean canScrollRight = currentViewport.right < maxViewport.right;
final boolean canScrollTop = currentViewport.top < maxViewport.top;
final boolean canScrollBottom = currentViewport.bottom > maxViewport.bottom;
boolean canScrollX = false;
boolean canScrollY = false;
if (canScrollLeft && distanceX <= 0) {
canScrollX = true;
} else if (canScrollRight && distanceX >= 0) {
canScrollX = true;
}
if (canScrollTop && distanceY <= 0) {
canScrollY = true;
} else if (canScrollBottom && distanceY >= 0) {
canScrollY = true;
}
if (canScrollX || canScrollY) {
computator.computeScrollSurfaceSize(surfaceSizeBuffer);
float viewportOffsetX = distanceX * visibleViewport.width() / contentRect.width();
float viewportOffsetY = -distanceY * visibleViewport.height() / contentRect.height();
computator
.setViewportTopLeft(currentViewport.left + viewportOffsetX, currentViewport.top + viewportOffsetY);
}
scrollResult.canScrollX = canScrollX;
scrollResult.canScrollY = canScrollY;
return canScrollX || canScrollY;
}
public boolean computeScrollOffset(ChartComputator computator) {
if (scroller.computeScrollOffset()) {
// The scroller isn't finished, meaning a fling or programmatic pan operation is
// currently active.
final Viewport maxViewport = computator.getMaximumViewport();
computator.computeScrollSurfaceSize(surfaceSizeBuffer);
final float currXRange = maxViewport.left + maxViewport.width() * scroller.getCurrX() /
surfaceSizeBuffer.x;
final float currYRange = maxViewport.top - maxViewport.height() * scroller.getCurrY() /
surfaceSizeBuffer.y;
computator.setViewportTopLeft(currXRange, currYRange);
return true;
}
return false;
}
public boolean fling(int velocityX, int velocityY, ChartComputator computator) {
// Flings use math in pixels (as opposed to math based on the viewport).
computator.computeScrollSurfaceSize(surfaceSizeBuffer);
scrollerStartViewport.set(computator.getCurrentViewport());
int startX = (int) (surfaceSizeBuffer.x * (scrollerStartViewport.left - computator.getMaximumViewport().left)
/ computator.getMaximumViewport().width());
int startY = (int) (surfaceSizeBuffer.y * (computator.getMaximumViewport().top - scrollerStartViewport.top) /
computator.getMaximumViewport().height());
// TODO probably should be mScroller.forceFinish but ScrollerCompat doesn't have that method.
scroller.abortAnimation();
final int width = computator.getContentRectMinusAllMargins().width();
final int height = computator.getContentRectMinusAllMargins().height();
scroller.fling(startX, startY, velocityX, velocityY, 0, surfaceSizeBuffer.x - width + 1, 0,
surfaceSizeBuffer.y - height + 1);
return true;
}
public static class ScrollResult {
public boolean canScrollX;
public boolean canScrollY;
}
}
@@ -0,0 +1,332 @@
package lecho.lib.hellocharts.gesture;
import android.content.Context;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.ViewParent;
import lecho.lib.hellocharts.computator.ChartComputator;
import lecho.lib.hellocharts.gesture.ChartScroller.ScrollResult;
import lecho.lib.hellocharts.model.SelectedValue;
import lecho.lib.hellocharts.renderer.ChartRenderer;
import lecho.lib.hellocharts.view.Chart;
/**
* Default touch handler for most charts. Handles value touch, scroll, fling and zoom.
*/
public class ChartTouchHandler {
protected GestureDetector gestureDetector;
protected ScaleGestureDetector scaleGestureDetector;
protected ChartScroller chartScroller;
protected ChartZoomer chartZoomer;
protected Chart chart;
protected ChartComputator computator;
protected ChartRenderer renderer;
protected boolean isZoomEnabled = true;
protected boolean isScrollEnabled = true;
protected boolean isValueTouchEnabled = true;
protected boolean isValueSelectionEnabled = false;
/**
* Used only for selection mode to avoid calling listener multiple times for the same selection. Small thing but it
* is more intuitive this way.
*/
protected SelectedValue selectionModeOldValue = new SelectedValue();
protected SelectedValue selectedValue = new SelectedValue();
protected SelectedValue oldSelectedValue = new SelectedValue();
/**
* ViewParent to disallow touch events interception if chart is within scroll container.
*/
protected ViewParent viewParent;
/**
* Type of scroll of container, horizontal or vertical.
*/
protected ContainerScrollType containerScrollType;
public ChartTouchHandler(Context context, Chart chart) {
this.chart = chart;
this.computator = chart.getChartComputator();
this.renderer = chart.getChartRenderer();
gestureDetector = new GestureDetector(context, new ChartGestureListener());
scaleGestureDetector = new ScaleGestureDetector(context, new ChartScaleGestureListener());
chartScroller = new ChartScroller(context);
chartZoomer = new ChartZoomer(context, ZoomType.HORIZONTAL_AND_VERTICAL);
}
public void resetTouchHandler() {
this.computator = chart.getChartComputator();
this.renderer = chart.getChartRenderer();
}
/**
* Computes scroll and zoom using {@link ChartScroller} and {@link ChartZoomer}. This method returns true if
* scroll/zoom was computed and chart needs to be invalidated.
*/
public boolean computeScroll() {
boolean needInvalidate = false;
if (isScrollEnabled && chartScroller.computeScrollOffset(computator)) {
needInvalidate = true;
}
if (isZoomEnabled && chartZoomer.computeZoom(computator)) {
needInvalidate = true;
}
return needInvalidate;
}
/**
* Handle chart touch event(gestures, clicks). Return true if gesture was handled and chart needs to be
* invalidated.
*/
public boolean handleTouchEvent(MotionEvent event) {
boolean needInvalidate = false;
// TODO: detectors always return true, use class member needInvalidate instead local variable as workaround.
// This flag should be computed inside gesture listeners methods to avoid invalidation.
needInvalidate = gestureDetector.onTouchEvent(event);
needInvalidate = scaleGestureDetector.onTouchEvent(event) || needInvalidate;
if (isZoomEnabled && scaleGestureDetector.isInProgress()) {
// Special case: if view is inside scroll container and user is scaling disable touch interception by
// parent.
disallowParentInterceptTouchEvent();
}
if (isValueTouchEnabled) {
needInvalidate = computeTouch(event) || needInvalidate;
}
return needInvalidate;
}
/**
* Handle chart touch event(gestures, clicks). Return true if gesture was handled and chart needs to be
* invalidated.
* If viewParent and containerScrollType are not null chart can be scrolled and scaled within horizontal or
* vertical
* scroll container like ViewPager.
*/
public boolean handleTouchEvent(MotionEvent event, ViewParent viewParent,
ContainerScrollType containerScrollType) {
this.viewParent = viewParent;
this.containerScrollType = containerScrollType;
return handleTouchEvent(event);
}
/**
* Disallow parent view from intercepting touch events. Use it for chart that is within some scroll container i.e.
* ViewPager.
*/
private void disallowParentInterceptTouchEvent() {
if (null != viewParent) {
viewParent.requestDisallowInterceptTouchEvent(true);
}
}
/**
* Allow parent view to intercept touch events if chart cannot be scroll horizontally or vertically according to
* the
* current value of {@link #containerScrollType}.
*/
private void allowParentInterceptTouchEvent(ScrollResult scrollResult) {
if (null != viewParent) {
if (ContainerScrollType.HORIZONTAL == containerScrollType && !scrollResult.canScrollX
&& !scaleGestureDetector.isInProgress()) {
viewParent.requestDisallowInterceptTouchEvent(false);
} else if (ContainerScrollType.VERTICAL == containerScrollType && !scrollResult.canScrollY
&& !scaleGestureDetector.isInProgress()) {
viewParent.requestDisallowInterceptTouchEvent(false);
}
}
}
private boolean computeTouch(MotionEvent event) {
boolean needInvalidate = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
boolean wasTouched = renderer.isTouched();
boolean isTouched = checkTouch(event.getX(), event.getY());
if (wasTouched != isTouched) {
needInvalidate = true;
if (isValueSelectionEnabled) {
selectionModeOldValue.clear();
if (wasTouched && !renderer.isTouched()) {
chart.callTouchListener();
}
}
}
break;
case MotionEvent.ACTION_UP:
if (renderer.isTouched()) {
if (checkTouch(event.getX(), event.getY())) {
if (isValueSelectionEnabled) {
// For selection mode call listener only if selected value changed,
// that means that should be
// first(selection) click on given value.
if (!selectionModeOldValue.equals(selectedValue)) {
selectionModeOldValue.set(selectedValue);
chart.callTouchListener();
}
} else {
chart.callTouchListener();
renderer.clearTouch();
}
} else {
renderer.clearTouch();
}
needInvalidate = true;
}
break;
case MotionEvent.ACTION_MOVE:
// If value was touched and now touch point is outside of value area - clear touch and invalidate, user
// probably moved finger away from given chart value.
if (renderer.isTouched()) {
if (!checkTouch(event.getX(), event.getY())) {
renderer.clearTouch();
needInvalidate = true;
}
}
break;
case MotionEvent.ACTION_CANCEL:
if (renderer.isTouched()) {
renderer.clearTouch();
needInvalidate = true;
}
break;
}
return needInvalidate;
}
private boolean checkTouch(float touchX, float touchY) {
oldSelectedValue.set(selectedValue);
selectedValue.clear();
if (renderer.checkTouch(touchX, touchY)) {
selectedValue.set(renderer.getSelectedValue());
}
// Check if selection is still on the same value, if not return false.
if (oldSelectedValue.isSet() && selectedValue.isSet() && !oldSelectedValue.equals(selectedValue)) {
return false;
} else {
return renderer.isTouched();
}
}
public boolean isZoomEnabled() {
return isZoomEnabled;
}
public void setZoomEnabled(boolean isZoomEnabled) {
this.isZoomEnabled = isZoomEnabled;
}
public boolean isScrollEnabled() {
return isScrollEnabled;
}
public void setScrollEnabled(boolean isScrollEnabled) {
this.isScrollEnabled = isScrollEnabled;
}
public ZoomType getZoomType() {
return chartZoomer.getZoomType();
}
public void setZoomType(ZoomType zoomType) {
chartZoomer.setZoomType(zoomType);
}
public boolean isValueTouchEnabled() {
return isValueTouchEnabled;
}
public void setValueTouchEnabled(boolean isValueTouchEnabled) {
this.isValueTouchEnabled = isValueTouchEnabled;
}
public boolean isValueSelectionEnabled() {
return isValueSelectionEnabled;
}
public void setValueSelectionEnabled(boolean isValueSelectionEnabled) {
this.isValueSelectionEnabled = isValueSelectionEnabled;
}
protected class ChartScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
@Override
public boolean onScale(ScaleGestureDetector detector) {
if (isZoomEnabled) {
float scale = 2.0f - detector.getScaleFactor();
if (Float.isInfinite(scale)) {
scale = 1;
}
return chartZoomer.scale(computator, detector.getFocusX(), detector.getFocusY(), scale);
}
return false;
}
}
protected class ChartGestureListener extends GestureDetector.SimpleOnGestureListener {
protected ScrollResult scrollResult = new ScrollResult();
@Override
public boolean onDown(MotionEvent e) {
if (isScrollEnabled) {
disallowParentInterceptTouchEvent();
return chartScroller.startScroll(computator);
}
return false;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
if (isZoomEnabled) {
return chartZoomer.startZoom(e, computator);
}
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (isScrollEnabled) {
boolean canScroll = chartScroller
.scroll(computator, distanceX, distanceY, scrollResult);
allowParentInterceptTouchEvent(scrollResult);
return canScroll;
}
return false;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (isScrollEnabled) {
return chartScroller.fling((int) -velocityX, (int) -velocityY, computator);
}
return false;
}
}
}
@@ -0,0 +1,96 @@
package lecho.lib.hellocharts.gesture;
import android.content.Context;
import android.graphics.PointF;
import android.view.MotionEvent;
import lecho.lib.hellocharts.computator.ChartComputator;
import lecho.lib.hellocharts.model.Viewport;
/**
* Encapsulates zooming functionality.
*/
public class ChartZoomer {
public static final float ZOOM_AMOUNT = 0.25f;
private ZoomerCompat zoomer;
private ZoomType zoomType;
private PointF zoomFocalPoint = new PointF();// Used for double tap zoom
private PointF viewportFocus = new PointF();
private Viewport scrollerStartViewport = new Viewport(); // Used only for zooms and flings
public ChartZoomer(Context context, ZoomType zoomType) {
zoomer = new ZoomerCompat(context);
this.zoomType = zoomType;
}
public boolean startZoom(MotionEvent e, ChartComputator computator) {
zoomer.forceFinished(true);
scrollerStartViewport.set(computator.getCurrentViewport());
if (!computator.rawPixelsToDataPoint(e.getX(), e.getY(), zoomFocalPoint)) {
// Focus point is not within content area.
return false;
}
zoomer.startZoom(ZOOM_AMOUNT);
return true;
}
public boolean computeZoom(ChartComputator computator) {
if (zoomer.computeZoom()) {
// Performs the zoom since a zoom is in progress.
final float newWidth = (1.0f - zoomer.getCurrZoom()) * scrollerStartViewport.width();
final float newHeight = (1.0f - zoomer.getCurrZoom()) * scrollerStartViewport.height();
final float pointWithinViewportX = (zoomFocalPoint.x - scrollerStartViewport.left)
/ scrollerStartViewport.width();
final float pointWithinViewportY = (zoomFocalPoint.y - scrollerStartViewport.bottom)
/ scrollerStartViewport.height();
float left = zoomFocalPoint.x - newWidth * pointWithinViewportX;
float top = zoomFocalPoint.y + newHeight * (1 - pointWithinViewportY);
float right = zoomFocalPoint.x + newWidth * (1 - pointWithinViewportX);
float bottom = zoomFocalPoint.y - newHeight * pointWithinViewportY;
setCurrentViewport(computator, left, top, right, bottom);
return true;
}
return false;
}
public boolean scale(ChartComputator computator, float focusX, float focusY, float scale) {
/**
* Smaller viewport means bigger zoom so for zoomIn scale should have value <1, for zoomOout >1
*/
final float newWidth = scale * computator.getCurrentViewport().width();
final float newHeight = scale * computator.getCurrentViewport().height();
if (!computator.rawPixelsToDataPoint(focusX, focusY, viewportFocus)) {
// Focus point is not within content area.
return false;
}
float left = viewportFocus.x - (focusX - computator.getContentRectMinusAllMargins().left)
* (newWidth / computator.getContentRectMinusAllMargins().width());
float top = viewportFocus.y + (focusY - computator.getContentRectMinusAllMargins().top)
* (newHeight / computator.getContentRectMinusAllMargins().height());
float right = left + newWidth;
float bottom = top - newHeight;
setCurrentViewport(computator, left, top, right, bottom);
return true;
}
private void setCurrentViewport(ChartComputator computator, float left, float top, float right, float bottom) {
Viewport currentViewport = computator.getCurrentViewport();
if (ZoomType.HORIZONTAL_AND_VERTICAL == zoomType) {
computator.setCurrentViewport(left, top, right, bottom);
} else if (ZoomType.HORIZONTAL == zoomType) {
computator.setCurrentViewport(left, currentViewport.top, right, currentViewport.bottom);
} else if (ZoomType.VERTICAL == zoomType) {
computator.setCurrentViewport(currentViewport.left, top, currentViewport.right, bottom);
}
}
public ZoomType getZoomType() {
return zoomType;
}
public void setZoomType(ZoomType zoomType) {
this.zoomType = zoomType;
}
}
@@ -0,0 +1,8 @@
package lecho.lib.hellocharts.gesture;
/**
* Enum used to inform chart in which type of container it exists.
*/
public enum ContainerScrollType {
HORIZONTAL, VERTICAL
}
@@ -0,0 +1,158 @@
package lecho.lib.hellocharts.gesture;
import android.content.Context;
import android.graphics.RectF;
import android.support.v4.widget.ScrollerCompat;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import lecho.lib.hellocharts.view.PieChartView;
/**
* Touch handler for PieChart. It doesn't handle zoom and scroll like default ChartTouchHandler. Instead it uses
* Scroller(ScrollerCompat) directly to compute PieChart rotation when user scroll. ChartScroller and ChartZoomer are
* not really used here.
*/
public class PieChartTouchHandler extends ChartTouchHandler {
/**
* The initial fling velocity is divided by this amount.
*/
public static final int FLING_VELOCITY_DOWNSCALE = 4;
/**
* PieChartTouchHandler uses its own instance of Scroller.
*/
protected ScrollerCompat scroller;
/**
* Reference to PieChartView to use some methods specific for that kind of chart.
*/
protected PieChartView pieChart;
private boolean isRotationEnabled = true;
public PieChartTouchHandler(Context context, PieChartView chart) {
super(context, chart);
pieChart = (PieChartView) chart;
scroller = ScrollerCompat.create(context);
gestureDetector = new GestureDetector(context, new ChartGestureListener());
scaleGestureDetector = new ScaleGestureDetector(context, new ChartScaleGestureListener());
isZoomEnabled = false;// Zoom is not supported by PieChart.
}
@Override
public boolean computeScroll() {
if (!isRotationEnabled) {
return false;
}
if (scroller.computeScrollOffset()) {
pieChart.setChartRotation(scroller.getCurrY(), false);
// pieChart.setChartRotation() will invalidate view so no need to return true;
}
return false;
}
@Override
public boolean handleTouchEvent(MotionEvent event) {
boolean needInvalidate = super.handleTouchEvent(event);
if (isRotationEnabled) {
needInvalidate = gestureDetector.onTouchEvent(event) || needInvalidate;
}
return needInvalidate;
}
public boolean isRotationEnabled() {
return isRotationEnabled;
}
public void setRotationEnabled(boolean isRotationEnabled) {
this.isRotationEnabled = isRotationEnabled;
}
private class ChartScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
@Override
public boolean onScale(ScaleGestureDetector detector) {
// No scale for PieChart.
return false;
}
}
private class ChartGestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onDown(MotionEvent e) {
if (isRotationEnabled) {
scroller.abortAnimation();
return true;
}
return false;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (isRotationEnabled) {
// Set the pie rotation directly.
final RectF circleOval = pieChart.getCircleOval();
final float centerX = circleOval.centerX();
final float centerY = circleOval.centerY();
float scrollTheta = vectorToScalarScroll(distanceX, distanceY, e2.getX() - centerX, e2.getY() -
centerY);
pieChart.setChartRotation(pieChart.getChartRotation() - (int) scrollTheta / FLING_VELOCITY_DOWNSCALE,
false);
return true;
}
return false;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (isRotationEnabled) {
// Set up the Scroller for a fling
final RectF circleOval = pieChart.getCircleOval();
final float centerX = circleOval.centerX();
final float centerY = circleOval.centerY();
float scrollTheta = vectorToScalarScroll(velocityX, velocityY, e2.getX() - centerX, e2.getY() -
centerY);
scroller.abortAnimation();
scroller.fling(0, (int) pieChart.getChartRotation(), 0, (int) scrollTheta / FLING_VELOCITY_DOWNSCALE,
0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE);
return true;
}
return false;
}
/**
* Helper method for translating (x,y) scroll vectors into scalar rotation of the pie.
*
* @param dx The x component of the current scroll vector.
* @param dy The y component of the current scroll vector.
* @param x The x position of the current touch, relative to the pie center.
* @param y The y position of the current touch, relative to the pie center.
* @return The scalar representing the change in angular position for this scroll.
*/
private float vectorToScalarScroll(float dx, float dy, float x, float y) {
// get the length of the vector
float l = (float) Math.sqrt(dx * dx + dy * dy);
// decide if the scalar should be negative or positive by finding
// the dot product of the vector perpendicular to (x,y).
float crossX = -y;
float crossY = x;
float dot = (crossX * dx + crossY * dy);
float sign = Math.signum(dot);
return l * sign;
}
}
}
@@ -0,0 +1,54 @@
package lecho.lib.hellocharts.gesture;
import android.content.Context;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import lecho.lib.hellocharts.view.Chart;
/**
* Touch Handler for preview charts. It scroll and zoom only preview area, not all preview chart data.
*/
public class PreviewChartTouchHandler extends ChartTouchHandler {
public PreviewChartTouchHandler(Context context, Chart chart) {
super(context, chart);
gestureDetector = new GestureDetector(context, new PreviewChartGestureListener());
scaleGestureDetector = new ScaleGestureDetector(context, new ChartScaleGestureListener());
// Disable value touch and selection mode, by default not needed for preview chart.
isValueTouchEnabled = false;
isValueSelectionEnabled = false;
}
protected class ChartScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
@Override
public boolean onScale(ScaleGestureDetector detector) {
if (isZoomEnabled) {
float scale = detector.getCurrentSpan() / detector.getPreviousSpan();
if (Float.isInfinite(scale)) {
scale = 1;
}
return chartZoomer.scale(computator, detector.getFocusX(), detector.getFocusY(), scale);
}
return false;
}
}
protected class PreviewChartGestureListener extends ChartGestureListener {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return super.onScroll(e1, e2, -distanceX, -distanceY);
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return super.onFling(e1, e2, -velocityX, -velocityY);
}
}
}
@@ -0,0 +1,6 @@
package lecho.lib.hellocharts.gesture;
public enum ZoomType {
HORIZONTAL, VERTICAL, HORIZONTAL_AND_VERTICAL;
}
@@ -0,0 +1,128 @@
/*
* Copyright 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lecho.lib.hellocharts.gesture;
import android.content.Context;
import android.os.SystemClock;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
/**
* A simple class that animates double-touch zoom gestures. Functionally similar to a {@link android.widget.Scroller}.
*/
public class ZoomerCompat {
private static final int DEFAULT_SHORT_ANIMATION_DURATION = 200;
/**
* The interpolator, used for making zooms animate 'naturally.'
*/
private Interpolator mInterpolator;
/**
* The total animation duration for a zoom.
*/
private long mAnimationDurationMillis;
/**
* Whether or not the current zoom has finished.
*/
private boolean mFinished = true;
/**
* The current zoom value; computed by {@link #computeZoom()}.
*/
private float mCurrentZoom;
/**
* The time the zoom started, computed using {@link android.os.SystemClock#elapsedRealtime()}.
*/
private long mStartRTC;
/**
* The destination zoom factor.
*/
private float mEndZoom;
public ZoomerCompat(Context context) {
mInterpolator = new DecelerateInterpolator();
// TODO: use constant
mAnimationDurationMillis = DEFAULT_SHORT_ANIMATION_DURATION;
}
/**
* Forces the zoom finished state to the given value. Unlike {@link #abortAnimation()}, the current zoom value isn't
* set to the ending value.
*
* @see android.widget.Scroller#forceFinished(boolean)
*/
public void forceFinished(boolean finished) {
mFinished = finished;
}
/**
* Aborts the animation, setting the current zoom value to the ending value.
*
* @see android.widget.Scroller#abortAnimation()
*/
public void abortAnimation() {
mFinished = true;
mCurrentZoom = mEndZoom;
}
/**
* Starts a zoom from 1.0 to (1.0 + endZoom). That is, to zoom from 100% to 125%, endZoom should by 0.25f.
*
* @see android.widget.Scroller#startScroll(int, int, int, int)
*/
public void startZoom(float endZoom) {
mStartRTC = SystemClock.elapsedRealtime();
mEndZoom = endZoom;
mFinished = false;
mCurrentZoom = 1f;
}
/**
* Computes the current zoom level, returning true if the zoom is still active and false if the zoom has finished.
*
* @see android.widget.Scroller#computeScrollOffset()
*/
public boolean computeZoom() {
if (mFinished) {
return false;
}
long tRTC = SystemClock.elapsedRealtime() - mStartRTC;
if (tRTC >= mAnimationDurationMillis) {
mFinished = true;
mCurrentZoom = mEndZoom;
return false;
}
float t = tRTC * 1f / mAnimationDurationMillis;
mCurrentZoom = mEndZoom * mInterpolator.getInterpolation(t);
return true;
}
/**
* Returns the current zoom level.
*
* @see android.widget.Scroller#getCurrX()
*/
public float getCurrZoom() {
return mCurrentZoom;
}
}
@@ -0,0 +1,10 @@
package lecho.lib.hellocharts.listener;
import lecho.lib.hellocharts.model.BubbleValue;
public interface BubbleChartOnValueSelectListener extends OnValueDeselectListener {
public void onValueSelected(int bubbleIndex, BubbleValue value);
}
@@ -0,0 +1,10 @@
package lecho.lib.hellocharts.listener;
import lecho.lib.hellocharts.model.SubcolumnValue;
public interface ColumnChartOnValueSelectListener extends OnValueDeselectListener {
public void onValueSelected(int columnIndex, int subcolumnIndex, SubcolumnValue value);
}
@@ -0,0 +1,13 @@
package lecho.lib.hellocharts.listener;
import lecho.lib.hellocharts.model.PointValue;
import lecho.lib.hellocharts.model.SubcolumnValue;
public interface ComboLineColumnChartOnValueSelectListener extends OnValueDeselectListener {
public void onColumnValueSelected(int columnIndex, int subcolumnIndex, SubcolumnValue value);
public void onPointValueSelected(int lineIndex, int pointIndex, PointValue value);
}
@@ -0,0 +1,17 @@
package lecho.lib.hellocharts.listener;
import lecho.lib.hellocharts.model.BubbleValue;
public class DummyBubbleChartOnValueSelectListener implements BubbleChartOnValueSelectListener {
@Override
public void onValueSelected(int bubbleIndex, BubbleValue value) {
}
@Override
public void onValueDeselected() {
}
}
@@ -0,0 +1,18 @@
package lecho.lib.hellocharts.listener;
import lecho.lib.hellocharts.model.SubcolumnValue;
public class DummyColumnChartOnValueSelectListener implements ColumnChartOnValueSelectListener {
@Override
public void onValueSelected(int columnIndex, int subcolumnIndex, SubcolumnValue value) {
}
@Override
public void onValueDeselected() {
}
}
@@ -0,0 +1,23 @@
package lecho.lib.hellocharts.listener;
import lecho.lib.hellocharts.model.PointValue;
import lecho.lib.hellocharts.model.SubcolumnValue;
public class DummyCompoLineColumnChartOnValueSelectListener implements ComboLineColumnChartOnValueSelectListener {
@Override
public void onColumnValueSelected(int columnIndex, int subcolumnIndex, SubcolumnValue value) {
}
@Override
public void onPointValueSelected(int lineIndex, int pointIndex, PointValue value) {
}
@Override
public void onValueDeselected() {
}
}
@@ -0,0 +1,17 @@
package lecho.lib.hellocharts.listener;
import lecho.lib.hellocharts.model.PointValue;
public class DummyLineChartOnValueSelectListener implements LineChartOnValueSelectListener {
@Override
public void onValueSelected(int lineIndex, int pointIndex, PointValue value) {
}
@Override
public void onValueDeselected() {
}
}
@@ -0,0 +1,17 @@
package lecho.lib.hellocharts.listener;
import lecho.lib.hellocharts.model.SliceValue;
public class DummyPieChartOnValueSelectListener implements PieChartOnValueSelectListener {
@Override
public void onValueSelected(int arcIndex, SliceValue value) {
}
@Override
public void onValueDeselected() {
}
}
@@ -0,0 +1,11 @@
package lecho.lib.hellocharts.listener;
import lecho.lib.hellocharts.model.Viewport;
public class DummyVieportChangeListener implements ViewportChangeListener {
@Override
public void onViewportChanged(Viewport viewport) {
// Do nothing
}
}
@@ -0,0 +1,10 @@
package lecho.lib.hellocharts.listener;
import lecho.lib.hellocharts.model.PointValue;
public interface LineChartOnValueSelectListener extends OnValueDeselectListener {
public void onValueSelected(int lineIndex, int pointIndex, PointValue value);
}
@@ -0,0 +1,11 @@
package lecho.lib.hellocharts.listener;
public interface OnValueDeselectListener {
/**
* Called only in chart selection mode when user touch empty space causing value deselection.
* Note: this method is not called when selection mode is disabled.
*/
public void onValueDeselected();
}
@@ -0,0 +1,10 @@
package lecho.lib.hellocharts.listener;
import lecho.lib.hellocharts.model.SliceValue;
public interface PieChartOnValueSelectListener extends OnValueDeselectListener {
public void onValueSelected(int arcIndex, SliceValue value);
}
@@ -0,0 +1,17 @@
package lecho.lib.hellocharts.listener;
import lecho.lib.hellocharts.model.Viewport;
/**
* Use implementations of this listener to be notified when chart viewport changed. For now it works only for preview
* charts. To make it works for other chart types you just need to uncomment last line in
* {@link lecho.lib.hellocharts.computator.ChartComputator#constrainViewport(float, float, float, float)} method.
*/
public interface ViewportChangeListener {
/**
* Called when current viewport of chart changed. You should not modify that viewport.
*/
public void onViewportChanged(Viewport viewport);
}
@@ -0,0 +1,166 @@
package lecho.lib.hellocharts.model;
import android.graphics.Color;
import android.graphics.Typeface;
import lecho.lib.hellocharts.util.ChartUtils;
/**
* Base class for most chart data models.
*/
public abstract class AbstractChartData implements ChartData {
public static final int DEFAULT_TEXT_SIZE_SP = 12;
protected Axis axisXBottom;
protected Axis axisYLeft;
protected Axis axisXTop;
protected Axis axisYRight;
protected int valueLabelTextColor = Color.WHITE;
protected int valueLabelTextSize = DEFAULT_TEXT_SIZE_SP;
protected Typeface valueLabelTypeface;
/**
* If true each value label will have background rectangle
*/
protected boolean isValueLabelBackgroundEnabled = true;
/**
* If true and {@link #isValueLabelBackgroundEnabled} is true each label will have background rectangle and that
* rectangle will be filled with color specified for given value.
*/
protected boolean isValueLabelBackgrountAuto = true;
/**
* If {@link #isValueLabelBackgroundEnabled} is true and {@link #isValueLabelBackgrountAuto} is false each label
* will have background rectangle and that rectangle will be filled with this color. Helpful if you want all labels
* to have the same background color.
*/
protected int valueLabelBackgroundColor = ChartUtils.darkenColor(ChartUtils.DEFAULT_DARKEN_COLOR);
public AbstractChartData() {
}
/**
* Copy constructor for deep copy.
*
* @param data
*/
public AbstractChartData(AbstractChartData data) {
if (null != data.axisXBottom) {
this.axisXBottom = new Axis(data.axisXBottom);
}
if (null != data.axisXTop) {
this.axisXTop = new Axis(data.axisXTop);
}
if (null != data.axisYLeft) {
this.axisYLeft = new Axis(data.axisYLeft);
}
if (null != data.axisYRight) {
this.axisYRight = new Axis(data.axisYRight);
}
this.valueLabelTextColor = data.valueLabelTextColor;
this.valueLabelTextSize = data.valueLabelTextSize;
this.valueLabelTypeface = data.valueLabelTypeface;
}
@Override
public Axis getAxisXBottom() {
return axisXBottom;
}
@Override
public void setAxisXBottom(Axis axisX) {
this.axisXBottom = axisX;
}
@Override
public Axis getAxisYLeft() {
return axisYLeft;
}
@Override
public void setAxisYLeft(Axis axisY) {
this.axisYLeft = axisY;
}
@Override
public Axis getAxisXTop() {
return axisXTop;
}
@Override
public void setAxisXTop(Axis axisX) {
this.axisXTop = axisX;
}
@Override
public Axis getAxisYRight() {
return axisYRight;
}
@Override
public void setAxisYRight(Axis axisY) {
this.axisYRight = axisY;
}
@Override
public int getValueLabelTextColor() {
return valueLabelTextColor;
}
@Override
public void setValueLabelsTextColor(int valueLabelTextColor) {
this.valueLabelTextColor = valueLabelTextColor;
}
@Override
public int getValueLabelTextSize() {
return valueLabelTextSize;
}
@Override
public void setValueLabelTextSize(int valueLabelTextSize) {
this.valueLabelTextSize = valueLabelTextSize;
}
@Override
public Typeface getValueLabelTypeface() {
return valueLabelTypeface;
}
@Override
public void setValueLabelTypeface(Typeface typeface) {
this.valueLabelTypeface = typeface;
}
@Override
public boolean isValueLabelBackgroundEnabled() {
return isValueLabelBackgroundEnabled;
}
@Override
public void setValueLabelBackgroundEnabled(boolean isValueLabelBackgroundEnabled) {
this.isValueLabelBackgroundEnabled = isValueLabelBackgroundEnabled;
}
@Override
public boolean isValueLabelBackgroundAuto() {
return isValueLabelBackgrountAuto;
}
@Override
public void setValueLabelBackgroundAuto(boolean isValueLabelBackgrountAuto) {
this.isValueLabelBackgrountAuto = isValueLabelBackgrountAuto;
}
@Override
public int getValueLabelBackgroundColor() {
return valueLabelBackgroundColor;
}
@Override
public void setValueLabelBackgroundColor(int valueLabelBackgroundColor) {
this.valueLabelBackgroundColor = valueLabelBackgroundColor;
}
}
@@ -0,0 +1,305 @@
package lecho.lib.hellocharts.model;
import android.graphics.Color;
import android.graphics.Typeface;
import java.util.ArrayList;
import java.util.List;
import lecho.lib.hellocharts.formatter.AxisValueFormatter;
import lecho.lib.hellocharts.formatter.SimpleAxisValueFormatter;
import lecho.lib.hellocharts.util.ChartUtils;
/**
* Single axis model. By default axis is auto-generated. Use {@link #setAutoGenerated(boolean)} to disable axis values
* generation and set values manually using {@link #setValues(List)}. If Axis is auto-generated {@link #setValues(List)}
* will be ignored but if you set some values Axis will switch to manual mode. Change how axis labels are displayed by
* changing formatter {@link #setFormatter(lecho.lib.hellocharts.formatter.AxisValueFormatter)}. Axis can have a name
* that should be displayed next to
* labels(that depends on renderer implementation), you can change name using {@link #setName(String)}, by default axis
* name is empty and therefore not displayed.
*/
public class Axis {
public static final int DEFAULT_TEXT_SIZE_SP = 12;
public static final int DEFAULT_MAX_AXIS_LABEL_CHARS = 3;
/**
* Text size for axis labels and name.
*/
private int textSize = DEFAULT_TEXT_SIZE_SP;
/**
* Maximum number of characters used for this axis. Used to determine axis dimensions.
*/
private int maxLabelChars = DEFAULT_MAX_AXIS_LABEL_CHARS;
/**
* Axis values, each value will be used to calculate its label position.
*/
private List<AxisValue> values = new ArrayList<AxisValue>();
/**
* Name for this axis.
*/
private String name;
/**
* If true axis will be generated to automatically fit chart ranges. *
*/
private boolean isAutoGenerated = true;
/**
* If true renderer will draw lines(grid) for this axis.
*/
private boolean hasLines = false;
/**
* If true axis labels will be drown inside chart area.
*/
private boolean isInside = false;
/**
* Axis labels and name text color.
*/
private int textColor = Color.LTGRAY;
/**
* Axis grid lines color.
*/
private int lineColor = ChartUtils.DEFAULT_DARKEN_COLOR;
/**
* Typeface for labels and name text.
*/
private Typeface typeface;
/**
* Formatter used to format labels.
*/
private AxisValueFormatter formatter = new SimpleAxisValueFormatter();
/**
* If true draws a line between the labels and the graph *
*/
private boolean hasSeparationLine = true;
private boolean hasTiltedLabels = false;
/**
* Creates auto-generated axis without name and with default formatter.
*/
public Axis() {
}
/**
* Creates axis with given values(not auto-generated) without name and with default formatter.
*/
public Axis(List<AxisValue> values) {
setValues(values);
}
public Axis(Axis axis) {
this.name = axis.name;
this.isAutoGenerated = axis.isAutoGenerated;
this.hasLines = axis.hasLines;
this.isInside = axis.isInside;
this.textColor = axis.textColor;
this.lineColor = axis.lineColor;
this.textSize = axis.textSize;
this.maxLabelChars = axis.maxLabelChars;
this.typeface = axis.typeface;
this.formatter = axis.formatter;
this.hasSeparationLine = axis.hasSeparationLine;
for (AxisValue axisValue : axis.values) {
this.values.add(new AxisValue(axisValue));
}
}
/**
* Generates Axis with values from start to stop inclusive.
*/
public static Axis generateAxisFromRange(float start, float stop, float step) {
List<AxisValue> values = new ArrayList<AxisValue>();
for (float value = start; value <= stop; value += step) {
AxisValue axisValue = new AxisValue(value);
values.add(axisValue);
}
Axis axis = new Axis(values);
return axis;
}
/**
* Generates Axis with values from given list.
*/
public static Axis generateAxisFromCollection(List<Float> axisValues) {
List<AxisValue> values = new ArrayList<AxisValue>();
int index = 0;
for (float value : axisValues) {
AxisValue axisValue = new AxisValue(value);
values.add(axisValue);
++index;
}
Axis axis = new Axis(values);
return axis;
}
/**
* Generates Axis with values and labels from given lists, both lists must have the same size.
*/
public static Axis generateAxisFromCollection(List<Float> axisValues, List<String> axisValuesLabels) {
if (axisValues.size() != axisValuesLabels.size()) {
throw new IllegalArgumentException("Values and labels lists must have the same size!");
}
List<AxisValue> values = new ArrayList<AxisValue>();
int index = 0;
for (float value : axisValues) {
AxisValue axisValue = new AxisValue(value).setLabel(axisValuesLabels.get(index));
values.add(axisValue);
++index;
}
Axis axis = new Axis(values);
return axis;
}
public List<AxisValue> getValues() {
return values;
}
public Axis setValues(List<AxisValue> values) {
if (null == values) {
this.values = new ArrayList<AxisValue>();
} else {
this.values = values;
}
this.isAutoGenerated = false;
return this;
}
public String getName() {
return name;
}
public Axis setName(String name) {
this.name = name;
return this;
}
public boolean isAutoGenerated() {
return isAutoGenerated;
}
public Axis setAutoGenerated(boolean isAutoGenerated) {
this.isAutoGenerated = isAutoGenerated;
return this;
}
public boolean hasLines() {
return hasLines;
}
public Axis setHasLines(boolean hasLines) {
this.hasLines = hasLines;
return this;
}
public int getTextColor() {
return textColor;
}
public Axis setTextColor(int color) {
this.textColor = color;
return this;
}
/**
* @see #setInside(boolean)
*/
public boolean isInside() {
return isInside;
}
/**
* Set to true if you want axis values to be drawn inside chart area(axis name still will be drawn outside), by
* default this is set to false and axis is drawn outside chart area.
*/
public Axis setInside(boolean isInside) {
this.isInside = isInside;
return this;
}
public int getLineColor() {
return lineColor;
}
public Axis setLineColor(int lineColor) {
this.lineColor = lineColor;
return this;
}
public int getTextSize() {
return textSize;
}
public Axis setTextSize(int textSize) {
this.textSize = textSize;
return this;
}
public int getMaxLabelChars() {
return maxLabelChars;
}
/**
* Set maximum number of characters for axis labels, min 0, max 32.
*/
public Axis setMaxLabelChars(int maxLabelChars) {
if (maxLabelChars < 0) {
maxLabelChars = 0;
} else if (maxLabelChars > 32) {
maxLabelChars = 32;
}
this.maxLabelChars = maxLabelChars;
return this;
}
public Typeface getTypeface() {
return typeface;
}
public Axis setTypeface(Typeface typeface) {
this.typeface = typeface;
return this;
}
public AxisValueFormatter getFormatter() {
return formatter;
}
public Axis setFormatter(AxisValueFormatter formatter) {
if (null == formatter) {
this.formatter = new SimpleAxisValueFormatter();
} else {
this.formatter = formatter;
}
return this;
}
/**
* Set true if you want to draw separation line for this axis, set false to hide separation line, by default true.
*/
public Axis setHasSeparationLine(boolean hasSeparationLine) {
this.hasSeparationLine = hasSeparationLine;
return this;
}
public boolean hasSeparationLine() {
return hasSeparationLine;
}
public boolean hasTiltedLabels() {
return hasTiltedLabels;
}
public Axis setHasTiltedLabels(boolean hasTiltedLabels) {
this.hasTiltedLabels = hasTiltedLabels;
return this;
}
}
@@ -0,0 +1,86 @@
package lecho.lib.hellocharts.model;
import java.util.Arrays;
/**
* Single axis value, use it to manually set axis labels position. You can use label attribute to display text instead
* of number but value formatter implementation have to handle it.
*/
public class AxisValue {
private float value;
private char[] label;
public AxisValue(float value) {
setValue(value);
}
@Deprecated
public AxisValue(float value, char[] label) {
this.value = value;
this.label = label;
}
public AxisValue(AxisValue axisValue) {
this.value = axisValue.value;
this.label = axisValue.label;
}
public float getValue() {
return value;
}
public AxisValue setValue(float value) {
this.value = value;
return this;
}
@Deprecated
public char[] getLabel() {
return label;
}
/**
* Set custom label for this axis value.
*
* @param label
*/
public AxisValue setLabel(String label) {
this.label = label.toCharArray();
return this;
}
public char[] getLabelAsChars() {
return label;
}
/**
* Set custom label for this axis value.
*
* @param label
*/
@Deprecated
public AxisValue setLabel(char[] label) {
this.label = label;
return this;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AxisValue axisValue = (AxisValue) o;
if (Float.compare(axisValue.value, value) != 0) return false;
if (!Arrays.equals(label, axisValue.label)) return false;
return true;
}
@Override
public int hashCode() {
int result = (value != +0.0f ? Float.floatToIntBits(value) : 0);
result = 31 * result + (label != null ? Arrays.hashCode(label) : 0);
return result;
}
}
@@ -0,0 +1,162 @@
package lecho.lib.hellocharts.model;
import java.util.ArrayList;
import java.util.List;
import lecho.lib.hellocharts.formatter.BubbleChartValueFormatter;
import lecho.lib.hellocharts.formatter.SimpleBubbleChartValueFormatter;
import lecho.lib.hellocharts.view.Chart;
/**
* Data for BubbleChart.
*/
public class BubbleChartData extends AbstractChartData {
public static final int DEFAULT_MIN_BUBBLE_RADIUS_DP = 6;
public static final float DEFAULT_BUBBLE_SCALE = 1f;
private BubbleChartValueFormatter formatter = new SimpleBubbleChartValueFormatter();
private boolean hasLabels = false;
private boolean hasLabelsOnlyForSelected = false;
private int minBubbleRadius = DEFAULT_MIN_BUBBLE_RADIUS_DP;
private float bubbleScale = DEFAULT_BUBBLE_SCALE;
// TODO: consider Collections.emptyList()
private List<BubbleValue> values = new ArrayList<BubbleValue>();
public BubbleChartData() {
}
public BubbleChartData(List<BubbleValue> values) {
setValues(values);
}
/**
* Copy constructor for deep copy.
*/
public BubbleChartData(BubbleChartData data) {
super(data);
this.formatter = data.formatter;
this.hasLabels = data.hasLabels;
this.hasLabelsOnlyForSelected = data.hasLabelsOnlyForSelected;
this.minBubbleRadius = data.minBubbleRadius;
this.bubbleScale = data.bubbleScale;
for (BubbleValue bubbleValue : data.getValues()) {
this.values.add(new BubbleValue(bubbleValue));
}
}
public static BubbleChartData generateDummyData() {
final int numValues = 4;
BubbleChartData data = new BubbleChartData();
List<BubbleValue> values = new ArrayList<BubbleValue>(numValues);
values.add(new BubbleValue(0, 20, 15000));
values.add(new BubbleValue(3, 22, 20000));
values.add(new BubbleValue(5, 25, 5000));
values.add(new BubbleValue(7, 30, 30000));
values.add(new BubbleValue(11, 22, 10));
data.setValues(values);
return data;
}
@Override
public void update(float scale) {
for (BubbleValue value : values) {
value.update(scale);
}
}
@Override
public void finish() {
for (BubbleValue value : values) {
value.finish();
}
}
public List<BubbleValue> getValues() {
return values;
}
public BubbleChartData setValues(List<BubbleValue> values) {
if (null == values) {
this.values = new ArrayList<BubbleValue>();
} else {
this.values = values;
}
return this;
}
public boolean hasLabels() {
return hasLabels;
}
public BubbleChartData setHasLabels(boolean hasLabels) {
this.hasLabels = hasLabels;
if (hasLabels) {
hasLabelsOnlyForSelected = false;
}
return this;
}
/**
* @see #setHasLabelsOnlyForSelected(boolean)
*/
public boolean hasLabelsOnlyForSelected() {
return hasLabelsOnlyForSelected;
}
/**
* Set true if you want to show value labels only for selected value, works best when chart has
* isValueSelectionEnabled set to true {@link Chart#setValueSelectionEnabled(boolean)}.
*/
public BubbleChartData setHasLabelsOnlyForSelected(boolean hasLabelsOnlyForSelected) {
this.hasLabelsOnlyForSelected = hasLabelsOnlyForSelected;
if (hasLabelsOnlyForSelected) {
this.hasLabels = false;
}
return this;
}
/**
* Returns minimal bubble radius in dp.
*
* @see #setMinBubbleRadius(int)
*/
public int getMinBubbleRadius() {
return minBubbleRadius;
}
/**
* Set minimal bubble radius in dp, helpful when you want small bubbles(bubbles with very small z values compared to
* other bubbles) to be visible on chart, default 6dp
*/
public void setMinBubbleRadius(int minBubbleRadius) {
this.minBubbleRadius = minBubbleRadius;
}
/**
* Returns bubble scale which is used to adjust bubble size.
*
* @see #setBubbleScale(float)
*/
public float getBubbleScale() {
return bubbleScale;
}
/**
* Set bubble scale which is used to adjust bubble size. If you want smaller bubbles set scale {@code <0, 1>},
* if you want bigger bubbles set scale greater than 1, default is 1.0f.
*/
public void setBubbleScale(float bubbleScale) {
this.bubbleScale = bubbleScale;
}
public BubbleChartValueFormatter getFormatter() {
return formatter;
}
public BubbleChartData setFormatter(BubbleChartValueFormatter formatter) {
if (null != formatter) {
this.formatter = formatter;
}
return this;
}
}
@@ -0,0 +1,212 @@
package lecho.lib.hellocharts.model;
import java.util.Arrays;
import lecho.lib.hellocharts.util.ChartUtils;
import lecho.lib.hellocharts.view.Chart;
/**
* Single value drawn as bubble on BubbleChart.
*/
public class BubbleValue {
/**
* Current X value.
*/
private float x;
/**
* Current Y value.
*/
private float y;
/**
* Current Z value , third bubble value interpreted as bubble area.
*/
private float z;
/**
* Origin X value, used during value animation.
*/
private float originX;
/**
* Origin Y value, used during value animation.
*/
private float originY;
/**
* Origin Z value, used during value animation.
*/
private float originZ;
/**
* Difference between originX value and target X value.
*/
private float diffX;
/**
* Difference between originX value and target X value.
*/
private float diffY;
/**
* Difference between originX value and target X value.
*/
private float diffZ;
private int color = ChartUtils.DEFAULT_COLOR;
private int darkenColor = ChartUtils.DEFAULT_DARKEN_COLOR;
private ValueShape shape = ValueShape.CIRCLE;
private char[] label;
public BubbleValue() {
set(0, 0, 0);
}
public BubbleValue(float x, float y, float z) {
set(x, y, z);
}
public BubbleValue(float x, float y, float z, int color) {
set(x, y, z);
setColor(color);
}
public BubbleValue(BubbleValue bubbleValue) {
set(bubbleValue.x, bubbleValue.y, bubbleValue.z);
setColor(bubbleValue.color);
this.label = bubbleValue.label;
}
public void update(float scale) {
x = originX + diffX * scale;
y = originY + diffY * scale;
z = originZ + diffZ * scale;
}
public void finish() {
set(originX + diffX, originY + diffY, originZ + diffZ);
}
public BubbleValue set(float x, float y, float z) {
this.x = x;
this.y = y;
this.z = z;
this.originX = x;
this.originY = y;
this.originZ = z;
this.diffX = 0;
this.diffY = 0;
this.diffZ = 0;
return this;
}
/**
* Set target values that should be reached when data animation finish then call {@link Chart#startDataAnimation()}
*/
public BubbleValue setTarget(float targetX, float targetY, float targetZ) {
set(x, y, z);
this.diffX = targetX - originX;
this.diffY = targetY - originY;
this.diffZ = targetZ - originZ;
return this;
}
public float getX() {
return this.x;
}
public float getY() {
return this.y;
}
public float getZ() {
return this.z;
}
public int getColor() {
return color;
}
public BubbleValue setColor(int color) {
this.color = color;
this.darkenColor = ChartUtils.darkenColor(color);
return this;
}
public int getDarkenColor() {
return darkenColor;
}
public ValueShape getShape() {
return shape;
}
public BubbleValue setShape(ValueShape shape) {
this.shape = shape;
return this;
}
@Deprecated
public char[] getLabel() {
return label;
}
public BubbleValue setLabel(String label) {
this.label = label.toCharArray();
return this;
}
public char[] getLabelAsChars() {
return label;
}
@Deprecated
public BubbleValue setLabel(char[] label) {
this.label = label;
return this;
}
@Override
public String toString() {
return "BubbleValue [x=" + x + ", y=" + y + ", z=" + z + "]";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BubbleValue that = (BubbleValue) o;
if (color != that.color) return false;
if (darkenColor != that.darkenColor) return false;
if (Float.compare(that.diffX, diffX) != 0) return false;
if (Float.compare(that.diffY, diffY) != 0) return false;
if (Float.compare(that.diffZ, diffZ) != 0) return false;
if (Float.compare(that.originX, originX) != 0) return false;
if (Float.compare(that.originY, originY) != 0) return false;
if (Float.compare(that.originZ, originZ) != 0) return false;
if (Float.compare(that.x, x) != 0) return false;
if (Float.compare(that.y, y) != 0) return false;
if (Float.compare(that.z, z) != 0) return false;
if (!Arrays.equals(label, that.label)) return false;
if (shape != that.shape) return false;
return true;
}
@Override
public int hashCode() {
int result = (x != +0.0f ? Float.floatToIntBits(x) : 0);
result = 31 * result + (y != +0.0f ? Float.floatToIntBits(y) : 0);
result = 31 * result + (z != +0.0f ? Float.floatToIntBits(z) : 0);
result = 31 * result + (originX != +0.0f ? Float.floatToIntBits(originX) : 0);
result = 31 * result + (originY != +0.0f ? Float.floatToIntBits(originY) : 0);
result = 31 * result + (originZ != +0.0f ? Float.floatToIntBits(originZ) : 0);
result = 31 * result + (diffX != +0.0f ? Float.floatToIntBits(diffX) : 0);
result = 31 * result + (diffY != +0.0f ? Float.floatToIntBits(diffY) : 0);
result = 31 * result + (diffZ != +0.0f ? Float.floatToIntBits(diffZ) : 0);
result = 31 * result + color;
result = 31 * result + darkenColor;
result = 31 * result + (shape != null ? shape.hashCode() : 0);
result = 31 * result + (label != null ? Arrays.hashCode(label) : 0);
return result;
}
}
@@ -0,0 +1,134 @@
package lecho.lib.hellocharts.model;
import android.graphics.Typeface;
/**
* Base interface for all chart data models.
*/
public interface ChartData {
/**
* Updates data by scale during animation.
*
* @param scale value from 0 to 1.0
*/
public void update(float scale);
/**
* Inform data that animation finished(data should be update with scale 1.0f).
*/
public void finish();
/**
* @see #setAxisXBottom(Axis)
*/
public Axis getAxisXBottom();
/**
* Set horizontal axis at the bottom of the chart. Pass null to remove that axis.
*
* @param axisX
*/
public void setAxisXBottom(Axis axisX);
/**
* @see #setAxisYLeft(Axis)
*/
public Axis getAxisYLeft();
/**
* Set vertical axis on the left of the chart. Pass null to remove that axis.
*
* @param axisY
*/
public void setAxisYLeft(Axis axisY);
/**
* @see #setAxisXTop(Axis)
*/
public Axis getAxisXTop();
/**
* Set horizontal axis at the top of the chart. Pass null to remove that axis.
*
* @param axisX
*/
public void setAxisXTop(Axis axisX);
/**
* @see #setAxisYRight(Axis)
*/
public Axis getAxisYRight();
/**
* Set vertical axis on the right of the chart. Pass null to remove that axis.
*
* @param axisY
*/
public void setAxisYRight(Axis axisY);
/**
* Returns color used to draw value label text.
*/
public int getValueLabelTextColor();
/**
* Set value label text color, by default Color.WHITE.
*/
public void setValueLabelsTextColor(int labelsTextColor);
/**
* Returns text size for value label in SP units.
*/
public int getValueLabelTextSize();
/**
* Set text size for value label in SP units.
*/
public void setValueLabelTextSize(int labelsTextSize);
/**
* Returns Typeface for value labels.
*
* @return Typeface or null if Typeface is not set.
*/
public Typeface getValueLabelTypeface();
/**
* Set Typeface for all values labels.
*
* @param typeface
*/
public void setValueLabelTypeface(Typeface typeface);
/**
* @see #setValueLabelBackgroundEnabled(boolean)
*/
public boolean isValueLabelBackgroundEnabled();
/**
* Set whether labels should have rectangle background. Default is true.
*/
public void setValueLabelBackgroundEnabled(boolean isValueLabelBackgroundEnabled);
/**
* @see #setValueLabelBackgroundAuto(boolean)
*/
public boolean isValueLabelBackgroundAuto();
/**
* Set false if you want to set custom color for all value labels. Default is true.
*/
public void setValueLabelBackgroundAuto(boolean isValueLabelBackgrountAuto);
/**
* @see #setValueLabelBackgroundColor(int)
*/
public int getValueLabelBackgroundColor();
/**
* Set value labels background. This value is used only if isValueLabelBackgroundAuto returns false. Default is
* green.
*/
public void setValueLabelBackgroundColor(int valueLabelBackgroundColor);
}
@@ -0,0 +1,109 @@
package lecho.lib.hellocharts.model;
import java.util.ArrayList;
import java.util.List;
import lecho.lib.hellocharts.formatter.ColumnChartValueFormatter;
import lecho.lib.hellocharts.formatter.SimpleColumnChartValueFormatter;
import lecho.lib.hellocharts.view.Chart;
/**
* Single column for ColumnChart. One column can be divided into multiple sub-columns(ColumnValues) especially for
* stacked ColumnChart.
* Note: you can set X value for columns or sub-columns, columns are by default indexed from 0 to numOfColumns-1 and
* column index is used as column X value, so first column has X value 0, second clumn has X value 1 etc.
* If you want to display AxisValue for given column you should initialize AxisValue with X value of that column.
*/
public class Column {
private boolean hasLabels = false;
private boolean hasLabelsOnlyForSelected = false;
private ColumnChartValueFormatter formatter = new SimpleColumnChartValueFormatter();
// TODO: consider Collections.emptyList()
private List<SubcolumnValue> values = new ArrayList<SubcolumnValue>();
public Column() {
}
public Column(List<SubcolumnValue> values) {
setValues(values);
}
public Column(Column column) {
this.hasLabels = column.hasLabels;
this.hasLabelsOnlyForSelected = column.hasLabelsOnlyForSelected;
this.formatter = column.formatter;
for (SubcolumnValue columnValue : column.values) {
this.values.add(new SubcolumnValue(columnValue));
}
}
public void update(float scale) {
for (SubcolumnValue value : values) {
value.update(scale);
}
}
public void finish() {
for (SubcolumnValue value : values) {
value.finish();
}
}
public List<SubcolumnValue> getValues() {
return values;
}
public Column setValues(List<SubcolumnValue> values) {
if (null == values) {
this.values = new ArrayList<SubcolumnValue>();
} else {
this.values = values;
}
return this;
}
public boolean hasLabels() {
return hasLabels;
}
public Column setHasLabels(boolean hasLabels) {
this.hasLabels = hasLabels;
if (hasLabels) {
this.hasLabelsOnlyForSelected = false;
}
return this;
}
/**
* @see #setHasLabelsOnlyForSelected(boolean)
*/
public boolean hasLabelsOnlyForSelected() {
return hasLabelsOnlyForSelected;
}
/**
* Set true if you want to show value labels only for selected value, works best when chart has
* isValueSelectionEnabled set to true {@link Chart#setValueSelectionEnabled(boolean)}.
*/
public Column setHasLabelsOnlyForSelected(boolean hasLabelsOnlyForSelected) {
this.hasLabelsOnlyForSelected = hasLabelsOnlyForSelected;
if (hasLabelsOnlyForSelected) {
this.hasLabels = false;
}
return this;
}
public ColumnChartValueFormatter getFormatter() {
return formatter;
}
public Column setFormatter(ColumnChartValueFormatter formatter) {
if (null != formatter) {
this.formatter = formatter;
}
return this;
}
}
@@ -0,0 +1,137 @@
package lecho.lib.hellocharts.model;
import java.util.ArrayList;
import java.util.List;
/**
* Data model for column chart. Note: you can set X value for columns or sub-columns, columns are by default indexed
* from 0 to numOfColumns-1 and
* column index is used as column X value, so first column has X value 0, second clumn has X value 1 etc.
* If you want to display AxisValue for given column you should initialize AxisValue with X value of that column.
*/
public class ColumnChartData extends AbstractChartData {
public static final float DEFAULT_FILL_RATIO = 0.75f;
public static final float DEFAULT_BASE_VALUE = 0.0f;
private float fillRatio = DEFAULT_FILL_RATIO;
private float baseValue = DEFAULT_BASE_VALUE;
private List<Column> columns = new ArrayList<Column>();
private boolean isStacked = false;
public ColumnChartData() {
}
public ColumnChartData(List<Column> columns) {
setColumns(columns);
}
/**
* Copy constructor for deep copy.
*/
public ColumnChartData(ColumnChartData data) {
super(data);
this.isStacked = data.isStacked;
this.fillRatio = data.fillRatio;
for (Column column : data.columns) {
this.columns.add(new Column(column));
}
}
public static ColumnChartData generateDummyData() {
final int numColumns = 4;
ColumnChartData data = new ColumnChartData();
List<Column> columns = new ArrayList<Column>(numColumns);
List<SubcolumnValue> values;
Column column;
for (int i = 1; i <= numColumns; ++i) {
values = new ArrayList<SubcolumnValue>(numColumns);
values.add(new SubcolumnValue(i));
column = new Column(values);
columns.add(column);
}
data.setColumns(columns);
return data;
}
@Override
public void update(float scale) {
for (Column column : columns) {
column.update(scale);
}
}
@Override
public void finish() {
for (Column column : columns) {
column.finish();
}
}
public List<Column> getColumns() {
return columns;
}
public ColumnChartData setColumns(List<Column> columns) {
if (null == columns) {
this.columns = new ArrayList<Column>();
} else {
this.columns = columns;
}
return this;
}
public boolean isStacked() {
return isStacked;
}
/**
* Set true if you want stacked column chart.
*
* @param isStacked
* @return
*/
public ColumnChartData setStacked(boolean isStacked) {
this.isStacked = isStacked;
return this;
}
public float getFillRatio() {
return fillRatio;
}
/**
* Set fill ration for columns, value from 0 to 1, 1 means that there will be almost no free space between columns,
* 0 means that columns will have minimum width(2px).
*
* @param fillRatio
* @return
*/
public ColumnChartData setFillRatio(float fillRatio) {
if (fillRatio < 0) {
fillRatio = 0;
}
if (fillRatio > 1) {
fillRatio = 1;
}
this.fillRatio = fillRatio;
return this;
}
/**
* @see #setBaseValue(float)
*/
public float getBaseValue() {
return baseValue;
}
/**
* Set value below which values will be drawn as negative, by default 0.
*/
public ColumnChartData setBaseValue(float baseValue) {
this.baseValue = baseValue;
return this;
}
}
@@ -0,0 +1,71 @@
package lecho.lib.hellocharts.model;
/**
* Data model for combo line-column chart. It uses ColumnChartData and LineChartData internally.
*/
public class ComboLineColumnChartData extends AbstractChartData {
private ColumnChartData columnChartData;
private LineChartData lineChartData;
public ComboLineColumnChartData() {
this.columnChartData = new ColumnChartData();
this.lineChartData = new LineChartData();
}
public ComboLineColumnChartData(ColumnChartData columnChartData, LineChartData lineChartData) {
setColumnChartData(columnChartData);
setLineChartData(lineChartData);
}
public ComboLineColumnChartData(ComboLineColumnChartData data) {
super(data);
setColumnChartData(new ColumnChartData(data.getColumnChartData()));
setLineChartData(new LineChartData(data.getLineChartData()));
}
public static ComboLineColumnChartData generateDummyData() {
ComboLineColumnChartData data = new ComboLineColumnChartData();
data.setColumnChartData(ColumnChartData.generateDummyData());
data.setLineChartData(LineChartData.generateDummyData());
return data;
}
@Override
public void update(float scale) {
columnChartData.update(scale);
lineChartData.update(scale);
}
@Override
public void finish() {
columnChartData.finish();
lineChartData.finish();
}
public ColumnChartData getColumnChartData() {
return columnChartData;
}
public void setColumnChartData(ColumnChartData columnChartData) {
if (null == columnChartData) {
this.columnChartData = new ColumnChartData();
} else {
this.columnChartData = columnChartData;
}
}
public LineChartData getLineChartData() {
return lineChartData;
}
public void setLineChartData(LineChartData lineChartData) {
if (null == lineChartData) {
this.lineChartData = new LineChartData();
} else {
this.lineChartData = lineChartData;
}
}
}
@@ -0,0 +1,306 @@
package lecho.lib.hellocharts.model;
import android.graphics.PathEffect;
import java.util.ArrayList;
import java.util.List;
import lecho.lib.hellocharts.formatter.LineChartValueFormatter;
import lecho.lib.hellocharts.formatter.SimpleLineChartValueFormatter;
import lecho.lib.hellocharts.util.ChartUtils;
import lecho.lib.hellocharts.view.Chart;
/**
* Single line for line chart.
*/
public class Line {
private static final int DEFAULT_LINE_STROKE_WIDTH_DP = 3;
private static final int DEFAULT_POINT_RADIUS_DP = 6;
private static final int DEFAULT_AREA_TRANSPARENCY = 64;
public static final int UNINITIALIZED = 0;
private int color = ChartUtils.DEFAULT_COLOR;
private int pointColor = UNINITIALIZED;
private int darkenColor = ChartUtils.DEFAULT_DARKEN_COLOR;
/**
* Transparency of area when line is filled. *
*/
private int areaTransparency = DEFAULT_AREA_TRANSPARENCY;
private int strokeWidth = DEFAULT_LINE_STROKE_WIDTH_DP;
private int pointRadius = DEFAULT_POINT_RADIUS_DP;
private boolean hasGradientToTransparent = false;
private boolean hasPoints = true;
private boolean hasLines = true;
private boolean hasLabels = false;
private boolean hasLabelsOnlyForSelected = false;
private boolean isCubic = false;
private boolean isSquare = false;
private boolean isFilled = false;
private ValueShape shape = ValueShape.CIRCLE;
private PathEffect pathEffect;
private LineChartValueFormatter formatter = new SimpleLineChartValueFormatter();
private List<PointValue> values = new ArrayList<PointValue>();
public Line() {
}
public Line(List<PointValue> values) {
setValues(values);
}
public Line(Line line) {
this.color = line.color;
this.pointColor = line.pointColor;
this.darkenColor = line.darkenColor;
this.areaTransparency = line.areaTransparency;
this.strokeWidth = line.strokeWidth;
this.pointRadius = line.pointRadius;
this.hasGradientToTransparent = line.hasGradientToTransparent;
this.hasPoints = line.hasPoints;
this.hasLines = line.hasLines;
this.hasLabels = line.hasLabels;
this.hasLabelsOnlyForSelected = line.hasLabelsOnlyForSelected;
this.isSquare = line.isSquare;
this.isCubic = line.isCubic;
this.isFilled = line.isFilled;
this.shape = line.shape;
this.pathEffect = line.pathEffect;
this.formatter = line.formatter;
for (PointValue pointValue : line.values) {
this.values.add(new PointValue(pointValue));
}
}
public void update(float scale) {
for (PointValue value : values) {
value.update(scale);
}
}
public void finish() {
for (PointValue value : values) {
value.finish();
}
}
public List<PointValue> getValues() {
return this.values;
}
public void setValues(List<PointValue> values) {
if (null == values) {
this.values = new ArrayList<PointValue>();
} else {
this.values = values;
}
}
public int getColor() {
return color;
}
public Line setColor(int color) {
this.color = color;
if (pointColor == UNINITIALIZED) {
this.darkenColor = ChartUtils.darkenColor(color);
}
return this;
}
public int getPointColor() {
if (pointColor == UNINITIALIZED) {
return color;
} else {
return pointColor;
}
}
public Line setPointColor(int pointColor) {
this.pointColor = pointColor;
if (pointColor == UNINITIALIZED) {
this.darkenColor = ChartUtils.darkenColor(color);
} else {
this.darkenColor = ChartUtils.darkenColor(pointColor);
}
return this;
}
public int getDarkenColor() {
return darkenColor;
}
/**
* @see #setAreaTransparency(int)
*/
public int getAreaTransparency() {
return areaTransparency;
}
/**
* Set area transparency(255 is full opacity) for filled lines
*
* @param areaTransparency
* @return
*/
public Line setAreaTransparency(int areaTransparency) {
this.areaTransparency = areaTransparency;
return this;
}
public int getStrokeWidth() {
return strokeWidth;
}
public Line setStrokeWidth(int strokeWidth) {
this.strokeWidth = strokeWidth;
return this;
}
public boolean hasPoints() {
return hasPoints;
}
public Line setHasPoints(boolean hasPoints) {
this.hasPoints = hasPoints;
return this;
}
public boolean hasLines() {
return hasLines;
}
public Line setHasLines(boolean hasLines) {
this.hasLines = hasLines;
return this;
}
public boolean hasLabels() {
return hasLabels;
}
public Line setHasLabels(boolean hasLabels) {
this.hasLabels = hasLabels;
if (hasLabels) {
this.hasLabelsOnlyForSelected = false;
}
return this;
}
/**
* @see #setHasLabelsOnlyForSelected(boolean)
*/
public boolean hasLabelsOnlyForSelected() {
return hasLabelsOnlyForSelected;
}
/**
* Set true if you want to show value labels only for selected value, works best when chart has
* isValueSelectionEnabled set to true {@link Chart#setValueSelectionEnabled(boolean)}.
*/
public Line setHasLabelsOnlyForSelected(boolean hasLabelsOnlyForSelected) {
this.hasLabelsOnlyForSelected = hasLabelsOnlyForSelected;
if (hasLabelsOnlyForSelected) {
this.hasLabels = false;
}
return this;
}
public int getPointRadius() {
return pointRadius;
}
/**
* Set radius for points for this line.
*
* @param pointRadius
* @return
*/
public Line setPointRadius(int pointRadius) {
this.pointRadius = pointRadius;
return this;
}
public boolean getGradientToTransparent() {
return hasGradientToTransparent;
}
public Line setHasGradientToTransparent(boolean hasGradientToTransparent) {
this.hasGradientToTransparent = hasGradientToTransparent;
return this;
}
public boolean isCubic() {
return isCubic;
}
public Line setCubic(boolean isCubic) {
this.isCubic = isCubic;
if (isSquare)
setSquare(false);
return this;
}
public boolean isSquare() {
return isSquare;
}
public Line setSquare(boolean isSquare) {
this.isSquare = isSquare;
if (isCubic)
setCubic(false);
return this;
}
public boolean isFilled() {
return isFilled;
}
public Line setFilled(boolean isFilled) {
this.isFilled = isFilled;
return this;
}
/**
* @see #setShape(ValueShape)
*/
public ValueShape getShape() {
return shape;
}
/**
* Set shape for points, possible values: SQUARE, CIRCLE
*
* @param shape
* @return
*/
public Line setShape(ValueShape shape) {
this.shape = shape;
return this;
}
public PathEffect getPathEffect() {
return pathEffect;
}
/**
* Set path effect for this line, note: it will slow down drawing, try to not use complicated effects,
* DashPathEffect should be safe choice.
*
* @param pathEffect
*/
public void setPathEffect(PathEffect pathEffect) {
this.pathEffect = pathEffect;
}
public LineChartValueFormatter getFormatter() {
return formatter;
}
public Line setFormatter(LineChartValueFormatter formatter) {
if (null != formatter) {
this.formatter = formatter;
}
return this;
}
}
@@ -0,0 +1,92 @@
package lecho.lib.hellocharts.model;
import java.util.ArrayList;
import java.util.List;
/**
* Data model for LineChartView.
*/
public class LineChartData extends AbstractChartData {
public static final float DEFAULT_BASE_VALUE = 0.0f;
private List<Line> lines = new ArrayList<Line>();
private float baseValue = DEFAULT_BASE_VALUE;
public LineChartData() {
}
public LineChartData(List<Line> lines) {
setLines(lines);
}
/**
* Copy constructor to perform deep copy of chart data.
*/
public LineChartData(LineChartData data) {
super(data);
this.baseValue = data.baseValue;
for (Line line : data.lines) {
this.lines.add(new Line(line));
}
}
public static LineChartData generateDummyData() {
final int numValues = 4;
LineChartData data = new LineChartData();
List<PointValue> values = new ArrayList<PointValue>(numValues);
values.add(new PointValue(0, 2));
values.add(new PointValue(1, 4));
values.add(new PointValue(2, 3));
values.add(new PointValue(3, 4));
Line line = new Line(values);
List<Line> lines = new ArrayList<Line>(1);
lines.add(line);
data.setLines(lines);
return data;
}
@Override
public void update(float scale) {
for (Line line : lines) {
line.update(scale);
}
}
@Override
public void finish() {
for (Line line : lines) {
line.finish();
}
}
public List<Line> getLines() {
return lines;
}
public LineChartData setLines(List<Line> lines) {
if (null == lines) {
this.lines = new ArrayList<Line>();
} else {
this.lines = lines;
}
return this;
}
/**
* @see #setBaseValue(float)
*/
public float getBaseValue() {
return baseValue;
}
/**
* Set value below which values will be drawn as negative, important attribute for drawing filled area charts, by
* default 0.
*/
public LineChartData setBaseValue(float baseValue) {
this.baseValue = baseValue;
return this;
}
}
@@ -0,0 +1,300 @@
package lecho.lib.hellocharts.model;
import android.graphics.Color;
import android.graphics.Typeface;
import java.util.ArrayList;
import java.util.List;
import lecho.lib.hellocharts.formatter.PieChartValueFormatter;
import lecho.lib.hellocharts.formatter.SimplePieChartValueFormatter;
import lecho.lib.hellocharts.view.Chart;
import lecho.lib.hellocharts.view.PieChartView;
/**
* Data for PieChart, by default it doesn't have axes.
*/
public class PieChartData extends AbstractChartData {
public static final int DEFAULT_CENTER_TEXT1_SIZE_SP = 42;
public static final int DEFAULT_CENTER_TEXT2_SIZE_SP = 16;
public static final float DEFAULT_CENTER_CIRCLE_SCALE = 0.6f;
private static final int DEFAULT_SLICE_SPACING_DP = 2;
private int centerText1FontSize = DEFAULT_CENTER_TEXT1_SIZE_SP;
private int centerText2FontSize = DEFAULT_CENTER_TEXT2_SIZE_SP;
private float centerCircleScale = DEFAULT_CENTER_CIRCLE_SCALE;
private int slicesSpacing = DEFAULT_SLICE_SPACING_DP;
private PieChartValueFormatter formatter = new SimplePieChartValueFormatter();
private boolean hasLabels = false;
private boolean hasLabelsOnlyForSelected = false;
private boolean hasLabelsOutside = false;
private boolean hasCenterCircle = false;
private int centerCircleColor = Color.TRANSPARENT;
private int centerText1Color = Color.BLACK;
private Typeface centerText1Typeface;
private String centerText1;
private int centerText2Color = Color.BLACK;
private Typeface centerText2Typeface;
private String centerText2;
private List<SliceValue> values = new ArrayList<SliceValue>();
public PieChartData() {
setAxisXBottom(null);
setAxisYLeft(null);
}
public PieChartData(List<SliceValue> values) {
setValues(values);
// Empty axes. Pie chart don't need axes.
setAxisXBottom(null);
setAxisYLeft(null);
}
public PieChartData(PieChartData data) {
super(data);
this.formatter = data.formatter;
this.hasLabels = data.hasLabels;
this.hasLabelsOnlyForSelected = data.hasLabelsOnlyForSelected;
this.hasLabelsOutside = data.hasLabelsOutside;
this.hasCenterCircle = data.hasCenterCircle;
this.centerCircleColor = data.centerCircleColor;
this.centerCircleScale = data.centerCircleScale;
this.centerText1Color = data.centerText1Color;
this.centerText1FontSize = data.centerText1FontSize;
this.centerText1Typeface = data.centerText1Typeface;
this.centerText1 = data.centerText1;
this.centerText2Color = data.centerText2Color;
this.centerText2FontSize = data.centerText2FontSize;
this.centerText2Typeface = data.centerText2Typeface;
this.centerText2 = data.centerText2;
for (SliceValue sliceValue : data.values) {
this.values.add(new SliceValue(sliceValue));
}
}
public static PieChartData generateDummyData() {
final int numValues = 4;
PieChartData data = new PieChartData();
List<SliceValue> values = new ArrayList<SliceValue>(numValues);
values.add(new SliceValue(40f));
values.add(new SliceValue(20f));
values.add(new SliceValue(30f));
values.add(new SliceValue(50f));
data.setValues(values);
return data;
}
@Override
public void update(float scale) {
for (SliceValue value : values) {
value.update(scale);
}
}
@Override
public void finish() {
for (SliceValue value : values) {
value.finish();
}
}
/**
* PieChart does not support axes so method call will be ignored
*/
@Override
public void setAxisXBottom(Axis axisX) {
super.setAxisXBottom(null);
}
/**
* PieChart does not support axes so method call will be ignored
*/
@Override
public void setAxisYLeft(Axis axisY) {
super.setAxisYLeft(null);
}
public List<SliceValue> getValues() {
return values;
}
public PieChartData setValues(List<SliceValue> values) {
if (null == values) {
this.values = new ArrayList<SliceValue>();
} else {
this.values = values;
}
return this;
}
public boolean hasLabels() {
return hasLabels;
}
public PieChartData setHasLabels(boolean hasLabels) {
this.hasLabels = hasLabels;
if (hasLabels) {
hasLabelsOnlyForSelected = false;
}
return this;
}
/**
* @see #setHasLabelsOnlyForSelected(boolean)
*/
public boolean hasLabelsOnlyForSelected() {
return hasLabelsOnlyForSelected;
}
/**
* Set true if you want to show value labels only for selected value, works best when chart has
* isValueSelectionEnabled set to true {@link Chart#setValueSelectionEnabled(boolean)}.
*/
public PieChartData setHasLabelsOnlyForSelected(boolean hasLabelsOnlyForSelected) {
this.hasLabelsOnlyForSelected = hasLabelsOnlyForSelected;
if (hasLabelsOnlyForSelected) {
this.hasLabels = false;
}
return this;
}
public boolean hasLabelsOutside() {
return hasLabelsOutside;
}
/**
* Set if labels should be drawn inside circle(false) or outside(true). By default false. If you set it to true you
* should also change chart fill ration using {@link PieChartView#setCircleFillRatio(float)}. This flag is used only
* if you also set hasLabels or hasLabelsOnlyForSelected flags.
*/
public PieChartData setHasLabelsOutside(boolean hasLabelsOutside) {
this.hasLabelsOutside = hasLabelsOutside;
return this;
}
public boolean hasCenterCircle() {
return hasCenterCircle;
}
public PieChartData setHasCenterCircle(boolean hasCenterCircle) {
this.hasCenterCircle = hasCenterCircle;
return this;
}
public int getCenterCircleColor() {
return centerCircleColor;
}
public PieChartData setCenterCircleColor(int centerCircleColor) {
this.centerCircleColor = centerCircleColor;
return this;
}
public float getCenterCircleScale() {
return centerCircleScale;
}
public PieChartData setCenterCircleScale(float centerCircleScale) {
this.centerCircleScale = centerCircleScale;
return this;
}
public int getCenterText1Color() {
return centerText1Color;
}
public PieChartData setCenterText1Color(int centerText1Color) {
this.centerText1Color = centerText1Color;
return this;
}
public int getCenterText1FontSize() {
return centerText1FontSize;
}
public PieChartData setCenterText1FontSize(int centerText1FontSize) {
this.centerText1FontSize = centerText1FontSize;
return this;
}
public Typeface getCenterText1Typeface() {
return centerText1Typeface;
}
public PieChartData setCenterText1Typeface(Typeface text1Typeface) {
this.centerText1Typeface = text1Typeface;
return this;
}
public String getCenterText1() {
return centerText1;
}
public PieChartData setCenterText1(String centerText1) {
this.centerText1 = centerText1;
return this;
}
public String getCenterText2() {
return centerText2;
}
/**
* Note that centerText2 will be drawn only if centerText1 is not empty/null.
*/
public PieChartData setCenterText2(String centerText2) {
this.centerText2 = centerText2;
return this;
}
public int getCenterText2Color() {
return centerText2Color;
}
public PieChartData setCenterText2Color(int centerText2Color) {
this.centerText2Color = centerText2Color;
return this;
}
public int getCenterText2FontSize() {
return centerText2FontSize;
}
public PieChartData setCenterText2FontSize(int centerText2FontSize) {
this.centerText2FontSize = centerText2FontSize;
return this;
}
public Typeface getCenterText2Typeface() {
return centerText2Typeface;
}
public PieChartData setCenterText2Typeface(Typeface text2Typeface) {
this.centerText2Typeface = text2Typeface;
return this;
}
public int getSlicesSpacing() {
return slicesSpacing;
}
public PieChartData setSlicesSpacing(int sliceSpacing) {
this.slicesSpacing = sliceSpacing;
return this;
}
public PieChartValueFormatter getFormatter() {
return formatter;
}
public PieChartData setFormatter(PieChartValueFormatter formatter) {
if (null != formatter) {
this.formatter = formatter;
}
return this;
}
}
@@ -0,0 +1,124 @@
package lecho.lib.hellocharts.model;
import java.util.Arrays;
import lecho.lib.hellocharts.view.Chart;
/**
* Single point coordinates, used for LineChartData.
*/
public class PointValue {
private float x;
private float y;
private float originX;
private float originY;
private float diffX;
private float diffY;
private char[] label;
public PointValue() {
set(0, 0);
}
public PointValue(float x, float y) {
set(x, y);
}
public PointValue(PointValue pointValue) {
set(pointValue.x, pointValue.y);
this.label = pointValue.label;
}
public void update(float scale) {
x = originX + diffX * scale;
y = originY + diffY * scale;
}
public void finish() {
set(originX + diffX, originY + diffY);
}
public PointValue set(float x, float y) {
this.x = x;
this.y = y;
this.originX = x;
this.originY = y;
this.diffX = 0;
this.diffY = 0;
return this;
}
/**
* Set target values that should be reached when data animation finish then call {@link Chart#startDataAnimation()}
*/
public PointValue setTarget(float targetX, float targetY) {
set(x, y);
this.diffX = targetX - originX;
this.diffY = targetY - originY;
return this;
}
public float getX() {
return this.x;
}
public float getY() {
return this.y;
}
@Deprecated
public char[] getLabel() {
return label;
}
public PointValue setLabel(String label) {
this.label = label.toCharArray();
return this;
}
public char[] getLabelAsChars() {
return label;
}
@Deprecated
public PointValue setLabel(char[] label) {
this.label = label;
return this;
}
@Override
public String toString() {
return "PointValue [x=" + x + ", y=" + y + "]";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PointValue that = (PointValue) o;
if (Float.compare(that.diffX, diffX) != 0) return false;
if (Float.compare(that.diffY, diffY) != 0) return false;
if (Float.compare(that.originX, originX) != 0) return false;
if (Float.compare(that.originY, originY) != 0) return false;
if (Float.compare(that.x, x) != 0) return false;
if (Float.compare(that.y, y) != 0) return false;
if (!Arrays.equals(label, that.label)) return false;
return true;
}
@Override
public int hashCode() {
int result = (x != +0.0f ? Float.floatToIntBits(x) : 0);
result = 31 * result + (y != +0.0f ? Float.floatToIntBits(y) : 0);
result = 31 * result + (originX != +0.0f ? Float.floatToIntBits(originX) : 0);
result = 31 * result + (originY != +0.0f ? Float.floatToIntBits(originY) : 0);
result = 31 * result + (diffX != +0.0f ? Float.floatToIntBits(diffX) : 0);
result = 31 * result + (diffY != +0.0f ? Float.floatToIntBits(diffY) : 0);
result = 31 * result + (label != null ? Arrays.hashCode(label) : 0);
return result;
}
}
@@ -0,0 +1,132 @@
package lecho.lib.hellocharts.model;
/**
* Holds selected values indexes, i.e. for LineChartModel it will be firstIndex=lineIndex; secondIndex=valueIndex.
*/
public class SelectedValue {
/**
* First index i.e for LineChart that will be line index.
*/
private int firstIndex;
/**
* Second index i.e for LineChart that will be PointValue index.
*/
private int secondIndex;
/**
* Used only for combo charts, in other cases should have value NONE.
*/
private SelectedValueType type = SelectedValueType.NONE;
public SelectedValue() {
clear();
}
public SelectedValue(int firstIndex, int secondIndex, SelectedValueType type) {
set(firstIndex, secondIndex, type);
}
public void set(int firstIndex, int secondIndex, SelectedValueType type) {
this.firstIndex = firstIndex;
this.secondIndex = secondIndex;
if (null != type) {
this.type = type;
} else {
this.type = SelectedValueType.NONE;
}
}
public void set(SelectedValue selectedValue) {
this.firstIndex = selectedValue.firstIndex;
this.secondIndex = selectedValue.secondIndex;
this.type = selectedValue.type;
}
public void clear() {
set(Integer.MIN_VALUE, Integer.MIN_VALUE, SelectedValueType.NONE);
}
/**
* Return true if selected value have meaningful value.
*/
public boolean isSet() {
if (firstIndex >= 0 && secondIndex >= 0) {
return true;
} else {
return false;
}
}
/**
* First index i.e for LineChart that will be line index.
*/
public int getFirstIndex() {
return firstIndex;
}
public void setFirstIndex(int firstIndex) {
this.firstIndex = firstIndex;
}
/**
* Second index i.e for LineChart that will be PointValue index.
*/
public int getSecondIndex() {
return secondIndex;
}
public void setSecondIndex(int secondIndex) {
this.secondIndex = secondIndex;
}
public SelectedValueType getType() {
return type;
}
public void setType(SelectedValueType type) {
this.type = type;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + firstIndex;
result = prime * result + secondIndex;
result = prime * result + ((type == null) ? 0 : type.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
SelectedValue other = (SelectedValue) obj;
if (firstIndex != other.firstIndex)
return false;
if (secondIndex != other.secondIndex)
return false;
if (type != other.type)
return false;
return true;
}
@Override
public String toString() {
return "SelectedValue [firstIndex=" + firstIndex + ", secondIndex=" + secondIndex + ", type=" + type + "]";
}
/**
* Used in combo chart to determine if selected value is used for line or column selection.
*/
public enum SelectedValueType {
NONE, LINE, COLUMN
}
}
@@ -0,0 +1,177 @@
package lecho.lib.hellocharts.model;
import java.util.Arrays;
import lecho.lib.hellocharts.util.ChartUtils;
import lecho.lib.hellocharts.view.Chart;
/**
* Model representing single slice on PieChart.
*/
public class SliceValue {
private static final int DEFAULT_SLICE_SPACING_DP = 2;
@Deprecated
/** Spacing between this slice and its neighbors. */
private int sliceSpacing = DEFAULT_SLICE_SPACING_DP;
/**
* Current value of this slice.
*/
private float value;
/**
* Origin value of this slice, used during value animation.
*/
private float originValue;
/**
* Difference between originValue and targetValue.
*/
private float diff;
/**
* Color of this slice.
*/
private int color = ChartUtils.DEFAULT_COLOR;
/**
* Darken color used to draw label background and give touch feedback.
*/
private int darkenColor = ChartUtils.DEFAULT_DARKEN_COLOR;
/**
* Custom label for this slice, if not set number formatting will be used.
*/
private char[] label;
public SliceValue() {
setValue(0);
}
public SliceValue(float value) {
setValue(value);
}
public SliceValue(float value, int color) {
setValue(value);
setColor(color);
}
public SliceValue(float value, int color, int sliceSpacing) {
setValue(value);
setColor(color);
this.sliceSpacing = sliceSpacing;
}
public SliceValue(SliceValue sliceValue) {
setValue(sliceValue.value);
setColor(sliceValue.color);
this.sliceSpacing = sliceValue.sliceSpacing;
this.label = sliceValue.label;
}
public void update(float scale) {
value = originValue + diff * scale;
}
public void finish() {
setValue(originValue + diff);
}
public float getValue() {
return value;
}
public SliceValue setValue(float value) {
this.value = value;
this.originValue = value;
this.diff = 0;
return this;
}
/**
* Set target value that should be reached when data animation finish then call {@link Chart#startDataAnimation()}
*
* @param target
* @return
*/
public SliceValue setTarget(float target) {
setValue(value);
this.diff = target - originValue;
return this;
}
public int getColor() {
return color;
}
public SliceValue setColor(int color) {
this.color = color;
this.darkenColor = ChartUtils.darkenColor(color);
return this;
}
public int getDarkenColor() {
return darkenColor;
}
@Deprecated
public int getSliceSpacing() {
return sliceSpacing;
}
@Deprecated
public SliceValue setSliceSpacing(int sliceSpacing) {
this.sliceSpacing = sliceSpacing;
return this;
}
@Deprecated
public char[] getLabel() {
return label;
}
@Deprecated
public SliceValue setLabel(char[] label) {
this.label = label;
return this;
}
public SliceValue setLabel(String label) {
this.label = label.toCharArray();
return this;
}
public char[] getLabelAsChars() {
return label;
}
@Override
public String toString() {
return "SliceValue [value=" + value + "]";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SliceValue that = (SliceValue) o;
if (color != that.color) return false;
if (darkenColor != that.darkenColor) return false;
if (Float.compare(that.diff, diff) != 0) return false;
if (Float.compare(that.originValue, originValue) != 0) return false;
if (sliceSpacing != that.sliceSpacing) return false;
if (Float.compare(that.value, value) != 0) return false;
if (!Arrays.equals(label, that.label)) return false;
return true;
}
@Override
public int hashCode() {
int result = (value != +0.0f ? Float.floatToIntBits(value) : 0);
result = 31 * result + (originValue != +0.0f ? Float.floatToIntBits(originValue) : 0);
result = 31 * result + (diff != +0.0f ? Float.floatToIntBits(diff) : 0);
result = 31 * result + color;
result = 31 * result + darkenColor;
result = 31 * result + sliceSpacing;
result = 31 * result + (label != null ? Arrays.hashCode(label) : 0);
return result;
}
}
@@ -0,0 +1,138 @@
package lecho.lib.hellocharts.model;
import java.util.Arrays;
import lecho.lib.hellocharts.util.ChartUtils;
import lecho.lib.hellocharts.view.Chart;
/**
* Single sub-column value for ColumnChart.
*/
public class SubcolumnValue {
private float value;
private float originValue;
private float diff;
private int color = ChartUtils.DEFAULT_COLOR;
private int darkenColor = ChartUtils.DEFAULT_DARKEN_COLOR;
private char[] label;
public SubcolumnValue() {
setValue(0);
}
public SubcolumnValue(float value) {
// point and targetPoint have to be different objects
setValue(value);
}
public SubcolumnValue(float value, int color) {
// point and targetPoint have to be different objects
setValue(value);
setColor(color);
}
public SubcolumnValue(SubcolumnValue columnValue) {
setValue(columnValue.value);
setColor(columnValue.color);
this.label = columnValue.label;
}
public void update(float scale) {
value = originValue + diff * scale;
}
public void finish() {
setValue(originValue + diff);
}
public float getValue() {
return value;
}
public SubcolumnValue setValue(float value) {
this.value = value;
this.originValue = value;
this.diff = 0;
return this;
}
/**
* Set target value that should be reached when data animation finish then call {@link Chart#startDataAnimation()}
*
* @param target
* @return
*/
public SubcolumnValue setTarget(float target) {
setValue(value);
this.diff = target - originValue;
return this;
}
public int getColor() {
return color;
}
public SubcolumnValue setColor(int color) {
this.color = color;
this.darkenColor = ChartUtils.darkenColor(color);
return this;
}
public int getDarkenColor() {
return darkenColor;
}
@Deprecated
public char[] getLabel() {
return label;
}
public SubcolumnValue setLabel(String label) {
this.label = label.toCharArray();
return this;
}
public char[] getLabelAsChars() {
return label;
}
@Deprecated
public SubcolumnValue setLabel(char[] label) {
this.label = label;
return this;
}
@Override
public String toString() {
return "ColumnValue [value=" + value + "]";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SubcolumnValue that = (SubcolumnValue) o;
if (color != that.color) return false;
if (darkenColor != that.darkenColor) return false;
if (Float.compare(that.diff, diff) != 0) return false;
if (Float.compare(that.originValue, originValue) != 0) return false;
if (Float.compare(that.value, value) != 0) return false;
if (!Arrays.equals(label, that.label)) return false;
return true;
}
@Override
public int hashCode() {
int result = (value != +0.0f ? Float.floatToIntBits(value) : 0);
result = 31 * result + (originValue != +0.0f ? Float.floatToIntBits(originValue) : 0);
result = 31 * result + (diff != +0.0f ? Float.floatToIntBits(diff) : 0);
result = 31 * result + color;
result = 31 * result + darkenColor;
result = 31 * result + (label != null ? Arrays.hashCode(label) : 0);
return result;
}
}
@@ -0,0 +1,5 @@
package lecho.lib.hellocharts.model;
public enum ValueShape {
CIRCLE, SQUARE, DIAMOND
}
@@ -0,0 +1,389 @@
package lecho.lib.hellocharts.model;
import android.os.Parcel;
import android.os.Parcelable;
/**
* Partial copy of android.graphics.Rect but here the top should be greater then the bottom. Viewport holds 4 float
* coordinates for a chart extremes. The viewport is represented by the coordinates of its 4 edges (left, top, right
* bottom). These fields can be accessed directly. Use width() and height() to retrieve the viewport's width and height.
* Note: most methods do not check to see that the coordinates are sorted correctly (i.e. left is less than right and
* bottom is less than top). Viewport implements Parcerable.
*/
public class Viewport implements Parcelable {
public float left;
public float top;
public float right;
public float bottom;
public static final Parcelable.Creator<Viewport> CREATOR = new Parcelable.Creator<Viewport>() {
/**
* Return a new viewport from the data in the specified parcel.
*/
public Viewport createFromParcel(Parcel in) {
Viewport v = new Viewport();
v.readFromParcel(in);
return v;
}
/**
* Return an array of viewports of the specified size.
*/
public Viewport[] newArray(int size) {
return new Viewport[size];
}
};
/**
* Create a new empty Viewport. All coordinates are initialized to 0.
*/
public Viewport() {
}
/**
* Create a new viewport with the specified coordinates. Note: no range checking is performed, so the caller must
* ensure that left is less than right and bottom is less than top.
*
* @param left The X coordinate of the left side of the viewport
* @param top The Y coordinate of the top of the viewport
* @param right The X coordinate of the right side of the viewport
* @param bottom The Y coordinate of the bottom of the viewport
*/
public Viewport(float left, float top, float right, float bottom) {
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
/**
* Create a new viewport, initialized with the values in the specified viewport (which is left unmodified).
*
* @param v The viewport whose coordinates are copied into the new viewport.
*/
public Viewport(Viewport v) {
if (v == null) {
left = top = right = bottom = 0.0f;
} else {
left = v.left;
top = v.top;
right = v.right;
bottom = v.bottom;
}
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Viewport other = (Viewport) obj;
if (Float.floatToIntBits(bottom) != Float.floatToIntBits(other.bottom))
return false;
if (Float.floatToIntBits(left) != Float.floatToIntBits(other.left))
return false;
if (Float.floatToIntBits(right) != Float.floatToIntBits(other.right))
return false;
if (Float.floatToIntBits(top) != Float.floatToIntBits(other.top))
return false;
return true;
}
/**
* Returns true if the viewport is empty {@code left >= right or bottom >= top}
*/
public final boolean isEmpty() {
return left >= right || bottom >= top;
}
/**
* Set the viewport to (0,0,0,0)
*/
public void setEmpty() {
left = right = top = bottom = 0;
}
/**
* @return the viewport's width. This does not check for a valid viewport (i.e. {@code left <= right}) so the
* result may be negative.
*/
public final float width() {
return right - left;
}
/**
* @return the viewport's height. This does not check for a valid viewport (i.e. {@code top <= bottom}) so the
* result may be negative.
*/
public final float height() {
return top - bottom;
}
/**
* @return the horizontal center of the viewport. This does not check for a valid viewport (i.e. {@code left <=
* right})
*/
public final float centerX() {
return (left + right) * 0.5f;
}
/**
* @return the vertical center of the viewport. This does not check for a valid viewport (i.e. {@code bottom <=
* top})
*/
public final float centerY() {
return (top + bottom) * 0.5f;
}
/**
* Set the viewport's coordinates to the specified values. Note: no range checking is performed, so it is up to the
* caller to ensure that {@code left <= right and bottom <= top}.
*
* @param left The X coordinate of the left side of the viewport
* @param top The Y coordinate of the top of the viewport
* @param right The X coordinate of the right side of the viewport
* @param bottom The Y coordinate of the bottom of the viewport
*/
public void set(float left, float top, float right, float bottom) {
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
/**
* Copy the coordinates from src into this viewport.
*
* @param src The viewport whose coordinates are copied into this viewport.
*/
public void set(Viewport src) {
this.left = src.left;
this.top = src.top;
this.right = src.right;
this.bottom = src.bottom;
}
/**
* Offset the viewport by adding dx to its left and right coordinates, and adding dy to its top and bottom
* coordinates.
*
* @param dx The amount to add to the viewport's left and right coordinates
* @param dy The amount to add to the viewport's top and bottom coordinates
*/
public void offset(float dx, float dy) {
left += dx;
top += dy;
right += dx;
bottom += dy;
}
/**
* Offset the viewport to a specific (left, top) position, keeping its width and height the same.
*
* @param newLeft The new "left" coordinate for the viewport
* @param newTop The new "top" coordinate for the viewport
*/
public void offsetTo(float newLeft, float newTop) {
right += newLeft - left;
bottom += newTop - top;
left = newLeft;
top = newTop;
}
/**
* Inset the viewport by (dx,dy). If dx is positive, then the sides are moved inwards, making the viewport narrower.
* If dx is negative, then the sides are moved outwards, making the viewport wider. The same holds true for dy and
* the top and bottom.
*
* @param dx The amount to add(subtract) from the viewport's left(right)
* @param dy The amount to add(subtract) from the viewport's top(bottom)
*/
public void inset(float dx, float dy) {
left += dx;
top -= dy;
right -= dx;
bottom += dy;
}
/**
* Returns true if (x,y) is inside the viewport. The left and top are considered to be inside, while the right and
* bottom are not. This means that for a x,y to be contained: {@code left <= x < right and bottom <= y < top}. An
* empty viewport never contains any point.
*
* @param x The X coordinate of the point being tested for containment
* @param y The Y coordinate of the point being tested for containment
* @return true iff (x,y) are contained by the viewport, where containment means {@code left <= x < right and top <=
* y < bottom}
*/
public boolean contains(float x, float y) {
return left < right && bottom < top // check for empty first
&& x >= left && x < right && y >= bottom && y < top;
}
/**
* Returns true iff the 4 specified sides of a viewport are inside or equal to this viewport. i.e. is this viewport
* a superset of the specified viewport. An empty viewport never contains another viewport.
*
* @param left The left side of the viewport being tested for containment
* @param top The top of the viewport being tested for containment
* @param right The right side of the viewport being tested for containment
* @param bottom The bottom of the viewport being tested for containment
* @return true iff the the 4 specified sides of a viewport are inside or equal to this viewport
*/
public boolean contains(float left, float top, float right, float bottom) {
// check for empty first
return this.left < this.right && this.bottom < this.top
// now check for containment
&& this.left <= left && this.top >= top && this.right >= right && this.bottom <= bottom;
}
/**
* Returns true iff the specified viewport r is inside or equal to this viewport. An empty viewport never contains
* another viewport.
*
* @param v The viewport being tested for containment.
* @return true iff the specified viewport r is inside or equal to this viewport
*/
public boolean contains(Viewport v) {
// check for empty first
return this.left < this.right && this.bottom < this.top
// now check for containment
&& left <= v.left && top >= v.top && right >= v.right && bottom <= v.bottom;
}
/**
* Update this Viewport to enclose itself and the specified viewport. If the specified viewport is empty, nothing is
* done. If this viewport is empty it is set to the specified viewport.
*
* @param left The left edge being unioned with this viewport
* @param top The top edge being unioned with this viewport
* @param right The right edge being unioned with this viewport
* @param bottom The bottom edge being unioned with this viewport
*/
public void union(float left, float top, float right, float bottom) {
if ((left < right) && (bottom < top)) {
if ((this.left < this.right) && (this.bottom < this.top)) {
if (this.left > left)
this.left = left;
if (this.top < top)
this.top = top;
if (this.right < right)
this.right = right;
if (this.bottom > bottom)
this.bottom = bottom;
} else {
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
}
}
/**
* Update this Viewport to enclose itself and the specified viewport. If the specified viewport is empty, nothing is
* done. If this viewport is empty it is set to the specified viewport.
*
* @param v The viewport being unioned with this viewport
*/
public void union(Viewport v) {
union(v.left, v.top, v.right, v.bottom);
}
/**
* If the viewport specified by left,top,right,bottom intersects this viewport, return true and set this viewport to
* that intersection, otherwise return false and do not change this viewport. No check is performed to see if either
* viewport is empty. Note: To just test for intersection, use intersects()
*
* @param left The left side of the viewport being intersected with this viewport
* @param top The top of the viewport being intersected with this viewport
* @param right The right side of the viewport being intersected with this viewport.
* @param bottom The bottom of the viewport being intersected with this viewport.
* @return true if the specified viewport and this viewport intersect (and this viewport is then set to that
* intersection) else return false and do not change this viewport.
*/
public boolean intersect(float left, float top, float right, float bottom) {
if (this.left < right && left < this.right && this.bottom < top && bottom < this.top) {
if (this.left < left) {
this.left = left;
}
if (this.top > top) {
this.top = top;
}
if (this.right > right) {
this.right = right;
}
if (this.bottom < bottom) {
this.bottom = bottom;
}
return true;
}
return false;
}
/**
* If the specified viewport intersects this viewport, return true and set this viewport to that intersection,
* otherwise return false and do not change this viewport. No check is performed to see if either viewport is empty.
* To just test for intersection, use intersects()
*
* @param v The viewport being intersected with this viewport.
* @return true if the specified viewport and this viewport intersect (and this viewport is then set to that
* intersection) else return false and do not change this viewport.
*/
public boolean intersect(Viewport v) {
return intersect(v.left, v.top, v.right, v.bottom);
}
@Override
public String toString() {
return "Viewport [left=" + left + ", top=" + top + ", right=" + right + ", bottom=" + bottom + "]";
}
// ** PARCERABLE **
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Float.floatToIntBits(bottom);
result = prime * result + Float.floatToIntBits(left);
result = prime * result + Float.floatToIntBits(right);
result = prime * result + Float.floatToIntBits(top);
return result;
}
/**
* Parcelable interface methods
*/
public int describeContents() {
return 0;
}
/**
* Write this viewport to the specified parcel. To restore a viewport from a parcel, use readFromParcel()
*
* @param out The parcel to write the viewport's coordinates into
*/
public void writeToParcel(Parcel out, int flags) {
out.writeFloat(left);
out.writeFloat(top);
out.writeFloat(right);
out.writeFloat(bottom);
}
/**
* Set the viewport's coordinates from the data stored in the specified parcel. To write a viewport to a parcel,
* call writeToParcel().
*
* @param in The parcel to read the viewport's coordinates from
*/
public void readFromParcel(Parcel in) {
left = in.readFloat();
top = in.readFloat();
right = in.readFloat();
bottom = in.readFloat();
}
}
@@ -0,0 +1,11 @@
package lecho.lib.hellocharts.provider;
import lecho.lib.hellocharts.model.BubbleChartData;
public interface BubbleChartDataProvider {
public BubbleChartData getBubbleChartData();
public void setBubbleChartData(BubbleChartData data);
}
@@ -0,0 +1,11 @@
package lecho.lib.hellocharts.provider;
import lecho.lib.hellocharts.model.ColumnChartData;
public interface ColumnChartDataProvider {
public ColumnChartData getColumnChartData();
public void setColumnChartData(ColumnChartData data);
}
@@ -0,0 +1,11 @@
package lecho.lib.hellocharts.provider;
import lecho.lib.hellocharts.model.ComboLineColumnChartData;
public interface ComboLineColumnChartDataProvider {
public ComboLineColumnChartData getComboLineColumnChartData();
public void setComboLineColumnChartData(ComboLineColumnChartData data);
}
@@ -0,0 +1,11 @@
package lecho.lib.hellocharts.provider;
import lecho.lib.hellocharts.model.LineChartData;
public interface LineChartDataProvider {
public LineChartData getLineChartData();
public void setLineChartData(LineChartData data);
}
@@ -0,0 +1,11 @@
package lecho.lib.hellocharts.provider;
import lecho.lib.hellocharts.model.PieChartData;
public interface PieChartDataProvider {
public PieChartData getPieChartData();
public void setPieChartData(PieChartData data);
}
@@ -0,0 +1,180 @@
package lecho.lib.hellocharts.renderer;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.RectF;
import android.graphics.Typeface;
import lecho.lib.hellocharts.computator.ChartComputator;
import lecho.lib.hellocharts.model.ChartData;
import lecho.lib.hellocharts.model.SelectedValue;
import lecho.lib.hellocharts.model.Viewport;
import lecho.lib.hellocharts.util.ChartUtils;
import lecho.lib.hellocharts.view.Chart;
/**
* Abstract renderer implementation, every chart renderer extends this class(although it is not required it helps).
*/
public abstract class AbstractChartRenderer implements ChartRenderer {
public int DEFAULT_LABEL_MARGIN_DP = 4;
protected Chart chart;
protected ChartComputator computator;
/**
* Paint for value labels.
*/
protected Paint labelPaint = new Paint();
/**
* Paint for labels background.
*/
protected Paint labelBackgroundPaint = new Paint();
/**
* Holds coordinates for label background rect.
*/
protected RectF labelBackgroundRect = new RectF();
/**
* Font metrics for label paint, used to determine text height.
*/
protected FontMetricsInt fontMetrics = new FontMetricsInt();
/**
* If true maximum and current viewport will be calculated when chart data change or during data animations.
*/
protected boolean isViewportCalculationEnabled = true;
protected float density;
protected float scaledDensity;
protected SelectedValue selectedValue = new SelectedValue();
protected char[] labelBuffer = new char[64];
protected int labelOffset;
protected int labelMargin;
protected boolean isValueLabelBackgroundEnabled;
protected boolean isValueLabelBackgroundAuto;
public AbstractChartRenderer(Context context, Chart chart) {
this.density = context.getResources().getDisplayMetrics().density;
this.scaledDensity = context.getResources().getDisplayMetrics().scaledDensity;
this.chart = chart;
this.computator = chart.getChartComputator();
labelMargin = ChartUtils.dp2px(density, DEFAULT_LABEL_MARGIN_DP);
labelOffset = labelMargin;
labelPaint.setAntiAlias(true);
labelPaint.setStyle(Paint.Style.FILL);
labelPaint.setTextAlign(Align.LEFT);
labelPaint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
labelPaint.setColor(Color.WHITE);
labelBackgroundPaint.setAntiAlias(true);
labelBackgroundPaint.setStyle(Paint.Style.FILL);
}
@Override
public void resetRenderer() {
this.computator = chart.getChartComputator();
}
@Override
public void onChartDataChanged() {
final ChartData data = chart.getChartData();
Typeface typeface = chart.getChartData().getValueLabelTypeface();
if (null != typeface) {
labelPaint.setTypeface(typeface);
}
labelPaint.setColor(data.getValueLabelTextColor());
labelPaint.setTextSize(ChartUtils.sp2px(scaledDensity, data.getValueLabelTextSize()));
labelPaint.getFontMetricsInt(fontMetrics);
this.isValueLabelBackgroundEnabled = data.isValueLabelBackgroundEnabled();
this.isValueLabelBackgroundAuto = data.isValueLabelBackgroundAuto();
this.labelBackgroundPaint.setColor(data.getValueLabelBackgroundColor());
// Important - clear selection when data changed.
selectedValue.clear();
}
/**
* Draws label text and label background if isValueLabelBackgroundEnabled is true.
*/
protected void drawLabelTextAndBackground(Canvas canvas, char[] labelBuffer, int startIndex, int numChars,
int autoBackgroundColor) {
final float textX;
final float textY;
if (isValueLabelBackgroundEnabled) {
if (isValueLabelBackgroundAuto) {
labelBackgroundPaint.setColor(autoBackgroundColor);
}
canvas.drawRect(labelBackgroundRect, labelBackgroundPaint);
textX = labelBackgroundRect.left + labelMargin;
textY = labelBackgroundRect.bottom - labelMargin;
} else {
textX = labelBackgroundRect.left;
textY = labelBackgroundRect.bottom;
}
canvas.drawText(labelBuffer, startIndex, numChars, textX, textY, labelPaint);
}
@Override
public boolean isTouched() {
return selectedValue.isSet();
}
@Override
public void clearTouch() {
selectedValue.clear();
}
@Override
public Viewport getMaximumViewport() {
return computator.getMaximumViewport();
}
@Override
public void setMaximumViewport(Viewport maxViewport) {
if (null != maxViewport) {
computator.setMaxViewport(maxViewport);
}
}
@Override
public Viewport getCurrentViewport() {
return computator.getCurrentViewport();
}
@Override
public void setCurrentViewport(Viewport viewport) {
if (null != viewport) {
computator.setCurrentViewport(viewport);
}
}
@Override
public boolean isViewportCalculationEnabled() {
return isViewportCalculationEnabled;
}
@Override
public void setViewportCalculationEnabled(boolean isEnabled) {
this.isViewportCalculationEnabled = isEnabled;
}
@Override
public void selectValue(SelectedValue selectedValue) {
this.selectedValue.set(selectedValue);
}
@Override
public SelectedValue getSelectedValue() {
return selectedValue;
}
}
@@ -0,0 +1,639 @@
package lecho.lib.hellocharts.renderer;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.text.TextUtils;
import lecho.lib.hellocharts.computator.ChartComputator;
import lecho.lib.hellocharts.model.Axis;
import lecho.lib.hellocharts.model.AxisValue;
import lecho.lib.hellocharts.model.Viewport;
import lecho.lib.hellocharts.util.AxisAutoValues;
import lecho.lib.hellocharts.util.ChartUtils;
import lecho.lib.hellocharts.util.FloatUtils;
import lecho.lib.hellocharts.view.Chart;
/**
* Default axes renderer. Can draw maximum four axes - two horizontal(top/bottom) and two vertical(left/right).
*/
public class AxesRenderer {
private static final int DEFAULT_AXIS_MARGIN_DP = 2;
/**
* Axis positions indexes, used for indexing tabs that holds axes parameters, see below.
*/
private static final int TOP = 0;
private static final int LEFT = 1;
private static final int RIGHT = 2;
private static final int BOTTOM = 3;
/**
* Used to measure label width. If label has mas 5 characters only 5 first characters of this array are used to
* measure text width.
*/
private static final char[] labelWidthChars = new char[]{
'0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
'0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
'0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
'0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0'};
private Chart chart;
private ChartComputator computator;
private int axisMargin;
private float density;
private float scaledDensity;
private Paint[] labelPaintTab = new Paint[]{new Paint(), new Paint(), new Paint(), new Paint()};
private Paint[] namePaintTab = new Paint[]{new Paint(), new Paint(), new Paint(), new Paint()};
private Paint[] linePaintTab = new Paint[]{new Paint(), new Paint(), new Paint(), new Paint()};
private float[] nameBaselineTab = new float[4];
private float[] labelBaselineTab = new float[4];
private float[] separationLineTab = new float[4];
private int[] labelWidthTab = new int[4];
private int[] labelTextAscentTab = new int[4];
private int[] labelTextDescentTab = new int[4];
private int[] labelDimensionForMarginsTab = new int[4];
private int[] labelDimensionForStepsTab = new int[4];
private int[] tiltedLabelXTranslation = new int[4];
private int[] tiltedLabelYTranslation = new int[4];
private FontMetricsInt[] fontMetricsTab = new FontMetricsInt[]{new FontMetricsInt(), new FontMetricsInt(),
new FontMetricsInt(), new FontMetricsInt()};
/**
* Holds formatted axis value label.
*/
private char[] labelBuffer = new char[64];
/**
* Holds number of values that should be drown for each axis.
*/
private int[] valuesToDrawNumTab = new int[4];
/**
* Holds raw values to draw for each axis.
*/
private float[][] rawValuesTab = new float[4][0];
/**
* Holds auto-generated values that should be drawn, i.e if axis is inside not all auto-generated values should be
* drawn to avoid overdrawing. Used only for auto axes.
*/
private float[][] autoValuesToDrawTab = new float[4][0];
/**
* Holds custom values that should be drawn, used only for custom axes.
*/
private AxisValue[][] valuesToDrawTab = new AxisValue[4][0];
/**
* Buffers for axes lines coordinates(to draw grid in the background).
*/
private float[][] linesDrawBufferTab = new float[4][0];
/**
* Buffers for auto-generated values for each axis, used only if there are auto axes.
*/
private AxisAutoValues[] autoValuesBufferTab = new AxisAutoValues[]{new AxisAutoValues(),
new AxisAutoValues(), new AxisAutoValues(), new AxisAutoValues()};
public AxesRenderer(Context context, Chart chart) {
this.chart = chart;
computator = chart.getChartComputator();
density = context.getResources().getDisplayMetrics().density;
scaledDensity = context.getResources().getDisplayMetrics().scaledDensity;
axisMargin = ChartUtils.dp2px(density, DEFAULT_AXIS_MARGIN_DP);
for (int position = 0; position < 4; ++position) {
labelPaintTab[position].setStyle(Paint.Style.FILL);
labelPaintTab[position].setAntiAlias(true);
namePaintTab[position].setStyle(Paint.Style.FILL);
namePaintTab[position].setAntiAlias(true);
linePaintTab[position].setStyle(Paint.Style.STROKE);
linePaintTab[position].setAntiAlias(true);
}
}
public void onChartSizeChanged() {
onChartDataOrSizeChanged();
}
public void onChartDataChanged() {
onChartDataOrSizeChanged();
}
private void onChartDataOrSizeChanged() {
initAxis(chart.getChartData().getAxisXTop(), TOP);
initAxis(chart.getChartData().getAxisXBottom(), BOTTOM);
initAxis(chart.getChartData().getAxisYLeft(), LEFT);
initAxis(chart.getChartData().getAxisYRight(), RIGHT);
}
public void resetRenderer() {
this.computator = chart.getChartComputator();
}
/**
* Initialize attributes and measurement for axes(left, right, top, bottom);
*/
private void initAxis(Axis axis, int position) {
if (null == axis) {
return;
}
initAxisAttributes(axis, position);
initAxisMargin(axis, position);
initAxisMeasurements(axis, position);
}
private void initAxisAttributes(Axis axis, int position) {
initAxisPaints(axis, position);
initAxisTextAlignment(axis, position);
if (axis.hasTiltedLabels()) {
initAxisDimensionForTiltedLabels(position);
intiTiltedLabelsTranslation(axis, position);
} else {
initAxisDimension(position);
}
}
private void initAxisPaints(Axis axis, int position) {
Typeface typeface = axis.getTypeface();
if (null != typeface) {
labelPaintTab[position].setTypeface(typeface);
namePaintTab[position].setTypeface(typeface);
}
labelPaintTab[position].setColor(axis.getTextColor());
labelPaintTab[position].setTextSize(ChartUtils.sp2px(scaledDensity, axis.getTextSize()));
labelPaintTab[position].getFontMetricsInt(fontMetricsTab[position]);
namePaintTab[position].setColor(axis.getTextColor());
namePaintTab[position].setTextSize(ChartUtils.sp2px(scaledDensity, axis.getTextSize()));
linePaintTab[position].setColor(axis.getLineColor());
labelTextAscentTab[position] = Math.abs(fontMetricsTab[position].ascent);
labelTextDescentTab[position] = Math.abs(fontMetricsTab[position].descent);
labelWidthTab[position] = (int) labelPaintTab[position].measureText(labelWidthChars, 0,
axis.getMaxLabelChars());
}
private void initAxisTextAlignment(Axis axis, int position) {
namePaintTab[position].setTextAlign(Align.CENTER);
if (TOP == position || BOTTOM == position) {
labelPaintTab[position].setTextAlign(Align.CENTER);
} else if (LEFT == position) {
if (axis.isInside()) {
labelPaintTab[position].setTextAlign(Align.LEFT);
} else {
labelPaintTab[position].setTextAlign(Align.RIGHT);
}
} else if (RIGHT == position) {
if (axis.isInside()) {
labelPaintTab[position].setTextAlign(Align.RIGHT);
} else {
labelPaintTab[position].setTextAlign(Align.LEFT);
}
}
}
private void initAxisDimensionForTiltedLabels(int position) {
int pythagoreanFromLabelWidth = (int) Math.sqrt(Math.pow(labelWidthTab[position], 2) / 2);
int pythagoreanFromAscent = (int) Math.sqrt(Math.pow(labelTextAscentTab[position], 2) / 2);
labelDimensionForMarginsTab[position] = pythagoreanFromAscent + pythagoreanFromLabelWidth;
labelDimensionForStepsTab[position] = Math.round(labelDimensionForMarginsTab[position] * 0.75f);
}
private void initAxisDimension(int position) {
if (LEFT == position || RIGHT == position) {
labelDimensionForMarginsTab[position] = labelWidthTab[position];
labelDimensionForStepsTab[position] = labelTextAscentTab[position];
} else if (TOP == position || BOTTOM == position) {
labelDimensionForMarginsTab[position] = labelTextAscentTab[position] +
labelTextDescentTab[position];
labelDimensionForStepsTab[position] = labelWidthTab[position];
}
}
private void intiTiltedLabelsTranslation(Axis axis, int position) {
int pythagoreanFromLabelWidth = (int) Math.sqrt(Math.pow(labelWidthTab[position], 2) / 2);
int pythagoreanFromAscent = (int) Math.sqrt(Math.pow(labelTextAscentTab[position], 2) / 2);
int dx = 0;
int dy = 0;
if (axis.isInside()) {
if (LEFT == position) {
dx = pythagoreanFromAscent;
} else if (RIGHT == position) {
dy = -pythagoreanFromLabelWidth / 2;
} else if (TOP == position) {
dy = (pythagoreanFromAscent + pythagoreanFromLabelWidth / 2) - labelTextAscentTab[position];
} else if (BOTTOM == position) {
dy = -pythagoreanFromLabelWidth / 2;
}
} else {
if (LEFT == position) {
dy = -pythagoreanFromLabelWidth / 2;
} else if (RIGHT == position) {
dx = pythagoreanFromAscent;
} else if (TOP == position) {
dy = -pythagoreanFromLabelWidth / 2;
} else if (BOTTOM == position) {
dy = (pythagoreanFromAscent + pythagoreanFromLabelWidth / 2) - labelTextAscentTab[position];
}
}
tiltedLabelXTranslation[position] = dx;
tiltedLabelYTranslation[position] = dy;
}
private void initAxisMargin(Axis axis, int position) {
int margin = 0;
if (!axis.isInside() && (axis.isAutoGenerated() || !axis.getValues().isEmpty())) {
margin += axisMargin + labelDimensionForMarginsTab[position];
}
margin += getAxisNameMargin(axis, position);
insetContentRectWithAxesMargins(margin, position);
}
private int getAxisNameMargin(Axis axis, int position) {
int margin = 0;
if (!TextUtils.isEmpty(axis.getName())) {
margin += labelTextAscentTab[position];
margin += labelTextDescentTab[position];
margin += axisMargin;
}
return margin;
}
private void insetContentRectWithAxesMargins(int axisMargin, int position) {
if (LEFT == position) {
chart.getChartComputator().insetContentRect(axisMargin, 0, 0, 0);
} else if (RIGHT == position) {
chart.getChartComputator().insetContentRect(0, 0, axisMargin, 0);
} else if (TOP == position) {
chart.getChartComputator().insetContentRect(0, axisMargin, 0, 0);
} else if (BOTTOM == position) {
chart.getChartComputator().insetContentRect(0, 0, 0, axisMargin);
}
}
private void initAxisMeasurements(Axis axis, int position) {
if (LEFT == position) {
if (axis.isInside()) {
labelBaselineTab[position] = computator.getContentRectMinusAllMargins().left + axisMargin;
nameBaselineTab[position] = computator.getContentRectMinusAxesMargins().left - axisMargin
- labelTextDescentTab[position];
} else {
labelBaselineTab[position] = computator.getContentRectMinusAxesMargins().left - axisMargin;
nameBaselineTab[position] = labelBaselineTab[position] - axisMargin
- labelTextDescentTab[position] - labelDimensionForMarginsTab[position];
}
separationLineTab[position] = computator.getContentRectMinusAllMargins().left;
} else if (RIGHT == position) {
if (axis.isInside()) {
labelBaselineTab[position] = computator.getContentRectMinusAllMargins().right - axisMargin;
nameBaselineTab[position] = computator.getContentRectMinusAxesMargins().right + axisMargin
+ labelTextAscentTab[position];
} else {
labelBaselineTab[position] = computator.getContentRectMinusAxesMargins().right + axisMargin;
nameBaselineTab[position] = labelBaselineTab[position] + axisMargin
+ labelTextAscentTab[position] + labelDimensionForMarginsTab[position];
}
separationLineTab[position] = computator.getContentRectMinusAllMargins().right;
} else if (BOTTOM == position) {
if (axis.isInside()) {
labelBaselineTab[position] = computator.getContentRectMinusAllMargins().bottom - axisMargin
- labelTextDescentTab[position];
nameBaselineTab[position] = computator.getContentRectMinusAxesMargins().bottom + axisMargin
+ labelTextAscentTab[position];
} else {
labelBaselineTab[position] = computator.getContentRectMinusAxesMargins().bottom + axisMargin
+ labelTextAscentTab[position];
nameBaselineTab[position] = labelBaselineTab[position] + axisMargin +
labelDimensionForMarginsTab[position];
}
separationLineTab[position] = computator.getContentRectMinusAllMargins().bottom;
} else if (TOP == position) {
if (axis.isInside()) {
labelBaselineTab[position] = computator.getContentRectMinusAllMargins().top + axisMargin
+ labelTextAscentTab[position];
nameBaselineTab[position] = computator.getContentRectMinusAxesMargins().top - axisMargin
- labelTextDescentTab[position];
} else {
labelBaselineTab[position] = computator.getContentRectMinusAxesMargins().top - axisMargin
- labelTextDescentTab[position];
nameBaselineTab[position] = labelBaselineTab[position] - axisMargin -
labelDimensionForMarginsTab[position];
}
separationLineTab[position] = computator.getContentRectMinusAllMargins().top;
} else {
throw new IllegalArgumentException("Invalid axis position: " + position);
}
}
/**
* Prepare axes coordinates and draw axes lines(if enabled) in the background.
*
* @param canvas
*/
public void drawInBackground(Canvas canvas) {
Axis axis = chart.getChartData().getAxisYLeft();
if (null != axis) {
prepareAxisToDraw(axis, LEFT);
drawAxisLines(canvas, axis, LEFT);
}
axis = chart.getChartData().getAxisYRight();
if (null != axis) {
prepareAxisToDraw(axis, RIGHT);
drawAxisLines(canvas, axis, RIGHT);
}
axis = chart.getChartData().getAxisXBottom();
if (null != axis) {
prepareAxisToDraw(axis, BOTTOM);
drawAxisLines(canvas, axis, BOTTOM);
}
axis = chart.getChartData().getAxisXTop();
if (null != axis) {
prepareAxisToDraw(axis, TOP);
drawAxisLines(canvas, axis, TOP);
}
}
private void prepareAxisToDraw(Axis axis, int position) {
if (axis.isAutoGenerated()) {
prepareAutoGeneratedAxis(axis, position);
} else {
prepareCustomAxis(axis, position);
}
}
/**
* Draw axes labels and names in the foreground.
*
* @param canvas
*/
public void drawInForeground(Canvas canvas) {
Axis axis = chart.getChartData().getAxisYLeft();
if (null != axis) {
drawAxisLabelsAndName(canvas, axis, LEFT);
}
axis = chart.getChartData().getAxisYRight();
if (null != axis) {
drawAxisLabelsAndName(canvas, axis, RIGHT);
}
axis = chart.getChartData().getAxisXBottom();
if (null != axis) {
drawAxisLabelsAndName(canvas, axis, BOTTOM);
}
axis = chart.getChartData().getAxisXTop();
if (null != axis) {
drawAxisLabelsAndName(canvas, axis, TOP);
}
}
private void prepareCustomAxis(Axis axis, int position) {
final Viewport maxViewport = computator.getMaximumViewport();
final Viewport visibleViewport = computator.getVisibleViewport();
final Rect contentRect = computator.getContentRectMinusAllMargins();
boolean isAxisVertical = isAxisVertical(position);
float viewportMin, viewportMax;
float scale = 1;
if (isAxisVertical) {
if (maxViewport.height() > 0 && visibleViewport.height() > 0) {
scale = contentRect.height() * (maxViewport.height() / visibleViewport.height());
}
viewportMin = visibleViewport.bottom;
viewportMax = visibleViewport.top;
} else {
if (maxViewport.width() > 0 && visibleViewport.width() > 0) {
scale = contentRect.width() * (maxViewport.width() / visibleViewport.width());
}
viewportMin = visibleViewport.left;
viewportMax = visibleViewport.right;
}
if (scale == 0) {
scale = 1;
}
int module = (int) Math.max(1,
Math.ceil((axis.getValues().size() * labelDimensionForStepsTab[position] * 1.5) / scale));
//Reinitialize tab to hold lines coordinates.
if (axis.hasLines() && (linesDrawBufferTab[position].length < axis.getValues().size() * 4)) {
linesDrawBufferTab[position] = new float[axis.getValues().size() * 4];
}
//Reinitialize tabs to hold all raw values to draw.
if (rawValuesTab[position].length < axis.getValues().size()) {
rawValuesTab[position] = new float[axis.getValues().size()];
}
//Reinitialize tabs to hold all raw values to draw.
if (valuesToDrawTab[position].length < axis.getValues().size()) {
valuesToDrawTab[position] = new AxisValue[axis.getValues().size()];
}
float rawValue;
int valueIndex = 0;
int valueToDrawIndex = 0;
for (AxisValue axisValue : axis.getValues()) {
// Draw axis values that are within visible viewport.
final float value = axisValue.getValue();
if (value >= viewportMin && value <= viewportMax) {
// Draw axis values that have 0 module value, this will hide some labels if there is no place for them.
if (0 == valueIndex % module) {
if (isAxisVertical) {
rawValue = computator.computeRawY(value);
} else {
rawValue = computator.computeRawX(value);
}
if (checkRawValue(contentRect, rawValue, axis.isInside(), position, isAxisVertical)) {
rawValuesTab[position][valueToDrawIndex] = rawValue;
valuesToDrawTab[position][valueToDrawIndex] = axisValue;
++valueToDrawIndex;
}
}
// If within viewport - increment valueIndex;
++valueIndex;
}
}
valuesToDrawNumTab[position] = valueToDrawIndex;
}
private void prepareAutoGeneratedAxis(Axis axis, int position) {
final Viewport visibleViewport = computator.getVisibleViewport();
final Rect contentRect = computator.getContentRectMinusAllMargins();
boolean isAxisVertical = isAxisVertical(position);
float start, stop;
int contentRectDimension;
if (isAxisVertical) {
start = visibleViewport.bottom;
stop = visibleViewport.top;
contentRectDimension = contentRect.height();
} else {
start = visibleViewport.left;
stop = visibleViewport.right;
contentRectDimension = contentRect.width();
}
FloatUtils.computeAutoGeneratedAxisValues(start, stop, Math.abs(contentRectDimension) /
labelDimensionForStepsTab[position] / 2, autoValuesBufferTab[position]);
//Reinitialize tab to hold lines coordinates.
if (axis.hasLines()
&& (linesDrawBufferTab[position].length < autoValuesBufferTab[position].valuesNumber * 4)) {
linesDrawBufferTab[position] = new float[autoValuesBufferTab[position].valuesNumber * 4];
}
//Reinitialize tabs to hold all raw and auto values.
if (rawValuesTab[position].length < autoValuesBufferTab[position].valuesNumber) {
rawValuesTab[position] = new float[autoValuesBufferTab[position].valuesNumber];
}
if (autoValuesToDrawTab[position].length < autoValuesBufferTab[position].valuesNumber) {
autoValuesToDrawTab[position] = new float[autoValuesBufferTab[position].valuesNumber];
}
float rawValue;
int valueToDrawIndex = 0;
for (int i = 0; i < autoValuesBufferTab[position].valuesNumber; ++i) {
if (isAxisVertical) {
rawValue = computator.computeRawY(autoValuesBufferTab[position].values[i]);
} else {
rawValue = computator.computeRawX(autoValuesBufferTab[position].values[i]);
}
if (checkRawValue(contentRect, rawValue, axis.isInside(), position, isAxisVertical)) {
rawValuesTab[position][valueToDrawIndex] = rawValue;
autoValuesToDrawTab[position][valueToDrawIndex] = autoValuesBufferTab[position].values[i];
++valueToDrawIndex;
}
}
valuesToDrawNumTab[position] = valueToDrawIndex;
}
private boolean checkRawValue(Rect rect, float rawValue, boolean axisInside, int position, boolean isVertical) {
if (axisInside) {
if (isVertical) {
float marginBottom = labelTextAscentTab[BOTTOM] + axisMargin;
float marginTop = labelTextAscentTab[TOP] + axisMargin;
if (rawValue <= rect.bottom - marginBottom && rawValue >= rect.top + marginTop) {
return true;
} else {
return false;
}
} else {
float margin = labelWidthTab[position] / 2;
if (rawValue >= rect.left + margin && rawValue <= rect.right - margin) {
return true;
} else {
return false;
}
}
}
return true;
}
private void drawAxisLines(Canvas canvas, Axis axis, int position) {
final Rect contentRectMargins = computator.getContentRectMinusAxesMargins();
float separationX1, separationY1, separationX2, separationY2;
separationX1 = separationY1 = separationX2 = separationY2 = 0;
float lineX1, lineY1, lineX2, lineY2;
lineX1 = lineY1 = lineX2 = lineY2 = 0;
boolean isAxisVertical = isAxisVertical(position);
if (LEFT == position || RIGHT == position) {
separationX1 = separationX2 = separationLineTab[position];
separationY1 = contentRectMargins.bottom;
separationY2 = contentRectMargins.top;
lineX1 = contentRectMargins.left;
lineX2 = contentRectMargins.right;
} else if (TOP == position || BOTTOM == position) {
separationX1 = contentRectMargins.left;
separationX2 = contentRectMargins.right;
separationY1 = separationY2 = separationLineTab[position];
lineY1 = contentRectMargins.top;
lineY2 = contentRectMargins.bottom;
}
// Draw separation line with the same color as axis labels and name.
if (axis.hasSeparationLine()) {
canvas.drawLine(separationX1, separationY1, separationX2, separationY2, labelPaintTab[position]);
}
if (axis.hasLines()) {
int valueToDrawIndex = 0;
for (; valueToDrawIndex < valuesToDrawNumTab[position]; ++valueToDrawIndex) {
if (isAxisVertical) {
lineY1 = lineY2 = rawValuesTab[position][valueToDrawIndex];
} else {
lineX1 = lineX2 = rawValuesTab[position][valueToDrawIndex];
}
linesDrawBufferTab[position][valueToDrawIndex * 4 + 0] = lineX1;
linesDrawBufferTab[position][valueToDrawIndex * 4 + 1] = lineY1;
linesDrawBufferTab[position][valueToDrawIndex * 4 + 2] = lineX2;
linesDrawBufferTab[position][valueToDrawIndex * 4 + 3] = lineY2;
}
canvas.drawLines(linesDrawBufferTab[position], 0, valueToDrawIndex * 4, linePaintTab[position]);
}
}
private void drawAxisLabelsAndName(Canvas canvas, Axis axis, int position) {
float labelX, labelY;
labelX = labelY = 0;
boolean isAxisVertical = isAxisVertical(position);
if (LEFT == position || RIGHT == position) {
labelX = labelBaselineTab[position];
} else if (TOP == position || BOTTOM == position) {
labelY = labelBaselineTab[position];
}
for (int valueToDrawIndex = 0; valueToDrawIndex < valuesToDrawNumTab[position]; ++valueToDrawIndex) {
int charsNumber = 0;
if (axis.isAutoGenerated()) {
final float value = autoValuesToDrawTab[position][valueToDrawIndex];
charsNumber = axis.getFormatter().formatValueForAutoGeneratedAxis(labelBuffer, value,
autoValuesBufferTab[position].decimals);
} else {
AxisValue axisValue = valuesToDrawTab[position][valueToDrawIndex];
charsNumber = axis.getFormatter().formatValueForManualAxis(labelBuffer, axisValue);
}
if (isAxisVertical) {
labelY = rawValuesTab[position][valueToDrawIndex];
} else {
labelX = rawValuesTab[position][valueToDrawIndex];
}
if (axis.hasTiltedLabels()) {
canvas.save();
canvas.translate(tiltedLabelXTranslation[position], tiltedLabelYTranslation[position]);
canvas.rotate(-45, labelX, labelY);
canvas.drawText(labelBuffer, labelBuffer.length - charsNumber, charsNumber, labelX, labelY,
labelPaintTab[position]);
canvas.restore();
} else {
canvas.drawText(labelBuffer, labelBuffer.length - charsNumber, charsNumber, labelX, labelY,
labelPaintTab[position]);
}
}
// Drawing axis name
final Rect contentRectMargins = computator.getContentRectMinusAxesMargins();
if (!TextUtils.isEmpty(axis.getName())) {
if (isAxisVertical) {
canvas.save();
canvas.rotate(-90, contentRectMargins.centerY(), contentRectMargins.centerY());
canvas.drawText(axis.getName(), contentRectMargins.centerY(), nameBaselineTab[position],
namePaintTab[position]);
canvas.restore();
} else {
canvas.drawText(axis.getName(), contentRectMargins.centerX(), nameBaselineTab[position],
namePaintTab[position]);
}
}
}
private boolean isAxisVertical(int position) {
if (LEFT == position || RIGHT == position) {
return true;
} else if (TOP == position || BOTTOM == position) {
return false;
} else {
throw new IllegalArgumentException("Invalid axis position " + position);
}
}
}
@@ -0,0 +1,351 @@
package lecho.lib.hellocharts.renderer;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import lecho.lib.hellocharts.computator.ChartComputator;
import lecho.lib.hellocharts.formatter.BubbleChartValueFormatter;
import lecho.lib.hellocharts.model.BubbleChartData;
import lecho.lib.hellocharts.model.BubbleValue;
import lecho.lib.hellocharts.model.SelectedValue.SelectedValueType;
import lecho.lib.hellocharts.model.ValueShape;
import lecho.lib.hellocharts.model.Viewport;
import lecho.lib.hellocharts.provider.BubbleChartDataProvider;
import lecho.lib.hellocharts.util.ChartUtils;
import lecho.lib.hellocharts.view.Chart;
public class BubbleChartRenderer extends AbstractChartRenderer {
private static final int DEFAULT_TOUCH_ADDITIONAL_DP = 4;
private static final int MODE_DRAW = 0;
private static final int MODE_HIGHLIGHT = 1;
private BubbleChartDataProvider dataProvider;
/**
* Additional value added to bubble radius when drawing highlighted bubble, used to give tauch feedback.
*/
private int touchAdditional;
/**
* Scales for bubble radius value, only one is used depending on screen orientation;
*/
private float bubbleScaleX;
private float bubbleScaleY;
/**
* True if bubbleScale = bubbleScaleX so the renderer should used {@link ChartComputator#computeRawDistanceX(float)}
* , if false bubbleScale = bubbleScaleY and renderer should use
* {@link ChartComputator#computeRawDistanceY(float)}.
*/
private boolean isBubbleScaledByX = true;
/**
* Maximum bubble radius.
*/
private float maxRadius;
/**
* Minimal bubble radius in pixels.
*/
private float minRawRadius;
private PointF bubbleCenter = new PointF();
private Paint bubblePaint = new Paint();
/**
* Rect used for drawing bubbles with SHAPE_SQUARE.
*/
private RectF bubbleRect = new RectF();
private boolean hasLabels;
private boolean hasLabelsOnlyForSelected;
private BubbleChartValueFormatter valueFormatter;
private Viewport tempMaximumViewport = new Viewport();
public BubbleChartRenderer(Context context, Chart chart, BubbleChartDataProvider dataProvider) {
super(context, chart);
this.dataProvider = dataProvider;
touchAdditional = ChartUtils.dp2px(density, DEFAULT_TOUCH_ADDITIONAL_DP);
bubblePaint.setAntiAlias(true);
bubblePaint.setStyle(Paint.Style.FILL);
}
@Override
public void onChartSizeChanged() {
final ChartComputator computator = chart.getChartComputator();
Rect contentRect = computator.getContentRectMinusAllMargins();
if (contentRect.width() < contentRect.height()) {
isBubbleScaledByX = true;
} else {
isBubbleScaledByX = false;
}
}
@Override
public void onChartDataChanged() {
super.onChartDataChanged();
BubbleChartData data = dataProvider.getBubbleChartData();
this.hasLabels = data.hasLabels();
this.hasLabelsOnlyForSelected = data.hasLabelsOnlyForSelected();
this.valueFormatter = data.getFormatter();
onChartViewportChanged();
}
@Override
public void onChartViewportChanged() {
if (isViewportCalculationEnabled) {
calculateMaxViewport();
computator.setMaxViewport(tempMaximumViewport);
computator.setCurrentViewport(computator.getMaximumViewport());
}
}
@Override
public void draw(Canvas canvas) {
drawBubbles(canvas);
if (isTouched()) {
highlightBubbles(canvas);
}
}
@Override
public void drawUnclipped(Canvas canvas) {
}
@Override
public boolean checkTouch(float touchX, float touchY) {
selectedValue.clear();
final BubbleChartData data = dataProvider.getBubbleChartData();
int valueIndex = 0;
for (BubbleValue bubbleValue : data.getValues()) {
float rawRadius = processBubble(bubbleValue, bubbleCenter);
if (ValueShape.SQUARE.equals(bubbleValue.getShape())) {
if (bubbleRect.contains(touchX, touchY)) {
selectedValue.set(valueIndex, valueIndex, SelectedValueType.NONE);
}
} else if (ValueShape.CIRCLE.equals(bubbleValue.getShape())) {
final float diffX = touchX - bubbleCenter.x;
final float diffY = touchY - bubbleCenter.y;
final float touchDistance = (float) Math.sqrt((diffX * diffX) + (diffY * diffY));
if (touchDistance <= rawRadius) {
selectedValue.set(valueIndex, valueIndex, SelectedValueType.NONE);
}
} else {
throw new IllegalArgumentException("Invalid bubble shape: " + bubbleValue.getShape());
}
++valueIndex;
}
return isTouched();
}
/**
* Removes empty spaces on sides of chart(left-right for landscape, top-bottom for portrait). *This method should be
* called after layout had been drawn*. Because most often chart is drawn as rectangle with proportions other than
* 1:1 and bubbles have to be drawn as circles not ellipses I am unable to calculate correct margins based on chart
* data only. I need to know chart dimension to remove extra empty spaces, that bad because viewport depends a
* little on contentRectMinusAllMargins.
*/
public void removeMargins() {
final Rect contentRect = computator.getContentRectMinusAllMargins();
if (contentRect.height() == 0 || contentRect.width() == 0) {
// View probably not yet measured, skip removing margins.
return;
}
final float pxX = computator.computeRawDistanceX(maxRadius * bubbleScaleX);
final float pxY = computator.computeRawDistanceY(maxRadius * bubbleScaleY);
final float scaleX = computator.getMaximumViewport().width() / contentRect.width();
final float scaleY = computator.getMaximumViewport().height() / contentRect.height();
float dx = 0;
float dy = 0;
if (isBubbleScaledByX) {
dy = (pxY - pxX) * scaleY * 0.75f;
} else {
dx = (pxX - pxY) * scaleX * 0.75f;
}
Viewport maxViewport = computator.getMaximumViewport();
maxViewport.inset(dx, dy);
Viewport currentViewport = computator.getCurrentViewport();
currentViewport.inset(dx, dy);
computator.setMaxViewport(maxViewport);
computator.setCurrentViewport(currentViewport);
}
private void drawBubbles(Canvas canvas) {
final BubbleChartData data = dataProvider.getBubbleChartData();
for (BubbleValue bubbleValue : data.getValues()) {
drawBubble(canvas, bubbleValue);
}
}
private void drawBubble(Canvas canvas, BubbleValue bubbleValue) {
float rawRadius = processBubble(bubbleValue, bubbleCenter);
// Not touched bubbles are a little smaller than touched to give user touch feedback.
rawRadius -= touchAdditional;
bubbleRect.inset(touchAdditional, touchAdditional);
bubblePaint.setColor(bubbleValue.getColor());
drawBubbleShapeAndLabel(canvas, bubbleValue, rawRadius, MODE_DRAW);
}
private void drawBubbleShapeAndLabel(Canvas canvas, BubbleValue bubbleValue, float rawRadius, int mode) {
if (ValueShape.SQUARE.equals(bubbleValue.getShape())) {
canvas.drawRect(bubbleRect, bubblePaint);
} else if (ValueShape.CIRCLE.equals(bubbleValue.getShape())) {
canvas.drawCircle(bubbleCenter.x, bubbleCenter.y, rawRadius, bubblePaint);
} else {
throw new IllegalArgumentException("Invalid bubble shape: " + bubbleValue.getShape());
}
if (MODE_HIGHLIGHT == mode) {
if (hasLabels || hasLabelsOnlyForSelected) {
drawLabel(canvas, bubbleValue, bubbleCenter.x, bubbleCenter.y);
}
} else if (MODE_DRAW == mode) {
if (hasLabels) {
drawLabel(canvas, bubbleValue, bubbleCenter.x, bubbleCenter.y);
}
} else {
throw new IllegalStateException("Cannot process bubble in mode: " + mode);
}
}
private void highlightBubbles(Canvas canvas) {
final BubbleChartData data = dataProvider.getBubbleChartData();
BubbleValue bubbleValue = data.getValues().get(selectedValue.getFirstIndex());
highlightBubble(canvas, bubbleValue);
}
private void highlightBubble(Canvas canvas, BubbleValue bubbleValue) {
float rawRadius = processBubble(bubbleValue, bubbleCenter);
bubblePaint.setColor(bubbleValue.getDarkenColor());
drawBubbleShapeAndLabel(canvas, bubbleValue, rawRadius, MODE_HIGHLIGHT);
}
/**
* Calculate bubble radius and center x and y coordinates. Center x and x will be stored in point parameter, radius
* will be returned as float value.
*/
private float processBubble(BubbleValue bubbleValue, PointF point) {
final float rawX = computator.computeRawX(bubbleValue.getX());
final float rawY = computator.computeRawY(bubbleValue.getY());
float radius = (float) Math.sqrt(Math.abs(bubbleValue.getZ()) / Math.PI);
float rawRadius;
if (isBubbleScaledByX) {
radius *= bubbleScaleX;
rawRadius = computator.computeRawDistanceX(radius);
} else {
radius *= bubbleScaleY;
rawRadius = computator.computeRawDistanceY(radius);
}
if (rawRadius < minRawRadius + touchAdditional) {
rawRadius = minRawRadius + touchAdditional;
}
bubbleCenter.set(rawX, rawY);
if (ValueShape.SQUARE.equals(bubbleValue.getShape())) {
bubbleRect.set(rawX - rawRadius, rawY - rawRadius, rawX + rawRadius, rawY + rawRadius);
}
return rawRadius;
}
private void drawLabel(Canvas canvas, BubbleValue bubbleValue, float rawX, float rawY) {
final Rect contentRect = computator.getContentRectMinusAllMargins();
final int numChars = valueFormatter.formatChartValue(labelBuffer, bubbleValue);
if (numChars == 0) {
// No need to draw empty label
return;
}
final float labelWidth = labelPaint.measureText(labelBuffer, labelBuffer.length - numChars, numChars);
final int labelHeight = Math.abs(fontMetrics.ascent);
float left = rawX - labelWidth / 2 - labelMargin;
float right = rawX + labelWidth / 2 + labelMargin;
float top = rawY - labelHeight / 2 - labelMargin;
float bottom = rawY + labelHeight / 2 + labelMargin;
if (top < contentRect.top) {
top = rawY;
bottom = rawY + labelHeight + labelMargin * 2;
}
if (bottom > contentRect.bottom) {
top = rawY - labelHeight - labelMargin * 2;
bottom = rawY;
}
if (left < contentRect.left) {
left = rawX;
right = rawX + labelWidth + labelMargin * 2;
}
if (right > contentRect.right) {
left = rawX - labelWidth - labelMargin * 2;
right = rawX;
}
labelBackgroundRect.set(left, top, right, bottom);
drawLabelTextAndBackground(canvas, labelBuffer, labelBuffer.length - numChars, numChars,
bubbleValue.getDarkenColor());
}
private void calculateMaxViewport() {
float maxZ = Float.MIN_VALUE;
tempMaximumViewport.set(Float.MAX_VALUE, Float.MIN_VALUE, Float.MIN_VALUE, Float.MAX_VALUE);
BubbleChartData data = dataProvider.getBubbleChartData();
// TODO: Optimize.
for (BubbleValue bubbleValue : data.getValues()) {
if (Math.abs(bubbleValue.getZ()) > maxZ) {
maxZ = Math.abs(bubbleValue.getZ());
}
if (bubbleValue.getX() < tempMaximumViewport.left) {
tempMaximumViewport.left = bubbleValue.getX();
}
if (bubbleValue.getX() > tempMaximumViewport.right) {
tempMaximumViewport.right = bubbleValue.getX();
}
if (bubbleValue.getY() < tempMaximumViewport.bottom) {
tempMaximumViewport.bottom = bubbleValue.getY();
}
if (bubbleValue.getY() > tempMaximumViewport.top) {
tempMaximumViewport.top = bubbleValue.getY();
}
}
maxRadius = (float) Math.sqrt(maxZ / Math.PI);
// Number 4 is determined by trials and errors method, no magic behind it:).
bubbleScaleX = tempMaximumViewport.width() / (maxRadius * 4);
if (bubbleScaleX == 0) {
// case for 0 viewport width.
bubbleScaleX = 1;
}
bubbleScaleY = tempMaximumViewport.height() / (maxRadius * 4);
if (bubbleScaleY == 0) {
// case for 0 viewport height.
bubbleScaleY = 1;
}
// For cases when user sets different than 1 bubble scale in BubbleChartData.
bubbleScaleX *= data.getBubbleScale();
bubbleScaleY *= data.getBubbleScale();
// Prevent cutting of bubbles on the edges of chart area.
tempMaximumViewport.inset(-maxRadius * bubbleScaleX, -maxRadius * bubbleScaleY);
minRawRadius = ChartUtils.dp2px(density, dataProvider.getBubbleChartData().getMinBubbleRadius());
}
}
@@ -0,0 +1,63 @@
package lecho.lib.hellocharts.renderer;
import android.graphics.Canvas;
import lecho.lib.hellocharts.model.SelectedValue;
import lecho.lib.hellocharts.model.Viewport;
/**
* Interface for all chart renderer.
*/
public interface ChartRenderer {
public void onChartSizeChanged();
public void onChartDataChanged();
public void onChartViewportChanged();
public void resetRenderer();
/**
* Draw chart data.
*/
public void draw(Canvas canvas);
/**
* Draw chart data that should not be clipped to contentRect area.
*/
public void drawUnclipped(Canvas canvas);
/**
* Checks if given pixel coordinates corresponds to any chart value. If yes return true and set selectedValue, if
* not selectedValue should be *cleared* and method should return false.
*/
public boolean checkTouch(float touchX, float touchY);
/**
* Returns true if there is value selected.
*/
public boolean isTouched();
/**
* Clear value selection.
*/
public void clearTouch();
public Viewport getMaximumViewport();
public void setMaximumViewport(Viewport maxViewport);
public Viewport getCurrentViewport();
public void setCurrentViewport(Viewport viewport);
public boolean isViewportCalculationEnabled();
public void setViewportCalculationEnabled(boolean isEnabled);
public void selectValue(SelectedValue selectedValue);
public SelectedValue getSelectedValue();
}
@@ -0,0 +1,422 @@
package lecho.lib.hellocharts.renderer;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Cap;
import android.graphics.PointF;
import android.graphics.RectF;
import lecho.lib.hellocharts.model.Column;
import lecho.lib.hellocharts.model.ColumnChartData;
import lecho.lib.hellocharts.model.SelectedValue.SelectedValueType;
import lecho.lib.hellocharts.model.SubcolumnValue;
import lecho.lib.hellocharts.model.Viewport;
import lecho.lib.hellocharts.provider.ColumnChartDataProvider;
import lecho.lib.hellocharts.util.ChartUtils;
import lecho.lib.hellocharts.view.Chart;
/**
* Magic renderer for ColumnChart.
*/
public class ColumnChartRenderer extends AbstractChartRenderer {
public static final int DEFAULT_SUBCOLUMN_SPACING_DP = 1;
public static final int DEFAULT_COLUMN_TOUCH_ADDITIONAL_WIDTH_DP = 4;
private static final int MODE_DRAW = 0;
private static final int MODE_CHECK_TOUCH = 1;
private static final int MODE_HIGHLIGHT = 2;
private ColumnChartDataProvider dataProvider;
/**
* Additional width for hightlighted column, used to give tauch feedback.
*/
private int touchAdditionalWidth;
/**
* Spacing between sub-columns.
*/
private int subcolumnSpacing;
/**
* Paint used to draw every column.
*/
private Paint columnPaint = new Paint();
/**
* Holds coordinates for currently processed column/sub-column.
*/
private RectF drawRect = new RectF();
/**
* Coordinated of user tauch.
*/
private PointF touchedPoint = new PointF();
private float fillRatio;
private float baseValue;
private Viewport tempMaximumViewport = new Viewport();
public ColumnChartRenderer(Context context, Chart chart, ColumnChartDataProvider dataProvider) {
super(context, chart);
this.dataProvider = dataProvider;
subcolumnSpacing = ChartUtils.dp2px(density, DEFAULT_SUBCOLUMN_SPACING_DP);
touchAdditionalWidth = ChartUtils.dp2px(density, DEFAULT_COLUMN_TOUCH_ADDITIONAL_WIDTH_DP);
columnPaint.setAntiAlias(true);
columnPaint.setStyle(Paint.Style.FILL);
columnPaint.setStrokeCap(Cap.SQUARE);
}
@Override
public void onChartSizeChanged() {
}
@Override
public void onChartDataChanged() {
super.onChartDataChanged();
ColumnChartData data = dataProvider.getColumnChartData();
fillRatio = data.getFillRatio();
baseValue = data.getBaseValue();
onChartViewportChanged();
}
@Override
public void onChartViewportChanged() {
if (isViewportCalculationEnabled) {
calculateMaxViewport();
computator.setMaxViewport(tempMaximumViewport);
computator.setCurrentViewport(computator.getMaximumViewport());
}
}
public void draw(Canvas canvas) {
final ColumnChartData data = dataProvider.getColumnChartData();
if (data.isStacked()) {
drawColumnForStacked(canvas);
if (isTouched()) {
highlightColumnForStacked(canvas);
}
} else {
drawColumnsForSubcolumns(canvas);
if (isTouched()) {
highlightColumnsForSubcolumns(canvas);
}
}
}
@Override
public void drawUnclipped(Canvas canvas) {
// Do nothing, for this kind of chart there is nothing to draw beyond clipped area
}
public boolean checkTouch(float touchX, float touchY) {
selectedValue.clear();
final ColumnChartData data = dataProvider.getColumnChartData();
if (data.isStacked()) {
checkTouchForStacked(touchX, touchY);
} else {
checkTouchForSubcolumns(touchX, touchY);
}
return isTouched();
}
private void calculateMaxViewport() {
final ColumnChartData data = dataProvider.getColumnChartData();
// Column chart always has X values from 0 to numColumns-1, to add some margin on the left and right I added
// extra 0.5 to the each side, that margins will be negative scaled according to number of columns, so for more
// columns there will be less margin.
tempMaximumViewport.set(-0.5f, baseValue, data.getColumns().size() - 0.5f, baseValue);
if (data.isStacked()) {
calculateMaxViewportForStacked(data);
} else {
calculateMaxViewportForSubcolumns(data);
}
}
private void calculateMaxViewportForSubcolumns(ColumnChartData data) {
for (Column column : data.getColumns()) {
for (SubcolumnValue columnValue : column.getValues()) {
if (columnValue.getValue() >= baseValue && columnValue.getValue() > tempMaximumViewport.top) {
tempMaximumViewport.top = columnValue.getValue();
}
if (columnValue.getValue() < baseValue && columnValue.getValue() < tempMaximumViewport.bottom) {
tempMaximumViewport.bottom = columnValue.getValue();
}
}
}
}
private void calculateMaxViewportForStacked(ColumnChartData data) {
for (Column column : data.getColumns()) {
float sumPositive = baseValue;
float sumNegative = baseValue;
for (SubcolumnValue columnValue : column.getValues()) {
if (columnValue.getValue() >= baseValue) {
sumPositive += columnValue.getValue();
} else {
sumNegative += columnValue.getValue();
}
}
if (sumPositive > tempMaximumViewport.top) {
tempMaximumViewport.top = sumPositive;
}
if (sumNegative < tempMaximumViewport.bottom) {
tempMaximumViewport.bottom = sumNegative;
}
}
}
private void drawColumnsForSubcolumns(Canvas canvas) {
final ColumnChartData data = dataProvider.getColumnChartData();
final float columnWidth = calculateColumnWidth();
int columnIndex = 0;
for (Column column : data.getColumns()) {
processColumnForSubcolumns(canvas, column, columnWidth, columnIndex, MODE_DRAW);
++columnIndex;
}
}
private void highlightColumnsForSubcolumns(Canvas canvas) {
final ColumnChartData data = dataProvider.getColumnChartData();
final float columnWidth = calculateColumnWidth();
Column column = data.getColumns().get(selectedValue.getFirstIndex());
processColumnForSubcolumns(canvas, column, columnWidth, selectedValue.getFirstIndex(), MODE_HIGHLIGHT);
}
private void checkTouchForSubcolumns(float touchX, float touchY) {
// Using member variable to hold touch point to avoid too much parameters in methods.
touchedPoint.x = touchX;
touchedPoint.y = touchY;
final ColumnChartData data = dataProvider.getColumnChartData();
final float columnWidth = calculateColumnWidth();
int columnIndex = 0;
for (Column column : data.getColumns()) {
// canvas is not needed for checking touch
processColumnForSubcolumns(null, column, columnWidth, columnIndex, MODE_CHECK_TOUCH);
++columnIndex;
}
}
private void processColumnForSubcolumns(Canvas canvas, Column column, float columnWidth, int columnIndex,
int mode) {
// For n subcolumns there will be n-1 spacing and there will be one
// subcolumn for every columnValue
float subcolumnWidth = (columnWidth - (subcolumnSpacing * (column.getValues().size() - 1)))
/ column.getValues().size();
if (subcolumnWidth < 1) {
subcolumnWidth = 1;
}
// Columns are indexes from 0 to n, column index is also column X value
final float rawX = computator.computeRawX(columnIndex);
final float halfColumnWidth = columnWidth / 2;
final float baseRawY = computator.computeRawY(baseValue);
// First subcolumn will starts at the left edge of current column,
// rawValueX is horizontal center of that column
float subcolumnRawX = rawX - halfColumnWidth;
int valueIndex = 0;
for (SubcolumnValue columnValue : column.getValues()) {
columnPaint.setColor(columnValue.getColor());
if (subcolumnRawX > rawX + halfColumnWidth) {
break;
}
final float rawY = computator.computeRawY(columnValue.getValue());
calculateRectToDraw(columnValue, subcolumnRawX, subcolumnRawX + subcolumnWidth, baseRawY, rawY);
switch (mode) {
case MODE_DRAW:
drawSubcolumn(canvas, column, columnValue, false);
break;
case MODE_HIGHLIGHT:
highlightSubcolumn(canvas, column, columnValue, valueIndex, false);
break;
case MODE_CHECK_TOUCH:
checkRectToDraw(columnIndex, valueIndex);
break;
default:
// There no else, every case should be handled or exception will
// be thrown
throw new IllegalStateException("Cannot process column in mode: " + mode);
}
subcolumnRawX += subcolumnWidth + subcolumnSpacing;
++valueIndex;
}
}
private void drawColumnForStacked(Canvas canvas) {
final ColumnChartData data = dataProvider.getColumnChartData();
final float columnWidth = calculateColumnWidth();
// Columns are indexes from 0 to n, column index is also column X value
int columnIndex = 0;
for (Column column : data.getColumns()) {
processColumnForStacked(canvas, column, columnWidth, columnIndex, MODE_DRAW);
++columnIndex;
}
}
private void highlightColumnForStacked(Canvas canvas) {
final ColumnChartData data = dataProvider.getColumnChartData();
final float columnWidth = calculateColumnWidth();
// Columns are indexes from 0 to n, column index is also column X value
Column column = data.getColumns().get(selectedValue.getFirstIndex());
processColumnForStacked(canvas, column, columnWidth, selectedValue.getFirstIndex(), MODE_HIGHLIGHT);
}
private void checkTouchForStacked(float touchX, float touchY) {
touchedPoint.x = touchX;
touchedPoint.y = touchY;
final ColumnChartData data = dataProvider.getColumnChartData();
final float columnWidth = calculateColumnWidth();
int columnIndex = 0;
for (Column column : data.getColumns()) {
// canvas is not needed for checking touch
processColumnForStacked(null, column, columnWidth, columnIndex, MODE_CHECK_TOUCH);
++columnIndex;
}
}
private void processColumnForStacked(Canvas canvas, Column column, float columnWidth, int columnIndex, int mode) {
final float rawX = computator.computeRawX(columnIndex);
final float halfColumnWidth = columnWidth / 2;
float mostPositiveValue = baseValue;
float mostNegativeValue = baseValue;
float subcolumnBaseValue = baseValue;
int valueIndex = 0;
for (SubcolumnValue columnValue : column.getValues()) {
columnPaint.setColor(columnValue.getColor());
if (columnValue.getValue() >= baseValue) {
// Using values instead of raw pixels make code easier to
// understand(for me)
subcolumnBaseValue = mostPositiveValue;
mostPositiveValue += columnValue.getValue();
} else {
subcolumnBaseValue = mostNegativeValue;
mostNegativeValue += columnValue.getValue();
}
final float rawBaseY = computator.computeRawY(subcolumnBaseValue);
final float rawY = computator.computeRawY(subcolumnBaseValue + columnValue.getValue());
calculateRectToDraw(columnValue, rawX - halfColumnWidth, rawX + halfColumnWidth, rawBaseY, rawY);
switch (mode) {
case MODE_DRAW:
drawSubcolumn(canvas, column, columnValue, true);
break;
case MODE_HIGHLIGHT:
highlightSubcolumn(canvas, column, columnValue, valueIndex, true);
break;
case MODE_CHECK_TOUCH:
checkRectToDraw(columnIndex, valueIndex);
break;
default:
// There no else, every case should be handled or exception will
// be thrown
throw new IllegalStateException("Cannot process column in mode: " + mode);
}
++valueIndex;
}
}
private void drawSubcolumn(Canvas canvas, Column column, SubcolumnValue columnValue, boolean isStacked) {
canvas.drawRect(drawRect, columnPaint);
if (column.hasLabels()) {
drawLabel(canvas, column, columnValue, isStacked, labelOffset);
}
}
private void highlightSubcolumn(Canvas canvas, Column column, SubcolumnValue columnValue, int valueIndex,
boolean isStacked) {
if (selectedValue.getSecondIndex() == valueIndex) {
columnPaint.setColor(columnValue.getDarkenColor());
canvas.drawRect(drawRect.left - touchAdditionalWidth, drawRect.top, drawRect.right + touchAdditionalWidth,
drawRect.bottom, columnPaint);
if (column.hasLabels() || column.hasLabelsOnlyForSelected()) {
drawLabel(canvas, column, columnValue, isStacked, labelOffset);
}
}
}
private void checkRectToDraw(int columnIndex, int valueIndex) {
if (drawRect.contains(touchedPoint.x, touchedPoint.y)) {
selectedValue.set(columnIndex, valueIndex, SelectedValueType.COLUMN);
}
}
private float calculateColumnWidth() {
// columnWidht should be at least 2 px
float columnWidth = fillRatio * computator.getContentRectMinusAllMargins().width() / computator
.getVisibleViewport().width();
if (columnWidth < 2) {
columnWidth = 2;
}
return columnWidth;
}
private void calculateRectToDraw(SubcolumnValue columnValue, float left, float right, float rawBaseY, float rawY) {
// Calculate rect that will be drawn as column, subcolumn or label background.
drawRect.left = left;
drawRect.right = right;
if (columnValue.getValue() >= baseValue) {
drawRect.top = rawY;
drawRect.bottom = rawBaseY - subcolumnSpacing;
} else {
drawRect.bottom = rawY;
drawRect.top = rawBaseY + subcolumnSpacing;
}
}
private void drawLabel(Canvas canvas, Column column, SubcolumnValue columnValue, boolean isStacked, float offset) {
final int numChars = column.getFormatter().formatChartValue(labelBuffer, columnValue);
if (numChars == 0) {
// No need to draw empty label
return;
}
final float labelWidth = labelPaint.measureText(labelBuffer, labelBuffer.length - numChars, numChars);
final int labelHeight = Math.abs(fontMetrics.ascent);
float left = drawRect.centerX() - labelWidth / 2 - labelMargin;
float right = drawRect.centerX() + labelWidth / 2 + labelMargin;
float top;
float bottom;
if (isStacked && labelHeight < drawRect.height() - (2 * labelMargin)) {
// For stacked columns draw label only if label height is less than subcolumn height - (2 * labelMargin).
if (columnValue.getValue() >= baseValue) {
top = drawRect.top;
bottom = drawRect.top + labelHeight + labelMargin * 2;
} else {
top = drawRect.bottom - labelHeight - labelMargin * 2;
bottom = drawRect.bottom;
}
} else if (!isStacked) {
// For not stacked draw label at the top for positive and at the bottom for negative values
if (columnValue.getValue() >= baseValue) {
top = drawRect.top - offset - labelHeight - labelMargin * 2;
if (top < computator.getContentRectMinusAllMargins().top) {
top = drawRect.top + offset;
bottom = drawRect.top + offset + labelHeight + labelMargin * 2;
} else {
bottom = drawRect.top - offset;
}
} else {
bottom = drawRect.bottom + offset + labelHeight + labelMargin * 2;
if (bottom > computator.getContentRectMinusAllMargins().bottom) {
top = drawRect.bottom - offset - labelHeight - labelMargin * 2;
bottom = drawRect.bottom - offset;
} else {
top = drawRect.bottom + offset;
}
}
} else {
// Draw nothing.
return;
}
labelBackgroundRect.set(left, top, right, bottom);
drawLabelTextAndBackground(canvas, labelBuffer, labelBuffer.length - numChars, numChars,
columnValue.getDarkenColor());
}
}
@@ -0,0 +1,99 @@
package lecho.lib.hellocharts.renderer;
import android.content.Context;
import android.graphics.Canvas;
import java.util.ArrayList;
import java.util.List;
import lecho.lib.hellocharts.model.Viewport;
import lecho.lib.hellocharts.view.Chart;
public class ComboChartRenderer extends AbstractChartRenderer {
protected List<ChartRenderer> renderers;
protected Viewport unionViewport = new Viewport();
public ComboChartRenderer(Context context, Chart chart) {
super(context, chart);
this.renderers = new ArrayList<>();
}
@Override
public void onChartSizeChanged() {
for (ChartRenderer renderer : renderers) {
renderer.onChartSizeChanged();
}
}
@Override
public void onChartDataChanged() {
super.onChartDataChanged();
for (ChartRenderer renderer : renderers) {
renderer.onChartDataChanged();
}
onChartViewportChanged();
}
@Override
public void onChartViewportChanged() {
if (isViewportCalculationEnabled) {
int rendererIndex = 0;
for (ChartRenderer renderer : renderers) {
renderer.onChartViewportChanged();
if (rendererIndex == 0) {
unionViewport.set(renderer.getMaximumViewport());
} else {
unionViewport.union(renderer.getMaximumViewport());
}
++rendererIndex;
}
computator.setMaxViewport(unionViewport);
computator.setCurrentViewport(unionViewport);
}
}
public void draw(Canvas canvas) {
for (ChartRenderer renderer : renderers) {
renderer.draw(canvas);
}
}
@Override
public void drawUnclipped(Canvas canvas) {
for (ChartRenderer renderer : renderers) {
renderer.drawUnclipped(canvas);
}
}
public boolean checkTouch(float touchX, float touchY) {
selectedValue.clear();
int rendererIndex = renderers.size() - 1;
for (; rendererIndex >= 0; rendererIndex--) {
ChartRenderer renderer = renderers.get(rendererIndex);
if (renderer.checkTouch(touchX, touchY)) {
selectedValue.set(renderer.getSelectedValue());
break;
}
}
//clear the rest of renderers if value was selected, if value was not selected this loop
// will not be executed.
for (rendererIndex--; rendererIndex >= 0; rendererIndex--) {
ChartRenderer renderer = renderers.get(rendererIndex);
renderer.clearTouch();
}
return isTouched();
}
@Override
public void clearTouch() {
for (ChartRenderer renderer : renderers) {
renderer.clearTouch();
}
selectedValue.clear();
}
}
@@ -0,0 +1,40 @@
package lecho.lib.hellocharts.renderer;
import android.content.Context;
import lecho.lib.hellocharts.provider.ColumnChartDataProvider;
import lecho.lib.hellocharts.provider.LineChartDataProvider;
import lecho.lib.hellocharts.view.Chart;
public class ComboLineColumnChartRenderer extends ComboChartRenderer {
private ColumnChartRenderer columnChartRenderer;
private LineChartRenderer lineChartRenderer;
public ComboLineColumnChartRenderer(Context context, Chart chart, ColumnChartDataProvider columnChartDataProvider,
LineChartDataProvider lineChartDataProvider) {
this(context, chart, new ColumnChartRenderer(context, chart, columnChartDataProvider),
new LineChartRenderer(context, chart, lineChartDataProvider));
}
public ComboLineColumnChartRenderer(Context context, Chart chart, ColumnChartRenderer columnChartRenderer,
LineChartDataProvider lineChartDataProvider) {
this(context, chart, columnChartRenderer, new LineChartRenderer(context, chart, lineChartDataProvider));
}
public ComboLineColumnChartRenderer(Context context, Chart chart, ColumnChartDataProvider columnChartDataProvider,
LineChartRenderer lineChartRenderer) {
this(context, chart, new ColumnChartRenderer(context, chart, columnChartDataProvider), lineChartRenderer);
}
public ComboLineColumnChartRenderer(Context context, Chart chart, ColumnChartRenderer columnChartRenderer,
LineChartRenderer lineChartRenderer) {
super(context, chart);
this.columnChartRenderer = columnChartRenderer;
this.lineChartRenderer = lineChartRenderer;
renderers.add(this.columnChartRenderer);
renderers.add(this.lineChartRenderer);
}
}
@@ -0,0 +1,514 @@
package lecho.lib.hellocharts.renderer;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Paint.Cap;
import android.graphics.Path;
import android.graphics.PorterDuff.Mode;
import android.graphics.Rect;
import android.graphics.Shader;
import lecho.lib.hellocharts.model.Line;
import lecho.lib.hellocharts.model.LineChartData;
import lecho.lib.hellocharts.model.PointValue;
import lecho.lib.hellocharts.model.SelectedValue.SelectedValueType;
import lecho.lib.hellocharts.model.ValueShape;
import lecho.lib.hellocharts.model.Viewport;
import lecho.lib.hellocharts.provider.LineChartDataProvider;
import lecho.lib.hellocharts.util.ChartUtils;
import lecho.lib.hellocharts.view.Chart;
/**
* Renderer for line chart. Can draw lines, cubic lines, filled area chart and scattered chart.
*/
public class LineChartRenderer extends AbstractChartRenderer {
private static final float LINE_SMOOTHNESS = 0.16f;
private static final int DEFAULT_LINE_STROKE_WIDTH_DP = 3;
private static final int DEFAULT_TOUCH_TOLERANCE_MARGIN_DP = 4;
private static final int MODE_DRAW = 0;
private static final int MODE_HIGHLIGHT = 1;
private LineChartDataProvider dataProvider;
private int checkPrecision;
private float baseValue;
private int touchToleranceMargin;
private Path path = new Path();
private Paint linePaint = new Paint();
private Paint pointPaint = new Paint();
private Bitmap softwareBitmap;
private Canvas softwareCanvas = new Canvas();
private Viewport tempMaximumViewport = new Viewport();
public LineChartRenderer(Context context, Chart chart, LineChartDataProvider dataProvider) {
super(context, chart);
this.dataProvider = dataProvider;
touchToleranceMargin = ChartUtils.dp2px(density, DEFAULT_TOUCH_TOLERANCE_MARGIN_DP);
linePaint.setAntiAlias(true);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeCap(Cap.ROUND);
linePaint.setStrokeWidth(ChartUtils.dp2px(density, DEFAULT_LINE_STROKE_WIDTH_DP));
pointPaint.setAntiAlias(true);
pointPaint.setStyle(Paint.Style.FILL);
checkPrecision = ChartUtils.dp2px(density, 2);
}
public void onChartSizeChanged() {
final int internalMargin = calculateContentRectInternalMargin();
computator.insetContentRectByInternalMargins(internalMargin, internalMargin,
internalMargin, internalMargin);
if (computator.getChartWidth() > 0 && computator.getChartHeight() > 0) {
softwareBitmap = Bitmap.createBitmap(computator.getChartWidth(), computator.getChartHeight(),
Bitmap.Config.ARGB_8888);
softwareCanvas.setBitmap(softwareBitmap);
}
}
@Override
public void onChartDataChanged() {
super.onChartDataChanged();
final int internalMargin = calculateContentRectInternalMargin();
computator.insetContentRectByInternalMargins(internalMargin, internalMargin,
internalMargin, internalMargin);
baseValue = dataProvider.getLineChartData().getBaseValue();
onChartViewportChanged();
}
@Override
public void onChartViewportChanged() {
if (isViewportCalculationEnabled) {
calculateMaxViewport();
computator.setMaxViewport(tempMaximumViewport);
computator.setCurrentViewport(computator.getMaximumViewport());
}
}
@Override
public void draw(Canvas canvas) {
final LineChartData data = dataProvider.getLineChartData();
final Canvas drawCanvas;
// softwareBitmap can be null if chart is rendered in layout editor. In that case use default canvas and not
// softwareCanvas.
if (null != softwareBitmap) {
drawCanvas = softwareCanvas;
drawCanvas.drawColor(Color.TRANSPARENT, Mode.CLEAR);
} else {
drawCanvas = canvas;
}
for (Line line : data.getLines()) {
if (line.hasLines()) {
if (line.isCubic()) {
drawSmoothPath(drawCanvas, line);
} else if (line.isSquare()) {
drawSquarePath(drawCanvas, line);
} else {
drawPath(drawCanvas, line);
}
}
}
if (null != softwareBitmap) {
canvas.drawBitmap(softwareBitmap, 0, 0, null);
}
}
@Override
public void drawUnclipped(Canvas canvas) {
final LineChartData data = dataProvider.getLineChartData();
int lineIndex = 0;
for (Line line : data.getLines()) {
if (checkIfShouldDrawPoints(line)) {
drawPoints(canvas, line, lineIndex, MODE_DRAW);
}
++lineIndex;
}
if (isTouched()) {
// Redraw touched point to bring it to the front
highlightPoints(canvas);
}
}
private boolean checkIfShouldDrawPoints(Line line) {
return line.hasPoints() || line.getValues().size() == 1;
}
@Override
public boolean checkTouch(float touchX, float touchY) {
selectedValue.clear();
final LineChartData data = dataProvider.getLineChartData();
int lineIndex = 0;
for (Line line : data.getLines()) {
if (checkIfShouldDrawPoints(line)) {
int pointRadius = ChartUtils.dp2px(density, line.getPointRadius());
int valueIndex = 0;
for (PointValue pointValue : line.getValues()) {
final float rawValueX = computator.computeRawX(pointValue.getX());
final float rawValueY = computator.computeRawY(pointValue.getY());
if (isInArea(rawValueX, rawValueY, touchX, touchY, pointRadius + touchToleranceMargin)) {
selectedValue.set(lineIndex, valueIndex, SelectedValueType.LINE);
}
++valueIndex;
}
}
++lineIndex;
}
return isTouched();
}
private void calculateMaxViewport() {
tempMaximumViewport.set(Float.MAX_VALUE, Float.MIN_VALUE, Float.MIN_VALUE, Float.MAX_VALUE);
LineChartData data = dataProvider.getLineChartData();
for (Line line : data.getLines()) {
// Calculate max and min for viewport.
for (PointValue pointValue : line.getValues()) {
if (pointValue.getX() < tempMaximumViewport.left) {
tempMaximumViewport.left = pointValue.getX();
}
if (pointValue.getX() > tempMaximumViewport.right) {
tempMaximumViewport.right = pointValue.getX();
}
if (pointValue.getY() < tempMaximumViewport.bottom) {
tempMaximumViewport.bottom = pointValue.getY();
}
if (pointValue.getY() > tempMaximumViewport.top) {
tempMaximumViewport.top = pointValue.getY();
}
}
}
}
private int calculateContentRectInternalMargin() {
int contentAreaMargin = 0;
final LineChartData data = dataProvider.getLineChartData();
for (Line line : data.getLines()) {
if (checkIfShouldDrawPoints(line)) {
int margin = line.getPointRadius() + DEFAULT_TOUCH_TOLERANCE_MARGIN_DP;
if (margin > contentAreaMargin) {
contentAreaMargin = margin;
}
}
}
return ChartUtils.dp2px(density, contentAreaMargin);
}
/**
* Draws lines, uses path for drawing filled area on software canvas. Line is drawn with canvas.drawLines() method.
*/
private void drawPath(Canvas canvas, final Line line) {
prepareLinePaint(line);
int valueIndex = 0;
for (PointValue pointValue : line.getValues()) {
final float rawX = computator.computeRawX(pointValue.getX());
final float rawY = computator.computeRawY(pointValue.getY());
if (valueIndex == 0) {
path.moveTo(rawX, rawY);
} else {
path.lineTo(rawX, rawY);
}
++valueIndex;
}
canvas.drawPath(path, linePaint);
if (line.isFilled()) {
drawArea(canvas, line);
}
path.reset();
}
private void drawSquarePath(Canvas canvas, final Line line) {
prepareLinePaint(line);
int valueIndex = 0;
float previousRawY = 0;
for (PointValue pointValue : line.getValues()) {
final float rawX = computator.computeRawX(pointValue.getX());
final float rawY = computator.computeRawY(pointValue.getY());
if (valueIndex == 0) {
path.moveTo(rawX, rawY);
} else {
path.lineTo(rawX, previousRawY);
path.lineTo(rawX, rawY);
}
previousRawY = rawY;
++valueIndex;
}
canvas.drawPath(path, linePaint);
if (line.isFilled()) {
drawArea(canvas, line);
}
path.reset();
}
private void drawSmoothPath(Canvas canvas, final Line line) {
prepareLinePaint(line);
final int lineSize = line.getValues().size();
float prePreviousPointX = Float.NaN;
float prePreviousPointY = Float.NaN;
float previousPointX = Float.NaN;
float previousPointY = Float.NaN;
float currentPointX = Float.NaN;
float currentPointY = Float.NaN;
float nextPointX = Float.NaN;
float nextPointY = Float.NaN;
for (int valueIndex = 0; valueIndex < lineSize; ++valueIndex) {
if (Float.isNaN(currentPointX)) {
PointValue linePoint = line.getValues().get(valueIndex);
currentPointX = computator.computeRawX(linePoint.getX());
currentPointY = computator.computeRawY(linePoint.getY());
}
if (Float.isNaN(previousPointX)) {
if (valueIndex > 0) {
PointValue linePoint = line.getValues().get(valueIndex - 1);
previousPointX = computator.computeRawX(linePoint.getX());
previousPointY = computator.computeRawY(linePoint.getY());
} else {
previousPointX = currentPointX;
previousPointY = currentPointY;
}
}
if (Float.isNaN(prePreviousPointX)) {
if (valueIndex > 1) {
PointValue linePoint = line.getValues().get(valueIndex - 2);
prePreviousPointX = computator.computeRawX(linePoint.getX());
prePreviousPointY = computator.computeRawY(linePoint.getY());
} else {
prePreviousPointX = previousPointX;
prePreviousPointY = previousPointY;
}
}
// nextPoint is always new one or it is equal currentPoint.
if (valueIndex < lineSize - 1) {
PointValue linePoint = line.getValues().get(valueIndex + 1);
nextPointX = computator.computeRawX(linePoint.getX());
nextPointY = computator.computeRawY(linePoint.getY());
} else {
nextPointX = currentPointX;
nextPointY = currentPointY;
}
if (valueIndex == 0) {
// Move to start point.
path.moveTo(currentPointX, currentPointY);
} else {
// Calculate control points.
final float firstDiffX = (currentPointX - prePreviousPointX);
final float firstDiffY = (currentPointY - prePreviousPointY);
final float secondDiffX = (nextPointX - previousPointX);
final float secondDiffY = (nextPointY - previousPointY);
final float firstControlPointX = previousPointX + (LINE_SMOOTHNESS * firstDiffX);
final float firstControlPointY = previousPointY + (LINE_SMOOTHNESS * firstDiffY);
final float secondControlPointX = currentPointX - (LINE_SMOOTHNESS * secondDiffX);
final float secondControlPointY = currentPointY - (LINE_SMOOTHNESS * secondDiffY);
path.cubicTo(firstControlPointX, firstControlPointY, secondControlPointX, secondControlPointY,
currentPointX, currentPointY);
}
// Shift values by one back to prevent recalculation of values that have
// been already calculated.
prePreviousPointX = previousPointX;
prePreviousPointY = previousPointY;
previousPointX = currentPointX;
previousPointY = currentPointY;
currentPointX = nextPointX;
currentPointY = nextPointY;
}
canvas.drawPath(path, linePaint);
if (line.isFilled()) {
drawArea(canvas, line);
}
path.reset();
}
private void prepareLinePaint(final Line line) {
linePaint.setStrokeWidth(ChartUtils.dp2px(density, line.getStrokeWidth()));
linePaint.setColor(line.getColor());
linePaint.setPathEffect(line.getPathEffect());
linePaint.setShader(null);
}
// TODO Drawing points can be done in the same loop as drawing lines but it
// may cause problems in the future with
// implementing point styles.
private void drawPoints(Canvas canvas, Line line, int lineIndex, int mode) {
pointPaint.setColor(line.getPointColor());
int valueIndex = 0;
for (PointValue pointValue : line.getValues()) {
int pointRadius = ChartUtils.dp2px(density, line.getPointRadius());
final float rawX = computator.computeRawX(pointValue.getX());
final float rawY = computator.computeRawY(pointValue.getY());
if (computator.isWithinContentRect(rawX, rawY, checkPrecision)) {
// Draw points only if they are within contentRectMinusAllMargins, using contentRectMinusAllMargins
// instead of viewport to avoid some
// float rounding problems.
if (MODE_DRAW == mode) {
drawPoint(canvas, line, pointValue, rawX, rawY, pointRadius);
if (line.hasLabels()) {
drawLabel(canvas, line, pointValue, rawX, rawY, pointRadius + labelOffset);
}
} else if (MODE_HIGHLIGHT == mode) {
highlightPoint(canvas, line, pointValue, rawX, rawY, lineIndex, valueIndex);
} else {
throw new IllegalStateException("Cannot process points in mode: " + mode);
}
}
++valueIndex;
}
}
private void drawPoint(Canvas canvas, Line line, PointValue pointValue, float rawX, float rawY,
float pointRadius) {
if (ValueShape.SQUARE.equals(line.getShape())) {
canvas.drawRect(rawX - pointRadius, rawY - pointRadius, rawX + pointRadius, rawY + pointRadius,
pointPaint);
} else if (ValueShape.CIRCLE.equals(line.getShape())) {
canvas.drawCircle(rawX, rawY, pointRadius, pointPaint);
} else if (ValueShape.DIAMOND.equals(line.getShape())) {
canvas.save();
canvas.rotate(45, rawX, rawY);
canvas.drawRect(rawX - pointRadius, rawY - pointRadius, rawX + pointRadius, rawY + pointRadius,
pointPaint);
canvas.restore();
} else {
throw new IllegalArgumentException("Invalid point shape: " + line.getShape());
}
}
private void highlightPoints(Canvas canvas) {
int lineIndex = selectedValue.getFirstIndex();
Line line = dataProvider.getLineChartData().getLines().get(lineIndex);
drawPoints(canvas, line, lineIndex, MODE_HIGHLIGHT);
}
private void highlightPoint(Canvas canvas, Line line, PointValue pointValue, float rawX, float rawY, int lineIndex,
int valueIndex) {
if (selectedValue.getFirstIndex() == lineIndex && selectedValue.getSecondIndex() == valueIndex) {
int pointRadius = ChartUtils.dp2px(density, line.getPointRadius());
pointPaint.setColor(line.getDarkenColor());
drawPoint(canvas, line, pointValue, rawX, rawY, pointRadius + touchToleranceMargin);
if (line.hasLabels() || line.hasLabelsOnlyForSelected()) {
drawLabel(canvas, line, pointValue, rawX, rawY, pointRadius + labelOffset);
}
}
}
private void drawLabel(Canvas canvas, Line line, PointValue pointValue, float rawX, float rawY, float offset) {
final Rect contentRect = computator.getContentRectMinusAllMargins();
final int numChars = line.getFormatter().formatChartValue(labelBuffer, pointValue);
if (numChars == 0) {
// No need to draw empty label
return;
}
final float labelWidth = labelPaint.measureText(labelBuffer, labelBuffer.length - numChars, numChars);
final int labelHeight = Math.abs(fontMetrics.ascent);
float left = rawX - labelWidth / 2 - labelMargin;
float right = rawX + labelWidth / 2 + labelMargin;
float top;
float bottom;
if (pointValue.getY() >= baseValue) {
top = rawY - offset - labelHeight - labelMargin * 2;
bottom = rawY - offset;
} else {
top = rawY + offset;
bottom = rawY + offset + labelHeight + labelMargin * 2;
}
if (top < contentRect.top) {
top = rawY + offset;
bottom = rawY + offset + labelHeight + labelMargin * 2;
}
if (bottom > contentRect.bottom) {
top = rawY - offset - labelHeight - labelMargin * 2;
bottom = rawY - offset;
}
if (left < contentRect.left) {
left = rawX;
right = rawX + labelWidth + labelMargin * 2;
}
if (right > contentRect.right) {
left = rawX - labelWidth - labelMargin * 2;
right = rawX;
}
labelBackgroundRect.set(left, top, right, bottom);
drawLabelTextAndBackground(canvas, labelBuffer, labelBuffer.length - numChars, numChars,
line.getDarkenColor());
}
private void drawArea(Canvas canvas, Line line) {
final int lineSize = line.getValues().size();
if (lineSize < 2) {
//No point to draw area for one point or empty line.
return;
}
final Rect contentRect = computator.getContentRectMinusAllMargins();
final float baseRawValue = Math.min(contentRect.bottom, Math.max(computator.computeRawY(baseValue),
contentRect.top));
//That checks works only if the last point is the right most one.
final float left = Math.max(computator.computeRawX(line.getValues().get(0).getX()), contentRect.left);
final float right = Math.min(computator.computeRawX(line.getValues().get(lineSize - 1).getX()),
contentRect.right);
path.lineTo(right, baseRawValue);
path.lineTo(left, baseRawValue);
path.close();
linePaint.setStyle(Paint.Style.FILL);
linePaint.setAlpha(line.getAreaTransparency());
linePaint.setShader(line.getGradientToTransparent() ?
new LinearGradient(0, 0, 0, canvas.getHeight(), line.getColor(),
line.getColor() & 0x00ffffff, Shader.TileMode.MIRROR) :
null);
canvas.drawPath(path, linePaint);
linePaint.setStyle(Paint.Style.STROKE);
}
private boolean isInArea(float x, float y, float touchX, float touchY, float radius) {
float diffX = touchX - x;
float diffY = touchY - y;
return Math.pow(diffX, 2) + Math.pow(diffY, 2) <= 2 * Math.pow(radius, 2);
}
}
@@ -0,0 +1,513 @@
package lecho.lib.hellocharts.renderer;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.PointF;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.text.TextUtils;
import lecho.lib.hellocharts.formatter.PieChartValueFormatter;
import lecho.lib.hellocharts.model.PieChartData;
import lecho.lib.hellocharts.model.SelectedValue;
import lecho.lib.hellocharts.model.SelectedValue.SelectedValueType;
import lecho.lib.hellocharts.model.SliceValue;
import lecho.lib.hellocharts.model.Viewport;
import lecho.lib.hellocharts.provider.PieChartDataProvider;
import lecho.lib.hellocharts.util.ChartUtils;
import lecho.lib.hellocharts.view.Chart;
/**
* Default renderer for PieChart. PieChart doesn't use viewport concept so it a little different than others chart
* types.
*/
public class PieChartRenderer extends AbstractChartRenderer {
private static final float MAX_WIDTH_HEIGHT = 100f;
private static final int DEFAULT_START_ROTATION = 45;
private static final float DEFAULT_LABEL_INSIDE_RADIUS_FACTOR = 0.7f;
private static final float DEFAULT_LABEL_OUTSIDE_RADIUS_FACTOR = 1.0f;
private static final int DEFAULT_TOUCH_ADDITIONAL_DP = 8;
private static final int MODE_DRAW = 0;
private static final int MODE_HIGHLIGHT = 1;
private int rotation = DEFAULT_START_ROTATION;
private PieChartDataProvider dataProvider;
private Paint slicePaint = new Paint();
private float maxSum;
private RectF originCircleOval = new RectF();
private RectF drawCircleOval = new RectF();
private PointF sliceVector = new PointF();
private int touchAdditional;
private float circleFillRatio = 1.0f;
// Center circle related attributes
private boolean hasCenterCircle;
private float centerCircleScale;
private Paint centerCirclePaint = new Paint();
// Text1
private Paint centerCircleText1Paint = new Paint();
private FontMetricsInt centerCircleText1FontMetrics = new FontMetricsInt();
// Text2
private Paint centerCircleText2Paint = new Paint();
private FontMetricsInt centerCircleText2FontMetrics = new FontMetricsInt();
// Separation lines
private Paint separationLinesPaint = new Paint();
private boolean hasLabelsOutside;
private boolean hasLabels;
private boolean hasLabelsOnlyForSelected;
private PieChartValueFormatter valueFormatter;
private Viewport tempMaximumViewport = new Viewport();
private Bitmap softwareBitmap;
private Canvas softwareCanvas = new Canvas();
public PieChartRenderer(Context context, Chart chart, PieChartDataProvider dataProvider) {
super(context, chart);
this.dataProvider = dataProvider;
touchAdditional = ChartUtils.dp2px(density, DEFAULT_TOUCH_ADDITIONAL_DP);
slicePaint.setAntiAlias(true);
slicePaint.setStyle(Paint.Style.FILL);
centerCirclePaint.setAntiAlias(true);
centerCirclePaint.setStyle(Paint.Style.FILL);
centerCirclePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
centerCircleText1Paint.setAntiAlias(true);
centerCircleText1Paint.setTextAlign(Align.CENTER);
centerCircleText2Paint.setAntiAlias(true);
centerCircleText2Paint.setTextAlign(Align.CENTER);
separationLinesPaint.setAntiAlias(true);
separationLinesPaint.setStyle(Paint.Style.STROKE);
separationLinesPaint.setStrokeCap(Paint.Cap.ROUND);
separationLinesPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
separationLinesPaint.setColor(Color.TRANSPARENT);
}
@Override
public void onChartSizeChanged() {
calculateCircleOval();
if (computator.getChartWidth() > 0 && computator.getChartHeight() > 0) {
softwareBitmap = Bitmap.createBitmap(computator.getChartWidth(), computator.getChartHeight(),
Bitmap.Config.ARGB_8888);
softwareCanvas.setBitmap(softwareBitmap);
}
}
@Override
public void onChartDataChanged() {
super.onChartDataChanged();
final PieChartData data = dataProvider.getPieChartData();
hasLabelsOutside = data.hasLabelsOutside();
hasLabels = data.hasLabels();
hasLabelsOnlyForSelected = data.hasLabelsOnlyForSelected();
valueFormatter = data.getFormatter();
hasCenterCircle = data.hasCenterCircle();
centerCircleScale = data.getCenterCircleScale();
centerCirclePaint.setColor(data.getCenterCircleColor());
if (null != data.getCenterText1Typeface()) {
centerCircleText1Paint.setTypeface(data.getCenterText1Typeface());
}
centerCircleText1Paint.setTextSize(ChartUtils.sp2px(scaledDensity, data.getCenterText1FontSize()));
centerCircleText1Paint.setColor(data.getCenterText1Color());
centerCircleText1Paint.getFontMetricsInt(centerCircleText1FontMetrics);
if (null != data.getCenterText2Typeface()) {
centerCircleText2Paint.setTypeface(data.getCenterText2Typeface());
}
centerCircleText2Paint.setTextSize(ChartUtils.sp2px(scaledDensity, data.getCenterText2FontSize()));
centerCircleText2Paint.setColor(data.getCenterText2Color());
centerCircleText2Paint.getFontMetricsInt(centerCircleText2FontMetrics);
onChartViewportChanged();
}
@Override
public void onChartViewportChanged() {
if (isViewportCalculationEnabled) {
calculateMaxViewport();
computator.setMaxViewport(tempMaximumViewport);
computator.setCurrentViewport(computator.getMaximumViewport());
}
}
@Override
public void draw(Canvas canvas) {
// softwareBitmap can be null if chart is rendered in layout editor. In that case use default canvas and not
// softwareCanvas.
final Canvas drawCanvas;
if (null != softwareBitmap) {
drawCanvas = softwareCanvas;
drawCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
} else {
drawCanvas = canvas;
}
drawSlices(drawCanvas);
drawSeparationLines(drawCanvas);
if (hasCenterCircle) {
drawCenterCircle(drawCanvas);
}
drawLabels(drawCanvas);
if (null != softwareBitmap) {
canvas.drawBitmap(softwareBitmap, 0, 0, null);
}
}
@Override
public void drawUnclipped(Canvas canvas) {
}
@Override
public boolean checkTouch(float touchX, float touchY) {
selectedValue.clear();
final PieChartData data = dataProvider.getPieChartData();
final float centerX = originCircleOval.centerX();
final float centerY = originCircleOval.centerY();
final float circleRadius = originCircleOval.width() / 2f;
sliceVector.set(touchX - centerX, touchY - centerY);
// Check if touch is on circle area, if not return false;
if (sliceVector.length() > circleRadius + touchAdditional) {
return false;
}
// Check if touch is not in center circle, if yes return false;
if (data.hasCenterCircle() && sliceVector.length() < circleRadius * data.getCenterCircleScale()) {
return false;
}
// Get touchAngle and align touch 0 degrees with chart 0 degrees, that why I subtracting start angle,
// adding 360
// and modulo 360 translates i.e -20 degrees to 340 degrees.
final float touchAngle = (pointToAngle(touchX, touchY, centerX, centerY) - rotation + 360f) % 360f;
final float sliceScale = 360f / maxSum;
float lastAngle = 0f; // No start angle here, see above
int sliceIndex = 0;
for (SliceValue sliceValue : data.getValues()) {
final float angle = Math.abs(sliceValue.getValue()) * sliceScale;
if (touchAngle >= lastAngle) {
selectedValue.set(sliceIndex, sliceIndex, SelectedValueType.NONE);
}
lastAngle += angle;
++sliceIndex;
}
return isTouched();
}
/**
* Draw center circle with text if {@link PieChartData#hasCenterCircle()} is set true.
*/
private void drawCenterCircle(Canvas canvas) {
final PieChartData data = dataProvider.getPieChartData();
final float circleRadius = originCircleOval.width() / 2f;
final float centerRadius = circleRadius * data.getCenterCircleScale();
final float centerX = originCircleOval.centerX();
final float centerY = originCircleOval.centerY();
canvas.drawCircle(centerX, centerY, centerRadius, centerCirclePaint);
// Draw center text1 and text2 if not empty.
if (!TextUtils.isEmpty(data.getCenterText1())) {
final int text1Height = Math.abs(centerCircleText1FontMetrics.ascent);
if (!TextUtils.isEmpty(data.getCenterText2())) {
// Draw text 2 only if text 1 is not empty.
final int text2Height = Math.abs(centerCircleText2FontMetrics.ascent);
canvas.drawText(data.getCenterText1(), centerX, centerY - text1Height * 0.2f, centerCircleText1Paint);
canvas.drawText(data.getCenterText2(), centerX, centerY + text2Height, centerCircleText2Paint);
} else {
canvas.drawText(data.getCenterText1(), centerX, centerY + text1Height / 4, centerCircleText1Paint);
}
}
}
/**
* Draw all slices for this PieChart, if mode == {@link #MODE_HIGHLIGHT} currently selected slices will be redrawn
* and
* highlighted.
*
* @param canvas
*/
private void drawSlices(Canvas canvas) {
final PieChartData data = dataProvider.getPieChartData();
final float sliceScale = 360f / maxSum;
float lastAngle = rotation;
int sliceIndex = 0;
for (SliceValue sliceValue : data.getValues()) {
final float angle = Math.abs(sliceValue.getValue()) * sliceScale;
if (isTouched() && selectedValue.getFirstIndex() == sliceIndex) {
drawSlice(canvas, sliceValue, lastAngle, angle, MODE_HIGHLIGHT);
} else {
drawSlice(canvas, sliceValue, lastAngle, angle, MODE_DRAW);
}
lastAngle += angle;
++sliceIndex;
}
}
private void drawSeparationLines(Canvas canvas) {
final PieChartData data = dataProvider.getPieChartData();
if (data.getValues().size() < 2) {
//No need for separation lines for 0 or 1 slices.
return;
}
final int sliceSpacing = ChartUtils.dp2px(density, data.getSlicesSpacing());
if (sliceSpacing < 1) {
//No need for separation lines
return;
}
final float sliceScale = 360f / maxSum;
float lastAngle = rotation;
final float circleRadius = originCircleOval.width() / 2f;
separationLinesPaint.setStrokeWidth(sliceSpacing);
for (SliceValue sliceValue : data.getValues()) {
final float angle = Math.abs(sliceValue.getValue()) * sliceScale;
sliceVector.set((float) (Math.cos(Math.toRadians(lastAngle))),
(float) (Math.sin(Math.toRadians(lastAngle))));
normalizeVector(sliceVector);
float x1 = sliceVector.x * (circleRadius + touchAdditional) + originCircleOval.centerX();
float y1 = sliceVector.y * (circleRadius + touchAdditional) + originCircleOval.centerY();
canvas.drawLine(originCircleOval.centerX(), originCircleOval.centerY(), x1, y1, separationLinesPaint);
lastAngle += angle;
}
}
public void drawLabels(Canvas canvas) {
final PieChartData data = dataProvider.getPieChartData();
final float sliceScale = 360f / maxSum;
float lastAngle = rotation;
int sliceIndex = 0;
for (SliceValue sliceValue : data.getValues()) {
final float angle = Math.abs(sliceValue.getValue()) * sliceScale;
if (isTouched()) {
if (hasLabels) {
drawLabel(canvas, sliceValue, lastAngle, angle);
} else if (hasLabelsOnlyForSelected && selectedValue.getFirstIndex() == sliceIndex) {
drawLabel(canvas, sliceValue, lastAngle, angle);
}
} else {
if (hasLabels) {
drawLabel(canvas, sliceValue, lastAngle, angle);
}
}
lastAngle += angle;
++sliceIndex;
}
}
/**
* Method draws single slice from lastAngle to lastAngle+angle, if mode = {@link #MODE_HIGHLIGHT} slice will be
* darken
* and will have bigger radius.
*/
private void drawSlice(Canvas canvas, SliceValue sliceValue, float lastAngle, float angle, int mode) {
sliceVector.set((float) (Math.cos(Math.toRadians(lastAngle + angle / 2))),
(float) (Math.sin(Math.toRadians(lastAngle + angle / 2))));
normalizeVector(sliceVector);
drawCircleOval.set(originCircleOval);
if (MODE_HIGHLIGHT == mode) {
// Add additional touch feedback by setting bigger radius for that slice and darken color.
drawCircleOval.inset(-touchAdditional, -touchAdditional);
slicePaint.setColor(sliceValue.getDarkenColor());
canvas.drawArc(drawCircleOval, lastAngle, angle, true, slicePaint);
} else {
slicePaint.setColor(sliceValue.getColor());
canvas.drawArc(drawCircleOval, lastAngle, angle, true, slicePaint);
}
}
private void drawLabel(Canvas canvas, SliceValue sliceValue, float lastAngle, float angle) {
sliceVector.set((float) (Math.cos(Math.toRadians(lastAngle + angle / 2))),
(float) (Math.sin(Math.toRadians(lastAngle + angle / 2))));
normalizeVector(sliceVector);
final int numChars = valueFormatter.formatChartValue(labelBuffer, sliceValue);
if (numChars == 0) {
// No need to draw empty label
return;
}
final float labelWidth = labelPaint.measureText(labelBuffer, labelBuffer.length - numChars, numChars);
final int labelHeight = Math.abs(fontMetrics.ascent);
final float centerX = originCircleOval.centerX();
final float centerY = originCircleOval.centerY();
final float circleRadius = originCircleOval.width() / 2f;
final float labelRadius;
if (hasLabelsOutside) {
labelRadius = circleRadius * DEFAULT_LABEL_OUTSIDE_RADIUS_FACTOR;
} else {
if (hasCenterCircle) {
labelRadius = circleRadius - (circleRadius - (circleRadius * centerCircleScale)) / 2;
} else {
labelRadius = circleRadius * DEFAULT_LABEL_INSIDE_RADIUS_FACTOR;
}
}
final float rawX = labelRadius * sliceVector.x + centerX;
final float rawY = labelRadius * sliceVector.y + centerY;
float left;
float right;
float top;
float bottom;
if (hasLabelsOutside) {
if (rawX > centerX) {
// Right half.
left = rawX + labelMargin;
right = rawX + labelWidth + labelMargin * 3;
} else {
left = rawX - labelWidth - labelMargin * 3;
right = rawX - labelMargin;
}
if (rawY > centerY) {
// Lower half.
top = rawY + labelMargin;
bottom = rawY + labelHeight + labelMargin * 3;
} else {
top = rawY - labelHeight - labelMargin * 3;
bottom = rawY - labelMargin;
}
} else {
left = rawX - labelWidth / 2 - labelMargin;
right = rawX + labelWidth / 2 + labelMargin;
top = rawY - labelHeight / 2 - labelMargin;
bottom = rawY + labelHeight / 2 + labelMargin;
}
labelBackgroundRect.set(left, top, right, bottom);
drawLabelTextAndBackground(canvas, labelBuffer, labelBuffer.length - numChars, numChars,
sliceValue.getDarkenColor());
}
private void normalizeVector(PointF point) {
final float abs = point.length();
point.set(point.x / abs, point.y / abs);
}
/**
* Calculates angle of touched point.
*/
private float pointToAngle(float x, float y, float centerX, float centerY) {
double diffX = x - centerX;
double diffY = y - centerY;
// Pass -diffX to get clockwise degrees order.
double radian = Math.atan2(-diffX, diffY);
float angle = ((float) Math.toDegrees(radian) + 360) % 360;
// Add 90 because atan2 returns 0 degrees at 6 o'clock.
angle += 90f;
return angle;
}
/**
* Calculates rectangle(square) that will constraint chart circle.
*/
private void calculateCircleOval() {
Rect contentRect = computator.getContentRectMinusAllMargins();
final float circleRadius = Math.min(contentRect.width() / 2f, contentRect.height() / 2f);
final float centerX = contentRect.centerX();
final float centerY = contentRect.centerY();
final float left = centerX - circleRadius + touchAdditional;
final float top = centerY - circleRadius + touchAdditional;
final float right = centerX + circleRadius - touchAdditional;
final float bottom = centerY + circleRadius - touchAdditional;
originCircleOval.set(left, top, right, bottom);
final float inest = 0.5f * originCircleOval.width() * (1.0f - circleFillRatio);
originCircleOval.inset(inest, inest);
}
/**
* Viewport is not really important for PieChart, this kind of chart doesn't relay on viewport but uses pixels
* coordinates instead. This method also calculates sum of all SliceValues.
*/
private void calculateMaxViewport() {
tempMaximumViewport.set(0, MAX_WIDTH_HEIGHT, MAX_WIDTH_HEIGHT, 0);
maxSum = 0.0f;
for (SliceValue sliceValue : dataProvider.getPieChartData().getValues()) {
maxSum += Math.abs(sliceValue.getValue());
}
}
public RectF getCircleOval() {
return originCircleOval;
}
public void setCircleOval(RectF orginCircleOval) {
this.originCircleOval = orginCircleOval;
}
public int getChartRotation() {
return rotation;
}
public void setChartRotation(int rotation) {
rotation = (rotation % 360 + 360) % 360;
this.rotation = rotation;
}
/**
* Returns SliceValue that is under given angle, selectedValue (if not null) will be hold slice index.
*/
public SliceValue getValueForAngle(int angle, SelectedValue selectedValue) {
final PieChartData data = dataProvider.getPieChartData();
final float touchAngle = (angle - rotation + 360f) % 360f;
final float sliceScale = 360f / maxSum;
float lastAngle = 0f;
int sliceIndex = 0;
for (SliceValue sliceValue : data.getValues()) {
final float tempAngle = Math.abs(sliceValue.getValue()) * sliceScale;
if (touchAngle >= lastAngle) {
if (null != selectedValue) {
selectedValue.set(sliceIndex, sliceIndex, SelectedValueType.NONE);
}
return sliceValue;
}
lastAngle += tempAngle;
++sliceIndex;
}
return null;
}
/**
* @see #setCircleFillRatio(float)
*/
public float getCircleFillRatio() {
return this.circleFillRatio;
}
/**
* Set how much of view area should be taken by chart circle. Value should be between 0 and 1. Default is 1 so
* circle will have radius equals min(View.width, View.height).
*/
public void setCircleFillRatio(float fillRatio) {
if (fillRatio < 0) {
fillRatio = 0;
} else if (fillRatio > 1) {
fillRatio = 1;
}
this.circleFillRatio = fillRatio;
calculateCircleOval();
}
}
@@ -0,0 +1,55 @@
package lecho.lib.hellocharts.renderer;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import lecho.lib.hellocharts.model.Viewport;
import lecho.lib.hellocharts.provider.ColumnChartDataProvider;
import lecho.lib.hellocharts.util.ChartUtils;
import lecho.lib.hellocharts.view.Chart;
/**
* Renderer for preview chart based on ColumnChart. In addition to drawing chart data it also draw current viewport as
* preview area.
*/
public class PreviewColumnChartRenderer extends ColumnChartRenderer {
private static final int DEFAULT_PREVIEW_TRANSPARENCY = 64;
private static final int FULL_ALPHA = 255;
private static final int DEFAULT_PREVIEW_STROKE_WIDTH_DP = 2;
private Paint previewPaint = new Paint();
public PreviewColumnChartRenderer(Context context, Chart chart, ColumnChartDataProvider dataProvider) {
super(context, chart, dataProvider);
previewPaint.setAntiAlias(true);
previewPaint.setColor(Color.LTGRAY);
previewPaint.setStrokeWidth(ChartUtils.dp2px(density, DEFAULT_PREVIEW_STROKE_WIDTH_DP));
}
@Override
public void drawUnclipped(Canvas canvas) {
super.drawUnclipped(canvas);
final Viewport currentViewport = computator.getCurrentViewport();
final float left = computator.computeRawX(currentViewport.left);
final float top = computator.computeRawY(currentViewport.top);
final float right = computator.computeRawX(currentViewport.right);
final float bottom = computator.computeRawY(currentViewport.bottom);
previewPaint.setAlpha(DEFAULT_PREVIEW_TRANSPARENCY);
previewPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(left, top, right, bottom, previewPaint);
previewPaint.setStyle(Paint.Style.STROKE);
previewPaint.setAlpha(FULL_ALPHA);
canvas.drawRect(left, top, right, bottom, previewPaint);
}
public int getPreviewColor() {
return previewPaint.getColor();
}
public void setPreviewColor(int color) {
previewPaint.setColor(color);
}
}
@@ -0,0 +1,54 @@
package lecho.lib.hellocharts.renderer;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import lecho.lib.hellocharts.model.Viewport;
import lecho.lib.hellocharts.provider.LineChartDataProvider;
import lecho.lib.hellocharts.util.ChartUtils;
import lecho.lib.hellocharts.view.Chart;
/**
* Renderer for preview chart based on LineChart. In addition to drawing chart data it also draw current viewport as
* preview area.
*/
public class PreviewLineChartRenderer extends LineChartRenderer {
private static final int DEFAULT_PREVIEW_TRANSPARENCY = 64;
private static final int FULL_ALPHA = 255;
private static final int DEFAULT_PREVIEW_STROKE_WIDTH_DP = 2;
private Paint previewPaint = new Paint();
public PreviewLineChartRenderer(Context context, Chart chart, LineChartDataProvider dataProvider) {
super(context, chart, dataProvider);
previewPaint.setAntiAlias(true);
previewPaint.setColor(Color.LTGRAY);
previewPaint.setStrokeWidth(ChartUtils.dp2px(density, DEFAULT_PREVIEW_STROKE_WIDTH_DP));
}
@Override
public void drawUnclipped(Canvas canvas) {
super.drawUnclipped(canvas);
final Viewport currentViewport = computator.getCurrentViewport();
final float left = computator.computeRawX(currentViewport.left);
final float top = computator.computeRawY(currentViewport.top);
final float right = computator.computeRawX(currentViewport.right);
final float bottom = computator.computeRawY(currentViewport.bottom);
previewPaint.setAlpha(DEFAULT_PREVIEW_TRANSPARENCY);
previewPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(left, top, right, bottom, previewPaint);
previewPaint.setStyle(Paint.Style.STROKE);
previewPaint.setAlpha(FULL_ALPHA);
canvas.drawRect(left, top, right, bottom, previewPaint);
}
public int getPreviewColor() {
return previewPaint.getColor();
}
public void setPreviewColor(int color) {
previewPaint.setColor(color);
}
}
@@ -0,0 +1,10 @@
package lecho.lib.hellocharts.util;
/**
* A simple class representing axis label values used only for auto generated axes.
*/
public class AxisAutoValues {
public float[] values = new float[]{};
public int valuesNumber;
public int decimals;
}
@@ -0,0 +1,70 @@
package lecho.lib.hellocharts.util;
import android.content.Context;
import android.graphics.Color;
import android.util.TypedValue;
public abstract class ChartUtils {
public static final int DEFAULT_COLOR = Color.parseColor("#DFDFDF");
public static final int DEFAULT_DARKEN_COLOR = Color.parseColor("#DDDDDD");
public static final int COLOR_BLUE = Color.parseColor("#33B5E5");
public static final int COLOR_VIOLET = Color.parseColor("#AA66CC");
public static final int COLOR_GREEN = Color.parseColor("#99CC00");
public static final int COLOR_ORANGE = Color.parseColor("#FFBB33");
public static final int COLOR_RED = Color.parseColor("#FF4444");
public static final int[] COLORS = new int[]{COLOR_BLUE, COLOR_VIOLET, COLOR_GREEN, COLOR_ORANGE, COLOR_RED};
private static final float DARKEN_SATURATION = 1.1f;
private static final float DARKEN_INTENSITY = 0.9f;
private static int COLOR_INDEX = 0;
public static final int pickColor() {
return COLORS[(int) Math.round(Math.random() * (COLORS.length - 1))];
}
public static final int nextColor() {
if (COLOR_INDEX >= COLORS.length) {
COLOR_INDEX = 0;
}
return COLORS[COLOR_INDEX++];
}
public static int dp2px(float density, int dp) {
if (dp == 0) {
return 0;
}
return (int) (dp * density + 0.5f);
}
public static int px2dp(float density, int px) {
return (int) Math.ceil(px / density);
}
public static int sp2px(float scaledDensity, int sp) {
if (sp == 0) {
return 0;
}
return (int) (sp * scaledDensity + 0.5f);
}
public static int px2sp(float scaledDensity, int px) {
return (int) Math.ceil(px / scaledDensity);
}
public static int mm2px(Context context, int mm) {
return (int) (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, mm, context.getResources()
.getDisplayMetrics()) + 0.5f);
}
public static int darkenColor(int color) {
float[] hsv = new float[3];
int alpha = Color.alpha(color);
Color.colorToHSV(color, hsv);
hsv[1] = Math.min(hsv[1] * DARKEN_SATURATION, 1.0f);
hsv[2] = hsv[2] * DARKEN_INTENSITY;
int tempColor = Color.HSVToColor(hsv);
return Color.argb(alpha, Color.red(tempColor), Color.green(tempColor), Color.blue(tempColor));
}
}
@@ -0,0 +1,196 @@
package lecho.lib.hellocharts.util;
public class FloatUtils {
public static final int POW10[] = {1, 10, 100, 1000, 10000, 100000, 1000000};
/**
* Returns next bigger float value considering precision of the argument.
*/
public static float nextUpF(float f) {
if (Float.isNaN(f) || f == Float.POSITIVE_INFINITY) {
return f;
} else {
f += 0.0f;
return Float.intBitsToFloat(Float.floatToRawIntBits(f) + ((f >= 0.0f) ? +1 : -1));
}
}
/**
* Returns next smaller float value considering precision of the argument.
*/
public static float nextDownF(float f) {
if (Float.isNaN(f) || f == Float.NEGATIVE_INFINITY) {
return f;
} else {
if (f == 0.0f) {
return -Float.MIN_VALUE;
} else {
return Float.intBitsToFloat(Float.floatToRawIntBits(f) + ((f > 0.0f) ? -1 : +1));
}
}
}
/**
* Returns next bigger double value considering precision of the argument.
*/
public static double nextUp(double d) {
if (Double.isNaN(d) || d == Double.POSITIVE_INFINITY) {
return d;
} else {
d += 0.0;
return Double.longBitsToDouble(Double.doubleToRawLongBits(d) + ((d >= 0.0) ? +1 : -1));
}
}
/**
* Returns next smaller float value considering precision of the argument.
*/
public static double nextDown(double d) {
if (Double.isNaN(d) || d == Double.NEGATIVE_INFINITY) {
return d;
} else {
if (d == 0.0f) {
return -Float.MIN_VALUE;
} else {
return Double.longBitsToDouble(Double.doubleToRawLongBits(d) + ((d > 0.0f) ? -1 : +1));
}
}
}
/**
* Method checks if two float numbers are similar.
*/
public static boolean almostEqual(float a, float b, float absoluteDiff, float relativeDiff) {
float diff = Math.abs(a - b);
if (diff <= absoluteDiff) {
return true;
}
a = Math.abs(a);
b = Math.abs(b);
float largest = (a > b) ? a : b;
if (diff <= largest * relativeDiff) {
return true;
}
return false;
}
/**
* Rounds the given number to the given number of significant digits. Based on an answer on <a
* href="http://stackoverflow.com/questions/202302">Stack Overflow</a>.
*/
public static float roundToOneSignificantFigure(double num) {
final float d = (float) Math.ceil((float) Math.log10(num < 0 ? -num : num));
final int power = 1 - (int) d;
final float magnitude = (float) Math.pow(10, power);
final long shifted = Math.round(num * magnitude);
return shifted / magnitude;
}
/**
* Formats a float value to the given number of decimals. Returns the length of the string. The string begins at
* [endIndex] - [return value] and ends at [endIndex]. It's up to you to check indexes correctness.
* Parameter [endIndex] can be helpful when you want to append some text to formatted value.
*
* @return number of characters of formatted value
*/
public static int formatFloat(final char[] formattedValue, float value, int endIndex, int digits, char separator) {
if (digits >= POW10.length) {
formattedValue[endIndex - 1] = '.';
return 1;
}
boolean negative = false;
if (value == 0) {
formattedValue[endIndex - 1] = '0';
return 1;
}
if (value < 0) {
negative = true;
value = -value;
}
if (digits > POW10.length) {
digits = POW10.length - 1;
}
value *= POW10[digits];
long lval = Math.round(value);
int index = endIndex - 1;
int charsNumber = 0;
while (lval != 0 || charsNumber < (digits + 1)) {
int digit = (int) (lval % 10);
lval = lval / 10;
formattedValue[index--] = (char) (digit + '0');
charsNumber++;
if (charsNumber == digits) {
formattedValue[index--] = separator;
charsNumber++;
}
}
if (formattedValue[index + 1] == separator) {
formattedValue[index--] = '0';
charsNumber++;
}
if (negative) {
formattedValue[index--] = '-';
charsNumber++;
}
return charsNumber;
}
/**
* Computes the set of axis labels to show given start and stop boundaries and an ideal number of stops between
* these boundaries.
*
* @param start The minimum extreme (e.g. the left edge) for the axis.
* @param stop The maximum extreme (e.g. the right edge) for the axis.
* @param steps The ideal number of stops to create. This should be based on available screen space; the more
* space
* there is, the more stops should be shown.
* @param outValues The destination {@link AxisAutoValues} object to populate.
*/
public static void computeAutoGeneratedAxisValues(float start, float stop, int steps, AxisAutoValues outValues) {
double range = stop - start;
if (steps == 0 || range <= 0) {
outValues.values = new float[]{};
outValues.valuesNumber = 0;
return;
}
double rawInterval = range / steps;
double interval = roundToOneSignificantFigure(rawInterval);
double intervalMagnitude = Math.pow(10, (int) Math.log10(interval));
int intervalSigDigit = (int) (interval / intervalMagnitude);
if (intervalSigDigit > 5) {
// Use one order of magnitude higher, to avoid intervals like 0.9 or 90
interval = Math.floor(10 * intervalMagnitude);
}
double first = Math.ceil(start / interval) * interval;
double last = nextUp(Math.floor(stop / interval) * interval);
double intervalValue;
int valueIndex;
int valuesNum = 0;
for (intervalValue = first; intervalValue <= last; intervalValue += interval) {
++valuesNum;
}
outValues.valuesNumber = valuesNum;
if (outValues.values.length < valuesNum) {
// Ensure stops contains at least numStops elements.
outValues.values = new float[valuesNum];
}
for (intervalValue = first, valueIndex = 0; valueIndex < valuesNum; intervalValue += interval, ++valueIndex) {
outValues.values[valueIndex] = (float) intervalValue;
}
if (interval < 1) {
outValues.decimals = (int) Math.ceil(-Math.log10(interval));
} else {
outValues.decimals = 0;
}
}
}
@@ -0,0 +1,507 @@
package lecho.lib.hellocharts.view;
import android.content.Context;
import android.graphics.Canvas;
import android.os.Build;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import lecho.lib.hellocharts.animation.ChartAnimationListener;
import lecho.lib.hellocharts.animation.ChartDataAnimator;
import lecho.lib.hellocharts.animation.ChartDataAnimatorV14;
import lecho.lib.hellocharts.animation.ChartDataAnimatorV8;
import lecho.lib.hellocharts.animation.ChartViewportAnimator;
import lecho.lib.hellocharts.animation.ChartViewportAnimatorV14;
import lecho.lib.hellocharts.animation.ChartViewportAnimatorV8;
import lecho.lib.hellocharts.computator.ChartComputator;
import lecho.lib.hellocharts.gesture.ChartTouchHandler;
import lecho.lib.hellocharts.gesture.ContainerScrollType;
import lecho.lib.hellocharts.gesture.ZoomType;
import lecho.lib.hellocharts.listener.ViewportChangeListener;
import lecho.lib.hellocharts.model.SelectedValue;
import lecho.lib.hellocharts.model.Viewport;
import lecho.lib.hellocharts.renderer.AxesRenderer;
import lecho.lib.hellocharts.renderer.ChartRenderer;
import lecho.lib.hellocharts.util.ChartUtils;
/**
* Abstract class for charts views.
*
* @author Leszek Wach
*/
public abstract class AbstractChartView extends View implements Chart {
protected ChartComputator chartComputator;
protected AxesRenderer axesRenderer;
protected ChartTouchHandler touchHandler;
protected ChartRenderer chartRenderer;
protected ChartDataAnimator dataAnimator;
protected ChartViewportAnimator viewportAnimator;
protected boolean isInteractive = true;
protected boolean isContainerScrollEnabled = false;
protected ContainerScrollType containerScrollType;
public AbstractChartView(Context context) {
this(context, null, 0);
}
public AbstractChartView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AbstractChartView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
chartComputator = new ChartComputator();
touchHandler = new ChartTouchHandler(context, this);
axesRenderer = new AxesRenderer(context, this);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
this.dataAnimator = new ChartDataAnimatorV8(this);
this.viewportAnimator = new ChartViewportAnimatorV8(this);
} else {
this.viewportAnimator = new ChartViewportAnimatorV14(this);
this.dataAnimator = new ChartDataAnimatorV14(this);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
super.onSizeChanged(width, height, oldWidth, oldHeight);
chartComputator.setContentRect(getWidth(), getHeight(), getPaddingLeft(), getPaddingTop(), getPaddingRight(),
getPaddingBottom());
chartRenderer.onChartSizeChanged();
axesRenderer.onChartSizeChanged();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (isEnabled()) {
axesRenderer.drawInBackground(canvas);
int clipRestoreCount = canvas.save();
canvas.clipRect(chartComputator.getContentRectMinusAllMargins());
chartRenderer.draw(canvas);
canvas.restoreToCount(clipRestoreCount);
chartRenderer.drawUnclipped(canvas);
axesRenderer.drawInForeground(canvas);
} else {
canvas.drawColor(ChartUtils.DEFAULT_COLOR);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
if (isInteractive) {
boolean needInvalidate;
if (isContainerScrollEnabled) {
needInvalidate = touchHandler.handleTouchEvent(event, getParent(), containerScrollType);
} else {
needInvalidate = touchHandler.handleTouchEvent(event);
}
if (needInvalidate) {
ViewCompat.postInvalidateOnAnimation(this);
}
return true;
} else {
return false;
}
}
@Override
public void computeScroll() {
super.computeScroll();
if (isInteractive) {
if (touchHandler.computeScroll()) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
}
@Override
public void startDataAnimation() {
dataAnimator.startAnimation(Long.MIN_VALUE);
}
@Override
public void startDataAnimation(long duration) {
dataAnimator.startAnimation(duration);
}
@Override
public void cancelDataAnimation() {
dataAnimator.cancelAnimation();
}
@Override
public void animationDataUpdate(float scale) {
getChartData().update(scale);
chartRenderer.onChartViewportChanged();
ViewCompat.postInvalidateOnAnimation(this);
}
@Override
public void animationDataFinished() {
getChartData().finish();
chartRenderer.onChartViewportChanged();
ViewCompat.postInvalidateOnAnimation(this);
}
@Override
public void setDataAnimationListener(ChartAnimationListener animationListener) {
dataAnimator.setChartAnimationListener(animationListener);
}
@Override
public void setViewportAnimationListener(ChartAnimationListener animationListener) {
viewportAnimator.setChartAnimationListener(animationListener);
}
@Override
public void setViewportChangeListener(ViewportChangeListener viewportChangeListener) {
chartComputator.setViewportChangeListener(viewportChangeListener);
}
@Override
public ChartRenderer getChartRenderer() {
return chartRenderer;
}
@Override
public void setChartRenderer(ChartRenderer renderer) {
chartRenderer = renderer;
resetRendererAndTouchHandler();
ViewCompat.postInvalidateOnAnimation(this);
}
@Override
public AxesRenderer getAxesRenderer() {
return axesRenderer;
}
@Override
public ChartComputator getChartComputator() {
return chartComputator;
}
@Override
public ChartTouchHandler getTouchHandler() {
return touchHandler;
}
@Override
public boolean isInteractive() {
return isInteractive;
}
@Override
public void setInteractive(boolean isInteractive) {
this.isInteractive = isInteractive;
}
@Override
public boolean isZoomEnabled() {
return touchHandler.isZoomEnabled();
}
@Override
public void setZoomEnabled(boolean isZoomEnabled) {
touchHandler.setZoomEnabled(isZoomEnabled);
}
@Override
public boolean isScrollEnabled() {
return touchHandler.isScrollEnabled();
}
@Override
public void setScrollEnabled(boolean isScrollEnabled) {
touchHandler.setScrollEnabled(isScrollEnabled);
}
@Override
public void moveTo(float x, float y) {
Viewport scrollViewport = computeScrollViewport(x, y);
setCurrentViewport(scrollViewport);
}
@Override
public void moveToWithAnimation(float x, float y) {
Viewport scrollViewport = computeScrollViewport(x, y);
setCurrentViewportWithAnimation(scrollViewport);
}
private Viewport computeScrollViewport(float x, float y) {
Viewport maxViewport = getMaximumViewport();
Viewport currentViewport = getCurrentViewport();
Viewport scrollViewport = new Viewport(currentViewport);
if (maxViewport.contains(x, y)) {
final float width = currentViewport.width();
final float height = currentViewport.height();
final float halfWidth = width / 2;
final float halfHeight = height / 2;
float left = x - halfWidth;
float top = y + halfHeight;
left = Math.max(maxViewport.left, Math.min(left, maxViewport.right - width));
top = Math.max(maxViewport.bottom + height, Math.min(top, maxViewport.top));
scrollViewport.set(left, top, left + width, top - height);
}
return scrollViewport;
}
@Override
public boolean isValueTouchEnabled() {
return touchHandler.isValueTouchEnabled();
}
@Override
public void setValueTouchEnabled(boolean isValueTouchEnabled) {
touchHandler.setValueTouchEnabled(isValueTouchEnabled);
}
@Override
public ZoomType getZoomType() {
return touchHandler.getZoomType();
}
@Override
public void setZoomType(ZoomType zoomType) {
touchHandler.setZoomType(zoomType);
}
@Override
public float getMaxZoom() {
return chartComputator.getMaxZoom();
}
@Override
public void setMaxZoom(float maxZoom) {
chartComputator.setMaxZoom(maxZoom);
ViewCompat.postInvalidateOnAnimation(this);
}
@Override
public float getZoomLevel() {
Viewport maxViewport = getMaximumViewport();
Viewport currentViewport = getCurrentViewport();
return Math.max(maxViewport.width() / currentViewport.width(), maxViewport.height() / currentViewport.height());
}
@Override
public void setZoomLevel(float x, float y, float zoomLevel) {
Viewport zoomViewport = computeZoomViewport(x, y, zoomLevel);
setCurrentViewport(zoomViewport);
}
@Override
public void setZoomLevelWithAnimation(float x, float y, float zoomLevel) {
Viewport zoomViewport = computeZoomViewport(x, y, zoomLevel);
setCurrentViewportWithAnimation(zoomViewport);
}
private Viewport computeZoomViewport(float x, float y, float zoomLevel) {
final Viewport maxViewport = getMaximumViewport();
Viewport zoomViewport = new Viewport(getMaximumViewport());
if (maxViewport.contains(x, y)) {
if (zoomLevel < 1) {
zoomLevel = 1;
} else if (zoomLevel > getMaxZoom()) {
zoomLevel = getMaxZoom();
}
final float newWidth = zoomViewport.width() / zoomLevel;
final float newHeight = zoomViewport.height() / zoomLevel;
final float halfWidth = newWidth / 2;
final float halfHeight = newHeight / 2;
float left = x - halfWidth;
float right = x + halfWidth;
float top = y + halfHeight;
float bottom = y - halfHeight;
if (left < maxViewport.left) {
left = maxViewport.left;
right = left + newWidth;
} else if (right > maxViewport.right) {
right = maxViewport.right;
left = right - newWidth;
}
if (top > maxViewport.top) {
top = maxViewport.top;
bottom = top - newHeight;
} else if (bottom < maxViewport.bottom) {
bottom = maxViewport.bottom;
top = bottom + newHeight;
}
ZoomType zoomType = getZoomType();
if (ZoomType.HORIZONTAL_AND_VERTICAL == zoomType) {
zoomViewport.set(left, top, right, bottom);
} else if (ZoomType.HORIZONTAL == zoomType) {
zoomViewport.left = left;
zoomViewport.right = right;
} else if (ZoomType.VERTICAL == zoomType) {
zoomViewport.top = top;
zoomViewport.bottom = bottom;
}
}
return zoomViewport;
}
@Override
public Viewport getMaximumViewport() {
return chartRenderer.getMaximumViewport();
}
@Override
public void setMaximumViewport(Viewport maxViewport) {
chartRenderer.setMaximumViewport(maxViewport);
ViewCompat.postInvalidateOnAnimation(this);
}
@Override
public void setCurrentViewportWithAnimation(Viewport targetViewport) {
if (null != targetViewport) {
viewportAnimator.cancelAnimation();
viewportAnimator.startAnimation(getCurrentViewport(), targetViewport);
}
ViewCompat.postInvalidateOnAnimation(this);
}
@Override
public void setCurrentViewportWithAnimation(Viewport targetViewport, long duration) {
if (null != targetViewport) {
viewportAnimator.cancelAnimation();
viewportAnimator.startAnimation(getCurrentViewport(), targetViewport, duration);
}
ViewCompat.postInvalidateOnAnimation(this);
}
@Override
public Viewport getCurrentViewport() {
return getChartRenderer().getCurrentViewport();
}
@Override
public void setCurrentViewport(Viewport targetViewport) {
if (null != targetViewport) {
chartRenderer.setCurrentViewport(targetViewport);
}
ViewCompat.postInvalidateOnAnimation(this);
}
@Override
public void resetViewports() {
chartRenderer.setMaximumViewport(null);
chartRenderer.setCurrentViewport(null);
}
@Override
public boolean isViewportCalculationEnabled() {
return chartRenderer.isViewportCalculationEnabled();
}
@Override
public void setViewportCalculationEnabled(boolean isEnabled) {
chartRenderer.setViewportCalculationEnabled(isEnabled);
}
@Override
public boolean isValueSelectionEnabled() {
return touchHandler.isValueSelectionEnabled();
}
@Override
public void setValueSelectionEnabled(boolean isValueSelectionEnabled) {
touchHandler.setValueSelectionEnabled(isValueSelectionEnabled);
}
@Override
public void selectValue(SelectedValue selectedValue) {
chartRenderer.selectValue(selectedValue);
callTouchListener();
ViewCompat.postInvalidateOnAnimation(this);
}
@Override
public SelectedValue getSelectedValue() {
return chartRenderer.getSelectedValue();
}
@Override
public boolean isContainerScrollEnabled() {
return isContainerScrollEnabled;
}
@Override
public void setContainerScrollEnabled(boolean isContainerScrollEnabled, ContainerScrollType containerScrollType) {
this.isContainerScrollEnabled = isContainerScrollEnabled;
this.containerScrollType = containerScrollType;
}
protected void onChartDataChange() {
chartComputator.resetContentRect();
chartRenderer.onChartDataChanged();
axesRenderer.onChartDataChanged();
ViewCompat.postInvalidateOnAnimation(this);
}
/**
* You should call this method in derived classes, most likely from constructor if you changed chart/axis renderer,
* touch handler or chart computator
*/
protected void resetRendererAndTouchHandler() {
this.chartRenderer.resetRenderer();
this.axesRenderer.resetRenderer();
this.touchHandler.resetTouchHandler();
}
/**
* When embedded in a ViewPager, this will be called in order to know if we can scroll.
* If this returns true, the ViewPager will ignore the drag so that we can scroll our content.
* If this return false, the ViewPager will assume we won't be able to scroll and will consume the drag
*
* @param direction Amount of pixels being scrolled (x axis)
* @return true if the chart can be scrolled (ie. zoomed and not against the edge of the chart)
*/
@Override
public boolean canScrollHorizontally(int direction) {
if (getZoomLevel() <= 1.0) {
return false;
}
final Viewport currentViewport = getCurrentViewport();
final Viewport maximumViewport = getMaximumViewport();
if (direction < 0) {
return currentViewport.left > maximumViewport.left;
} else {
return currentViewport.right < maximumViewport.right;
}
}
}
@@ -0,0 +1,102 @@
package lecho.lib.hellocharts.view;
import android.content.Context;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.Log;
import lecho.lib.hellocharts.BuildConfig;
import lecho.lib.hellocharts.listener.BubbleChartOnValueSelectListener;
import lecho.lib.hellocharts.listener.DummyBubbleChartOnValueSelectListener;
import lecho.lib.hellocharts.model.BubbleChartData;
import lecho.lib.hellocharts.model.BubbleValue;
import lecho.lib.hellocharts.model.ChartData;
import lecho.lib.hellocharts.model.SelectedValue;
import lecho.lib.hellocharts.provider.BubbleChartDataProvider;
import lecho.lib.hellocharts.renderer.BubbleChartRenderer;
/**
* BubbleChart, supports circle bubbles and square bubbles.
*
* @author lecho
*/
public class BubbleChartView extends AbstractChartView implements BubbleChartDataProvider {
private static final String TAG = "BubbleChartView";
protected BubbleChartData data;
protected BubbleChartOnValueSelectListener onValueTouchListener = new DummyBubbleChartOnValueSelectListener();
protected BubbleChartRenderer bubbleChartRenderer;
public BubbleChartView(Context context) {
this(context, null, 0);
}
public BubbleChartView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BubbleChartView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
bubbleChartRenderer = new BubbleChartRenderer(context, this, this);
setChartRenderer(bubbleChartRenderer);
setBubbleChartData(BubbleChartData.generateDummyData());
}
@Override
public BubbleChartData getBubbleChartData() {
return data;
}
@Override
public void setBubbleChartData(BubbleChartData data) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Setting data for BubbleChartView");
}
if (null == data) {
this.data = BubbleChartData.generateDummyData();
} else {
this.data = data;
}
super.onChartDataChange();
}
@Override
public ChartData getChartData() {
return data;
}
@Override
public void callTouchListener() {
SelectedValue selectedValue = chartRenderer.getSelectedValue();
if (selectedValue.isSet()) {
BubbleValue value = data.getValues().get(selectedValue.getFirstIndex());
onValueTouchListener.onValueSelected(selectedValue.getFirstIndex(), value);
} else {
onValueTouchListener.onValueDeselected();
}
}
public BubbleChartOnValueSelectListener getOnValueTouchListener() {
return onValueTouchListener;
}
public void setOnValueTouchListener(BubbleChartOnValueSelectListener touchListener) {
if (null != touchListener) {
this.onValueTouchListener = touchListener;
}
}
/**
* Removes empty spaces, top-bottom for portrait orientation and left-right for landscape. This method has to be
* called after view View#onSizeChanged() method is called and chart data is set. This method may be inaccurate.
*
* @see BubbleChartRenderer#removeMargins()
*/
public void removeMargins() {
bubbleChartRenderer.removeMargins();
ViewCompat.postInvalidateOnAnimation(this);
}
}
@@ -0,0 +1,285 @@
package lecho.lib.hellocharts.view;
import lecho.lib.hellocharts.animation.ChartAnimationListener;
import lecho.lib.hellocharts.computator.ChartComputator;
import lecho.lib.hellocharts.gesture.ChartTouchHandler;
import lecho.lib.hellocharts.gesture.ContainerScrollType;
import lecho.lib.hellocharts.gesture.ZoomType;
import lecho.lib.hellocharts.listener.ViewportChangeListener;
import lecho.lib.hellocharts.model.ChartData;
import lecho.lib.hellocharts.model.SelectedValue;
import lecho.lib.hellocharts.model.Viewport;
import lecho.lib.hellocharts.renderer.AxesRenderer;
import lecho.lib.hellocharts.renderer.ChartRenderer;
/**
* Interface for all charts. Every chart must implements this interface but chart doesn't really have to extends View or
* ViewGroup class. It can be any java class for example chart that only draw on in-memory bitmap and saves it on sd
* card.
*/
public interface Chart {
/**
* Returns generic chart data. For specific class call get*ChartData method from data provider implementation.
*/
public ChartData getChartData();
public ChartRenderer getChartRenderer();
public void setChartRenderer(ChartRenderer renderer);
public AxesRenderer getAxesRenderer();
public ChartComputator getChartComputator();
public ChartTouchHandler getTouchHandler();
/**
* Updates chart data with given scale. Called during chart data animation update.
*/
public void animationDataUpdate(float scale);
/**
* Called when data animation finished.
*/
public void animationDataFinished();
/**
* Starts chart data animation for given duration. Before you call this method you should change target values of
* chart data.
*/
public void startDataAnimation();
/**
* Starts chart data animation for given duration. If duration is negative the default value of 500ms will be used.
* Before you call this method you should change target values of chart data.
*/
public void startDataAnimation(long duration);
/**
* Stops chart data animation. All chart data values are set to their target values.
*/
public void cancelDataAnimation();
/**
* Return true if auto viewports recalculations are enabled, false otherwise.
*/
public boolean isViewportCalculationEnabled();
/**
* Set true to enable viewports(max and current) recalculations during animations or after set*ChartData method is
* called. If you disable viewports calculations viewports will not change until you change them manually or enable
* calculations again. Disabled viewport calculations is usefull if you want show only part of chart by setting
* custom viewport and don't want any operation to change that viewport
*/
public void setViewportCalculationEnabled(boolean isEnabled);
/**
* Set listener for data animation to be notified when data animation started and finished. By default that flag is
* set to true so be careful with animation and custom viewports.
*/
public void setDataAnimationListener(ChartAnimationListener animationListener);
/**
* Set listener for viewport animation to be notified when viewport animation started and finished.
*/
public void setViewportAnimationListener(ChartAnimationListener animationListener);
/**
* Set listener for current viewport changes. It will be called when viewport change either by gesture or
* programmatically. Note! This method works only for preview charts. It is intentionally disabled for other types
* of charts to avoid unnecessary method calls during invalidation.
*/
public void setViewportChangeListener(ViewportChangeListener viewportChangeListener);
public void callTouchListener();
/**
* Returns true if chart is interactive.
*
* @see #setInteractive(boolean)
*/
public boolean isInteractive();
/**
* Set true to allow user use touch gestures. If set to false user will not be able zoom, scroll or select/touch
* value. By default true.
*/
public void setInteractive(boolean isInteractive);
/**
* Returns true if pitch to zoom and double tap zoom is enabled.
*
* @see #setZoomEnabled(boolean)
*/
public boolean isZoomEnabled();
/**
* Set true to enable zoom, false to disable, by default true;
*/
public void setZoomEnabled(boolean isZoomEnabled);
/**
* Returns true if scrolling is enabled.
*
* @see #setScrollEnabled(boolean)
*/
public boolean isScrollEnabled();
/**
* Set true to enable touch scroll/fling, false to disable touch scroll/fling, by default true;
*/
public void setScrollEnabled(boolean isScrollEnabled);
/**
* Move/Srcoll viewport to position x,y(that position must be within maximum chart viewport). If possible viewport
* will be centered at this point. Width and height of viewport will not be modified.
*
* @see #setCurrentViewport(lecho.lib.hellocharts.model.Viewport)
*/
public void moveTo(float x, float y);
/**
* Animate viewport to position x,y(that position must be within maximum chart viewport). If possible viewport
* will be centered at this point. Width and height of viewport will not be modified.
*
* @see #setCurrentViewport(lecho.lib.hellocharts.model.Viewport) ;
*/
public void moveToWithAnimation(float x, float y);
/**
* Returns current zoom type for this chart.
*
* @see #setZoomType(ZoomType)
*/
public ZoomType getZoomType();
/**
* Set zoom type, available options: ZoomType.HORIZONTAL_AND_VERTICAL, ZoomType.HORIZONTAL, ZoomType.VERTICAL. By
* default HORIZONTAL_AND_VERTICAL.
*/
public void setZoomType(ZoomType zoomType);
/**
* Returns current maximum zoom value.
*/
public float getMaxZoom();
/**
* Set max zoom value. Default maximum zoom is 20.
*/
public void setMaxZoom(float maxZoom);
/**
* Returns current zoom level.
*/
public float getZoomLevel();
/**
* Programatically zoom chart to given point(viewport point). Call this method after chart data had been set.
*
* @param x x within chart maximum viewport
* @param y y within chart maximum viewport
* @param zoomLevel value from 1 to maxZoom(default 20). 1 means chart has no zoom.
*/
public void setZoomLevel(float x, float y, float zoomLevel);
/**
* Programatically zoom chart to given point(viewport point) with animation. Call this method after chart data
* had been set.
*
* @param x x within chart maximum viewport
* @param y y within chart maximum viewport
* @param zoomLevel value from 1 to maxZoom(default 20). 1 means chart has no zoom.
*/
public void setZoomLevelWithAnimation(float x, float y, float zoomLevel);
/**
* Return true if chart value can be touched.
*
* @see #setValueTouchEnabled(boolean)
*/
public boolean isValueTouchEnabled();
/**
* Set true if you want allow user to click value on chart, set false to disable that option. By default true.
*/
public void setValueTouchEnabled(boolean isValueTouchEnabled);
/**
* Returns maximum viewport for this chart. Don't modify it directly, use {@link #setMaximumViewport(Viewport)}
* instead.
*
* @see #setMaximumViewport(Viewport)
*/
public Viewport getMaximumViewport();
/**
* Set maximum viewport. If you set bigger maximum viewport data will be more concentrate and there will be more
* empty spaces on sides. Note. MaxViewport have to be set after chartData has been set.
*/
public void setMaximumViewport(Viewport maxViewport);
/**
* Returns current viewport. Don't modify it directly, use {@link #setCurrentViewport(Viewport)} instead.
*
* @see #setCurrentViewport(Viewport)
*/
public Viewport getCurrentViewport();
/**
* Sets current viewport. Note. viewport have to be set after chartData has been set.
*/
public void setCurrentViewport(Viewport targetViewport);
/**
* Sets current viewport with animation. Note. viewport have to be set after chartData has been set.
*/
public void setCurrentViewportWithAnimation(Viewport targetViewport);
/**
* Sets current viewport with animation. Note. viewport have to be set after chartData has been set.
*/
public void setCurrentViewportWithAnimation(Viewport targetViewport, long duration);
/**
* Reset maximum viewport and current viewport. Values for both viewports will be auto-calculated using current
* chart data ranges.
*/
public void resetViewports();
/**
* Return true if value selection mode is enabled.
*
* @see #setValueSelectionEnabled(boolean)
*/
public boolean isValueSelectionEnabled();
/**
* Set true if you want value selection with touch - value will stay selected until you touch somewhere else on the
* chart area. By default false and value is automatically unselected when user stop pressing on it.
*/
public void setValueSelectionEnabled(boolean isValueSelectionEnabled);
/**
* Select single value on chart. If indexes are not valid IndexOutOfBoundsException will be thrown.
*/
public void selectValue(SelectedValue selectedValue);
/**
* Return currently selected value indexes.
*/
public SelectedValue getSelectedValue();
/**
* @see #setContainerScrollEnabled(boolean, ContainerScrollType)
*/
public boolean isContainerScrollEnabled();
/**
* Set isContainerScrollEnabled to true and containerScrollType to HORIZONTAL or VERTICAL if you are using chart
* within scroll container.
*/
public void setContainerScrollEnabled(boolean isContainerScrollEnabled, ContainerScrollType containerScrollType);
}
@@ -0,0 +1,88 @@
package lecho.lib.hellocharts.view;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import lecho.lib.hellocharts.BuildConfig;
import lecho.lib.hellocharts.listener.ColumnChartOnValueSelectListener;
import lecho.lib.hellocharts.listener.DummyColumnChartOnValueSelectListener;
import lecho.lib.hellocharts.model.ColumnChartData;
import lecho.lib.hellocharts.model.SelectedValue;
import lecho.lib.hellocharts.model.SubcolumnValue;
import lecho.lib.hellocharts.provider.ColumnChartDataProvider;
import lecho.lib.hellocharts.renderer.ColumnChartRenderer;
/**
* ColumnChart/BarChart, supports subcolumns, stacked collumns and negative values.
*
* @author Leszek Wach
*/
public class ColumnChartView extends AbstractChartView implements ColumnChartDataProvider {
private static final String TAG = "ColumnChartView";
private ColumnChartData data;
private ColumnChartOnValueSelectListener onValueTouchListener = new DummyColumnChartOnValueSelectListener();
public ColumnChartView(Context context) {
this(context, null, 0);
}
public ColumnChartView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ColumnChartView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setChartRenderer(new ColumnChartRenderer(context, this, this));
setColumnChartData(ColumnChartData.generateDummyData());
}
@Override
public ColumnChartData getColumnChartData() {
return data;
}
@Override
public void setColumnChartData(ColumnChartData data) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Setting data for ColumnChartView");
}
if (null == data) {
this.data = ColumnChartData.generateDummyData();
} else {
this.data = data;
}
super.onChartDataChange();
}
@Override
public ColumnChartData getChartData() {
return data;
}
@Override
public void callTouchListener() {
SelectedValue selectedValue = chartRenderer.getSelectedValue();
if (selectedValue.isSet()) {
SubcolumnValue value = data.getColumns().get(selectedValue.getFirstIndex()).getValues()
.get(selectedValue.getSecondIndex());
onValueTouchListener.onValueSelected(selectedValue.getFirstIndex(), selectedValue.getSecondIndex(), value);
} else {
onValueTouchListener.onValueDeselected();
}
}
public ColumnChartOnValueSelectListener getOnValueTouchListener() {
return onValueTouchListener;
}
public void setOnValueTouchListener(ColumnChartOnValueSelectListener touchListener) {
if (null != touchListener) {
this.onValueTouchListener = touchListener;
}
}
}
@@ -0,0 +1,154 @@
package lecho.lib.hellocharts.view;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import lecho.lib.hellocharts.BuildConfig;
import lecho.lib.hellocharts.listener.ComboLineColumnChartOnValueSelectListener;
import lecho.lib.hellocharts.listener.DummyCompoLineColumnChartOnValueSelectListener;
import lecho.lib.hellocharts.model.ChartData;
import lecho.lib.hellocharts.model.ColumnChartData;
import lecho.lib.hellocharts.model.ComboLineColumnChartData;
import lecho.lib.hellocharts.model.LineChartData;
import lecho.lib.hellocharts.model.PointValue;
import lecho.lib.hellocharts.model.SelectedValue;
import lecho.lib.hellocharts.model.SelectedValue.SelectedValueType;
import lecho.lib.hellocharts.model.SubcolumnValue;
import lecho.lib.hellocharts.provider.ColumnChartDataProvider;
import lecho.lib.hellocharts.provider.ComboLineColumnChartDataProvider;
import lecho.lib.hellocharts.provider.LineChartDataProvider;
import lecho.lib.hellocharts.renderer.ColumnChartRenderer;
import lecho.lib.hellocharts.renderer.ComboLineColumnChartRenderer;
import lecho.lib.hellocharts.renderer.LineChartRenderer;
/**
* ComboChart, supports ColumnChart combined with LineChart. Lines are always drawn on top.
*
* @author Leszek Wach
*/
public class ComboLineColumnChartView extends AbstractChartView implements ComboLineColumnChartDataProvider {
private static final String TAG = "ComboLineColumnChartView";
protected ComboLineColumnChartData data;
protected ColumnChartDataProvider columnChartDataProvider = new ComboColumnChartDataProvider();
protected LineChartDataProvider lineChartDataProvider = new ComboLineChartDataProvider();
protected ComboLineColumnChartOnValueSelectListener onValueTouchListener = new
DummyCompoLineColumnChartOnValueSelectListener();
public ComboLineColumnChartView(Context context) {
this(context, null, 0);
}
public ComboLineColumnChartView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ComboLineColumnChartView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setChartRenderer(new ComboLineColumnChartRenderer(context, this, columnChartDataProvider,
lineChartDataProvider));
setComboLineColumnChartData(ComboLineColumnChartData.generateDummyData());
}
@Override
public ComboLineColumnChartData getComboLineColumnChartData() {
return data;
}
@Override
public void setComboLineColumnChartData(ComboLineColumnChartData data) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Setting data for ComboLineColumnChartView");
}
if (null == data) {
this.data = null;// generateDummyData();
} else {
this.data = data;
}
super.onChartDataChange();
}
@Override
public ChartData getChartData() {
return data;
}
@Override
public void callTouchListener() {
SelectedValue selectedValue = chartRenderer.getSelectedValue();
if (selectedValue.isSet()) {
if (SelectedValueType.COLUMN.equals(selectedValue.getType())) {
SubcolumnValue value = data.getColumnChartData().getColumns().get(selectedValue.getFirstIndex())
.getValues().get(selectedValue.getSecondIndex());
onValueTouchListener.onColumnValueSelected(selectedValue.getFirstIndex(),
selectedValue.getSecondIndex(), value);
} else if (SelectedValueType.LINE.equals(selectedValue.getType())) {
PointValue value = data.getLineChartData().getLines().get(selectedValue.getFirstIndex()).getValues()
.get(selectedValue.getSecondIndex());
onValueTouchListener.onPointValueSelected(selectedValue.getFirstIndex(), selectedValue.getSecondIndex(),
value);
} else {
throw new IllegalArgumentException("Invalid selected value type " + selectedValue.getType().name());
}
} else {
onValueTouchListener.onValueDeselected();
}
}
public ComboLineColumnChartOnValueSelectListener getOnValueTouchListener() {
return onValueTouchListener;
}
public void setOnValueTouchListener(ComboLineColumnChartOnValueSelectListener touchListener) {
if (null != touchListener) {
this.onValueTouchListener = touchListener;
}
}
public void setColumnChartRenderer(Context context, ColumnChartRenderer columnChartRenderer){
setChartRenderer(new ComboLineColumnChartRenderer(context, this , columnChartRenderer, lineChartDataProvider));
}
public void setLineChartRenderer(Context context, LineChartRenderer lineChartRenderer){
setChartRenderer(new ComboLineColumnChartRenderer(context, this, columnChartDataProvider, lineChartRenderer));
}
private class ComboLineChartDataProvider implements LineChartDataProvider {
@Override
public LineChartData getLineChartData() {
return ComboLineColumnChartView.this.data.getLineChartData();
}
@Override
public void setLineChartData(LineChartData data) {
ComboLineColumnChartView.this.data.setLineChartData(data);
}
}
private class ComboColumnChartDataProvider implements ColumnChartDataProvider {
@Override
public ColumnChartData getColumnChartData() {
return ComboLineColumnChartView.this.data.getColumnChartData();
}
@Override
public void setColumnChartData(ColumnChartData data) {
ComboLineColumnChartView.this.data.setColumnChartData(data);
}
}
}
@@ -0,0 +1,89 @@
package lecho.lib.hellocharts.view;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import lecho.lib.hellocharts.BuildConfig;
import lecho.lib.hellocharts.listener.DummyLineChartOnValueSelectListener;
import lecho.lib.hellocharts.listener.LineChartOnValueSelectListener;
import lecho.lib.hellocharts.model.ChartData;
import lecho.lib.hellocharts.model.LineChartData;
import lecho.lib.hellocharts.model.PointValue;
import lecho.lib.hellocharts.model.SelectedValue;
import lecho.lib.hellocharts.provider.LineChartDataProvider;
import lecho.lib.hellocharts.renderer.LineChartRenderer;
/**
* LineChart, supports cubic lines, filled lines, circle and square points. Point radius and stroke width can be
* adjusted using LineChartData attributes.
*
* @author Leszek Wach
*/
public class LineChartView extends AbstractChartView implements LineChartDataProvider {
private static final String TAG = "LineChartView";
protected LineChartData data;
protected LineChartOnValueSelectListener onValueTouchListener = new DummyLineChartOnValueSelectListener();
public LineChartView(Context context) {
this(context, null, 0);
}
public LineChartView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LineChartView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setChartRenderer(new LineChartRenderer(context, this, this));
setLineChartData(LineChartData.generateDummyData());
}
@Override
public LineChartData getLineChartData() {
return data;
}
@Override
public void setLineChartData(LineChartData data) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Setting data for LineChartView");
}
if (null == data) {
this.data = LineChartData.generateDummyData();
} else {
this.data = data;
}
super.onChartDataChange();
}
@Override
public ChartData getChartData() {
return data;
}
@Override
public void callTouchListener() {
SelectedValue selectedValue = chartRenderer.getSelectedValue();
if (selectedValue.isSet()) {
PointValue point = data.getLines().get(selectedValue.getFirstIndex()).getValues()
.get(selectedValue.getSecondIndex());
onValueTouchListener.onValueSelected(selectedValue.getFirstIndex(), selectedValue.getSecondIndex(), point);
} else {
onValueTouchListener.onValueDeselected();
}
}
public LineChartOnValueSelectListener getOnValueTouchListener() {
return onValueTouchListener;
}
public void setOnValueTouchListener(LineChartOnValueSelectListener touchListener) {
if (null != touchListener) {
this.onValueTouchListener = touchListener;
}
}
}
@@ -0,0 +1,192 @@
package lecho.lib.hellocharts.view;
import android.content.Context;
import android.graphics.RectF;
import android.os.Build;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import lecho.lib.hellocharts.BuildConfig;
import lecho.lib.hellocharts.animation.PieChartRotationAnimator;
import lecho.lib.hellocharts.animation.PieChartRotationAnimatorV14;
import lecho.lib.hellocharts.animation.PieChartRotationAnimatorV8;
import lecho.lib.hellocharts.gesture.PieChartTouchHandler;
import lecho.lib.hellocharts.listener.DummyPieChartOnValueSelectListener;
import lecho.lib.hellocharts.listener.PieChartOnValueSelectListener;
import lecho.lib.hellocharts.model.ChartData;
import lecho.lib.hellocharts.model.PieChartData;
import lecho.lib.hellocharts.model.SelectedValue;
import lecho.lib.hellocharts.model.SliceValue;
import lecho.lib.hellocharts.provider.PieChartDataProvider;
import lecho.lib.hellocharts.renderer.PieChartRenderer;
/**
* PieChart is a little different than others charts. It doesn't have axes. It doesn't support viewport so changing
* viewport wont work. Instead it support "Circle Oval". Pinch-to-Zoom and double tap zoom wont work either. Instead of
* scroll there is chart rotation if isChartRotationEnabled is set to true. PieChart looks the best when it has the same
* width and height, drawing chart on rectangle with proportions other than 1:1 will left some empty spaces.
*
* @author Leszek Wach
*/
public class PieChartView extends AbstractChartView implements PieChartDataProvider {
private static final String TAG = "PieChartView";
protected PieChartData data;
protected PieChartOnValueSelectListener onValueTouchListener = new DummyPieChartOnValueSelectListener();
protected PieChartRenderer pieChartRenderer;
protected PieChartRotationAnimator rotationAnimator;
public PieChartView(Context context) {
this(context, null, 0);
}
public PieChartView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PieChartView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
pieChartRenderer = new PieChartRenderer(context, this, this);
touchHandler = new PieChartTouchHandler(context, this);
setChartRenderer(pieChartRenderer);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
this.rotationAnimator = new PieChartRotationAnimatorV8(this);
} else {
this.rotationAnimator = new PieChartRotationAnimatorV14(this);
}
setPieChartData(PieChartData.generateDummyData());
}
@Override
public PieChartData getPieChartData() {
return data;
}
@Override
public void setPieChartData(PieChartData data) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Setting data for ColumnChartView");
}
if (null == data) {
this.data = PieChartData.generateDummyData();
} else {
this.data = data;
}
super.onChartDataChange();
}
@Override
public ChartData getChartData() {
return data;
}
@Override
public void callTouchListener() {
SelectedValue selectedValue = chartRenderer.getSelectedValue();
if (selectedValue.isSet()) {
SliceValue sliceValue = data.getValues().get(selectedValue.getFirstIndex());
onValueTouchListener.onValueSelected(selectedValue.getFirstIndex(), sliceValue);
} else {
onValueTouchListener.onValueDeselected();
}
}
public PieChartOnValueSelectListener getOnValueTouchListener() {
return onValueTouchListener;
}
public void setOnValueTouchListener(PieChartOnValueSelectListener touchListener) {
if (null != touchListener) {
this.onValueTouchListener = touchListener;
}
}
/**
* Returns rectangle that will constraint pie chart area.
*/
public RectF getCircleOval() {
return pieChartRenderer.getCircleOval();
}
/**
* Use this to change pie chart area. Because by default CircleOval is calculated onSizeChanged() you must call this
* method after size of PieChartView is calculated. In most cases it will probably be easier to use
* {@link #setCircleFillRatio(float)} to change chart area or just use view padding.
*/
public void setCircleOval(RectF orginCircleOval) {
pieChartRenderer.setCircleOval(orginCircleOval);
ViewCompat.postInvalidateOnAnimation(this);
}
/**
* Returns pie chart rotation, 0 rotation means that 0 degrees is at 3 o'clock. Don't confuse with
* {@link View#getRotation()}.
*
* @return
*/
public int getChartRotation() {
return pieChartRenderer.getChartRotation();
}
/**
* Set pie chart rotation. Don't confuse with {@link View#getRotation()}.
*
* @param rotation
* @see #getChartRotation()
*/
public void setChartRotation(int rotation, boolean isAnimated) {
if (isAnimated) {
rotationAnimator.cancelAnimation();
rotationAnimator.startAnimation(pieChartRenderer.getChartRotation(), rotation);
} else {
pieChartRenderer.setChartRotation(rotation);
}
ViewCompat.postInvalidateOnAnimation(this);
}
public boolean isChartRotationEnabled() {
if (touchHandler instanceof PieChartTouchHandler) {
return ((PieChartTouchHandler) touchHandler).isRotationEnabled();
} else {
return false;
}
}
/**
* Set false if you don't wont the chart to be rotated by touch gesture. Rotating programmatically will still work.
*
* @param isRotationEnabled
*/
public void setChartRotationEnabled(boolean isRotationEnabled) {
if (touchHandler instanceof PieChartTouchHandler) {
((PieChartTouchHandler) touchHandler).setRotationEnabled(isRotationEnabled);
}
}
/**
* Returns SliceValue that is under given angle, selectedValue (if not null) will be hold slice index.
*/
public SliceValue getValueForAngle(int angle, SelectedValue selectedValue) {
return pieChartRenderer.getValueForAngle(angle, selectedValue);
}
/**
* @see #setCircleFillRatio(float)
*/
public float getCircleFillRatio() {
return pieChartRenderer.getCircleFillRatio();
}
/**
* Set how much of view area should be taken by chart circle. Value should be between 0 and 1. Default is 1 so
* circle will have radius equals min(View.width, View.height).
*/
public void setCircleFillRatio(float fillRatio) {
pieChartRenderer.setCircleFillRatio(fillRatio);
ViewCompat.postInvalidateOnAnimation(this);
}
}

Some files were not shown because too many files have changed in this diff Show More