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
+36
View File
@@ -0,0 +1,36 @@
# built application files
*.apk
*.ap_
# files for the dex VM
*.dex
# Java class files
*.class
# generated files
bin/
gen/
build/
target/
# Local configuration file (sdk path, etc)
local.properties
# Eclipse project files
.classpath
.project
.settings
# Proguard folder generated by Eclipse
proguard/
# Intellij project files
*.iml
*.ipr
*.iws
.idea/
/.settings
/.gradle
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
+163
View File
@@ -0,0 +1,163 @@
# HelloCharts for Android
Charting library for Android compatible with API 8+(Android 2.2).
Works best when hardware acceleration is available, so API 14+(Android 4.0) is recommended.
Apache License 2.0.
[![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-HelloCharts%20for%20Android-brightgreen.svg?style=flat)](https://android-arsenal.com/details/1/1068)
<a href="https://scan.coverity.com/projects/4121">
<img alt="Coverity Scan Build Status"
src="https://scan.coverity.com/projects/4121/badge.svg"/>
</a>
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.lecho/hellocharts-library/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.lecho/hellocharts-library)
[![Release](https://img.shields.io/github/release/lecho/hellocharts-android.svg?label=JitPack.io)](https://jitpack.io/#lecho/hellocharts-android)
## Features
- Line chart(cubic lines, filled lines, scattered points)
- Column chart(grouped, stacked, negative values)
- Pie chart
- Bubble chart
- Combo chart(columns/lines)
- Preview charts(for column chart and line chart)
- Zoom(pinch to zoom, double tap zoom), scroll and fling
- Custom and auto-generated axes(top, bottom, left, right, inside)
- Animations
## Screens and Demos
- Code of a demo application is in `hellocharts-samples` directory, requires appcompat v21.
- The **demo app** is also ready for download on [**Google Play**](https://play.google.com/store/apps/details?id=lecho.lib.hellocharts.samples).
- Short **video** is available on [**YouTube**](https://www.youtube.com/watch?v=xbSBjyjH2SY).
![](screens/scr_dependecy_preview.gif)
![](screens/scr-tempo.png)
![](screens/scr-dependency.png)
![](screens/scr-preview-column.png)
![](screens/scr-pie1.png)
![](screens/scr-bubble1.png)
![](screens/scr-combo.png)
## Download and Import
#### Android Studio/Gradle
- Maven Central/jCenter, add dependency to your `build.gradle`:
```groovy
dependencies{
compile 'com.github.lecho:hellocharts-library:1.5.8@aar'
}
```
- JitPack.io, add `jitpack.io` repositiory and dependency to your `build.gradle`:
```groovy
repositories {
maven {
url "https://jitpack.io"
}
}
dependencies {
compile 'com.github.lecho:hellocharts-android:v1.5.8'
}
```
#### Eclipse/ADT
- Download the latest [release jar file](https://github.com/lecho/hellocharts-android/releases).
- Copy `hellocharts-library-<version>.jar` into the `libs` folder of your application project.
## Usage
Every chart view can be defined in layout xml file:
```xml
<lecho.lib.hellocharts.view.LineChartView
android:id="@+id/chart"
android:layout_width="match_parent"
android:layout_height="match_parent" />
```
or created in code and added to layout later:
```java
LineChartView chart = new LineChartView(context);
layout.addView(chart);
```
Use methods from *Chart classes to define chart behaviour, example methods:
```java
Chart.setInteractive(boolean isInteractive);
Chart.setZoomType(ZoomType zoomType);
Chart.setContainerScrollEnabled(boolean isEnabled, ContainerScrollType type);
```
Use methods from data models to define how chart looks like, example methods:
```java
ChartData.setAxisXBottom(Axis axisX);
ColumnChartData.setStacked(boolean isStacked);
Line.setStrokeWidth(int strokeWidthDp);
```
Every chart has its own method to set chart data and its own data model, example for line chart:
```java
List<PointValue> values = new ArrayList<PointValue>();
values.add(new PointValue(0, 2));
values.add(new PointValue(1, 4));
values.add(new PointValue(2, 3));
values.add(new PointValue(3, 4));
//In most cased you can call data model methods in builder-pattern-like manner.
Line line = new Line(values).setColor(Color.BLUE).setCubic(true);
List<Line> lines = new ArrayList<Line>();
lines.add(line);
LineChartData data = new LineChartData();
data.setLines(lines);
LineChartView chart = new LineChartView(context);
chart.setLineChartData(data);
```
After the chart data has been set you can still modify its attributes but right after that you should call
`set*ChartData()` method again to let chart recalculate and redraw data. There is also an option to use copy constructor for deep copy of
chart data. You can safely modify copy in other threads and pass it to `set*ChartData()` method later.
## Contributing
Yes:) If you found a bug, have an idea how to improve library or have a question, please create new issue or comment existing one. If you would like to contribute code fork the repository and send a pull request.
# License
HelloCharts
Copyright 2014 Leszek Wach
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.
---
HelloCharts library uses code from InteractiveChart sample available
on Android Developers page:
http://developer.android.com/training/gestures/scale.html
+9
View File
@@ -0,0 +1,9 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
}
}
+19
View File
@@ -0,0 +1,19 @@
VERSION_NAME=1.5.8
VERSION_CODE=13
GROUP=com.github.lecho
POM_DESCRIPTION=Charting library for Android compatible with API 8+(Android 2.2).
POM_URL=https://github.com/lecho/hellocharts-android
POM_SCM_URL=https://github.com/lecho/hellocharts-android
POM_SCM_CONNECTION=scm:git@github.com:lecho/hellocharts-android.git
POM_SCM_DEV_CONNECTION=scm:git@github.com:lecho/hellocharts-android.git
POM_LICENCE_NAME=The Apache Software License, Version 2.0
POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
POM_LICENCE_DIST=repo
POM_DEVELOPER_ID=lecho
POM_DEVELOPER_NAME=Leszek Wach
ANDROID_BUILD_MIN_SDK_VERSION=8
ANDROID_BUILD_TARGET_SDK_VERSION=23
ANDROID_BUILD_TOOLS_VERSION=23.0.3
ANDROID_BUILD_SDK_VERSION=23
Binary file not shown.
@@ -0,0 +1,6 @@
#Wed Apr 10 15:27:10 PDT 2013
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=http\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+164
View File
@@ -0,0 +1,164 @@
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >&-
APP_HOME="`pwd -P`"
cd "$SAVED" >&-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
+90
View File
@@ -0,0 +1,90 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@@ -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);
}
}

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