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
+1
View File
@@ -0,0 +1 @@
/build
+55
View File
@@ -0,0 +1,55 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
setLibDefaultConfig(android)
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
lintOptions {
abortOnError false
}
namespace 'cn.jingzhuan.lib.chart'
compileOptions.coreLibraryDesugaringEnabled true
libraryVariants.configureEach {
it.generateBuildConfig.enabled = false
}
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
implementation fileTree(dir: 'libs', include: ['*.jar'])
androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', {
exclude group: 'com.android.support', module: 'support-annotations'
})
implementation 'androidx.appcompat:appcompat:1.6.1'
testImplementation 'junit:junit:4.13.2'
implementation 'androidx.annotation:annotation:1.7.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$ver.kotlinVersion"
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
apply plugin: 'com.vanniktech.maven.publish'
publishing {
repositories {
maven {
def releasesRepoUrl = "http://192.168.0.251:8081/repository/maven-releases"
def snapshotsRepoUrl = "http://192.168.0.251:8081/repository/maven-snapshots"
url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
allowInsecureProtocol = true
credentials {
username 'admin'
password 'admin123'
}
}
}
}
Binary file not shown.
@@ -0,0 +1,6 @@
#Tue Jul 25 16:13:11 CST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-milestone-1-all.zip
+160
View File
@@ -0,0 +1,160 @@
#!/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
# 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\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
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"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# 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-lineDataSet 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 lineDataSet 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 lineDataSet
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
+25
View File
@@ -0,0 +1,25 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /home/donglua/Android/Sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# 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 *;
#}
# Uncomment this to preserve the lineDataSet number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the lineDataSet number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,24 @@
package cn.jingzhuan.chart;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumentation test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class) public class ExampleInstrumentedTest {
@Test public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("cn.jingzhuan.lib.chart.test", appContext.getPackageName());
}
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
</application>
</manifest>
@@ -0,0 +1,11 @@
package cn.jingzhuan.lib.chart;
/**
* Created by Donglua on 17/7/17.
*/
public class AxisAutoValues {
public float[] values = new float[]{};
public int number;
public int decimals;
}
@@ -0,0 +1,58 @@
package cn.jingzhuan.lib.chart;
import android.graphics.Rect;
import android.graphics.RectF;
/**
* Created by Donglua on 17/7/17.
*/
public class Viewport extends RectF {
public static final float AXIS_X_MIN = 0f;
public static final float AXIS_X_MAX = 1f;
public static final float AXIS_Y_MIN = -1f;
public static final float AXIS_Y_MAX = 1f;
public Viewport() {
this(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX);
}
public Viewport(float left, float top, float right, float bottom) {
super(left, top, right, bottom);
}
public Viewport(RectF r) {
super(r);
}
public Viewport(Rect r) {
super(r);
}
/**
* Ensures that current viewport is inside the viewport extremes defined by {@link #AXIS_X_MIN},
* {@link #AXIS_X_MAX}, {@link #AXIS_Y_MIN} and {@link #AXIS_Y_MAX}.
*/
public void constrainViewport() {
left = Math.max(AXIS_X_MIN, left);
right = Math.max(Math.nextUp(left), right);
}
public boolean initialized() {
if (left == 0f && right == 1f && top == -1f && bottom == 1f) {
return false;
}
return true;
}
public Viewport moveToEnd() {
Viewport vp = new Viewport(this);
float width = this.width();
vp.right = 1f;
vp.left = right - width;
return vp;
}
}
@@ -0,0 +1,130 @@
/*
* 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 cn.jingzhuan.lib.chart;
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 Zoomer {
/**
* The interpolator, used for making zooms animate 'naturally.'
*/
private Interpolator mInterpolator;
/**
* The total animation duration for a zoom.
*/
private int 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 SystemClock#elapsedRealtime()}.
*/
private long mStartRTC;
/**
* The destination zoom factor.
*/
private float mEndZoom;
public Zoomer(Context context) {
mInterpolator = new DecelerateInterpolator();
mAnimationDurationMillis = context.getResources().getInteger(
android.R.integer.config_shortAnimTime);
}
/**
* 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,205 @@
/*
Copyright 2018 Philipp Jahoda
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 cn.jingzhuan.lib.chart.animation;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import static cn.jingzhuan.lib.chart.animation.Easing.*;
/**
* Object responsible for all animations in the Chart. Animations require API level 11.
*
* @author Philipp Jahoda
* @author Mick Ashton
*/
public class ChartAnimator {
/** object that is updated upon animation update */
private AnimatorUpdateListener mListener;
/** The phase of drawn values on the y-axis. 0 - 1 */
@SuppressWarnings("WeakerAccess")
protected float mPhaseY = 1f;
/** The phase of drawn values on the x-axis. 0 - 1 */
@SuppressWarnings("WeakerAccess")
protected float mPhaseX = 1f;
public ChartAnimator() { }
public ChartAnimator(AnimatorUpdateListener listener) {
mListener = listener;
}
private ObjectAnimator xAnimator(int duration, EasingFunction easing) {
ObjectAnimator animatorX = ObjectAnimator.ofFloat(this, "phaseX", 0f, 1f);
animatorX.setInterpolator(easing);
animatorX.setDuration(duration);
return animatorX;
}
private ObjectAnimator yAnimator(int duration, EasingFunction easing) {
ObjectAnimator animatorY = ObjectAnimator.ofFloat(this, "phaseY", 0f, 1f);
animatorY.setInterpolator(easing);
animatorY.setDuration(duration);
return animatorY;
}
/**
* Animates values along the X axis, in a linear fashion.
*
* @param durationMillis animation duration
*/
public void animateX(int durationMillis) {
animateX(durationMillis, Linear);
}
/**
* Animates values along the X axis.
*
* @param durationMillis animation duration
* @param easing EasingFunction
*/
public void animateX(int durationMillis, EasingFunction easing) {
ObjectAnimator animatorX = xAnimator(durationMillis, easing);
animatorX.addUpdateListener(mListener);
animatorX.start();
}
/**
* Animates values along both the X and Y axes, in a linear fashion.
*
* @param durationMillisX animation duration along the X axis
* @param durationMillisY animation duration along the Y axis
*/
public void animateXY(int durationMillisX, int durationMillisY) {
animateXY(durationMillisX, durationMillisY, Linear, Linear);
}
/**
* Animates values along both the X and Y axes.
*
* @param durationMillisX animation duration along the X axis
* @param durationMillisY animation duration along the Y axis
* @param easing EasingFunction for both axes
*/
public void animateXY(int durationMillisX, int durationMillisY, EasingFunction easing) {
ObjectAnimator xAnimator = xAnimator(durationMillisX, easing);
ObjectAnimator yAnimator = yAnimator(durationMillisY, easing);
if (durationMillisX > durationMillisY) {
xAnimator.addUpdateListener(mListener);
} else {
yAnimator.addUpdateListener(mListener);
}
xAnimator.start();
yAnimator.start();
}
/**
* Animates values along both the X and Y axes.
*
* @param durationMillisX animation duration along the X axis
* @param durationMillisY animation duration along the Y axis
* @param easingX EasingFunction for the X axis
* @param easingY EasingFunction for the Y axis
*/
public void animateXY(int durationMillisX, int durationMillisY, EasingFunction easingX,
EasingFunction easingY) {
ObjectAnimator xAnimator = xAnimator(durationMillisX, easingX);
ObjectAnimator yAnimator = yAnimator(durationMillisY, easingY);
if (durationMillisX > durationMillisY) {
xAnimator.addUpdateListener(mListener);
} else {
yAnimator.addUpdateListener(mListener);
}
xAnimator.start();
yAnimator.start();
}
/**
* Animates values along the Y axis, in a linear fashion.
*
* @param durationMillis animation duration
*/
public void animateY(int durationMillis) {
animateY(durationMillis, Linear);
}
/**
* Animates values along the Y axis.
*
* @param durationMillis animation duration
* @param easing EasingFunction
*/
public void animateY(int durationMillis, EasingFunction easing) {
ObjectAnimator animatorY = yAnimator(durationMillis, easing);
animatorY.addUpdateListener(mListener);
animatorY.start();
}
/**
* Gets the Y axis phase of the animation.
*
* @return float value of {@link #mPhaseY}
*/
public float getPhaseY() {
return mPhaseY;
}
/**
* Sets the Y axis phase of the animation.
*
* @param phase float value between 0 - 1
*/
public void setPhaseY(float phase) {
if (phase > 1f) {
phase = 1f;
} else if (phase < 0f) {
phase = 0f;
}
mPhaseY = phase;
}
/**
* Gets the X axis phase of the animation.
*
* @return float value of {@link #mPhaseX}
*/
public float getPhaseX() {
return mPhaseX;
}
/**
* Sets the X axis phase of the animation.
*
* @param phase float value between 0 - 1
*/
public void setPhaseX(float phase) {
if (phase > 1f) {
phase = 1f;
} else if (phase < 0f) {
phase = 0f;
}
mPhaseX = phase;
}
}
@@ -0,0 +1,316 @@
/*
Copyright 2018 Philipp Jahoda
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 cn.jingzhuan.lib.chart.animation;
import android.animation.TimeInterpolator;
/**
* Easing options.
*
* @author Daniel Cohen Gindi
* @author Mick Ashton
*/
@SuppressWarnings("WeakerAccess")
public class Easing {
public interface EasingFunction extends TimeInterpolator {
@Override
float getInterpolation(float input);
}
private static final float DOUBLE_PI = 2f * (float) Math.PI;
@SuppressWarnings("unused")
public static final EasingFunction Linear = new EasingFunction() {
public float getInterpolation(float input) {
return input;
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseInQuad = new EasingFunction() {
public float getInterpolation(float input) {
return input * input;
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseOutQuad = new EasingFunction() {
public float getInterpolation(float input) {
return -input * (input - 2f);
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseInOutQuad = new EasingFunction() {
public float getInterpolation(float input) {
input *= 2f;
if (input < 1f) {
return 0.5f * input * input;
}
return -0.5f * ((--input) * (input - 2f) - 1f);
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseInCubic = new EasingFunction() {
public float getInterpolation(float input) {
return (float) Math.pow(input, 3);
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseOutCubic = new EasingFunction() {
public float getInterpolation(float input) {
input--;
return (float) Math.pow(input, 3) + 1f;
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseInOutCubic = new EasingFunction() {
public float getInterpolation(float input) {
input *= 2f;
if (input < 1f) {
return 0.5f * (float) Math.pow(input, 3);
}
input -= 2f;
return 0.5f * ((float) Math.pow(input, 3) + 2f);
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseInQuart = new EasingFunction() {
public float getInterpolation(float input) {
return (float) Math.pow(input, 4);
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseOutQuart = new EasingFunction() {
public float getInterpolation(float input) {
input--;
return -((float) Math.pow(input, 4) - 1f);
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseInOutQuart = new EasingFunction() {
public float getInterpolation(float input) {
input *= 2f;
if (input < 1f) {
return 0.5f * (float) Math.pow(input, 4);
}
input -= 2f;
return -0.5f * ((float) Math.pow(input, 4) - 2f);
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseInSine = new EasingFunction() {
public float getInterpolation(float input) {
return -(float) Math.cos(input * (Math.PI / 2f)) + 1f;
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseOutSine = new EasingFunction() {
public float getInterpolation(float input) {
return (float) Math.sin(input * (Math.PI / 2f));
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseInOutSine = new EasingFunction() {
public float getInterpolation(float input) {
return -0.5f * ((float) Math.cos(Math.PI * input) - 1f);
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseInExpo = new EasingFunction() {
public float getInterpolation(float input) {
return (input == 0) ? 0f : (float) Math.pow(2f, 10f * (input - 1f));
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseOutExpo = new EasingFunction() {
public float getInterpolation(float input) {
return (input == 1f) ? 1f : (-(float) Math.pow(2f, -10f * (input + 1f)));
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseInOutExpo = new EasingFunction() {
public float getInterpolation(float input) {
if (input == 0) {
return 0f;
} else if (input == 1f) {
return 1f;
}
input *= 2f;
if (input < 1f) {
return 0.5f * (float) Math.pow(2f, 10f * (input - 1f));
}
return 0.5f * (-(float) Math.pow(2f, -10f * --input) + 2f);
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseInCirc = new EasingFunction() {
public float getInterpolation(float input) {
return -((float) Math.sqrt(1f - input * input) - 1f);
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseOutCirc = new EasingFunction() {
public float getInterpolation(float input) {
input--;
return (float) Math.sqrt(1f - input * input);
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseInOutCirc = new EasingFunction() {
public float getInterpolation(float input) {
input *= 2f;
if (input < 1f) {
return -0.5f * ((float) Math.sqrt(1f - input * input) - 1f);
}
return 0.5f * ((float) Math.sqrt(1f - (input -= 2f) * input) + 1f);
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseInElastic = new EasingFunction() {
public float getInterpolation(float input) {
if (input == 0) {
return 0f;
} else if (input == 1) {
return 1f;
}
float p = 0.3f;
float s = p / DOUBLE_PI * (float) Math.asin(1f);
return -((float) Math.pow(2f, 10f * (input -= 1f))
*(float) Math.sin((input - s) * DOUBLE_PI / p));
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseOutElastic = new EasingFunction() {
public float getInterpolation(float input) {
if (input == 0) {
return 0f;
} else if (input == 1) {
return 1f;
}
float p = 0.3f;
float s = p / DOUBLE_PI * (float) Math.asin(1f);
return 1f
+ (float) Math.pow(2f, -10f * input)
* (float) Math.sin((input - s) * DOUBLE_PI / p);
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseInOutElastic = new EasingFunction() {
public float getInterpolation(float input) {
if (input == 0) {
return 0f;
}
input *= 2f;
if (input == 2) {
return 1f;
}
float p = 1f / 0.45f;
float s = 0.45f / DOUBLE_PI * (float) Math.asin(1f);
if (input < 1f) {
return -0.5f
* ((float) Math.pow(2f, 10f * (input -= 1f))
* (float) Math.sin((input * 1f - s) * DOUBLE_PI * p));
}
return 1f + 0.5f
* (float) Math.pow(2f, -10f * (input -= 1f))
* (float) Math.sin((input * 1f - s) * DOUBLE_PI * p);
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseInBack = new EasingFunction() {
public float getInterpolation(float input) {
final float s = 1.70158f;
return input * input * ((s + 1f) * input - s);
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseOutBack = new EasingFunction() {
public float getInterpolation(float input) {
final float s = 1.70158f;
input--;
return (input * input * ((s + 1f) * input + s) + 1f);
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseInOutBack = new EasingFunction() {
public float getInterpolation(float input) {
float s = 1.70158f;
input *= 2f;
if (input < 1f) {
return 0.5f * (input * input * (((s *= (1.525f)) + 1f) * input - s));
}
return 0.5f * ((input -= 2f) * input * (((s *= (1.525f)) + 1f) * input + s) + 2f);
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseInBounce = new EasingFunction() {
public float getInterpolation(float input) {
return 1f - EaseOutBounce.getInterpolation(1f - input);
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseOutBounce = new EasingFunction() {
public float getInterpolation(float input) {
float s = 7.5625f;
if (input < (1f / 2.75f)) {
return s * input * input;
} else if (input < (2f / 2.75f)) {
return s * (input -= (1.5f / 2.75f)) * input + 0.75f;
} else if (input < (2.5f / 2.75f)) {
return s * (input -= (2.25f / 2.75f)) * input + 0.9375f;
}
return s * (input -= (2.625f / 2.75f)) * input + 0.984375f;
}
};
@SuppressWarnings("unused")
public static final EasingFunction EaseInOutBounce = new EasingFunction() {
public float getInterpolation(float input) {
if (input < 0.5f) {
return EaseInBounce.getInterpolation(input * 2f) * 0.5f;
}
return EaseOutBounce.getInterpolation(input * 2f - 1f) * 0.5f + 0.5f;
}
};
}
@@ -0,0 +1,309 @@
package cn.jingzhuan.lib.chart.base;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.os.Build;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.MotionEvent;
import cn.jingzhuan.lib.chart.animation.ChartAnimator;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import cn.jingzhuan.lib.chart.component.Highlight;
import cn.jingzhuan.lib.chart.event.HighlightStatusChangeListener;
import cn.jingzhuan.lib.chart.event.OnHighlightListener;
import cn.jingzhuan.lib.chart.renderer.AbstractDataRenderer;
import cn.jingzhuan.lib.chart.renderer.AxisRenderer;
import cn.jingzhuan.lib.chart2.renderer.LineRenderer;
import static cn.jingzhuan.lib.chart.animation.Easing.*;
/**
* Created by Donglua on 17/7/17.
*/
public class BaseChart extends Chart {
protected AbstractDataRenderer mRenderer;
protected List<AxisRenderer> mAxisRenderers;
protected Highlight[] mHighlights;
private HighlightStatusChangeListener mHighlightStatusChangeListener;
private OnHighlightListener mHighlightListener;
protected WeakReference<Bitmap> mDrawBitmap;
protected Canvas mBitmapCanvas;
protected Bitmap.Config mBitmapConfig = Bitmap.Config.ARGB_8888;
private ChartAnimator mChartAnimator;
public BaseChart(Context context) {
super(context);
}
public BaseChart(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public BaseChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public BaseChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public void initChart() {
mAxisRenderers = new ArrayList<>(4);
mAxisRenderers.add(new AxisRenderer(this, mAxisTop));
mAxisRenderers.add(new AxisRenderer(this, mAxisBottom));
mAxisRenderers.add(new AxisRenderer(this, mAxisLeft));
mAxisRenderers.add(new AxisRenderer(this, mAxisRight));
mChartAnimator = new ChartAnimator(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
postInvalidate();
}
});
}
@Override protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mBitmapCanvas != null) {
mBitmapCanvas.setBitmap(null);
mBitmapCanvas = null;
}
if (mDrawBitmap != null) {
if (mDrawBitmap.get() != null) mDrawBitmap.get().recycle();
mDrawBitmap.clear();
mDrawBitmap = null;
}
}
@Override protected void createBitmapCache(Canvas canvas) {
int width = getContentRect().width() + getContentRect().left;
int height = getContentRect().height();
if (mDrawBitmap == null
|| (mDrawBitmap.get().getWidth() != width)
|| (mDrawBitmap.get().getHeight() != height)) {
if (width > 0 && height > 0) {
mDrawBitmap = new WeakReference<>(Bitmap.createBitmap(width, height, mBitmapConfig));
mBitmapCanvas = new Canvas(mDrawBitmap.get());
} else
return;
}
mDrawBitmap.get().eraseColor(Color.TRANSPARENT);
}
@Override protected Bitmap getDrawBitmap() {
return mDrawBitmap.get();
}
@Override protected Paint getRenderPaint() {
return mRenderer.getRenderPaint();
}
@Override
public Canvas getBitmapCanvas() {
return mBitmapCanvas;
}
@Override
protected void drawAxis(Canvas canvas) {
for (AxisRenderer axisRenderer : mAxisRenderers) {
axisRenderer.renderer(canvas);
}
}
@Override protected void drawGridLine(Canvas canvas) {
for (AxisRenderer axisRenderer : mAxisRenderers) {
axisRenderer.drawGridLines(canvas);
}
}
@Override
protected void drawLabels(Canvas canvas) {
for (AxisRenderer axisRenderer : mAxisRenderers) {
axisRenderer.drawLabels(canvas);
}
}
@Override protected void onTouchPoint(MotionEvent e) {
if (e.getPointerCount() == 1) {
for (OnTouchPointChangeListener touchPointChangeListener : mTouchPointChangeListeners) {
touchPointChangeListener.touch(e.getX(), e.getY());
}
}
}
@Override
public void highlightValue(Highlight highlight) {
if (highlight == null) return;
final Highlight[] highlights = new Highlight[] { highlight };
if (mHighlightStatusChangeListener != null) {
mHighlightStatusChangeListener.onHighlightShow(highlights);
}
if (mHighlightListener != null) {
mHighlightListener.highlight(highlights);
}
mHighlights = highlights;
invalidate();
}
@Override
public void cleanHighlight() {
mHighlights = null;
if (mHighlightStatusChangeListener != null)
mHighlightStatusChangeListener.onHighlightHide();
invalidate();
}
public void setRenderer(AbstractDataRenderer renderer) {
this.mRenderer = renderer;
}
@Override
protected final void render(final Canvas canvas) {
if (mRenderer != null) {
mRenderer.renderer(canvas);
}
drawEdgeEffectsUnclipped(canvas);
renderHighlighted(canvas);
}
public void renderHighlighted(Canvas canvas) {
if (mRenderer != null && getHighlights() != null) {
mRenderer.renderHighlighted(canvas, getHighlights());
}
}
public void setHighlightColor(int color) {
mRenderer.setHighlightColor(color);
}
public int getHighlightColor() {
return mRenderer.getHighlightColor();
}
public Highlight[] getHighlights() {
return mHighlights;
}
public void setHighlights(Highlight[] highlights) {
this.mHighlights = highlights;
}
public void setOnHighlightStatusChangeListener(HighlightStatusChangeListener mHighlightStatusChangeListener) {
this.mHighlightStatusChangeListener = mHighlightStatusChangeListener;
}
public HighlightStatusChangeListener getOnHighlightStatusChangeListener() {
return mHighlightStatusChangeListener;
}
public void setOnHighlightListener(OnHighlightListener highlightListener) {
this.mHighlightListener = highlightListener;
}
public void enableHighlightDashPathEffect(float intervals[], float phase) {
this.mRenderer.enableHighlightDashPathEffect(intervals, phase);
}
public void setMinVisibleEntryCount(int minVisibleEntryCount) {
mRenderer.setMinVisibleEntryCount(minVisibleEntryCount);
}
public void setMaxVisibleEntryCount(int maxVisibleEntryCount) {
mRenderer.setMaxVisibleEntryCount(maxVisibleEntryCount);
}
public void setDefaultVisibleEntryCount(int defaultVisibleEntryCount) {
mRenderer.setDefaultVisibleEntryCount(defaultVisibleEntryCount);
}
@Override protected int getEntryIndexByCoordinate(float x, float y) {
return mRenderer.getEntryIndexByCoordinate(x, y);
}
public void setTypeface(Typeface tf) {
for (AxisRenderer mAxisRenderer : mAxisRenderers) {
mAxisRenderer.setTypeface(tf);
}
postInvalidate();
}
public void releaseBitmap() {
if (mBitmapCanvas != null) {
mBitmapCanvas.setBitmap(null);
mBitmapCanvas = null;
}
if (mDrawBitmap != null) {
if (mDrawBitmap.get() != null) mDrawBitmap.get().recycle();
mDrawBitmap.clear();
mDrawBitmap = null;
}
}
public ChartAnimator getChartAnimator() {
return mChartAnimator;
}
public void animateXY(int durationMillisX, int durationMillisY, EasingFunction easingX,
EasingFunction easingY) {
mChartAnimator.animateXY(durationMillisX, durationMillisY, easingX, easingY);
}
public void animateXY(int durationMillisX, int durationMillisY, EasingFunction easing) {
mChartAnimator.animateXY(durationMillisX, durationMillisY, easing);
}
public void animateX(int durationMillis, EasingFunction easing) {
mChartAnimator.animateX(durationMillis, easing);
}
public void animateY(int durationMillis, EasingFunction easing) {
mChartAnimator.animateY(durationMillis, easing);
}
public void animateX(int durationMillis) {
mChartAnimator.animateX(durationMillis);
}
public void animateY(int durationMillis) {
mChartAnimator.animateY(durationMillis);
}
public void animateXY(int durationMillisX, int durationMillisY) {
mChartAnimator.animateXY(durationMillisX, durationMillisY);
}
}
@@ -0,0 +1,157 @@
package cn.jingzhuan.lib.chart.base;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Build;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.View;
import java.lang.ref.WeakReference;
import cn.jingzhuan.lib.chart.Viewport;
public abstract class BitmapCachedChart extends View {
protected WeakReference<Bitmap> mDrawBitmap;
protected Canvas mBitmapCanvas;
protected Bitmap.Config mBitmapConfig = Bitmap.Config.ARGB_8888;
/**
* The current viewport. This rectangle represents the currently visible lib domain
* and range. The currently visible lib X values are from this rectangle's left to its right.
* The currently visible lib Y values are from this rectangle's top to its bottom.
* <p>
* Note that this rectangle's top is actually the smaller Y value, and its bottom is the larger
* Y value. Since the lib is drawn onscreen in such a way that lib Y values increase
* towards the top of the screen (decreasing pixel Y positions), this rectangle's "top" is drawn
* above this rectangle's "bottom" value.
*
* @see #mContentRect
*/
protected Viewport mCurrentViewport = new Viewport();
/**
* The current destination rectangle (in pixel coordinates) into which the lib data should
* be drawn. Chart labels are drawn outside this area.
*
* @see #mCurrentViewport
*/
protected Rect mContentRect = new Rect();
public BitmapCachedChart(Context context) {
super(context);
}
public BitmapCachedChart(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public BitmapCachedChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public BitmapCachedChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public Rect getContentRect() {
return mContentRect;
}
public Viewport getCurrentViewport() {
return mCurrentViewport;
}
public Canvas getBitmapCanvas() {
return mBitmapCanvas;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawAxis(canvas); // 坐标轴刻度在最底层
// Clips the next few drawing operations to the content area
int clipRestoreCount = canvas.save();
canvas.clipRect(mContentRect);
createBitmapCache(canvas);
if (getBitmapCanvas() != null) {
drawGridLine(getBitmapCanvas());
render(getBitmapCanvas());
canvas.drawBitmap(getDrawBitmap(), 0, 0, getRenderPaint());
}
// Removes clipping rectangle
canvas.restoreToCount(clipRestoreCount);
drawLabels(canvas); // 坐标轴刻度在最上层
}
protected void createBitmapCache(Canvas canvas) {
int width = getContentRect().width() + getContentRect().left;
int height = getContentRect().height();
if (mDrawBitmap == null
|| (mDrawBitmap.get() == null)
|| (mDrawBitmap.get().getWidth() != width)
|| (mDrawBitmap.get().getHeight() != height)) {
if (width > 0 && height > 0) {
mDrawBitmap = new WeakReference<>(Bitmap.createBitmap(width, height, mBitmapConfig));
mBitmapCanvas = new Canvas(mDrawBitmap.get());
} else
return;
}
mDrawBitmap.get().eraseColor(Color.TRANSPARENT);
}
protected Bitmap getDrawBitmap() {
return mDrawBitmap.get();
}
@Override protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mBitmapCanvas != null) {
mBitmapCanvas.setBitmap(null);
mBitmapCanvas = null;
}
if (mDrawBitmap != null) {
if (mDrawBitmap.get() != null) mDrawBitmap.get().recycle();
mDrawBitmap.clear();
mDrawBitmap = null;
}
}
public void releaseBitmap() {
if (mBitmapCanvas != null) {
mBitmapCanvas.setBitmap(null);
mBitmapCanvas = null;
}
if (mDrawBitmap != null) {
if (mDrawBitmap.get() != null) mDrawBitmap.get().recycle();
mDrawBitmap.clear();
mDrawBitmap = null;
}
}
protected abstract void drawAxis(Canvas canvas);
protected abstract void drawGridLine(Canvas canvas);
protected abstract void render(Canvas canvas);
protected abstract Paint getRenderPaint();
protected abstract void drawLabels(Canvas canvas);
}
@@ -0,0 +1,822 @@
package cn.jingzhuan.lib.chart.base;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.RectF;
import android.os.Build;
import androidx.annotation.FloatRange;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.widget.EdgeEffect;
import android.widget.OverScroller;
import cn.jingzhuan.lib.chart.R;
import cn.jingzhuan.lib.chart.Viewport;
import cn.jingzhuan.lib.chart.Zoomer;
import cn.jingzhuan.lib.chart.event.OnEntryClickListener;
import cn.jingzhuan.lib.chart.utils.ForceAlign;
import cn.jingzhuan.lib.chart.utils.ForceAlign.XForce;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import cn.jingzhuan.lib.chart.component.Axis;
import cn.jingzhuan.lib.chart.component.AxisX;
import cn.jingzhuan.lib.chart.component.AxisY;
import cn.jingzhuan.lib.chart.component.Highlight;
import cn.jingzhuan.lib.chart.event.OnViewportChangeListener;
/**
* Created by Donglua on 17/7/17.
*/
public abstract class Chart extends BitmapCachedChart {
protected AxisY mAxisLeft = new AxisY(AxisY.LEFT_INSIDE);
protected AxisY mAxisRight = new AxisY(AxisY.RIGHT_INSIDE);
protected AxisX mAxisTop = new AxisX(AxisX.TOP);
protected AxisX mAxisBottom = new AxisX(AxisX.BOTTOM);
// State objects and values related to gesture tracking.
private ScaleGestureDetector mScaleGestureDetector;
private GestureDetector mGestureDetector;
private OverScroller mScroller;
private Zoomer mZoomer;
private PointF mZoomFocalPoint = new PointF();
private RectF mScrollerStartViewport = new RectF(); // Used only for zooms and flings.
private boolean mScaleXEnable = true;
private boolean mDraggingToMoveEnable = true;
private boolean mDoubleTapToZoom = false;
private boolean mScaleGestureEnable = true;
private boolean mHighlightDisable = false;
protected List<OnTouchPointChangeListener> mTouchPointChangeListeners;
private List<OnViewportChangeListener> mOnViewportChangeListeners;
protected OnViewportChangeListener mInternalViewportChangeListener;
protected OnEntryClickListener onEntryClickListener;
/**
* The scaling factor for a single zoom 'step'.
*
* @see #zoomIn()
* @see #zoomOut()
*/
private static final float ZOOM_AMOUNT = 0.2f;
private final Point mSurfaceSizeBuffer = new Point();
// Edge effect / overscroll tracking objects.
private EdgeEffect mEdgeEffectLeft;
private EdgeEffect mEdgeEffectRight;
private boolean mEdgeEffectLeftActive;
private boolean mEdgeEffectRightActive;
private boolean isTouching = false;
public Chart(Context context) {
this(context, null, 0);
}
public Chart(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public Chart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public Chart(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs, defStyleAttr);
}
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs, R.styleable.Chart, defStyleAttr, defStyleAttr);
mTouchPointChangeListeners = Collections.synchronizedList(new ArrayList<OnTouchPointChangeListener>());
mOnViewportChangeListeners = Collections.synchronizedList(new ArrayList<OnViewportChangeListener>());
mAxisTop.setGridLineEnable(false);
mAxisTop.setLabelEnable(false);
try {
List<Axis> axisList = new ArrayList<>(4);
axisList.add(mAxisLeft);
axisList.add(mAxisRight);
axisList.add(mAxisTop);
axisList.add(mAxisBottom);
float labelTextSize = a.getDimension(R.styleable.Chart_labelTextSize, 28);
float labelSeparation = a.getDimensionPixelSize(R.styleable.Chart_labelSeparation, 10);
float gridThickness = a.getDimension(R.styleable.Chart_gridThickness, 2);
float axisThickness = a.getDimension(R.styleable.Chart_axisThickness, 2);
int gridColor = a.getColor(R.styleable.Chart_gridColor, Color.GRAY);
int axisColor = a.getColor(R.styleable.Chart_axisColor, Color.GRAY);
int labelTextColor = a.getColor(R.styleable.Chart_labelTextColor, Color.GRAY);
for (Axis axis : axisList) {
axis.setLabelTextSize(labelTextSize);
axis.setLabelTextColor(labelTextColor);
axis.setLabelSeparation(labelSeparation);
axis.setGridColor(gridColor);
axis.setGridThickness(gridThickness);
axis.setAxisColor(axisColor);
axis.setAxisThickness(axisThickness);
}
} finally {
a.recycle();
}
initChart();
setupInteractions(context);
setupEdgeEffect(context);
}
public abstract void initChart();
public abstract void highlightValue(Highlight highlight);
public abstract void cleanHighlight();
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
getContentRect().set(
getPaddingLeft() + (mAxisLeft.isInside() ? 0 : mAxisLeft.getLabelWidth()),
getPaddingTop(),
getWidth() - getPaddingRight() - (mAxisRight.isInside() ? 0 : mAxisRight.getLabelWidth()),
getHeight() - getPaddingBottom() - mAxisBottom.getLabelHeight()
);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int minChartSize = getResources().getDimensionPixelSize(R.dimen.jz_chart_min_size);
setMeasuredDimension(
Math.max(getSuggestedMinimumWidth(),
resolveSize(minChartSize + getPaddingLeft()
+ (mAxisLeft.isInside() ? 0 : mAxisLeft.getLabelWidth())
+ getPaddingRight(),
widthMeasureSpec)),
Math.max(getSuggestedMinimumHeight(),
resolveSize(minChartSize + getPaddingTop()
+ (mAxisBottom.isInside() ? 0 : mAxisBottom.getLabelHeight())
+ getPaddingBottom(),
heightMeasureSpec)));
}
protected abstract int getEntryIndexByCoordinate(float x, float y);
// -------- -------- --------
private void setupInteractions(Context context) {
mScaleGestureDetector = new ScaleGestureDetector(context, mScaleGestureListener);
mGestureDetector = new GestureDetector(context, mGestureListener);
mScroller = new OverScroller(context);
mZoomer = new Zoomer(context);
}
/**
* Finds the lib point (i.e. within the lib's domain and range) represented by the
* given pixel coordinates, if that pixel is within the lib region described by
* {@link #mContentRect}. 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.
*/
private boolean hitTest(float x, float y, PointF dest) {
if (!mContentRect.contains((int) x, (int) y)) {
return false;
}
dest.set(mCurrentViewport.left
+ mCurrentViewport.width()
* (x - mContentRect.left) / mContentRect.width(),
mCurrentViewport.top
+ mCurrentViewport.height()
* (y - mContentRect.bottom) / -mContentRect.height());
return true;
}
/**
* The scale listener, used for handling multi-finger scale gestures.
*/
private final ScaleGestureDetector.OnScaleGestureListener mScaleGestureListener
= new ScaleGestureDetector.SimpleOnScaleGestureListener() {
/**
* This is the active focal point in terms of the viewport. Could be a local
* variable but kept here to minimize per-frame allocations.
*/
private PointF viewportFocus = new PointF();
private float lastSpanX;
@Override
public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) {
if (!mScaleGestureEnable) return super.onScaleBegin(scaleGestureDetector);
lastSpanX = scaleGestureDetector.getCurrentSpanX();
return true;
}
@Override
public boolean onScale(ScaleGestureDetector scaleGestureDetector) {
if (!mScaleXEnable) return false;
if (!mScaleGestureEnable) return super.onScale(scaleGestureDetector);
float spanX = scaleGestureDetector.getCurrentSpanX();
float newWidth = lastSpanX / spanX * mCurrentViewport.width();
if (newWidth < mCurrentViewport.width() && mCurrentViewport.width() < 0.001) {
return true;
}
float focusX = scaleGestureDetector.getFocusX();
float focusY = scaleGestureDetector.getFocusY();
hitTest(focusX, focusY, viewportFocus);
mCurrentViewport.left = viewportFocus.x
- newWidth * (focusX - mContentRect.left)
/ mContentRect.width();
mCurrentViewport.right = mCurrentViewport.left + newWidth;
mCurrentViewport.constrainViewport();
triggerViewportChange();
lastSpanX = spanX;
return true;
}
};
protected abstract void onTouchPoint(MotionEvent e);
/**
* The gesture listener, used for handling simple gestures such as double touches, scrolls,
* and flings.
*/
private final GestureDetector.SimpleOnGestureListener mGestureListener
= new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
releaseEdgeEffects();
mScrollerStartViewport.set(mCurrentViewport);
mScroller.forceFinished(true);
postInvalidateOnAnimation();
return true;
}
@Override public void onLongPress(MotionEvent e) {
mGestureDetector.onTouchEvent(e);
mGestureDetector.setIsLongpressEnabled(false);
}
@Override
public void onShowPress(MotionEvent e) {
super.onShowPress(e);
onTouchPoint(e);
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
if (onEntryClickListener != null) {
int index = getEntryIndexByCoordinate(e.getX(), e.getY());
if (index >= 0) {
onEntryClickListener.onEntryClick(Chart.this, index);
if (isHighlightDisable()) {
cleanHighlight();
} else {
onTouchPoint(e);
}
} else {
cleanHighlight();
performClick();
}
} else {
cleanHighlight();
performClick();
}
return true;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
if (mDoubleTapToZoom) {
mZoomer.forceFinished(true);
if (hitTest(e.getX(), e.getY(), mZoomFocalPoint)) {
mZoomer.startZoom(ZOOM_AMOUNT);
}
postInvalidateOnAnimation();
}
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (!isDraggingToMoveEnable()) {
onTouchPoint(e2);
return super.onScroll(e1, e2, distanceX, distanceY);
}
// 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}.
*/
float viewportOffsetX = distanceX * mCurrentViewport.width() / mContentRect.width();
// float viewportOffsetY = -distanceY * mCurrentViewport.height() / mContentRect.height();
computeScrollSurfaceSize(mSurfaceSizeBuffer);
int scrolledX = (int) (mSurfaceSizeBuffer.x
* (mCurrentViewport.left + viewportOffsetX - Viewport.AXIS_X_MIN)
/ (Viewport.AXIS_X_MAX - Viewport.AXIS_X_MIN));
boolean canScrollX = mCurrentViewport.left > Viewport.AXIS_X_MIN
|| mCurrentViewport.right < Viewport.AXIS_X_MAX;
setViewportBottomLeft(
mCurrentViewport.left + viewportOffsetX
);
if (canScrollX && scrolledX < 0) {
mEdgeEffectLeft.onPull(scrolledX / (float) mContentRect.width());
mEdgeEffectLeftActive = true;
}
if (canScrollX && scrolledX > mSurfaceSizeBuffer.x - mContentRect.width()) {
mEdgeEffectRight.onPull((scrolledX - mSurfaceSizeBuffer.x + mContentRect.width())
/ (float) mContentRect.width());
mEdgeEffectRightActive = true;
}
onTouchPoint(e2);
//triggerViewportChange();
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (!isDraggingToMoveEnable()) return super.onFling(e1, e2, velocityX, velocityY);
fling((int) -velocityX);
onTouchPoint(e2);
return true;
}
};
protected void triggerViewportChange() {
postInvalidateOnAnimation();
if (mInternalViewportChangeListener != null) {
mInternalViewportChangeListener.onViewportChange(mCurrentViewport);
}
if (mOnViewportChangeListeners != null && !mOnViewportChangeListeners.isEmpty()) {
synchronized (this) {
for (OnViewportChangeListener mOnViewportChangeListener : mOnViewportChangeListeners) {
mOnViewportChangeListener.onViewportChange(mCurrentViewport);
}
}
}
}
private void fling(int velocityX) {
releaseEdgeEffects();
// Flings use math in pixels (as opposed to math based on the viewport).
computeScrollSurfaceSize(mSurfaceSizeBuffer);
mScrollerStartViewport.set(mCurrentViewport);
int startX = (int) (mSurfaceSizeBuffer.x * (mScrollerStartViewport.left - Viewport.AXIS_X_MIN) / (
Viewport.AXIS_X_MAX - Viewport.AXIS_X_MIN));
mScroller.forceFinished(true);
mScroller.fling(
startX,
0,
velocityX,
0,
0, mSurfaceSizeBuffer.x - mContentRect.width(),
0, mSurfaceSizeBuffer.y - mContentRect.height(),
mContentRect.width() / 2,
0);
postInvalidateOnAnimation();
}
/**
* Computes the current scrollable surface size, in pixels. For example, if the entire lib
* area is visible, this is simply the current size of {@link #mContentRect}. If the lib
* is zoomed in 200% in both directions, the returned size will be twice as large horizontally
* and vertically.
*/
private void computeScrollSurfaceSize(Point out) {
out.set((int) (mContentRect.width() * (Viewport.AXIS_X_MAX - Viewport.AXIS_X_MIN)
/ mCurrentViewport.width()),
(int) (mContentRect.height() * (Viewport.AXIS_Y_MAX - Viewport.AXIS_Y_MIN)
/ mCurrentViewport.height()));
}
/**
* Smoothly zooms the lib in one step.
*/
public void zoomIn() {
zoom(ZOOM_AMOUNT);
}
/**
* Smoothly zooms the lib out one step.
*/
public void zoomOut() {
zoom(-ZOOM_AMOUNT);
}
public void zoom(float scalingFactor) {
mScrollerStartViewport.set(mCurrentViewport);
mZoomer.forceFinished(true);
mZoomer.startZoom(scalingFactor);
mZoomFocalPoint.set(
(mCurrentViewport.right + mCurrentViewport.left) / 2,
(mCurrentViewport.bottom + mCurrentViewport.top) / 2);
triggerViewportChange();
}
public void zoomOut(@XForce int forceAlignX) {
mScrollerStartViewport.set(mCurrentViewport);
mZoomer.forceFinished(true);
mZoomer.startZoom(-ZOOM_AMOUNT);
float forceX;
switch (forceAlignX) {
case ForceAlign.LEFT:
forceX = mCurrentViewport.left;
break;
case ForceAlign.RIGHT:
forceX = mCurrentViewport.right;
break;
case ForceAlign.CENTER:
forceX = (mCurrentViewport.right + mCurrentViewport.left) / 2;
break;
default:
forceX = (mCurrentViewport.right + mCurrentViewport.left) / 2;
break;
}
mZoomFocalPoint.set(forceX,
(mCurrentViewport.bottom + mCurrentViewport.top) / 2);
postInvalidateOnAnimation();
}
/**
* Smoothly zooms the lib in one step.
*/
public void zoomIn(@XForce int forceAlignX) {
mScrollerStartViewport.set(mCurrentViewport);
mZoomer.forceFinished(true);
mZoomer.startZoom(ZOOM_AMOUNT);
float forceX;
switch (forceAlignX) {
case ForceAlign.LEFT:
forceX = mCurrentViewport.left;
break;
case ForceAlign.RIGHT:
forceX = mCurrentViewport.right;
break;
case ForceAlign.CENTER:
forceX = (mCurrentViewport.right + mCurrentViewport.left) / 2;
break;
default:
forceX = (mCurrentViewport.right + mCurrentViewport.left) / 2;
break;
}
mZoomFocalPoint.set(forceX,
(mCurrentViewport.bottom + mCurrentViewport.top) / 2);
triggerViewportChange();
}
@Override
public void computeScroll() {
super.computeScroll();
boolean needsInvalidate = false;
if (mScroller.computeScrollOffset()) {
// The scroller isn't finished, meaning a fling or programmatic pan operation is
// currently active.
computeScrollSurfaceSize(mSurfaceSizeBuffer);
int currX = mScroller.getCurrX();
boolean canScrollX = (mCurrentViewport.left > Viewport.AXIS_X_MIN
|| mCurrentViewport.right < Viewport.AXIS_X_MAX);
if (canScrollX
&& currX < 0
&& mEdgeEffectLeft.isFinished()
&& !mEdgeEffectLeftActive) {
mEdgeEffectLeft.onAbsorb((int) mScroller.getCurrVelocity());
mEdgeEffectLeftActive = true;
needsInvalidate = true;
} else if (canScrollX
&& currX > (mSurfaceSizeBuffer.x - mContentRect.width())
&& mEdgeEffectRight.isFinished()
&& !mEdgeEffectRightActive) {
mEdgeEffectRight.onAbsorb((int) mScroller.getCurrVelocity());
mEdgeEffectRightActive = true;
needsInvalidate = true;
}
float currXRange = Viewport.AXIS_X_MIN + (Viewport.AXIS_X_MAX - Viewport.AXIS_X_MIN)
* currX / mSurfaceSizeBuffer.x;
setViewportBottomLeft(currXRange);
}
if (mZoomer.computeZoom()) {
// Performs the zoom since a zoom is in progress (either programmatically or via
// double-touch).
float newWidth = (1f - mZoomer.getCurrZoom()) * mScrollerStartViewport.width();
float newHeight = (1f - mZoomer.getCurrZoom()) * mScrollerStartViewport.height();
float pointWithinViewportX = (mZoomFocalPoint.x - mScrollerStartViewport.left)
/ mScrollerStartViewport.width();
float pointWithinViewportY = (mZoomFocalPoint.y - mScrollerStartViewport.top)
/ mScrollerStartViewport.height();
mCurrentViewport.set(
mZoomFocalPoint.x - newWidth * pointWithinViewportX,
mZoomFocalPoint.y - newHeight * pointWithinViewportY,
mZoomFocalPoint.x + newWidth * (1 - pointWithinViewportX),
mZoomFocalPoint.y + newHeight * (1 - pointWithinViewportY));
mCurrentViewport.constrainViewport();
needsInvalidate = true;
}
if (needsInvalidate) {
triggerViewportChange();
}
}
/**
* Sets the current viewport (defined by {@link #mCurrentViewport}) to the given
* X and Y positions. Note that the Y value represents the topmost pixel position, and thus
* the bottom of the {@link #mCurrentViewport} rectangle. For more details on why top and
* bottom are flipped, see {@link #mCurrentViewport}.
*/
private void setViewportBottomLeft(float x) {
/**
* 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.
*/
float curWidth = mCurrentViewport.width();
x = Math.max(Viewport.AXIS_X_MIN, Math.min(x, Viewport.AXIS_X_MAX - curWidth));
mCurrentViewport.left = x;
mCurrentViewport.right = x + curWidth;
mCurrentViewport.constrainViewport();
triggerViewportChange();
}
/**
* Sets the lib's current viewport.
*
* @see #getCurrentViewport()
*/
public void setCurrentViewport(RectF viewport) {
mCurrentViewport.set(viewport.left, viewport.top, viewport.right, viewport.bottom);
mCurrentViewport.constrainViewport();
triggerViewportChange();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN || event.getAction() == MotionEvent.ACTION_MOVE) {
isTouching = true;
} else if (event.getAction() == MotionEvent.ACTION_UP) {
isTouching = false;
}
boolean retVal = event.getPointerCount() > 1 && mScaleGestureDetector.onTouchEvent(event);
retVal = mGestureDetector.onTouchEvent(event) || retVal;
return retVal || super.onTouchEvent(event);
}
protected void setupEdgeEffect(Context context) {
// Sets up edge effects
mEdgeEffectLeft = new EdgeEffect(context);
mEdgeEffectRight = new EdgeEffect(context);
}
/**
* Draws the overscroll "glow" at the four edges of the lib region, if necessary. The edges
* of the lib region are stored in {@link #mContentRect}.
*
* @see EdgeEffect
*/
protected void drawEdgeEffectsUnclipped(Canvas canvas) {
// The methods below rotate and translate the canvas as needed before drawing the glow,
// since EdgeEffectCompat always draws a top-glow at 0,0.
boolean needsInvalidate = false;
if (!mEdgeEffectLeft.isFinished()) {
final int restoreCount = canvas.save();
canvas.translate(mContentRect.left, mContentRect.bottom);
canvas.rotate(-90, 0, 0);
mEdgeEffectLeft.setSize(mContentRect.height(), mContentRect.width());
if (mEdgeEffectLeft.draw(canvas)) {
needsInvalidate = true;
}
canvas.restoreToCount(restoreCount);
}
if (!mEdgeEffectRight.isFinished()) {
final int restoreCount = canvas.save();
canvas.translate(mContentRect.right, mContentRect.top);
canvas.rotate(90, 0, 0);
mEdgeEffectRight.setSize(mContentRect.height(), mContentRect.width());
if (mEdgeEffectRight.draw(canvas)) {
needsInvalidate = true;
}
canvas.restoreToCount(restoreCount);
}
if (needsInvalidate) {
triggerViewportChange();
}
}
private void releaseEdgeEffects() {
mEdgeEffectLeftActive
= mEdgeEffectRightActive
= false;
mEdgeEffectLeft.onRelease();
mEdgeEffectRight.onRelease();
}
public AxisY getAxisLeft() {
return mAxisLeft;
}
public AxisY getAxisRight() {
return mAxisRight;
}
public AxisX getAxisTop() {
return mAxisTop;
}
public AxisX getAxisBottom() {
return mAxisBottom;
}
public void addOnViewportChangeListener(OnViewportChangeListener onViewportChangeListener) {
synchronized (this) {
this.mOnViewportChangeListeners.add(onViewportChangeListener);
}
}
public boolean isScaleXEnable() {
return mScaleXEnable;
}
public void setScaleXEnable(boolean scaleXEnable) {
this.mScaleXEnable = scaleXEnable;
}
public void setDoubleTapToZoom(boolean doubleTapToZoom) {
this.mDoubleTapToZoom = doubleTapToZoom;
}
public interface OnTouchPointChangeListener {
void touch(float x, float y);
}
public void addOnTouchPointChangeListener(OnTouchPointChangeListener touchPointChangeListener) {
synchronized (this) {
this.mTouchPointChangeListeners.add(touchPointChangeListener);
}
}
public void removeOnTouchPointChangeListener(OnTouchPointChangeListener touchPointChangeListener) {
synchronized (this) {
this.mTouchPointChangeListeners.remove(touchPointChangeListener);
}
}
public void setDraggingToMoveEnable(boolean draggingToMoveEnable) {
this.mDraggingToMoveEnable = draggingToMoveEnable;
}
public boolean isDraggingToMoveEnable() {
return mDraggingToMoveEnable;
}
public void moveLeft() {
moveLeft(0.2f);
}
public void moveRight() {
moveRight(0.2f);
}
public void moveLeft(@FloatRange(from = 0f, to = 1.0f) float percent) {
releaseEdgeEffects();
computeScrollSurfaceSize(mSurfaceSizeBuffer);
mScrollerStartViewport.set(mCurrentViewport);
float moveDistance = mContentRect.width() * percent;
int startX = (int) (mSurfaceSizeBuffer.x * (mScrollerStartViewport.left - Viewport.AXIS_X_MIN)
/ (Viewport.AXIS_X_MAX - Viewport.AXIS_X_MIN));
if (!mScroller.isFinished()) {
mScroller.forceFinished(true);
}
mScroller.startScroll(startX, 0, (int) -moveDistance, 0, 300);
postInvalidateOnAnimation();
}
public void moveRight(@FloatRange(from = 0f, to = 1.0f) float percent) {
// releaseEdgeEffects();
computeScrollSurfaceSize(mSurfaceSizeBuffer);
mScrollerStartViewport.set(mCurrentViewport);
float moveDistance = mContentRect.width() * percent;
int startX = (int) (mSurfaceSizeBuffer.x * (mScrollerStartViewport.left - Viewport.AXIS_X_MIN)
/ (Viewport.AXIS_X_MAX - Viewport.AXIS_X_MIN));
if (!mScroller.isFinished()) {
mScroller.forceFinished(true);
}
mScroller.startScroll(startX, 0, (int) moveDistance, 0, 300);
postInvalidateOnAnimation();
}
public void setInternalViewportChangeListener(
OnViewportChangeListener mInternalViewportChangeListener) {
this.mInternalViewportChangeListener = mInternalViewportChangeListener;
}
public void setScaleGestureEnable(boolean mScaleGestureEnable) {
this.mScaleGestureEnable = mScaleGestureEnable;
}
public boolean isScaleGestureEnable() {
return mScaleGestureEnable;
}
public boolean isHighlightDisable() {
return mHighlightDisable;
}
public void setHighlightDisable(boolean highlightDisable) {
this.mHighlightDisable = highlightDisable;
}
public void setOnEntryClickListener(OnEntryClickListener onEntryClickListener) {
this.onEntryClickListener = onEntryClickListener;
}
public OnEntryClickListener getOnEntryClickListener() {
return onEntryClickListener;
}
public Zoomer getZoomer() {
return mZoomer;
}
public boolean isTouching() {
return isTouching;
}
}
@@ -0,0 +1,16 @@
package cn.jingzhuan.lib.chart.component;
public abstract class AbstractComponent implements Component {
private boolean isEnable = true;
@Override
public void setEnable(boolean enable) {
isEnable = enable;
}
@Override
public boolean isEnable() {
return isEnable;
}
}
@@ -0,0 +1,253 @@
package cn.jingzhuan.lib.chart.component;
import android.graphics.Color;
import android.graphics.Paint;
import cn.jingzhuan.lib.chart.AxisAutoValues;
import cn.jingzhuan.lib.chart.data.GirdLineColorSetter;
import cn.jingzhuan.lib.chart.data.LabelColorSetter;
import cn.jingzhuan.lib.chart.data.ValueFormatter;
import cn.jingzhuan.lib.chart.data.ValueIndexFormatter;
import java.util.List;
/**
* Created by Donglua on 17/7/17.
*/
public class Axis extends AbstractComponent {
private int mAxisPosition;
private int mGridColor = Color.GRAY;
private GirdLineColorSetter mGirdLineColorSetter = null;
private float mGridThickness = 1;
private int mGridCount = 3;
private float mLabelTextSize;
private float mLabelSeparation = 0;
private int mLabelTextColor = Color.GREEN;
private Paint mLabelTextPaint;
private int mLabelWidth = 100;
private int mLabelHeight = 0;
private int mAxisColor = Color.GRAY;
private float mAxisThickness = 2;
private List<String> mLabels;
private AxisAutoValues axisAutoValues = new AxisAutoValues();
private ValueFormatter mLabelValueFormatter;
private ValueIndexFormatter mValueIndexFormatter;
public float[] mLabelEntries = new float[]{};
private boolean gridLineEnable = true;
private boolean labelEnable = true;
private float mDashedGridIntervals[] = null;
private float mDashedGridPhase = -1;
private LabelColorSetter mLabelColorSetter;
private int isGridSlidIndex = -1;
Axis(int axisPosition) {
this.mAxisPosition = axisPosition;
}
public void setLabelTextSize(float mLabelTextSize) {
this.mLabelTextSize = mLabelTextSize;
}
public void setLabelSeparation(float mLabelSeparation) {
this.mLabelSeparation = mLabelSeparation;
}
public void setLabelTextColor(int mLabelTextColor) {
this.mLabelTextColor = mLabelTextColor;
}
public void setLabelTextPaint(Paint mLabelTextPaint) {
this.mLabelTextPaint = mLabelTextPaint;
}
public void setLabelWidth(int mLabelWidth) {
this.mLabelWidth = mLabelWidth;
}
public void setLabelHeight(int mLabelHeight) {
this.mLabelHeight = mLabelHeight;
}
public float getLabelTextSize() {
return mLabelTextSize;
}
public int getLabelSeparation() {
return Math.round(mLabelSeparation);
}
public int getLabelTextColor() {
return mLabelTextColor;
}
public Paint getLabelTextPaint() {
return mLabelTextPaint;
}
public int getLabelWidth() {
if (isInside()) {
return 0;
}
return mLabelWidth;
}
public boolean isInside() {
switch (getAxisPosition()) {
case AxisY.LEFT_INSIDE:
case AxisY.RIGHT_INSIDE:
case AxisX.BOTTOM_INSIDE:
case AxisX.TOP_INSIDE:
return true;
default:
return false;
}
}
public int getLabelHeight() {
return mLabelHeight;
}
public AxisAutoValues getAxisAutoValues() {
return axisAutoValues;
}
public int getAxisPosition() {
return mAxisPosition;
}
public void setAxisPosition(int mAxisPosition) {
this.mAxisPosition = mAxisPosition;
}
public int getGridColor() {
return mGridColor;
}
public void setGridColor(int mGridColor) {
this.mGridColor = mGridColor;
}
public float getGridThickness() {
return mGridThickness;
}
public void setGridThickness(float mGridThickness) {
this.mGridThickness = mGridThickness;
}
public int getAxisColor() {
return mAxisColor;
}
public void setAxisColor(int mAxisColor) {
this.mAxisColor = mAxisColor;
}
public float getAxisThickness() {
return mAxisThickness;
}
public void setAxisThickness(float mAxisThickness) {
this.mAxisThickness = mAxisThickness;
}
public int getGridCount() {
return mGridCount;
}
public void setGridCount(int mGridCount) {
this.mGridCount = mGridCount;
}
public void setGridLineEnable(boolean gridLineEnable) {
this.gridLineEnable = gridLineEnable;
}
public boolean isGridLineEnable() {
return gridLineEnable;
}
public boolean isLabelEnable() {
return labelEnable;
}
public void setLabelEnable(boolean labelEnable) {
this.labelEnable = labelEnable;
}
public ValueFormatter getLabelValueFormatter() {
return mLabelValueFormatter;
}
public void setLabelValueFormatter(ValueFormatter mValueFormatter) {
this.mLabelValueFormatter = mValueFormatter;
}
public ValueIndexFormatter getValueIndexFormatter() {
return mValueIndexFormatter;
}
public void setValueIndexFormatter(ValueIndexFormatter mValueIndexFormatter) {
this.mValueIndexFormatter = mValueIndexFormatter;
}
public float getDashedGridPhase() {
return mDashedGridPhase;
}
public float[] getDashedGridIntervals() {
return mDashedGridIntervals;
}
public void enableGridDashPathEffect(float intervals[], float phase) {
this.mDashedGridIntervals = intervals;
this.mDashedGridPhase = phase;
}
public void setGirdLineColorSetter(GirdLineColorSetter mGirdLineColorSetter) {
this.mGirdLineColorSetter = mGirdLineColorSetter;
}
public GirdLineColorSetter getGirdLineColorSetter() {
return mGirdLineColorSetter;
}
public List<String> getLabels() {
return mLabels;
}
public void setLabels(List<String> mLabels) {
this.mLabels = mLabels;
}
public LabelColorSetter getLabelColorSetter() {
return mLabelColorSetter;
}
public void setLabelColorSetter(LabelColorSetter mLabelColorSetter) {
this.mLabelColorSetter = mLabelColorSetter;
}
public int getIsGridSlidIndex() {
return isGridSlidIndex;
}
public void setIsGridSlidIndex(int isGridSlidIndex) {
this.isGridSlidIndex = isGridSlidIndex;
}
}
@@ -0,0 +1,30 @@
package cn.jingzhuan.lib.chart.component;
import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Created by Donglua on 17/7/17.
*/
public class AxisX extends Axis {
public final static int TOP = 101;
public final static int BOTTOM = 102;
public final static int TOP_INSIDE = 103;
public final static int BOTTOM_INSIDE = 104;
@Retention(RetentionPolicy.SOURCE)
@IntDef({TOP, BOTTOM, TOP_INSIDE, BOTTOM_INSIDE})
@interface AxisXPosition {}
public AxisX(@AxisXPosition int axisPosition) {
super(axisPosition);
}
}
@@ -0,0 +1,52 @@
package cn.jingzhuan.lib.chart.component;
import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Created by Donglua on 17/7/17.
*/
public class AxisY extends Axis {
public final static int LEFT_OUTSIDE = 111;
public final static int LEFT_INSIDE = 112;
public final static int RIGHT_OUTSIDE = 113;
public final static int RIGHT_INSIDE = 114;
public final static int DEPENDENCY_LEFT = 23;
public final static int DEPENDENCY_RIGHT = 24;
public final static int DEPENDENCY_BOTH = 25;
private float mYMin = Float.MAX_VALUE;
private float mYMax = -Float.MAX_VALUE;
@Retention(RetentionPolicy.SOURCE)
@IntDef({DEPENDENCY_LEFT, DEPENDENCY_RIGHT, DEPENDENCY_BOTH})
public @interface AxisDependency{}
@Retention(RetentionPolicy.SOURCE)
@IntDef({LEFT_OUTSIDE, LEFT_INSIDE, RIGHT_OUTSIDE, RIGHT_INSIDE})
public @interface AxisYPosition {}
public AxisY(int axisPosition) {
super(axisPosition);
}
public void setYMin(float value) {
this.mYMin = value;
}
public void setYMax(float value) {
this.mYMax = value;
}
public float getYMin() {
return mYMin;
}
public float getYMax() {
return mYMax;
}
}
@@ -0,0 +1,12 @@
package cn.jingzhuan.lib.chart.component;
/**
* Created by Donglua on 17/8/2.
*/
public interface Component {
void setEnable(boolean enable);
boolean isEnable();
}
@@ -0,0 +1,15 @@
package cn.jingzhuan.lib.chart.component;
/**
* Created by donglua on 11/22/17.
*/
public interface HasValueXOffset {
float getStartXOffset();
float getEndXOffset();
void setStartXOffset(float startXOffset);
void setEndXOffset(float endXOffset);
}
@@ -0,0 +1,21 @@
package cn.jingzhuan.lib.chart.component;
/**
* Created by donglua on 11/22/17.
*/
public interface HasValueYOffset {
float getMaxValueOffsetPercent();
float getMinValueOffsetPercent();
float getOffsetPercent();
void setMinValueOffsetPercent(float minValueOffsetPercent);
void setMaxValueOffsetPercent(float maxValueOffsetPercent);
void setOffsetPercent(float offsetPercent);
}
@@ -0,0 +1,65 @@
package cn.jingzhuan.lib.chart.component;
/**
* Created by donglua on 7/31/17.
*/
public class Highlight {
private float x = Float.NaN;
private float y = Float.NaN;
private int dataIndex = 0;
private float touchX = Float.NaN;
private float touchY = Float.NaN;
public Highlight() {
}
public Highlight(float x, float y, int dataIndex) {
this.x = x;
this.y = y;
this.dataIndex = dataIndex;
}
public float getX() {
return x;
}
public void setX(float x) {
this.x = x;
}
public float getY() {
return y;
}
public void setY(float y) {
this.y = y;
}
public void setDataIndex(int dataIndex) {
this.dataIndex = dataIndex;
}
public int getDataIndex() {
return dataIndex;
}
public float getTouchX() {
return touchX;
}
public void setTouchX(float touchX) {
this.touchX = touchX;
}
public float getTouchY() {
return touchY;
}
public void setTouchY(float touchY) {
this.touchY = touchY;
}
}
@@ -0,0 +1,260 @@
package cn.jingzhuan.lib.chart.data;
import android.graphics.Color;
import android.graphics.Rect;
import cn.jingzhuan.lib.chart.Viewport;
import cn.jingzhuan.lib.chart.component.AxisY;
import cn.jingzhuan.lib.chart.component.AxisY.AxisDependency;
import cn.jingzhuan.lib.chart.component.HasValueYOffset;
import java.util.ArrayList;
import java.util.List;
/**
* Abstract DataSet
* <p>
* Created by Donglua on 17/7/20.
*/
public abstract class AbstractDataSet<T extends Value> extends AbstractVisible implements IDataSet, HasValueYOffset {
protected float mViewportYMin = Float.MAX_VALUE;
protected float mViewportYMax = -Float.MAX_VALUE;
protected float minValueOffsetPercent = 0F;
protected float maxValueOffsetPercent = 0F;
protected float offsetPercent = 0F;
protected float startXOffset = 0f;
protected float endXOffset = 0f;
private int mAxisDependency = AxisY.DEPENDENCY_LEFT;
private int mColor = Color.GRAY;
private boolean mColorDynamic = false;
private int maxVisibleEntryCount = 500;
private int minVisibleEntryCount = 15;
private int defaultVisibleEntryCount = -1;
private boolean isHighlightedVerticalEnable = false;
private boolean isHighlightedHorizontalEnable = false;
private boolean enable = true;
private int minValueCount = -1;
private int drawIndex = -1;
private String tag;
private DataFormatter formatter; // 数据格式化器
public AbstractDataSet() {
}
public AbstractDataSet(String tag) {
this.tag = tag;
}
@Override
public void calcMinMax(Viewport viewport) {}
@Override
public void calcMinMax(Viewport viewport, Rect content, float max, float mix) {
calcMinMax(viewport);
}
public abstract List<T> getValues();
public abstract void setValues(List<T> values);
public abstract boolean addEntry(T e);
public abstract boolean removeEntry(T e);
public abstract int getEntryIndex(T e);
public abstract T getEntryForIndex(int index);
@AxisDependency
public int getAxisDependency() {
return mAxisDependency;
}
public void setAxisDependency(@AxisDependency int mAxisDependency) {
this.mAxisDependency = mAxisDependency;
}
public float getViewportYMin() {
return mViewportYMin;
}
public float getViewportYMax() {
return mViewportYMax;
}
public int getColor() {
return mColor;
}
public void setColor(int barColor) {
this.mColor = barColor;
}
public boolean getColorDynamic() {
return mColorDynamic;
}
public void setColorDynamic(boolean colorDynamic) {
this.mColorDynamic = colorDynamic;
}
public boolean isHighlightedVerticalEnable() {
return isHighlightedVerticalEnable;
}
public void setHighlightedVerticalEnable(boolean highlightedVerticalEnable) {
isHighlightedVerticalEnable = highlightedVerticalEnable;
}
public boolean isHighlightedHorizontalEnable() {
return isHighlightedHorizontalEnable;
}
public void setHighlightedHorizontalEnable(boolean highlightedHorizontalEnable) {
isHighlightedHorizontalEnable = highlightedHorizontalEnable;
}
public List<T> getVisiblePoints(Viewport viewport) {
ArrayList<T> allValue = new ArrayList<>(getValues());
int listSize = allValue.size();
int from = Math.round(viewport.left * listSize);
int to = Math.round(viewport.right * listSize);
if (listSize <= minVisibleEntryCount) {
return allValue;
}
return safeSubList(allValue, from, to);
}
private List<T> safeSubList(List<T> list, int from, int to) {
if (list == null || list.isEmpty()) return list;
int size = list.size();
int safeFrom = (from >= size) ? 0 : from;
int safeTo = Math.min(to, size);
return list.subList(safeFrom, safeTo);
}
public float getVisibleRange(Viewport viewport) {
return (viewport.right - viewport.left) * getEntryCount();
}
@Override
public void setMaxVisibleEntryCount(int maxVisibleEntryCount) {
this.maxVisibleEntryCount = maxVisibleEntryCount;
}
public int getMaxVisibleEntryCount() {
return maxVisibleEntryCount;
}
@Override
public void setMinVisibleEntryCount(int minVisibleEntryCount) {
this.minVisibleEntryCount = minVisibleEntryCount;
}
public int getMinVisibleEntryCount() {
return minVisibleEntryCount;
}
@Override
public void setDefaultVisibleEntryCount(int defaultVisibleEntryCount) {
this.defaultVisibleEntryCount = defaultVisibleEntryCount;
}
public int getDefaultVisibleEntryCount() {
return defaultVisibleEntryCount;
}
@Override
public boolean isEnable() {
return enable;
}
public void setEnable(boolean enable) {
this.enable = enable;
this.setVisible(enable);
}
@Override
public int getMinValueCount() {
return minValueCount;
}
@Override
public void setMinValueCount(int minValueCount) {
this.minValueCount = minValueCount;
}
public int getStartIndexOffset() {
int startIndex = 0;
// if (minValueCount > 0 && getValues() != null && getValues().size() > 0) {
// startIndex = getEntryCount() - getValues().size();
// }
return startIndex;
}
public int getDrawIndex() {
return drawIndex;
}
public void setDrawIndex(int drawIndex) {
this.drawIndex = drawIndex;
}
public String getTag() {
return tag;
}
public void setTag(String tag) {
this.tag = tag;
}
public DataFormatter getFormatter() {
return formatter;
}
public void setFormatter(DataFormatter formatter) {
this.formatter = formatter;
}
@Override
public float getMaxValueOffsetPercent() {
return maxValueOffsetPercent;
}
@Override
public float getMinValueOffsetPercent() {
return minValueOffsetPercent;
}
@Override
public void setMinValueOffsetPercent(float minValueOffsetPercent) {
this.minValueOffsetPercent = minValueOffsetPercent;
}
@Override
public void setMaxValueOffsetPercent(float maxValueOffsetPercent) {
this.maxValueOffsetPercent = maxValueOffsetPercent;
}
@Override
public float getOffsetPercent() {
return this.offsetPercent;
}
@Override
public void setOffsetPercent(float offsetPercent) {
this.offsetPercent = offsetPercent;
}
}
@@ -0,0 +1,19 @@
package cn.jingzhuan.lib.chart.data;
/**
* Created by Donglua on 17/8/2.
*/
abstract class AbstractVisible {
private boolean isVisible = true;
public boolean isVisible() {
return isVisible;
}
public void setVisible(boolean visible) {
this.isVisible = visible;
}
}
@@ -0,0 +1,9 @@
package cn.jingzhuan.lib.chart.data;
/**
* Created by Donglua on 17/8/2.
*/
public class BarData extends ChartData<BarDataSet> {
}
@@ -0,0 +1,208 @@
package cn.jingzhuan.lib.chart.data;
import android.graphics.Color;
import cn.jingzhuan.lib.chart.Viewport;
import java.util.ArrayList;
import java.util.List;
import cn.jingzhuan.lib.chart.component.AxisY;
import cn.jingzhuan.lib.chart.component.AxisY.AxisDependency;
/**
* Bar Data Set
* Created by Donglua on 17/8/1.
*/
public class BarDataSet extends AbstractDataSet<BarValue> {
private List<BarValue> mBarValues;
private float mBarWidth = 20;
private float mMinBarWidth = 0.0f;
private boolean mAutoBarWidth = false;
private int mForceValueCount = -1;
private float strokeThickness = 2;
private float mBarWidthPercent = 0.8f;
private boolean drawValueEnable = false;
private int valueColor = Color.BLACK;
private float valueTextSize = 24F;
private ValueFormatter valueFormatter;
public BarDataSet(List<BarValue> barValues) {
this(barValues, AxisY.DEPENDENCY_BOTH);
}
public BarDataSet(List<BarValue> mBarValues, @AxisDependency int axisDependency) {
this.mBarValues = mBarValues;
setAxisDependency(axisDependency);
}
@Override
public int getEntryCount() {
if (mForceValueCount > 0) return mForceValueCount;
if (mBarValues != null) {
return Math.max(getMinValueCount(), mBarValues.size());
}
return 0;
}
@Override
public void calcMinMax(Viewport viewport) {
if (mBarValues == null || mBarValues.isEmpty())
return;
mViewportYMax = -Float.MAX_VALUE;
mViewportYMin = Float.MAX_VALUE;
for (BarValue e : getVisiblePoints(viewport)) {
calcMinMaxY(e);
}
float range = mViewportYMax - mViewportYMin;
if (Float.compare(getMinValueOffsetPercent(), 0f) > 0f) {
mViewportYMin = mViewportYMin - range * getMinValueOffsetPercent();
}
if (Float.compare(getMaxValueOffsetPercent(), 0f) > 0f) {
mViewportYMax = mViewportYMax + range * getMaxValueOffsetPercent();
}
}
protected void calcMinMaxY(BarValue e) {
if (e == null || !e.isEnable()) return;
for (float v : e.getValues()) {
if (!Float.isNaN(v) && !Float.isInfinite(v)) {
mViewportYMin = Math.min(mViewportYMin, v);
mViewportYMax = Math.max(mViewportYMax, v);
}
}
}
@Override
public void setValues(List<BarValue> values) {
this.mBarValues = values;
}
@Override
public List<BarValue> getValues() {
return mBarValues;
}
@Override
public boolean addEntry(BarValue e) {
if (e == null)
return false;
if (mBarValues == null) {
mBarValues = new ArrayList<>();
}
calcMinMaxY(e);
return mBarValues.add(e);
}
@Override
public boolean removeEntry(BarValue e) {
if (e == null) return false;
calcMinMaxY(e);
return mBarValues.remove(e);
}
@Override
public int getEntryIndex(BarValue e) {
return mBarValues.indexOf(e);
}
@Override
public BarValue getEntryForIndex(int index) {
return mBarValues.get(index);
}
public float getBarWidth() {
return mBarWidth;
}
public void setBarWidth(float mBarWidth) {
this.mBarWidth = mBarWidth;
}
public float getMinBarWidth() {
return mMinBarWidth;
}
public void setMinBarWidth(float minBarWidth) {
this.mMinBarWidth = minBarWidth;
}
public void setAutoBarWidth(boolean mAutoBarWidth) {
this.mAutoBarWidth = mAutoBarWidth;
}
public boolean isAutoBarWidth() {
return mAutoBarWidth;
}
public void setForceValueCount(int forceValueCount) {
this.mForceValueCount = forceValueCount;
}
public int getForceValueCount() {
return mForceValueCount;
}
public float getStrokeThickness() {
return strokeThickness;
}
public void setStrokeThickness(float strokeThickness) {
this.strokeThickness = strokeThickness;
}
public void setDrawValueEnable(boolean drawValueEnable) {
this.drawValueEnable = drawValueEnable;
}
public boolean isDrawValueEnable() {
return drawValueEnable;
}
public int getValueColor() {
return valueColor;
}
public void setValueColor(int valueColor) {
this.valueColor = valueColor;
}
public float getValueTextSize() {
return valueTextSize;
}
public void setValueTextSize(float valueTextSize) {
this.valueTextSize = valueTextSize;
}
public ValueFormatter getValueFormatter() {
return valueFormatter;
}
public void setValueFormatter(ValueFormatter valueFormatter) {
this.valueFormatter = valueFormatter;
}
public void setBarWidthPercent(float mBarWidthPercent) {
this.mBarWidthPercent = mBarWidthPercent;
}
public float getBarWidthPercent() {
return mBarWidthPercent;
}
}
@@ -0,0 +1,123 @@
package cn.jingzhuan.lib.chart.data;
import android.graphics.Paint;
/**
* Created by Donglua on 17/8/1.
*/
public class BarValue extends Value {
private float[] yValues;
private int mColor = -2;
private Paint.Style mPaintStyle = Paint.Style.FILL;
private boolean isEnable = true;
private int[] gradientColors = null;
/**
* 这里tag比dataSet的优先级高
*/
private String tag = null;
public BarValue(float[] yValues) {
this.yValues = yValues;
}
public BarValue(float[] yValues, int color) {
this.yValues = yValues;
this.mColor = color;
}
public BarValue(float value1, float value2, int color) {
this.yValues = new float[] {value1, value2};
this.mColor = color;
}
public BarValue(float value1, float value2, int color, Paint.Style paintStyle) {
this.yValues = new float[] {value1, value2};
this.mColor = color;
this.mPaintStyle = paintStyle;
}
public BarValue(float value, int color, Paint.Style paintStyle) {
this.yValues = new float[] {value, 0f};
this.mColor = color;
this.mPaintStyle = paintStyle;
}
public BarValue(float[] values, int color, Paint.Style paintStyle) {
this.yValues = values;
this.mColor = color;
this.mPaintStyle = paintStyle;
}
public BarValue(float yValue) {
this.yValues = new float[] { yValue, 0f };
}
public BarValue(float yValue, int color) {
this.yValues = new float[] { yValue, 0f };
this.mColor = color;
}
public float[] getValues() {
return yValues;
}
public void setValues(float[] yValues) {
this.yValues = yValues;
}
public void setValues(float yValue) {
this.yValues = new float[] { yValue, 0f };
}
public int getValueCount() {
if (yValues == null) return 0;
return yValues.length;
}
public int getColor() {
return mColor;
}
public void setColor(int color) {
mColor = color;
}
public void setTag(String tag) {
this.tag = tag;
}
public String getTag() {
return this.tag;
}
public void setPaintStyle(Paint.Style mPaintStyle) {
this.mPaintStyle = mPaintStyle;
}
public Paint.Style getPaintStyle() {
return mPaintStyle;
}
public void setEnable(boolean enable) {
isEnable = enable;
}
public boolean isEnable() {
return isEnable;
}
public void setGradientColors(int colorTop, int colorBottom) {
this.gradientColors = new int[] {colorTop, colorBottom};
}
public int[] getGradientColors() {
return gradientColors;
}
}
@@ -0,0 +1,9 @@
package cn.jingzhuan.lib.chart.data;
/**
* Created by donglua on 8/29/17.
*/
public class CandlestickData extends ChartData<CandlestickDataSet> {
}
@@ -0,0 +1,326 @@
package cn.jingzhuan.lib.chart.data;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.Pair;
import android.util.SparseArray;
import cn.jingzhuan.lib.chart.Viewport;
import cn.jingzhuan.lib.chart.component.AxisY;
import java.util.ArrayList;
import java.util.List;
/**
* Created by donglua on 8/29/17.
*/
public class CandlestickDataSet extends AbstractDataSet<CandlestickValue> {
private List<CandlestickValue> candlestickValues;
private boolean mAutoWidth = true;
private float mCandleWidth = -1;
/**
* 阳线 颜色
*/
private int mIncreasingColor = 0xFFf84b4b;
/**
* 阴线 颜色
*/
private int mDecreasingColor = 0xFF1deb5b;
/**
* 十字光标线 颜色
*/
private int mNeutralColor = Color.WHITE;
private int mLimitUpColor = Color.TRANSPARENT;
private int mLimitUpColor20 = Color.TRANSPARENT;
/**
* 缺口 颜色
*/
private int mGapColor = Color.LTGRAY;
private Paint.Style mLimitUpPaintStyle = null;
private float strokeThickness = 4;
/**
* 阳线阴线样式-> 实心或空心
*/
private Paint.Style mIncreasingPaintStyle = Paint.Style.FILL;
private Paint.Style mDecreasingPaintStyle = Paint.Style.FILL;
/**
* 是否显示缺口
*/
private boolean mEnableGap = false;
private SparseArray<Pair<Float, Float>> mLowGaps;
private SparseArray<Pair<Float, Float>> mHighGaps;
private float candleWidthPercent = 0.8f;
public CandlestickDataSet(List<CandlestickValue> candlestickValues) {
this(candlestickValues, AxisY.DEPENDENCY_BOTH);
}
public CandlestickDataSet(List<CandlestickValue> candlestickValues, @AxisY.AxisDependency int axisDependency) {
this.candlestickValues = candlestickValues;
setAxisDependency(axisDependency);
}
@Override
public void calcMinMax(Viewport viewport) {
if (candlestickValues == null || candlestickValues.isEmpty()) return;
mViewportYMax = -Float.MAX_VALUE;
mViewportYMin = Float.MAX_VALUE;
List<CandlestickValue> visiblePoints = getVisiblePoints(viewport);
for (int i = 0; i < visiblePoints.size(); i++) {
CandlestickValue e = visiblePoints.get(i);
calcViewportMinMax(e);
}
if (mEnableGap) {
float max = -Float.MAX_VALUE;
float min = Float.MAX_VALUE;
for (int i = candlestickValues.size() - 1; i >= 0; i--) {
CandlestickValue e = candlestickValues.get(i);
if (Float.isNaN(e.getLow())) continue;
if (Float.isNaN(e.getHigh())) continue;
if (Float.isInfinite(e.getLow())) continue;
if (Float.isInfinite(e.getHigh())) continue;
if (min != Float.MAX_VALUE && max != -Float.MAX_VALUE) {
if (min - e.getHigh() > 0.01f) { // 上涨缺口
mHighGaps.put(i, new Pair<>(e.getHigh(), min));
}
if (e.getLow() - max > 0.01f) { // 下跌缺口
mLowGaps.put(i, new Pair<>(e.getLow(), max));
}
}
if (e.getLow() < min) min = e.getLow();
if (e.getHigh() > max) max = e.getHigh();
}
}
float range = mViewportYMax - mViewportYMin;
if (Float.compare(getOffsetPercent(), 0f) > 0f) {
mViewportYMin = mViewportYMin - range * getOffsetPercent();
}
if (Float.compare(getOffsetPercent(), 0f) > 0f) {
mViewportYMax = mViewportYMax + range * getOffsetPercent();
}
}
public void setEnableGap(boolean enableGap) {
this.mEnableGap = enableGap;
if (mLowGaps == null) {
mLowGaps = new SparseArray<>();
}
if (mHighGaps == null) {
mHighGaps = new SparseArray<>();
}
}
private void calcViewportMinMax(CandlestickValue e) {
if (e == null || !e.isVisible()) return;
if (Float.isNaN(e.getLow())) return;
if (Float.isNaN(e.getHigh())) return;
if (Float.isInfinite(e.getLow())) return;
if (Float.isInfinite(e.getHigh())) return;
if (e.getLow() < mViewportYMin) {
mViewportYMin = e.getLow();
}
if (e.getHigh() > mViewportYMax) {
mViewportYMax = e.getHigh();
}
}
@Override
public int getEntryCount() {
if (candlestickValues == null) return 0;
return Math.max(getMinValueCount(), candlestickValues.size());
}
@Override
public void setValues(List<CandlestickValue> values) {
this.candlestickValues = values;
}
@Override
public List<CandlestickValue> getValues() {
return candlestickValues;
}
@Override
public boolean addEntry(CandlestickValue value) {
if (value == null)
return false;
if (candlestickValues == null) {
candlestickValues = new ArrayList<>();
}
calcViewportMinMax(value);
// add the entry
return candlestickValues.add(value);
}
@Override
public boolean removeEntry(CandlestickValue value) {
if (value == null) return false;
calcViewportMinMax(value);
return candlestickValues.remove(value);
}
@Override
public int getEntryIndex(CandlestickValue e) {
return candlestickValues.indexOf(e);
}
@Override
public CandlestickValue getEntryForIndex(int index) {
return candlestickValues.get(index);
}
public void setAutoWidth(boolean mAutoWidth) {
this.mAutoWidth = mAutoWidth;
}
public boolean isAutoWidth() {
return mAutoWidth;
}
public int getDecreasingColor() {
return mDecreasingColor;
}
public void setDecreasingColor(int decreasingColor) {
this.mDecreasingColor = decreasingColor;
}
public int getIncreasingColor() {
return mIncreasingColor;
}
public void setIncreasingColor(int increasingColor) {
this.mIncreasingColor = increasingColor;
}
public float getCandleWidth() {
return mCandleWidth;
}
public void setCandleWidth(float mCandleWidth) {
this.mCandleWidth = mCandleWidth;
}
public int getNeutralColor() {
return mNeutralColor;
}
public void setNeutralColor(int mNeutralColor) {
this.mNeutralColor = mNeutralColor;
}
public void setLimitUpColor(int mLimitUpColor) {
this.mLimitUpColor = mLimitUpColor;
}
public int getLimitUpColor() {
return mLimitUpColor;
}
public void setLimitUpColor20(int mLimitUpColor) {
this.mLimitUpColor20 = mLimitUpColor;
}
public int getLimitUpColor20() {
return mLimitUpColor20;
}
public float getStrokeThickness() {
return strokeThickness;
}
public void setStrokeThickness(float strokeThickness) {
this.strokeThickness = strokeThickness;
}
public Paint.Style getIncreasingPaintStyle() {
return mIncreasingPaintStyle;
}
public void setIncreasingPaintStyle(Paint.Style increasingPaintStyle) {
this.mIncreasingPaintStyle = increasingPaintStyle;
}
public Paint.Style getDecreasingPaintStyle() {
return mDecreasingPaintStyle;
}
public void setDecreasingPaintStyle(Paint.Style decreasingPaintStyle) {
this.mDecreasingPaintStyle = decreasingPaintStyle;
}
public Paint.Style getLimitUpPaintStyle() {
return mLimitUpPaintStyle;
}
public void setLimitUpPaintStyle(Paint.Style limitUpPaintStyle) {
this.mLimitUpPaintStyle = limitUpPaintStyle;
}
public SparseArray<Pair<Float, Float>> getLowGaps() {
return mLowGaps;
}
public SparseArray<Pair<Float, Float>> getHighGaps() {
return mHighGaps;
}
public boolean isEnableGap() {
return mEnableGap;
}
public int getGapColor() {
return mGapColor;
}
public void setGapColor(int mGapColor) {
this.mGapColor = mGapColor;
}
public float getCandleWidthPercent() {
return candleWidthPercent;
}
public void setCandleWidthPercent(float candleWidthPercent) {
this.candleWidthPercent = candleWidthPercent;
}
}
@@ -0,0 +1,169 @@
package cn.jingzhuan.lib.chart.data;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Build;
import java.util.Arrays;
import java.util.Objects;
/**
* Created by donglua on 8/29/17.
*/
public class CandlestickValue extends Value {
public final static int COLOR_NONE = Color.TRANSPARENT;
private float high = 0f;
private float low = 0f;
private float open = 0f;
private float close = 0f;
private long time = -1;
private boolean visible = true;
private Paint.Style mPaintStyle = null;
private int color = COLOR_NONE;
private int fillBackgroundColor = COLOR_NONE;
public CandlestickValue(float high, float low, float open, float close) {
this.high = high;
this.low = low;
this.open = open;
this.close = close;
}
public CandlestickValue(float high, float low, float open, float close, long time) {
this.high = high;
this.low = low;
this.open = open;
this.close = close;
this.time = time;
}
public CandlestickValue(float high, float low, float open, float close, int color) {
this.high = high;
this.low = low;
this.open = open;
this.close = close;
this.color = color;
}
public CandlestickValue(float high, float low, float open, float close, long time, int color) {
this.high = high;
this.low = low;
this.open = open;
this.close = close;
this.color = color;
this.time = time;
}
public CandlestickValue(float high, float low, float open, float close, Paint.Style mPaintStyle,
int color) {
this.high = high;
this.low = low;
this.open = open;
this.close = close;
this.mPaintStyle = mPaintStyle;
this.color = color;
}
public CandlestickValue(float high, float low, float open, float close, Paint.Style mPaintStyle) {
this.high = high;
this.low = low;
this.open = open;
this.close = close;
this.mPaintStyle = mPaintStyle;
}
public float getHigh() {
return high;
}
public void setHigh(float high) {
this.high = high;
}
public float getLow() {
return low;
}
public void setLow(float low) {
this.low = low;
}
public float getOpen() {
return open;
}
public void setOpen(float open) {
this.open = open;
}
public float getClose() {
return close;
}
public void setClose(float close) {
this.close = close;
}
public void setPaintStyle(Paint.Style mPaintStyle) {
this.mPaintStyle = mPaintStyle;
}
public Paint.Style getPaintStyle() {
return mPaintStyle;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
public void setTime(long time) {
this.time = time;
}
public long getTime() {
return time;
}
public boolean isVisible() {
return visible;
}
public void setVisible(boolean visible) {
this.visible = visible;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CandlestickValue that = (CandlestickValue) o;
return Float.compare(that.high, high) == 0 &&
Float.compare(that.low, low) == 0 &&
Float.compare(that.open, open) == 0 &&
Float.compare(that.close, close) == 0 &&
time == that.time &&
color == that.color &&
mPaintStyle == that.mPaintStyle;
}
@Override
public int hashCode() {
return Objects.hash(high, low, open, close, time, mPaintStyle, color, getX(), getY());
}
public void setFillBackgroundColor(int fillBackgroundColor) {
this.fillBackgroundColor = fillBackgroundColor;
}
public int getFillBackgroundColor() {
return fillBackgroundColor;
}
}
@@ -0,0 +1,235 @@
package cn.jingzhuan.lib.chart.data;
import android.graphics.Rect;
import android.util.Log;
import cn.jingzhuan.lib.chart.base.Chart;
import cn.jingzhuan.lib.chart.Viewport;
import cn.jingzhuan.lib.chart.component.AxisY;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Created by Donglua on 17/8/2.
*/
public class ChartData<T extends IDataSet> {
private List<T> chartData;
protected float leftMin = Float.MAX_VALUE;
protected float leftMax = -Float.MAX_VALUE;
protected float rightMin = Float.MAX_VALUE;
protected float rightMax = -Float.MAX_VALUE;
private int entryCount = 0;
protected AxisY leftAxis;
protected AxisY rightAxis;
protected int maxVisibleEntryCount = 500;
protected int minVisibleEntryCount = 15;
protected int defaultVisibleEntryCount = -1;
private int minValueCount = -1;
public ChartData() {
this.chartData = Collections.synchronizedList(new ArrayList<T>());
}
public List<T> getDataSets() {
if (chartData == null) {
chartData = Collections.synchronizedList(new ArrayList<T>());
}
return chartData;
}
public boolean add(T e) {
if (e == null) return false;
e.setDefaultVisibleEntryCount(defaultVisibleEntryCount);
e.setMinVisibleEntryCount(minVisibleEntryCount);
e.setMaxVisibleEntryCount(maxVisibleEntryCount);
synchronized (this) {
return getDataSets().add(e);
}
}
public boolean remove(T e) {
synchronized (this) {
return e != null && getDataSets().remove(e);
}
}
public void clear() {
synchronized (this) {
getDataSets().clear();
}
leftMin = Float.MAX_VALUE;
leftMax = -Float.MAX_VALUE;
rightMin = Float.MAX_VALUE;
rightMax = -Float.MAX_VALUE;
}
public void setMinMax() {
if (leftAxis != null && leftMin != Float.MAX_VALUE) {
leftAxis.setYMin(leftMin);
leftAxis.setYMax(leftMax);
}
if (rightAxis != null && rightMin != Float.MAX_VALUE) {
rightAxis.setYMin(rightMin);
rightAxis.setYMax(rightMax);
}
}
public void calcMaxMin(Viewport viewport, Rect content) {
calcMaxMin(viewport, content, -Float.MAX_VALUE, Float.MAX_VALUE, -Float.MAX_VALUE, Float.MAX_VALUE);
}
public void calcMaxMin(Viewport viewport, Rect content, float lMax, float lMin, float rMax, float rMin) {
leftMin = lMin;
leftMax = lMax;
rightMin = rMin;
rightMax = rMax;
entryCount = 0;
if (!getDataSets().isEmpty()) {
synchronized (this) {
for (T t : getDataSets()) {
if (!t.isEnable() || t instanceof ScatterDataSet) continue;
t.calcMinMax(viewport, content, -Float.MAX_VALUE, Float.MAX_VALUE);
if (t.getAxisDependency() == AxisY.DEPENDENCY_BOTH || t.getAxisDependency() == AxisY.DEPENDENCY_LEFT) {
leftMax = Math.max(leftMax, t.getViewportYMax());
leftMin = Math.min(leftMin, t.getViewportYMin());
}
if (t.getAxisDependency() == AxisY.DEPENDENCY_BOTH || t.getAxisDependency() == AxisY.DEPENDENCY_RIGHT) {
rightMax = Math.max(rightMax, t.getViewportYMax());
rightMin = Math.min(rightMin, t.getViewportYMin());
}
if (t.getEntryCount() > entryCount) {
entryCount = t.getEntryCount();
}
}
// Log.d("ChartData", "calcMaxMin_0 leftMax:" + leftMax
// + ", leftMin:" + leftMin + ", rightMax:" + rightMax + ", rightMin:" + rightMin);
for (T t : getDataSets()) {
if (!t.isEnable() || !(t instanceof ScatterDataSet)) continue;
if (t.getAxisDependency() == AxisY.DEPENDENCY_BOTH || t.getAxisDependency() == AxisY.DEPENDENCY_LEFT) {
t.calcMinMax(viewport, content, leftMax, leftMin);
}
if (t.getAxisDependency() == AxisY.DEPENDENCY_BOTH || t.getAxisDependency() == AxisY.DEPENDENCY_RIGHT) {
t.calcMinMax(viewport, content, rightMax, rightMin);
}
if (t.getAxisDependency() == AxisY.DEPENDENCY_BOTH || t.getAxisDependency() == AxisY.DEPENDENCY_LEFT) {
leftMax = Math.max(leftMax, t.getViewportYMax());
leftMin = Math.min(leftMin, t.getViewportYMin());
}
if (t.getAxisDependency() == AxisY.DEPENDENCY_BOTH || t.getAxisDependency() == AxisY.DEPENDENCY_RIGHT) {
rightMax = Math.max(rightMax, t.getViewportYMax());
rightMin = Math.min(rightMin, t.getViewportYMin());
}
if (t.getEntryCount() > entryCount) {
entryCount = t.getEntryCount();
}
}
}
// Log.d("ChartData", "calcMaxMin_1 leftMax:" + leftMax
// + ", leftMin:" + leftMin + ", rightMax:" + rightMax + ", rightMin:" + rightMin);
setMinMax();
}
}
public float getLeftMin() {
return leftMin;
}
public float getRightMax() {
return rightMax;
}
public float getRightMin() {
return rightMin;
}
public float getLeftMax() {
return leftMax;
}
public void setLeftMin(float leftMin) {
this.leftMin = leftMin;
}
public void setLeftMax(float leftMax) {
this.leftMax = leftMax;
}
public void setRightMin(float rightMin) {
this.rightMin = rightMin;
}
public void setRightMax(float rightMax) {
this.rightMax = rightMax;
}
public int getEntryCount() {
return entryCount;
}
public void setChart(Chart chart) {
this.leftAxis = chart.getAxisLeft();
this.rightAxis = chart.getAxisRight();
}
public void setChart(cn.jingzhuan.lib.chart2.base.Chart chart) {
this.leftAxis = chart.getAxisLeft();
this.rightAxis = chart.getAxisRight();
}
public void setMaxVisibleEntryCount(int maxVisibleEntryCount) {
this.maxVisibleEntryCount = maxVisibleEntryCount;
synchronized (getDataSets()) {
for (T t : getDataSets()) {
t.setMaxVisibleEntryCount(maxVisibleEntryCount);
}
}
}
public void setMinVisibleEntryCount(int minVisibleEntryCount) {
this.minVisibleEntryCount = minVisibleEntryCount;
synchronized (getDataSets()) {
for (T t : getDataSets()) {
t.setMinVisibleEntryCount(minVisibleEntryCount);
}
}
}
public void setDefaultVisibleEntryCount(int defaultVisibleEntryCount) {
this.defaultVisibleEntryCount = defaultVisibleEntryCount;
synchronized (getDataSets()) {
for (T t : getDataSets()) {
t.setDefaultVisibleEntryCount(defaultVisibleEntryCount);
}
}
}
public int getMinValueCount() {
return minValueCount;
}
public void setMinValueCount(int minValueCount) {
this.minValueCount = minValueCount;
synchronized (getDataSets()) {
for (T t : getDataSets()) {
t.setMinValueCount(minValueCount);
}
}
}
}
@@ -0,0 +1,334 @@
package cn.jingzhuan.lib.chart.data;
import android.graphics.Rect;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import cn.jingzhuan.lib.chart.Viewport;
/**
* Created by Donglua on 17/8/2.
*/
public class CombineData extends ChartData<AbstractDataSet> {
private TreeData treeData;
private BarData barData;
private LineData lineData;
private CandlestickData candlestickData;
private ScatterData scatterData;
private PointLineData pointLineData;
private ScatterTextData scatterTextData;
public CombineData() {
treeData = new TreeData();
barData = new BarData();
lineData = new LineData();
candlestickData = new CandlestickData();
scatterData = new ScatterData();
pointLineData = new PointLineData();
scatterTextData = new ScatterTextData();
}
public List<BarDataSet> getBarData() {
return barData.getDataSets();
}
public List<LineDataSet> getLineData() {
return lineData.getDataSets();
}
public List<CandlestickDataSet> getCandlestickData() {
return candlestickData.getDataSets();
}
public List<ScatterDataSet> getScatterData() {
return scatterData.getDataSets();
}
public List<PointLineDataSet> getPointLineData() {
return pointLineData.getDataSets();
}
public List<ScatterTextDataSet> getScatterTextData() {
return scatterTextData.getDataSets();
}
public List<TreeDataSet> getTreeData() {
return treeData.getDataSets();
}
public BarData getBarChartData() {
return barData;
}
public LineData getLineChartData() {
return lineData;
}
public PointLineData getPointLineChartData() {
return pointLineData;
}
public CandlestickData getCandlestickChartData() {
return candlestickData;
}
public ScatterData getScatterChartData() {
return scatterData;
}
public ScatterTextData getScatterTextChartData() {
return scatterTextData;
}
public TreeData getTreeChartData() {
return treeData;
}
public boolean addDataSet(BarDataSet dataSet) {
return barData.add(dataSet);
}
public boolean addDataSet(LineDataSet dataSet) {
return lineData.add(dataSet);
}
public boolean addDataSet(CandlestickDataSet dataSet) {
return candlestickData.add(dataSet);
}
public boolean addDataSet(ScatterDataSet dataSet) {
return scatterData.add(dataSet);
}
public boolean addDataSet(PointLineDataSet dataSet) {
return pointLineData.add(dataSet);
}
public boolean addDataSet(ScatterTextDataSet dataSet) {
return scatterTextData.add(dataSet);
}
public boolean addDataSet(TreeDataSet dataSet) {
return treeData.add(dataSet);
}
public void setCombineData(CombineData combineData) {
this.leftMin = combineData.leftMin;
this.rightMin = combineData.rightMin;
this.leftMax = combineData.leftMax;
this.rightMax = combineData.rightMax;
treeData.setLeftMax(leftMax);
barData.setLeftMax(leftMax);
lineData.setLeftMax(leftMax);
candlestickData.setLeftMax(leftMax);
scatterData.setLeftMax(leftMax);
pointLineData.setLeftMax(leftMax);
scatterTextData.setLeftMax(leftMax);
treeData.setLeftMin(leftMin);
barData.setLeftMin(leftMin);
lineData.setLeftMin(leftMin);
candlestickData.setLeftMin(leftMin);
scatterData.setLeftMin(leftMin);
pointLineData.setLeftMin(leftMin);
scatterTextData.setLeftMin(leftMin);
treeData.setRightMax(rightMax);
barData.setRightMax(rightMax);
lineData.setRightMax(rightMax);
candlestickData.setRightMax(rightMax);
scatterData.setRightMax(rightMax);
pointLineData.setRightMax(rightMax);
scatterTextData.setRightMax(rightMax);
treeData.setRightMin(rightMin);
barData.setRightMin(rightMin);
lineData.setRightMin(rightMin);
candlestickData.setRightMin(rightMin);
scatterData.setRightMin(rightMin);
pointLineData.setRightMin(rightMin);
scatterTextData.setRightMin(rightMin);
treeData.getDataSets().addAll(combineData.getTreeData());
barData.getDataSets().addAll(combineData.getBarData());
lineData.getDataSets().addAll(combineData.getLineData());
candlestickData.getDataSets().addAll(combineData.getCandlestickData());
scatterData.getDataSets().addAll(combineData.getScatterData());
pointLineData.getDataSets().addAll(combineData.getPointLineData());
scatterTextData.getDataSets().addAll(combineData.getScatterTextData());
}
@Override
public void calcMaxMin(Viewport viewport, Rect content) {
leftMin = Float.MAX_VALUE;
leftMax = -Float.MAX_VALUE;
rightMin = Float.MAX_VALUE;
rightMax = -Float.MAX_VALUE;
if (!treeData.getDataSets().isEmpty()) {
treeData.calcMaxMin(viewport, content);
leftMin = Math.min(treeData.leftMin, leftMin);
leftMax = Math.max(treeData.leftMax, leftMax);
rightMin = Math.min(treeData.rightMin, rightMin);
rightMax = Math.max(treeData.rightMax, rightMax);
}
if (!candlestickData.getDataSets().isEmpty()) {
candlestickData.calcMaxMin(viewport, content);
leftMin = Math.min(candlestickData.leftMin, leftMin);
leftMax = Math.max(candlestickData.leftMax, leftMax);
rightMin = Math.min(candlestickData.rightMin, rightMin);
rightMax = Math.max(candlestickData.rightMax, rightMax);
}
if (!lineData.getDataSets().isEmpty()) {
lineData.calcMaxMin(viewport, content);
leftMin = Math.min(lineData.leftMin, leftMin);
leftMax = Math.max(lineData.leftMax, leftMax);
rightMin = Math.min(lineData.rightMin, rightMin);
rightMax = Math.max(lineData.rightMax, rightMax);
}
if (!barData.getDataSets().isEmpty()) {
barData.calcMaxMin(viewport, content);
leftMin = Math.min(barData.leftMin, leftMin);
leftMax = Math.max(barData.leftMax, leftMax);
rightMin = Math.min(barData.rightMin, rightMin);
rightMax = Math.max(barData.rightMax, rightMax);
}
if (!pointLineData.getDataSets().isEmpty()) {
pointLineData.calcMaxMin(viewport, content);
leftMin = Math.min(pointLineData.leftMin, leftMin);
leftMax = Math.max(pointLineData.leftMax, leftMax);
rightMin = Math.min(pointLineData.rightMin, rightMin);
rightMax = Math.max(pointLineData.rightMax, rightMax);
}
if (!scatterTextData.getDataSets().isEmpty()) {
scatterTextData.calcMaxMin(viewport, content);
leftMin = Math.min(scatterTextData.leftMin, leftMin);
leftMax = Math.max(scatterTextData.leftMax, leftMax);
rightMin = Math.min(scatterTextData.rightMin, rightMin);
rightMax = Math.max(scatterTextData.rightMax, rightMax);
}
// Log.d("CombineData", "calcMaxMin_0 leftMax:" + leftMax
// + ", leftMin:" + leftMin + ", rightMax:" + rightMax + ", rightMin:" + rightMin);
/** ScatterData的calcMaxMin放在其它Data后面调用,利用其它Data计算好的leftMax, leftMin, rightMax, rightMin,计算要扩展的数值。**/
if (!scatterData.getDataSets().isEmpty()) {
scatterData.calcMaxMin(viewport, content, leftMax, leftMin, rightMax, rightMin);
leftMin = Math.min(scatterData.leftMin, leftMin);
leftMax = Math.max(scatterData.leftMax, leftMax);
rightMin = Math.min(scatterData.rightMin, rightMin);
rightMax = Math.max(scatterData.rightMax, rightMax);
}
// Log.d("CombineData", "calcMaxMin_1 leftMax:" + leftMax
// + ", leftMin:" + leftMin + ", rightMax:" + rightMax + ", rightMin:" + rightMin);
treeData.setLeftMax(leftMax);
barData.setLeftMax(leftMax);
lineData.setLeftMax(leftMax);
candlestickData.setLeftMax(leftMax);
pointLineData.setLeftMax(leftMax);
scatterTextData.setLeftMax(leftMax);
scatterData.setLeftMax(leftMax);
treeData.setLeftMin(leftMin);
barData.setLeftMin(leftMin);
lineData.setLeftMin(leftMin);
candlestickData.setLeftMin(leftMin);
pointLineData.setLeftMin(leftMin);
scatterTextData.setLeftMin(leftMin);
scatterData.setLeftMin(leftMin);
treeData.setRightMax(rightMax);
barData.setRightMax(rightMax);
lineData.setRightMax(rightMax);
candlestickData.setRightMax(rightMax);
pointLineData.setRightMax(rightMax);
scatterTextData.setRightMax(rightMax);
scatterData.setRightMax(rightMax);
treeData.setRightMin(rightMin);
barData.setRightMin(rightMin);
lineData.setRightMin(rightMin);
candlestickData.setRightMin(rightMin);
pointLineData.setRightMin(rightMin);
scatterTextData.setRightMin(rightMin);
scatterData.setRightMin(rightMin);
setMinMax();
}
@Override
public boolean add(AbstractDataSet e) {
if (e instanceof TreeDataSet) {
return addDataSet((TreeDataSet) e);
}
if (e instanceof CandlestickDataSet) {
return addDataSet((CandlestickDataSet) e);
}
if (e instanceof LineDataSet) {
return addDataSet((LineDataSet) e);
}
if (e instanceof BarDataSet) {
return addDataSet((BarDataSet) e);
}
if (e instanceof ScatterDataSet) {
return addDataSet((ScatterDataSet) e);
}
if (e instanceof PointLineDataSet) {
return addDataSet((PointLineDataSet) e);
}
if (e instanceof ScatterTextDataSet) {
return addDataSet((ScatterTextDataSet) e);
}
return super.add(e);
}
@Override
public void setMinMax() {
if (leftAxis != null) {
leftAxis.setYMin(leftMin);
leftAxis.setYMax(leftMax);
}
if (rightAxis != null) {
rightAxis.setYMin(rightMin);
rightAxis.setYMax(rightMax);
}
}
public <T extends AbstractDataSet> void addAll(List<T> dataSets) {
for (AbstractDataSet dataSet : dataSets) {
add(dataSet);
}
}
public List<AbstractDataSet<?>> getAllDataSet() {
List allDataSet = Collections.synchronizedList(new ArrayList());
// 按分类顺序添加当drawIndex是默认值-1时按下列顺序绘制
allDataSet.addAll(treeData.getDataSets());
allDataSet.addAll(barData.getDataSets());
allDataSet.addAll(candlestickData.getDataSets());
allDataSet.addAll(lineData.getDataSets());
allDataSet.addAll(scatterData.getDataSets());
allDataSet.addAll(pointLineData.getDataSets());
allDataSet.addAll(scatterTextData.getDataSets());
Collections.sort(allDataSet, new Comparator<AbstractDataSet>() {
@Override
public int compare(AbstractDataSet dataSet1, AbstractDataSet dataSet2) {
return dataSet1.getDrawIndex() - dataSet2.getDrawIndex();
}
});
return allDataSet;
}
}
@@ -0,0 +1,186 @@
package cn.jingzhuan.lib.chart.data;
public class DataFormatter {
public static final String HIDE = "HIDE";
public static final String UNIT_TRILLION = "万亿";
public static final String UNIT_TEN_THOUSAND = "";
public static final String UNIT_BILLION = "亿";
public static final String UNIT_PERCENT = "%"; // 数值先*100再添加%
public static final String UNIT_PERCENT_DIRECT = "+%"; // 数值不变直接添加%
public static final String UNIT_CALC = "*"; // 不指定单位由客户端根据数值范围决定基本单位这时精度默认为2
public static final String UNIT_CALC_SUFFIX = "*+"; // 指定附加单位由客户端根据数值范围决定基本单位这时精度默认为2
public static final String UNIT_EMPTY = "";
private int precision = 2; // 数据精度
private String unit = ""; // 数据基本单位
private String unitSuffix = ""; // 数据附加单位
private boolean isHide = false;
public static DataFormatter TRILLION() {
return new DataFormatter(UNIT_TRILLION);
}
public static DataFormatter TEN_THOUSAND() {
return new DataFormatter(UNIT_TEN_THOUSAND);
}
public static DataFormatter BILLION() {
return new DataFormatter(UNIT_BILLION);
}
public static DataFormatter PERCENT() {
return new DataFormatter(UNIT_PERCENT);
}
public static DataFormatter PERCENT_DIRECT() {
return new DataFormatter(UNIT_PERCENT_DIRECT);
}
public static DataFormatter CALC() {
return new DataFormatter(UNIT_CALC);
}
public static DataFormatter CALC_SUFFIX(String unitSuffix) {
return new DataFormatter(UNIT_CALC_SUFFIX, unitSuffix);
}
public static DataFormatter EMPTY() {
return new DataFormatter(UNIT_EMPTY);
}
public static DataFormatter HIDE() {
return new DataFormatter(true);
}
public DataFormatter() {}
public DataFormatter(boolean isHide) {
this.isHide = isHide;
}
public DataFormatter(String unit) {
this(unit, "");
}
public DataFormatter(String unit, String unitSuffix) {
if (unit == null || (!unit.equals(UNIT_TRILLION)
&& !unit.equals(UNIT_TEN_THOUSAND) && !unit.equals(UNIT_BILLION)
&& !unit.equals(UNIT_PERCENT)&& !unit.equals(UNIT_PERCENT_DIRECT)
&& !unit.equals(UNIT_CALC) && !unit.equals(UNIT_CALC_SUFFIX)
&& !unit.equals(UNIT_EMPTY))) {
unit = "";
}
this.precision = 2;
this.unit = unit;
this.unitSuffix = unitSuffix;
}
public DataFormatter(int precision, String unit) {
if (unit == null || (!unit.equals(UNIT_TRILLION)
&& !unit.equals(UNIT_TEN_THOUSAND) && !unit.equals(UNIT_BILLION)
&& !unit.equals(UNIT_PERCENT)&& !unit.equals(UNIT_PERCENT_DIRECT)
&& !unit.equals(UNIT_CALC) && !unit.equals(UNIT_EMPTY))) {
unit = "";
}
if (precision < 0) {
precision = 2;
}
this.precision = precision;
this.unit = unit;
}
public DataFormatter(int unit) {
switch (unit) {
case 1:
this.unit = UNIT_TEN_THOUSAND;
break;
case 2:
this.unit = UNIT_BILLION;
break;
case 3:
this.unit = UNIT_PERCENT;
break;
case 4:
this.unit = UNIT_CALC; // 不指定单位由客户端根据数值范围决定单位这时精度默认为2
this.precision = 2;
break;
case 5:
this.unit = UNIT_PERCENT_DIRECT; // 数值不变直接添加%
break;
default:
this.unit = UNIT_EMPTY;
this.precision = 2;
break;
}
}
public DataFormatter(int precision, int unit) {
this(unit);
if (precision < 0) {
precision = 2;
}
this.precision = precision;
}
// public DataFormatter(String formatterString) {
// if (formatterString == null || formatterString.length() == 0) {
// return;
// }
// if (formatterString.equals(HIDE)) {
// isHide = true;
// } else {
// String[] formatterList = formatterString.toUpperCase(Locale.ROOT).split("_");
// for (String formatter : formatterList) {
// if (formatter.contains("W")) {
// unit = UNIT_TEN_THOUSAND;
// String precisionString = formatter.replace("W", "");
// precision = parsePrecision(precisionString);
// } else if (formatter.contains("Y")) {
// unit = UNIT_BILLION;
// String precisionString = formatter.replace("Y", "");
// precision = parsePrecision(precisionString);
// } else if (formatter.contains("F")) {
//// unit = "%"; // PERCENT 分支再决定
// String precisionString = formatter.replace("F", "");
// precision = parsePrecision(precisionString);
// } else if (formatter.contains("PERCENT")) {
// unit = UNIT_PERCENT;
// }
// }
//
// }
// }
//
// private int parsePrecision(String precisionString) {
// int result = 2;
// if (precisionString != null && precisionString.length() != 0) {
// try {
// result = Integer.parseInt(precisionString);
// } catch (NumberFormatException e) {
// }
// }
// return result;
// }
public int getPrecision() {
return precision;
}
public void setPrecision(int precision) {
this.precision = precision;
}
public String getUnit() {
return unit;
}
public void setUnit(String unit) {
this.unit = unit;
}
public String getUnitSuffix() {
return unitSuffix;
}
public void setUnitSuffix(String unitSuffix) {
this.unitSuffix = unitSuffix;
}
public boolean isHide() {
return isHide;
}
public void setHide(boolean hide) {
isHide = hide;
}
}
@@ -0,0 +1,12 @@
package cn.jingzhuan.lib.chart.data;
import androidx.annotation.ColorInt;
/**
* Created by donglua on 11/13/17.
*/
public interface GirdLineColorSetter {
@ColorInt int getColorByIndex(int color, int position);
}
@@ -0,0 +1,37 @@
package cn.jingzhuan.lib.chart.data;
import android.graphics.Rect;
import cn.jingzhuan.lib.chart.Viewport;
import cn.jingzhuan.lib.chart.component.AxisY.AxisDependency;
/**
* Created by Donglua on 17/7/19.
*/
public interface IDataSet {
void calcMinMax(Viewport viewport);
void calcMinMax(Viewport viewport, Rect content, float max, float mix);
int getEntryCount();
float getViewportYMin();
float getViewportYMax();
void setMaxVisibleEntryCount(int maxVisibleEntryCount);
void setMinVisibleEntryCount(int minVisibleEntryCount);
void setDefaultVisibleEntryCount(int defaultVisibleEntryCount);
@AxisDependency int getAxisDependency();
boolean isEnable();
int getMinValueCount();
void setMinValueCount(int minValueCount);
}
@@ -0,0 +1,12 @@
package cn.jingzhuan.lib.chart.data;
import androidx.annotation.ColorInt;
/**
* Created by Donglua on 17/7/30.
*/
public interface LabelColorSetter {
@ColorInt int getColorByIndex(int position);
}
@@ -0,0 +1,13 @@
package cn.jingzhuan.lib.chart.data
data class Leaf(
val high: Float,
val leftValue: Float,
val rightValue: Float,
val sumValue: Float
) {
override fun toString(): String {
return "${String.format("%.2f", leftValue/100f)}手(${String.format("%.2f", leftValue / sumValue * 100f)}%) <- $high -> ${String.format("%.2f", rightValue/100f)}手(${String.format("%.2f", rightValue / sumValue * 100f)}%)"
// return "${leftValue/100f}(${String.format("%.2f", leftValue / sumValue * 100f)}%) <- $high -> ${rightValue/100f}(${String.format("%.2f", rightValue / sumValue * 100f)}%)"
}
}
@@ -0,0 +1,9 @@
package cn.jingzhuan.lib.chart.data;
/**
* Created by Donglua on 17/8/2.
*/
public class LineData extends ChartData<LineDataSet> {
}
@@ -0,0 +1,258 @@
package cn.jingzhuan.lib.chart.data;
import android.graphics.Shader;
import java.util.ArrayList;
import java.util.List;
import cn.jingzhuan.lib.chart.Viewport;
import static cn.jingzhuan.lib.chart.component.AxisY.*;
/**
* Created by Donglua on 17/7/19.
*/
public class LineDataSet extends AbstractDataSet<PointValue> {
private int mLineThickness = 2;
protected List<PointValue> mPointValues;
protected PointValue mHeadPoint;
private int mForceValueCount = -1;
private Shader shader;
private Shader mShaderTop;
private Shader mShaderBottom;
private float mShaderBaseValue = Float.NaN;
private boolean isLineVisible = true;
private boolean isPartLine = false; // 线段
private boolean isDrawBand = false; // 带状线
private boolean isHorizontalLine = false; // 单值水平线
public LineDataSet(List<PointValue> pointValues) {
this(pointValues, DEPENDENCY_BOTH);
}
public LineDataSet(List<PointValue> pointValues, @AxisDependency int depsAxis) {
mPointValues = pointValues;
if (mPointValues == null)
mPointValues = new ArrayList<>();
setAxisDependency(depsAxis);
}
public int getLineThickness() {
return mLineThickness;
}
public void setLineThickness(int mLineThickness) {
this.mLineThickness = mLineThickness;
}
public List<PointValue> getLines() {
return mPointValues;
}
@Override
public int getEntryCount() {
if (mPointValues == null) return 0;
if (mForceValueCount > 0) return mForceValueCount;
return Math.max(getMinValueCount(), mPointValues.size());
}
@Override
public void calcMinMax(Viewport viewport) {
if (mPointValues == null || mPointValues.isEmpty())
return;
calcViewportY(viewport);
}
private void calcViewportY(Viewport viewport) {
mViewportYMax = -Float.MAX_VALUE;
mViewportYMin = Float.MAX_VALUE;
if (mHeadPoint != null) {
calcViewportMinMax(mHeadPoint);
}
for (PointValue e : getVisiblePoints(viewport)) {
calcViewportMinMax(e);
}
float range = mViewportYMax - mViewportYMin;
if (Float.compare(getOffsetPercent(), 0f) > 0f) {
mViewportYMin = mViewportYMin - range * getOffsetPercent();
}
if (Float.compare(getOffsetPercent(), 0f) > 0f) {
mViewportYMax = mViewportYMax + range * getOffsetPercent();
}
// if (mViewportYMax == 0 && mViewportYMin == 0) {
// mViewportYMax = 1;
// mViewportYMin = -1;
// }
if (mViewportYMax == 0 && mViewportYMin == 0) {
mViewportYMax = 0.01f;
mViewportYMin = -0.01f;
}
}
@Override
public void setValues(List<PointValue> values) {
this.mPointValues = values;
}
@Override
public List<PointValue> getValues() {
return mPointValues;
}
public PointValue getHeadPoint() {
return mHeadPoint;
}
public void setHeadPoint(PointValue headPoint) {
this.mHeadPoint = headPoint;
}
private void calcViewportMinMax(PointValue e) {
if (Float.isNaN(e.getValue()) || Float.isInfinite(e.getValue())) return;
if (e.getValue() < mViewportYMin)
mViewportYMin = e.getValue();
if (e.getValue() > mViewportYMax)
mViewportYMax = e.getValue();
if (isDrawBand){
if (e.getSecondValue() < e.getValue()){
if (e.getSecondValue() < mViewportYMin)
mViewportYMin = e.getSecondValue();
}
if (e.getSecondValue() > e.getValue()){
if (e.getSecondValue() > mViewportYMax)
mViewportYMax = e.getSecondValue();
}
}
}
@Override
public boolean addEntry(PointValue e) {
if (e == null)
return false;
if (mPointValues == null) {
mPointValues = new ArrayList<>();
}
calcViewportMinMax(e);
// add the entry
return mPointValues.add(e);
}
@Override
public boolean removeEntry(PointValue e) {
if (e == null)
return false;
if (mPointValues == null)
return false;
// remove the entry
boolean removed = mPointValues.remove(e);
//if (removed) {
// setMinMax();
//}
return removed;
}
@Override
public int getEntryIndex(PointValue e) {
return mPointValues.indexOf(e);
}
@Override
public PointValue getEntryForIndex(int index) {
return mPointValues.get(index);
}
public void setForceValueCount(int mForceValueCount) {
this.mForceValueCount = mForceValueCount;
}
public int getForceValueCount() {
return mForceValueCount;
}
public Shader getShader() {
return shader;
}
public void setShader(Shader shader) {
this.shader = shader;
}
public float getShaderBaseValue() {
return mShaderBaseValue;
}
public void setShaderBaseValue(float shaderBaseValue, Shader shaderTop, Shader shaderBottom) {
this.mShaderBaseValue = shaderBaseValue;
this.mShaderTop = shaderTop;
this.mShaderBottom = shaderBottom;
}
public Shader getShaderTop() {
return mShaderTop;
}
public Shader getShaderBottom() {
return mShaderBottom;
}
public void setLineVisible(boolean lineVisible) {
isLineVisible = lineVisible;
}
public boolean isLineVisible() {
return isLineVisible;
}
public boolean isPartLine() {
return isPartLine;
}
public void setPartLine(boolean partLine) {
isPartLine = partLine;
}
public boolean isDrawBand() {
return isDrawBand;
}
public void setDrawBand(boolean drawBand) {
isDrawBand = drawBand;
}
public boolean isHorizontalLine() {
return isHorizontalLine;
}
public void setHorizontalLine(boolean horizontalLine) {
isHorizontalLine = horizontalLine;
}
}
@@ -0,0 +1,52 @@
package cn.jingzhuan.lib.chart.data;
import java.util.List;
import cn.jingzhuan.lib.chart.Viewport;
import cn.jingzhuan.lib.chart.component.AxisY;
/**
* Created by Donglua on 17/7/26.
*/
public class MinuteLine extends LineDataSet {
private float mLastClose = -1;
public MinuteLine(List<PointValue> pointValues) {
super(pointValues);
}
public MinuteLine(List<PointValue> pointValues, @AxisY.AxisDependency int axisDependency) {
super(pointValues, axisDependency);
}
public float getLastClose() {
return mLastClose;
}
public void setLastClose(float lastClose) {
this.mLastClose = lastClose;
}
@Override
public void calcMinMax(Viewport viewport) {
super.calcMinMax(viewport);
if (mLastClose > 0) {
if (getValues() != null && getValues().size() > 0) {
float maxDiff = Math.max(Math.abs(mViewportYMin - mLastClose), Math.abs(mViewportYMax - mLastClose));
maxDiff = Math.max(mLastClose * 0.01f, maxDiff);
mViewportYMin = mLastClose - maxDiff;
mViewportYMax = mLastClose + maxDiff;
} else {
mViewportYMin = mLastClose * 0.99f;
mViewportYMax = mLastClose * 1.01f;
}
}
}
}
@@ -0,0 +1,36 @@
package cn.jingzhuan.lib.chart.data;
import android.graphics.Path;
public class PartLineData {
public PartLineData(Path path, int color) {
this.path = path;
this.color = color;
}
public PartLineData(Path path) {
this.path = path;
}
//xian线段
private Path path;
//颜色
private int color;
public Path getPath() {
return path;
}
public void setPath(Path path) {
this.path = path;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
}
@@ -0,0 +1,4 @@
package cn.jingzhuan.lib.chart.data;
public class PointLineData extends ChartData<PointLineDataSet> {
}
@@ -0,0 +1,211 @@
package cn.jingzhuan.lib.chart.data;
import static cn.jingzhuan.lib.chart.component.AxisY.DEPENDENCY_BOTH;
import android.graphics.Shader;
import java.util.ArrayList;
import java.util.List;
import cn.jingzhuan.lib.chart.Viewport;
import cn.jingzhuan.lib.chart.component.AxisY;
public class PointLineDataSet extends AbstractDataSet<PointValue>{
private int mLineThickness = 2;
protected List<PointValue> mPointValues;
private int mForceValueCount = -1;
private Shader shader;
private Shader mShaderTop;
private Shader mShaderBottom;
private float mShaderBaseValue = Float.NaN;
private boolean isLineVisible = true;
public float getInterval() {
return interval;
}
public void setInterval(float interval) {
this.interval = interval;
}
public float getPhase() {
return phase;
}
public void setPhase(float phase) {
this.phase = phase;
}
public float getRadius() {
return radius;
}
public void setRadius(float radius) {
this.radius = radius;
}
private float interval = 2;
private float phase = 0;
private float radius = 2;
public PointLineDataSet(List<PointValue> pointValues) {
this(pointValues, DEPENDENCY_BOTH);
}
public PointLineDataSet(List<PointValue> pointValues, @AxisY.AxisDependency int depsAxis) {
mPointValues = pointValues;
if (mPointValues == null)
mPointValues = new ArrayList<>();
setAxisDependency(depsAxis);
}
public int getLineThickness() {
return mLineThickness;
}
public void setLineThickness(int mLineThickness) {
this.mLineThickness = mLineThickness;
}
public List<PointValue> getLines() {
return mPointValues;
}
@Override
public int getEntryCount() {
if (mPointValues == null) return 0;
if (mForceValueCount > 0) return mForceValueCount;
return Math.max(getMinValueCount(), mPointValues.size());
}
@Override
public void calcMinMax(Viewport viewport) {
if (mPointValues == null || mPointValues.isEmpty())
return;
calcViewportY(viewport);
}
private void calcViewportY(Viewport viewport) {
mViewportYMax = -Float.MAX_VALUE;
mViewportYMin = Float.MAX_VALUE;
for (PointValue e : getVisiblePoints(viewport)) {
calcViewportMinMax(e);
}
}
@Override
public void setValues(List<PointValue> values) {
this.mPointValues = values;
}
@Override
public List<PointValue> getValues() {
return mPointValues;
}
private void calcViewportMinMax(PointValue e) {
if (Float.isNaN(e.getValue()) || Float.isInfinite(e.getValue())) return;
if (e.getValue() < mViewportYMin)
mViewportYMin = e.getValue();
if (e.getValue() > mViewportYMax)
mViewportYMax = e.getValue();
}
@Override
public boolean addEntry(PointValue e) {
if (e == null)
return false;
if (mPointValues == null) {
mPointValues = new ArrayList<>();
}
calcViewportMinMax(e);
// add the entry
return mPointValues.add(e);
}
@Override
public boolean removeEntry(PointValue e) {
if (e == null)
return false;
if (mPointValues == null)
return false;
// remove the entry
boolean removed = mPointValues.remove(e);
//if (removed) {
// setMinMax();
//}
return removed;
}
@Override
public int getEntryIndex(PointValue e) {
return mPointValues.indexOf(e);
}
@Override
public PointValue getEntryForIndex(int index) {
return mPointValues.get(index);
}
public void setForceValueCount(int mForceValueCount) {
this.mForceValueCount = mForceValueCount;
}
public int getForceValueCount() {
return mForceValueCount;
}
public Shader getShader() {
return shader;
}
public void setShader(Shader shader) {
this.shader = shader;
}
public float getShaderBaseValue() {
return mShaderBaseValue;
}
public void setShaderBaseValue(float shaderBaseValue, Shader shaderTop, Shader shaderBottom) {
this.mShaderBaseValue = shaderBaseValue;
this.mShaderTop = shaderTop;
this.mShaderBottom = shaderBottom;
}
public Shader getShaderTop() {
return mShaderTop;
}
public Shader getShaderBottom() {
return mShaderBottom;
}
public void setLineVisible(boolean lineVisible) {
isLineVisible = lineVisible;
}
public boolean isLineVisible() {
return isLineVisible;
}
}
@@ -0,0 +1,106 @@
package cn.jingzhuan.lib.chart.data;
/**
* Created by Donglua on 17/7/18.
*/
public class PointValue extends Value {
private float value;
private boolean isPathEnd = false;
private int pathColor = -1; //多段path 不同颜色 应用于: 指标 神龙趋势线
private float secondValue; //第二条数据 应用于: 指标 高抛低吸
private float secondY;//第二条数据: y轴坐标
private int mColor = -2;
/**
* 是否画圆点 此属性暂时只用于PointLineDataSet
*/
private boolean drawCircle = true;
public PointValue(float value) {
this.value = value;
}
public PointValue(float value, int color) {
this.value = value;
this.mColor = color;
}
public PointValue(float value, float secondValue) {
this.value = value;
this.secondValue = secondValue;
}
public PointValue(float value, boolean drawCircle) {
this.value = value;
this.drawCircle = drawCircle;
}
public PointValue() {
}
public float getValue() {
return value;
}
public void setValue(float value) {
this.value = value;
}
public int getColor() {
return mColor;
}
public void setColor(int color) {
mColor = color;
}
public boolean isPathEnd() {
return isPathEnd;
}
public void setPathEnd(boolean pathEnd) {
isPathEnd = pathEnd;
}
public boolean isValueNaN() {
return Float.isNaN(value);
}
public int getPathColor() {
return pathColor;
}
public void setPathColor(int pathColor) {
this.pathColor = pathColor;
}
public boolean applyMultiPath() {
return pathColor != -1;
}
public float getSecondValue() {
return secondValue;
}
public void setSecondValue(float secondValue) {
this.secondValue = secondValue;
}
public float getSecondY() {
return secondY;
}
public void setSecondY(float secondY) {
this.secondY = secondY;
}
public void setDrawCircle(boolean drawCircle) {
this.drawCircle = drawCircle;
}
public boolean isDrawCircle() {
return drawCircle;
}
}
@@ -0,0 +1,9 @@
package cn.jingzhuan.lib.chart.data;
/**
* Created by donglua on 10/19/17.
*/
public class ScatterData extends ChartData<ScatterDataSet> {
}
@@ -0,0 +1,361 @@
package cn.jingzhuan.lib.chart.data;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import cn.jingzhuan.lib.chart.Viewport;
import cn.jingzhuan.lib.chart.component.HasValueXOffset;
import cn.jingzhuan.lib.chart.renderer.TextValueRenderer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Created by donglua on 10/19/17.
*/
public class ScatterDataSet extends AbstractDataSet<ScatterValue> implements HasValueXOffset {
public final static int SHAPE_ALIGN_CENTER = 1;
public final static int SHAPE_ALIGN_TOP = 2;
public final static int SHAPE_ALIGN_BOTTOM = 3;
public final static int SHAPE_ALIGN_PARENT_TOP = 4;
public final static int SHAPE_ALIGN_PARENT_BOTTOM = 5;
private List<ScatterValue> scatterValues;
private Drawable shape;
private int shapeLevel = 0; // 正数向上扩展负数向下扩展0上下扩展一半
private float drawOffsetX = 0f;
private float drawOffsetY = 0f;
private float shapeMinWidth = 0f;
private float shapeMaxWidth = Float.NaN;
private int shapeAlign = SHAPE_ALIGN_CENTER;
// private String shapeAlignDesc = "CENTER";
protected float ORIGINAL_VIEWPORT_YMIN = Float.MAX_VALUE;
protected float ORIGINAL_VIEWPORT_YMAX = -Float.MAX_VALUE;
private Rect mContentRect;
private boolean autoWidth = true;
private boolean autoExpand = true;
private List<TextValueRenderer> mTextValueRenderers;
private int mForceValueCount = -1;
public ScatterDataSet(List<ScatterValue> scatterValues) {
this.scatterValues = scatterValues;
}
@Override public void calcMinMax(Viewport viewport, Rect content, float max, float mix) {
mContentRect = content;
mViewportYMax = max;
mViewportYMin = mix;
List<ScatterValue> visiblePoints = getVisiblePoints(viewport);
for (int i = 0; i < visiblePoints.size(); i++) {
ScatterValue e = visiblePoints.get(i);
calcViewportMinMax(i, e, content);
}
ORIGINAL_VIEWPORT_YMAX = mViewportYMax;
ORIGINAL_VIEWPORT_YMIN = mViewportYMin;
if (isAutoExpand()) {
for (int i = 0; i < visiblePoints.size(); i++) {
ScatterValue e = visiblePoints.get(i);
calcViewportMinMaxExpansion(i, e, viewport, content);
}
}
float range = mViewportYMax - mViewportYMin;
if (Float.compare(getMinValueOffsetPercent(), 0f) > 0f) {
mViewportYMin = mViewportYMin - range * getMinValueOffsetPercent();
}
if (Float.compare(getMaxValueOffsetPercent(), 0f) > 0f) {
mViewportYMax = mViewportYMax + range * getMaxValueOffsetPercent();
}
}
private void calcViewportMinMax(int i, ScatterValue e, Rect content) {
if (e == null || !e.isVisible()) return;
if (Float.isNaN(e.getValue())) return;
if (e.getValue() < mViewportYMin) {
mViewportYMin = e.getValue();
// Log.w("ScatterDataSet", "calcViewportMinMax:" + i
// + ", e.value:" + e.getValue()
// + ", mViewportYMax:" + mViewportYMax
// + ", mViewportYMin:" + mViewportYMin);
}
if (e.getValue() > mViewportYMax) {
mViewportYMax = e.getValue();
// Log.w("ScatterDataSet", "calcViewportMinMax:" + i
// + ", e.value:" + e.getValue()
// + ", mViewportYMax:" + mViewportYMax
// + ", mViewportYMin:" + mViewportYMin);
}
}
private void calcViewportMinMaxExpansion(int i, ScatterValue e, Viewport viewport, Rect content) {
if (e == null || !e.isVisible()) return;
if (Float.isNaN(e.getValue())) return;
if (content == null) return;
if (shape == null) return;
float range = mViewportYMax - mViewportYMin;
// float oldViewportYMax = mViewportYMax;
// float oldViewportYMin = mViewportYMin;
if (range <= 0f) return;
// float shapeHeight = shape.getIntrinsicHeight() + 4f; // 间隙4px
final float width = (mContentRect.width() - getStartXOffset() - getEndXOffset())
/ getVisibleRange(viewport) + 1;
float shapeWidth = shape.getIntrinsicWidth();
float shapeHeight = shape.getIntrinsicHeight();
if (isAutoWidth()) {
shapeWidth = Math.max(width * 0.8f, getShapeMinWidth());
if (!Float.isNaN(getShapeMaxWidth())) {
shapeWidth = Math.min(shapeWidth, getShapeMaxWidth());
}
shapeHeight = shapeHeight * (shapeWidth / ((float) shape.getIntrinsicWidth()));
shapeHeight += 4f;
}
// Log.d("ScatterDataSet", "calcViewportMinMaxExpansion:" + i
// + ", shapeAlign:" + shapeAlignDesc
// + ", e.value:" + e.getValue()
// + ", e.isVisible:" + e.isVisible()
// + ", VIEWPORT_YMAX:" + VIEWPORT_YMAX_BEFORE_EXPAN
// + ", VIEWPORT_YMIN:" + VIEWPORT_YMIN_BEFORE_EXPAN
// + ", mViewportYMax:" + mViewportYMax
// + ", mViewportYMin:" + mViewportYMin
// + ", range:" + range + ", shapeHeight:" + shapeHeight + ", contentRect.height:" + content.height()
// );
float percent = shapeHeight / (float) content.height();
// float expand = (float) Math.ceil(range * percent);
float expand = range * percent;
if (expand <= 0f) return;
float anchor;
if (shapeAlign == SHAPE_ALIGN_PARENT_TOP) {
anchor = ORIGINAL_VIEWPORT_YMAX;
} else if (shapeAlign == SHAPE_ALIGN_PARENT_BOTTOM) {
anchor = ORIGINAL_VIEWPORT_YMIN;
} else {
anchor = e.getValue();
}
float newValue;
if (shapeAlign == SHAPE_ALIGN_BOTTOM || shapeAlign == SHAPE_ALIGN_PARENT_TOP) {
newValue = anchor + expand;
mViewportYMax = Math.max(newValue, mViewportYMax);
} else if (shapeAlign == SHAPE_ALIGN_TOP || shapeAlign == SHAPE_ALIGN_PARENT_BOTTOM) {
newValue = anchor - expand;
mViewportYMin = Math.min(newValue, mViewportYMin);
} else { // shapeAlign == SHAPE_ALIGN_CENTER
float newMaxValue = anchor + (expand / 2);
float newMinValue = anchor - (expand / 2);
mViewportYMax = Math.max(newMaxValue, mViewportYMax);
mViewportYMin = Math.min(newMinValue, mViewportYMin);
}
// if (oldViewportYMax != mViewportYMax) {
// Log.w("ScatterDataSet", "calcViewportMinMaxExpansion:" + i
// + ", expand:" + expand
// + ", anchor:" + anchor
// + ", shapeAlign:" + shapeAlignDesc
// + ", e.value:" + e.getValue()
// + ", VIEWPORT_YMAX:" + VIEWPORT_YMAX_BEFORE_EXPAN
// + ", VIEWPORT_YMIN:" + VIEWPORT_YMIN_BEFORE_EXPAN
// + ", mViewportYMax:" + mViewportYMax
// + ", mViewportYMin:" + mViewportYMin
// + ", range:" + range + ", shapeHeight:" + shapeHeight + ", contentRect.height:" + content.height()
// + ", percent:" + percent
// + ", (range * percent):" + (range * percent)
// );
// }
// if (oldViewportYMin != mViewportYMin) {
// Log.w("ScatterDataSet", "calcViewportMinMaxExpansion:" + i
// + ", expand:" + expand
// + ", anchor:" + anchor
// + ", shapeAlign:" + shapeAlignDesc
// + ", e.value:" + e.getValue()
// + ", VIEWPORT_YMAX:" + VIEWPORT_YMAX_BEFORE_EXPAN
// + ", VIEWPORT_YMIN:" + VIEWPORT_YMIN_BEFORE_EXPAN
// + ", mViewportYMax:" + mViewportYMax
// + ", mViewportYMin:" + mViewportYMin
// + ", range:" + range + ", shapeHeight:" + shapeHeight + ", contentRect.height:" + content.height()
// + ", percent:" + percent
// + ", (range * percent):" + (range * percent)
// );
// }
}
@Override public int getEntryCount() {
if (getValues() == null) return 0;
if (mForceValueCount > 0) return mForceValueCount;
return Math.max(getMinValueCount(), getValues().size());
}
@Override public void setValues(List<ScatterValue> values) {
this.scatterValues = values;
}
@Override public List<ScatterValue> getValues() {
return scatterValues;
}
@Override public boolean addEntry(ScatterValue e) {
calcViewportMinMax(scatterValues.size(), e, mContentRect);
return scatterValues.add(e);
}
@Override public boolean removeEntry(ScatterValue e) {
return scatterValues.remove(e);
}
@Override public int getEntryIndex(ScatterValue e) {
return scatterValues.indexOf(e);
}
@Override public ScatterValue getEntryForIndex(int index) {
return scatterValues.get(index);
}
public void setShape(Drawable shape) {
this.shape = shape;
}
public Drawable getShape() {
return shape;
}
public float getDrawOffsetX() {
return drawOffsetX;
}
public void setDrawOffsetX(float drawOffsetX) {
this.drawOffsetX = drawOffsetX;
}
public float getDrawOffsetY() {
return drawOffsetY;
}
public void setDrawOffsetY(float drawOffsetY) {
this.drawOffsetY = drawOffsetY;
}
public void setAutoWidth(boolean autoWidth) {
this.autoWidth = autoWidth;
}
public boolean isAutoWidth() {
return autoWidth;
}
public boolean isAutoExpand() {
return autoExpand;
}
public void setAutoExpand(boolean autoExpand) {
this.autoExpand = autoExpand;
}
public float getStartXOffset() {
return startXOffset;
}
public void setStartXOffset(float startXOffset) {
this.startXOffset = startXOffset;
}
public float getEndXOffset() {
return endXOffset;
}
public void setEndXOffset(float endXOffset) {
this.endXOffset = endXOffset;
}
public void addTextValueRenderer(TextValueRenderer textValueRenderer) {
if (mTextValueRenderers == null) {
mTextValueRenderers = Collections.synchronizedList(new ArrayList<TextValueRenderer>());
}
this.mTextValueRenderers.add(textValueRenderer);
}
public float getShapeMinWidth() {
return shapeMinWidth;
}
public float getShapeMaxWidth() {
return shapeMaxWidth;
}
public void setShapeMinWidth(float shapeMinWidth) {
this.shapeMinWidth = shapeMinWidth;
}
public void setShapeMaxWidth(float shapeMaxWidth) {
this.shapeMaxWidth = shapeMaxWidth;
}
public List<TextValueRenderer> getTextValueRenderers() {
return mTextValueRenderers;
}
public int getShapeAlign() {
return shapeAlign;
}
public void setShapeAlign(int shapeAlign) {
this.shapeAlign = shapeAlign;
// switch (shapeAlign) {
// case SHAPE_ALIGN_CENTER:
// shapeAlignDesc = "CENTER";
// break;
// case SHAPE_ALIGN_TOP:
// shapeAlignDesc = "TOP";
// break;
// case SHAPE_ALIGN_BOTTOM:
// shapeAlignDesc = "BOTTOM";
// break;
// case SHAPE_ALIGN_PARENT_TOP:
// shapeAlignDesc = "PARENT_TOP";
// break;
// case SHAPE_ALIGN_PARENT_BOTTOM:
// shapeAlignDesc = "PARENT_BOTTOM";
// break;
// default:
// shapeAlignDesc = "Error";
// break;
// }
}
public int getShapeLevel() {
return shapeLevel;
}
public void setShapeLevel(int shapeLevel) {
this.shapeLevel = shapeLevel;
}
public void setForceValueCount(int mForceValueCount) {
this.mForceValueCount = mForceValueCount;
}
public int getForceValueCount() {
return mForceValueCount;
}
}
@@ -0,0 +1,4 @@
package cn.jingzhuan.lib.chart.data;
public class ScatterTextData extends ChartData<ScatterTextDataSet> {
}
@@ -0,0 +1,199 @@
package cn.jingzhuan.lib.chart.data;
import java.util.List;
import cn.jingzhuan.lib.chart.Viewport;
public class ScatterTextDataSet extends AbstractDataSet<ScatterTextValue>{
public final static int ALIGN_TOP = 1;
public final static int ALIGN_BOTTOM = 2;
private List<ScatterTextValue> scatterTextValues;
private int textBgColor;
private int lineColor;
private int frameColor;
private String text;
private int textColor;
private int textSize = 11;
private int textPadding = 10;
private int lineDashHeight = 40;
/**
* 临时字段 只适合一个字的背景
*/
private boolean bgCircle = false;
private int align = ALIGN_TOP;
private int mForceValueCount = -1;
public int getTextBgColor() {
return textBgColor;
}
public void setTextBgColor(int textBgColor) {
this.textBgColor = textBgColor;
}
public int getLineColor() {
return lineColor;
}
public void setLineColor(int lineColor) {
this.lineColor = lineColor;
}
public int getFrameColor() {
return frameColor;
}
public void setFrameColor(int frameColor) {
this.frameColor = frameColor;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public int getTextColor() {
return textColor;
}
public void setTextColor(int textColor) {
this.textColor = textColor;
}
public int getAlign() {
return align;
}
public void setAlign(int align) {
this.align = align;
}
public ScatterTextDataSet(List<ScatterTextValue> scatterTextValues) {
this.scatterTextValues = scatterTextValues;
}
@Override
public List<ScatterTextValue> getValues() {
return scatterTextValues;
}
@Override
public void setValues(List<ScatterTextValue> values) {
this.scatterTextValues = values;
}
private void calcViewportMinMax(ScatterTextValue e) {
// if (Float.isNaN(e.getHigh())) return;
// if (e.getHigh() < mViewportYMin)
// mViewportYMin = e.getHigh();
//
// if (e.getHigh() > mViewportYMax)
// mViewportYMax = e.getHigh();
}
@Override
public boolean addEntry(ScatterTextValue e) {
calcViewportMinMax(e);
return scatterTextValues.add(e);
}
@Override
public boolean removeEntry(ScatterTextValue e) {
return scatterTextValues.remove(e);
}
@Override
public int getEntryIndex(ScatterTextValue e) {
return scatterTextValues.indexOf(e);
}
@Override
public ScatterTextValue getEntryForIndex(int index) {
return scatterTextValues.get(index);
}
@Override
public void calcMinMax(Viewport viewport) {
mViewportYMax = -Float.MAX_VALUE;
mViewportYMin = Float.MAX_VALUE;
List<ScatterTextValue> list = getVisiblePoints(viewport);
if (list.size() == 1) {
mViewportYMin = list.get(0).getLow();
mViewportYMax = list.get(0).getHigh();
float range = mViewportYMax - mViewportYMin;
mViewportYMin = mViewportYMin - range * 0.2f;
return;
}
for (ScatterTextValue e : list) {
calcViewportMinMax(e);
}
}
@Override
public int getEntryCount() {
if (getValues() == null) return 0;
if (mForceValueCount > 0) return mForceValueCount;
return Math.max(getMinValueCount(), getValues().size());
}
public int getTextSize() {
return textSize;
}
public void setTextSize(int textSize) {
this.textSize = textSize;
}
/**
* @return 获取文本边距
*/
public int getTextPadding() {
return this.textPadding;
}
/**
* @param padding 文本边距
*/
public void setTextPadding(int padding) {
this.textPadding = padding;
}
/**
* @return 获取虚线长度
*/
public int getLineDashHeight() {
return this.lineDashHeight;
}
/**
* @param dashHeight 虚线长度
*/
public void setLineDashLength(int dashHeight) {
this.lineDashHeight = dashHeight;
}
public boolean isBgCircle() {
return bgCircle;
}
public void setBgCircle(boolean bgCircle) {
this.bgCircle = bgCircle;
}
public void setForceValueCount(int mForceValueCount) {
this.mForceValueCount = mForceValueCount;
}
public int getForceValueCount() {
return mForceValueCount;
}
}
@@ -0,0 +1,61 @@
package cn.jingzhuan.lib.chart.data;
import android.graphics.RectF;
public class ScatterTextValue extends Value{
private boolean visible = false;
private float high;
private float low;
private RectF roundRect;
private float anchor;
public ScatterTextValue() {
}
public ScatterTextValue(boolean visible, float high, float low) {
this.visible = visible;
this.high = high;
this.low = low;
}
public boolean isVisible() {
return visible;
}
public void setVisible(boolean visible) {
this.visible = visible;
}
public float getHigh() {
return high;
}
public void setHigh(float high) {
this.high = high;
}
public float getLow() {
return low;
}
public void setLow(float low) {
this.low = low;
}
public RectF getRoundRect() {
return roundRect;
}
public void setRoundRect(RectF roundRect) {
this.roundRect = roundRect;
}
public float getAnchor() {
return anchor;
}
public void setAnchor(float anchor) {
this.anchor = anchor;
}
}
@@ -0,0 +1,63 @@
package cn.jingzhuan.lib.chart.data;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
/**
* Created by donglua on 10/19/17.
*/
public class ScatterValue extends Value {
private float value;
private boolean visible = true;
private int color = Color.TRANSPARENT;
private Drawable shape = null;
public ScatterValue(float value) {
this.value = value;
}
public ScatterValue(float value, boolean visible) {
this.value = value;
this.visible = visible;
}
public ScatterValue(float value, boolean visible, int color) {
this.value = value;
this.visible = visible;
this.color = color;
}
public float getValue() {
return value;
}
public void setValue(float value) {
this.value = value;
}
public boolean isVisible() {
return visible;
}
public void setVisible(boolean visible) {
this.visible = visible;
}
public void setColor(int color) {
this.color = color;
}
public int getColor() {
return color;
}
public Drawable getShape() {
return shape;
}
public void setShape(Drawable shape) {
this.shape = shape;
}
}
@@ -0,0 +1,9 @@
package cn.jingzhuan.lib.chart.data;
/**
* Created by guobosheng on 22/6/24.
*/
public class TreeData extends ChartData<TreeDataSet> {
}
@@ -0,0 +1,154 @@
package cn.jingzhuan.lib.chart.data;
import android.graphics.Color;
import java.util.ArrayList;
import java.util.List;
import cn.jingzhuan.lib.chart.Viewport;
import cn.jingzhuan.lib.chart.component.AxisY;
import cn.jingzhuan.lib.chart.component.AxisY.AxisDependency;
/**
* Statistic Data Set
* Created by guobosheng on 22/6/23.
*/
public class TreeDataSet extends AbstractDataSet<TreeValue> {
private List<TreeValue> mTreeValues;
private float strokeThickness = 2;
private int positiveColor = Color.RED;
private int negativeColor = Color.GREEN;
private int colorAlpha = 255;
public TreeDataSet(List<TreeValue> treeValues) {
this(treeValues, AxisY.DEPENDENCY_BOTH);
}
public TreeDataSet(List<TreeValue> treeValues, @AxisDependency int axisDependency) {
this.mTreeValues = treeValues;
setAxisDependency(axisDependency);
}
@Override
public int getEntryCount() {
if (mTreeValues != null) {
return Math.max(getMinValueCount(), mTreeValues.size());
}
return 0;
}
@Override
public void calcMinMax(Viewport viewport) {
if (mTreeValues == null || mTreeValues.isEmpty())
return;
mViewportYMax = -Float.MAX_VALUE;
mViewportYMin = Float.MAX_VALUE;
for (TreeValue e : getVisiblePoints(viewport)) {
calcMinMaxY(e);
}
float range = mViewportYMax - mViewportYMin;
if (Float.compare(getMinValueOffsetPercent(), 0f) > 0f) {
mViewportYMin = mViewportYMin - range * getMinValueOffsetPercent();
}
if (Float.compare(getMaxValueOffsetPercent(), 0f) > 0f) {
mViewportYMax = mViewportYMax + range * getMaxValueOffsetPercent();
}
}
protected void calcMinMaxY(TreeValue e) {
if (e == null || !e.isEnable()) return;
float high = e.getHigh();
float low = e.getLow();
if (!Float.isNaN(high) && !Float.isInfinite(high)) {
mViewportYMin = Math.min(mViewportYMin, low);
mViewportYMax = Math.max(mViewportYMax, high);
}
}
@Override
public void setValues(List<TreeValue> values) {
this.mTreeValues = values;
}
@Override
public List<TreeValue> getValues() {
return mTreeValues;
}
@Override
public boolean addEntry(TreeValue e) {
if (e == null) return false;
if (mTreeValues == null) {
mTreeValues = new ArrayList<>();
}
calcMinMaxY(e);
return mTreeValues.add(e);
}
@Override
public boolean removeEntry(TreeValue e) {
if (e == null) return false;
calcMinMaxY(e);
return mTreeValues.remove(e);
}
@Override
public int getEntryIndex(TreeValue e) {
return mTreeValues.indexOf(e);
}
@Override
public TreeValue getEntryForIndex(int index) {
return mTreeValues.get(index);
}
public float getStrokeThickness() {
return strokeThickness;
}
public void setStrokeThickness(float strokeThickness) {
this.strokeThickness = strokeThickness;
}
public int getPositiveColor() {
return positiveColor;
}
public void setPositiveColor(int positiveColor) {
this.positiveColor = positiveColor;
}
public int getNegativeColor() {
return negativeColor;
}
public void setNegativeColor(int negativeColor) {
this.negativeColor = negativeColor;
}
public int getColorAlpha() {
return colorAlpha;
}
public void setColorAlpha(int colorAlpha) {
if (colorAlpha < 0) {
this.colorAlpha = 0;
return;
}
if (colorAlpha > 255) {
this.colorAlpha = 255;
return;
}
this.colorAlpha = colorAlpha;
}
}
@@ -0,0 +1,31 @@
package cn.jingzhuan.lib.chart.data
import kotlin.math.max
import kotlin.math.roundToInt
/**
* Created by guobosheng on 22/6/23.
*/
class TreeValue(var leafs: List<Leaf>?) : Value() {
var isEnable = true
fun getLeaf(high: Float) = if (high.isNaN()) null else leafs?.findLast { (it.high * 100.0f).roundToInt() == (high * 100.0f).roundToInt() } // 精确到小数点后2位
val high: Float
get() = leafs?.maxBy { it.high }?.high ?: Float.NaN
val low: Float
get() = leafs?.minBy { it.high }?.high ?: Float.NaN
val leftValue: Float
get() = leafs?.sumOf { it.leftValue.toDouble() }?.toFloat() ?: Float.NaN
val rightValue: Float
get() = leafs?.sumOf { it.rightValue.toDouble() }?.toFloat() ?: Float.NaN
val maxLeafValue: Float
get() = max(leafs?.maxBy { it.leftValue }?.leftValue ?: Float.NaN, leafs?.maxBy { it.rightValue }?.rightValue ?: Float.NaN)
val leafCount: Int
get() = leafs?.size ?: 0
}
@@ -0,0 +1,32 @@
package cn.jingzhuan.lib.chart.data;
/**
* Created by Donglua on 17/8/1.
*/
public abstract class Value {
private float x = -1;
private float y = -1;
public void setCoordinate(float x, float y) {
this.x = x;
this.y = y;
}
public float getX() {
return x;
}
public float getY() {
return y;
}
public void setX(float x) {
this.x = x;
}
public void setY(float y) {
this.y = y;
}
}
@@ -0,0 +1,11 @@
package cn.jingzhuan.lib.chart.data;
/**
* Created by Donglua on 17/8/8.
*/
public interface ValueFormatter {
String format(float value, int index);
}
@@ -0,0 +1,11 @@
package cn.jingzhuan.lib.chart.data;
/**
* Created by Donglua on 17/8/8.
*/
public interface ValueIndexFormatter {
String format(int index);
}
@@ -0,0 +1,15 @@
package cn.jingzhuan.lib.chart.event;
import cn.jingzhuan.lib.chart.component.Highlight;
/**
* Created by donglua on 8/22/17.
*/
public interface HighlightStatusChangeListener {
void onHighlightShow(Highlight[] highlights);
void onHighlightHide();
}
@@ -0,0 +1,13 @@
package cn.jingzhuan.lib.chart.event;
import cn.jingzhuan.lib.chart.base.Chart;
/**
* Created by donglua on 11/20/17.
*/
@FunctionalInterface
public interface OnEntryClickListener {
void onEntryClick(Chart chart, int position);
}
@@ -0,0 +1,13 @@
package cn.jingzhuan.lib.chart.event;
import cn.jingzhuan.lib.chart.component.Highlight;
/**
* Created by donglua on 8/23/17.
*/
@FunctionalInterface
public interface OnHighlightListener {
void highlight(Highlight[] highlights);
}
@@ -0,0 +1,6 @@
package cn.jingzhuan.lib.chart.event;
@FunctionalInterface
public interface OnLoadMoreKlineListener {
void onLoadMoreKline(int scrolledX);
}
@@ -0,0 +1,16 @@
package cn.jingzhuan.lib.chart.event;
import cn.jingzhuan.lib.chart.Viewport;
/**
* @since 2023-02-26
*/
public interface OnScaleListener {
void onScaleStart(Viewport viewport);
void onScale(Viewport viewport);
void onScaleEnd(Viewport viewport);
}
@@ -0,0 +1,14 @@
package cn.jingzhuan.lib.chart.event;
import cn.jingzhuan.lib.chart2.base.Chart;
/**
* @since 2023-07-13
*/
@FunctionalInterface
public interface OnSingleEntryClickListener {
void onEntryClick(Chart chart, int position);
}
@@ -0,0 +1,13 @@
package cn.jingzhuan.lib.chart.event;
import cn.jingzhuan.lib.chart.Viewport;
/**
* Created by Donglua on 17/7/24.
*/
@FunctionalInterface
public interface OnViewportChangeListener {
void onViewportChange(Viewport viewport);
}
@@ -0,0 +1,143 @@
package cn.jingzhuan.lib.chart.renderer;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import androidx.annotation.NonNull;
import cn.jingzhuan.lib.chart.animation.ChartAnimator;
import cn.jingzhuan.lib.chart.base.BaseChart;
import cn.jingzhuan.lib.chart.data.AbstractDataSet;
import cn.jingzhuan.lib.chart.data.ChartData;
import java.util.List;
import cn.jingzhuan.lib.chart.base.Chart;
import cn.jingzhuan.lib.chart.Viewport;
import cn.jingzhuan.lib.chart.component.Highlight;
/**
* Created by Donglua on 17/7/19.
*/
public abstract class AbstractDataRenderer<T extends AbstractDataSet> implements Renderer {
protected Viewport mViewport;
protected Rect mContentRect;
protected Paint mRenderPaint;
protected DashPathEffect mHighlightedDashPathEffect;
private int mHighlightColor = Color.WHITE;
protected ChartAnimator mChartAnimator;
public AbstractDataRenderer(Chart chart) {
this.mViewport = chart.getCurrentViewport();
this.mContentRect = chart.getContentRect();
if (chart instanceof BaseChart) {
this.mChartAnimator = ((BaseChart) chart).getChartAnimator();
}
getChartData().setChart(chart);
mRenderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mRenderPaint.setStyle(Paint.Style.STROKE);
}
@Override
public final void renderer(Canvas canvas) {
renderDataSet(canvas);
}
protected void renderDataSet(Canvas canvas) {
renderDataSet(canvas, getChartData());
}
protected abstract void renderDataSet(Canvas canvas, ChartData<T> chartData);
public abstract void renderHighlighted(Canvas canvas, @NonNull Highlight[] highlights);
/**
* Computes the pixel offset for the given X lib value. This may be outside the view bounds.
*/
protected float getDrawX(float x) {
return mContentRect.left
+ mContentRect.width() * (x - mViewport.left) / mViewport.width();
}
/**
* Computes the pixel offset for the given Y lib value. This may be outside the view bounds.
*/
protected float getDrawY(float y) {
return mContentRect.bottom
- mContentRect.height() * (y - mViewport.top) / mViewport.height();
}
public void addDataSet(T dataSet) {
if (dataSet == null) return;
getChartData().add(dataSet);
calcDataSetMinMax();
}
public abstract void removeDataSet(T dataSet);
public abstract void clearDataSet();
protected abstract List<T> getDataSet();
public abstract ChartData<T> getChartData();
public int getEntryIndexByCoordinate(float x, float y) {
float valueCount = getChartData().getEntryCount();
int index =
(int) (((x - mContentRect.left) * mViewport.width() / mContentRect.width() + mViewport.left) * valueCount);
if (index >= getChartData().getEntryCount()) index = getChartData().getEntryCount() - 1;
if (index < 0) index = 0;
return index;
}
protected void calcDataSetMinMax() {
getChartData().calcMaxMin(mViewport, mContentRect);
}
public void setHighlightColor(int highlightColor) {
this.mHighlightColor = highlightColor;
}
public int getHighlightColor() {
return mHighlightColor;
}
public void enableHighlightDashPathEffect(float intervals[], float phase) {
mHighlightedDashPathEffect = new DashPathEffect(intervals, phase);
}
public void setMaxVisibleEntryCount(int maxVisibleEntryCount) {
if (maxVisibleEntryCount <= 0) return;
getChartData().setMaxVisibleEntryCount(maxVisibleEntryCount);
}
public void setMinVisibleEntryCount(int minVisibleEntryCount) {
if (minVisibleEntryCount <= 0) return;
getChartData().setMinVisibleEntryCount(minVisibleEntryCount);
}
public void setDefaultVisibleEntryCount(int defaultVisibleEntryCount) {
if (defaultVisibleEntryCount <= 0) return;
getChartData().setDefaultVisibleEntryCount(defaultVisibleEntryCount);
}
public Paint getRenderPaint() {
return mRenderPaint;
}
public ChartAnimator getChartAnimator() {
return mChartAnimator;
}
public void setTypeface(Typeface tf) {
}
}
@@ -0,0 +1,534 @@
package cn.jingzhuan.lib.chart.renderer;
import android.graphics.Canvas;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import cn.jingzhuan.lib.chart.AxisAutoValues;
import cn.jingzhuan.lib.chart.base.Chart;
import cn.jingzhuan.lib.chart.Viewport;
import cn.jingzhuan.lib.chart.component.Axis;
import cn.jingzhuan.lib.chart.component.AxisX;
import cn.jingzhuan.lib.chart.component.AxisY;
import cn.jingzhuan.lib.chart.data.LabelColorSetter;
import cn.jingzhuan.lib.chart.data.ValueFormatter;
import cn.jingzhuan.lib.chart.utils.FloatUtils;
import java.util.List;
/**
* Created by Donglua on 17/7/17.
*/
public class AxisRenderer implements Renderer {
private Viewport mCurrentViewport;
private Rect mContentRect;
private Axis mAxis;
private final char[] mLabelBuffer = new char[100];
private Paint mGridPaint;
private Paint mLabelTextPaint;
private Paint mAxisPaint;
private static final int POW10[] = {1, 10, 100, 1000, 10000, 100000, 1000000};
public AxisRenderer(Chart chart, Axis axis) {
this.mCurrentViewport = chart.getCurrentViewport();
this.mContentRect = chart.getContentRect();
this.mAxis = axis;
initPaints();
}
public AxisRenderer(cn.jingzhuan.lib.chart2.base.Chart chart, Axis axis) {
this.mCurrentViewport = chart.getCurrentViewport();
this.mContentRect = chart.getContentRect();
this.mAxis = axis;
initPaints();
}
protected void initPaints() {
mGridPaint = new Paint();
mGridPaint.setStrokeWidth(mAxis.getGridThickness());
mGridPaint.setColor(mAxis.getGridColor());
mGridPaint.setStyle(Paint.Style.STROKE);
mLabelTextPaint = new Paint();
mLabelTextPaint.setAntiAlias(true);
mLabelTextPaint.setTextSize(mAxis.getLabelTextSize());
mLabelTextPaint.setColor(mAxis.getLabelTextColor());
mAxis.setLabelWidth((int) mLabelTextPaint.measureText("0000"));
if (mAxis.getLabelTextSize() > 0) {
mAxis.setLabelHeight((int) Math.abs(mLabelTextPaint.getFontMetrics().top));
}
mAxisPaint = new Paint();
mAxisPaint.setStrokeWidth(mAxis.getAxisThickness());
mAxisPaint.setColor(mAxis.getAxisColor());
mAxisPaint.setStyle(Paint.Style.STROKE);
}
@Override
public void renderer(Canvas canvas) {
if (mAxis.getLabels() == null) {
if (mAxis instanceof AxisX) {
computeAxisStopsX(mCurrentViewport.left, mCurrentViewport.right,
(AxisX) mAxis, null);
} else if (mAxis instanceof AxisY) {
computeAxisStopsY((AxisY) mAxis);
}
}
// Draws lib container
drawAxisLine(canvas);
}
private static void computeAxisStopsY(AxisY axis) {
double min = axis.getYMin();
double max = axis.getYMax();
int count = axis.getGridCount() + 1;
double interval = (max - min) / count;
axis.mLabelEntries = new float[count + 1];
if (min < Float.MAX_VALUE && max > -Float.MAX_VALUE && min <= max) {
double f = min;
for (int j = 0; j < count + 1; f += interval, j++) {
axis.mLabelEntries[j] = (float) f;
}
}
}
// 坐标轴
private void drawAxisLine(Canvas canvas) {
float startX = 0f, startY = 0f, stopX = 0f, stopY = 0f;
float halfThickness = mAxis.getAxisThickness() * .5f;
switch (mAxis.getAxisPosition()) {
case AxisX.TOP:
case AxisX.TOP_INSIDE:
startX = mContentRect.left;
startY = mContentRect.top + halfThickness;
stopX = mContentRect.right;
stopY = startY;
break;
case AxisX.BOTTOM:
case AxisX.BOTTOM_INSIDE:
startX = mContentRect.left;
startY = mContentRect.bottom - halfThickness;
stopX = mContentRect.right;
stopY = startY;
break;
case AxisY.LEFT_INSIDE:
case AxisY.LEFT_OUTSIDE:
startX = mContentRect.left + halfThickness;
startY = mContentRect.top;
stopX = startX;
stopY = mContentRect.bottom;
break;
case AxisY.RIGHT_INSIDE:
case AxisY.RIGHT_OUTSIDE:
startX = mContentRect.right - halfThickness;
startY = mContentRect.top;
stopX = startX;
stopY = mContentRect.bottom;
break;
}
// Draw axis line
if (mAxis.isEnable()) {
mAxisPaint.setStrokeWidth(mAxis.getAxisThickness());
mAxisPaint.setColor(mAxis.getAxisColor());
canvas.drawLine(startX, startY, stopX, stopY, mAxisPaint);
}
}
private static void computeAxisStopsX(float start, float stop, AxisX axis, AxisAutoValues autoValues) {
double range = stop - start;
if (axis.getGridCount() == 0 || range <= 0) {
return;
}
final int count = axis.getGridCount() + 1;
double rawInterval = range / count;
double interval = roundToOneSignificantFigure(rawInterval);
double first = Math.ceil(start / interval) * interval;
double f;
int i;
axis.mLabelEntries = new float[count + 1];
for (f = first, i = 0; i < count + 1; f += interval, ++i) {
axis.mLabelEntries[i] = (float) f;
}
}
/**
* Rounds the given number to the given number of significant digits. Based on an answer on
* <a href="http://stackoverflow.com/questions/202302">Stack Overflow</a>.
*/
private static float roundToOneSignificantFigure(double num) {
final float d = (float) Math.ceil((float) Math.log10(num < 0 ? -num : num));
final int power = 1 - (int) d;
final float magnitude = (float) Math.pow(10, power);
final long shifted = Math.round(num * magnitude);
return shifted / magnitude;
}
/**
* Computes the pixel offset for the given X lib value. This may be outside the view bounds.
*/
private float getDrawX(float x) {
return mContentRect.left
+ mContentRect.width()
* (x - mCurrentViewport.left) / mCurrentViewport.width();
}
/**
* Computes the pixel offset for the given Y lib value. This may be outside the view bounds.
*/
private float getDrawY(float y) {
return mContentRect.bottom
- mContentRect.height()
* (y - mCurrentViewport.top) / mCurrentViewport.height();
}
/**
* Formats a float value to the given number of decimals. Returns the length of the string.
* The string begins at out.length - [return value].
*/
private static int formatFloat(final char[] out, float val, int digits) {
boolean negative = false;
if (val == 0) {
out[out.length - 1] = '0';
return 1;
}
if (val < 0) {
negative = true;
val = -val;
}
if (digits > POW10.length) {
digits = POW10.length - 1;
}
val *= POW10[digits];
long lval = Math.round(val);
int index = out.length - 1;
int charCount = 0;
while (lval != 0 || charCount < (digits + 1)) {
int digit = (int) (lval % 10);
lval = lval / 10;
out[index--] = (char) (digit + '0');
charCount++;
if (charCount == digits) {
out[index--] = '.';
charCount++;
}
}
if (negative) {
out[index--] = '-';
charCount++;
}
return charCount;
}
// 网絡线
public void drawGridLines(Canvas canvas) {
if (mAxis.isEnable() && mAxis.isGridLineEnable() && mAxis.getGridCount() > 0) {
int count = mAxis.getGridCount() + 1;
mGridPaint.setStrokeWidth(mAxis.getGridThickness());
mGridPaint.setColor(mAxis.getGridColor());
DashPathEffect pathEffect = null;
if (mAxis.getDashedGridIntervals() != null && mAxis.getDashedGridPhase() > 0) {
pathEffect = new DashPathEffect(mAxis.getDashedGridIntervals(), mAxis.getDashedGridPhase());
}
if (mAxis instanceof AxisX) {
final float width = mContentRect.width() / ((float) count);
for (int i = 1; i < count; i++) {
if (mAxis.getIsGridSlidIndex() == i) {
mGridPaint.setPathEffect(null);
} else {
mGridPaint.setPathEffect(pathEffect);
}
if (mAxis.getGirdLineColorSetter() != null) {
mGridPaint.setColor(mAxis.getGirdLineColorSetter().getColorByIndex(mAxis.getGridColor(), i));
}
canvas.drawLine(
mContentRect.left + i * width,
mContentRect.top,
mContentRect.left + i * width,
mContentRect.bottom,
mGridPaint);
}
}
if (mAxis instanceof AxisY) {
final float height = mContentRect.height() / ((float) count);
for (int i = 1; i < count; i++) {
if (mAxis.getIsGridSlidIndex() == i) {
mGridPaint.setPathEffect(null);
} else {
mGridPaint.setPathEffect(pathEffect);
}
if (mAxis.getGirdLineColorSetter() != null) {
mGridPaint.setColor(mAxis.getGirdLineColorSetter().getColorByIndex(mAxis.getGridColor(), i));
}
canvas.drawLine(
mContentRect.left,
mContentRect.top + i * height,
mContentRect.right,
mContentRect.top + i * height,
mGridPaint);
}
}
}
}
public void drawAxisLabels(Canvas canvas) {
if (!mAxis.isLabelEnable()) return;
if (mAxis.getLabels() == null || mAxis.getLabels().size() <= 0) return;
List<String> labels = mAxis.getLabels();
mLabelTextPaint.setColor(mAxis.getLabelTextColor());
mLabelTextPaint.setTextSize(mAxis.getLabelTextSize());
float x = 0f, y = 0f;
switch (mAxis.getAxisPosition()) {
case AxisX.TOP:
case AxisX.TOP_INSIDE:
x = mContentRect.left;
y = mContentRect.top;
break;
case AxisX.BOTTOM:
case AxisX.BOTTOM_INSIDE:
x = mContentRect.left;
y = mContentRect.top + mContentRect.height();
break;
case AxisY.LEFT_INSIDE:
case AxisY.LEFT_OUTSIDE:
x = mContentRect.left;
y = mContentRect.bottom;
break;
case AxisY.RIGHT_INSIDE:
case AxisY.RIGHT_OUTSIDE:
x = mContentRect.right;
y = mContentRect.bottom;
break;
}
int labelOffset;
int labelLength;
if (mAxis instanceof AxisX) { // X轴
final float width = mContentRect.width() / ((float) labels.size());
for (int i = 0; i < labels.size(); i++) {
char[] labelCharArray = labels.get(i).toCharArray();
labelLength = labelCharArray.length;
System.arraycopy(labelCharArray, 0, mLabelBuffer,
mLabelBuffer.length - labelLength, labelLength);
labelOffset = mLabelBuffer.length - labelLength;
mLabelTextPaint.setTextAlign(Paint.Align.CENTER);
LabelColorSetter colorSetter = mAxis.getLabelColorSetter();
if (colorSetter != null) {
mLabelTextPaint.setColor(colorSetter.getColorByIndex(i));
}
canvas.drawText(mLabelBuffer, labelOffset, labelLength,
width * 0.5f + getDrawX(i / ((float) labels.size())),
y + mLabelTextPaint.getTextSize(), mLabelTextPaint);
}
} else { // Y轴
final float height = mContentRect.height() / (mAxis.getLabels().size() - 1F);
float separation = 0;
for (int i = 0; i < mAxis.getLabels().size(); i++) {
char[] labelCharArray = mAxis.getLabels().get(i).toCharArray();
labelLength = labelCharArray.length;
System.arraycopy(labelCharArray, 0, mLabelBuffer,
mLabelBuffer.length - labelLength, labelLength);
labelOffset = mLabelBuffer.length - labelLength;
switch (mAxis.getAxisPosition()) {
case AxisY.LEFT_OUTSIDE:
case AxisY.RIGHT_INSIDE:
mLabelTextPaint.setTextAlign(Paint.Align.RIGHT);
separation = -mAxis.getLabelSeparation();
break;
case AxisY.LEFT_INSIDE:
case AxisY.RIGHT_OUTSIDE:
mLabelTextPaint.setTextAlign(Paint.Align.LEFT);
separation = mAxis.getLabelSeparation();
break;
}
float textHeightOffset = (mLabelTextPaint.descent() + mLabelTextPaint.ascent()) / 2;
LabelColorSetter colorSetter = mAxis.getLabelColorSetter();
if (colorSetter != null) {
mLabelTextPaint.setColor(colorSetter.getColorByIndex(i));
}
canvas.drawText(mLabelBuffer, labelOffset, labelLength,
x + separation,
y - i * height - textHeightOffset,
mLabelTextPaint);
}
}
}
private void drawGridLabels(Canvas canvas) {
if (!mAxis.isLabelEnable()) return;
float[] labels = mAxis.mLabelEntries;
if (labels == null || labels.length < 1) {
return;
}
mLabelTextPaint.setColor(mAxis.getLabelTextColor());
mLabelTextPaint.setTextSize(mAxis.getLabelTextSize());
float x = 0f, y = 0f;
switch (mAxis.getAxisPosition()) {
case AxisX.TOP:
case AxisX.TOP_INSIDE:
x = mContentRect.left;
y = mContentRect.top;
break;
case AxisX.BOTTOM:
case AxisX.BOTTOM_INSIDE:
x = mContentRect.left;
y = mContentRect.top + mContentRect.height();
break;
case AxisY.LEFT_INSIDE:
case AxisY.LEFT_OUTSIDE:
x = mContentRect.left;
y = mContentRect.bottom;
break;
case AxisY.RIGHT_INSIDE:
case AxisY.RIGHT_OUTSIDE:
x = mContentRect.right;
y = mContentRect.bottom;
break;
}
int labelOffset;
int labelLength;
if (mAxis instanceof AxisX) { // X轴
final float width = mContentRect.width() / (labels.length - 1F);
for (int i = 0; i < labels.length; i++) {
ValueFormatter valueFormatter = mAxis.getLabelValueFormatter();
if (valueFormatter == null) {
labelLength = FloatUtils.formatFloatValue(mLabelBuffer, labels[i], 2);
} else {
char[] labelCharArray = valueFormatter.format(labels[i], i).toCharArray();
labelLength = labelCharArray.length;
System.arraycopy(labelCharArray,
0,
mLabelBuffer,
mLabelBuffer.length - labelLength,
labelLength);
}
labelOffset = mLabelBuffer.length - labelLength;
if (i == 0) {
mLabelTextPaint.setTextAlign(Paint.Align.LEFT);
} else if (i == labels.length - 1) {
mLabelTextPaint.setTextAlign(Paint.Align.RIGHT);
} else {
mLabelTextPaint.setTextAlign(Paint.Align.CENTER);
}
LabelColorSetter colorSetter = ((AxisX) mAxis).getLabelColorSetter();
if (colorSetter != null) {
mLabelTextPaint.setColor(colorSetter.getColorByIndex(i));
}
canvas.drawText(mLabelBuffer, labelOffset, labelLength,
x + i * width, // - textWidth * 0.5f,
y + mLabelTextPaint.getTextSize(),
mLabelTextPaint);
}
} else { // Y轴
final float height = mContentRect.height() / (labels.length - 1F);
float separation = 0;
for (int i = 0; i < labels.length; i++) {
ValueFormatter valueFormatter = mAxis.getLabelValueFormatter();
if (valueFormatter == null) {
labelLength = FloatUtils.formatFloatValue(mLabelBuffer, labels[i], 2);
} else {
char[] labelCharArray = valueFormatter.format(labels[i], i).toCharArray();
labelLength = labelCharArray.length;
System.arraycopy(labelCharArray,
0,
mLabelBuffer,
mLabelBuffer.length - labelLength,
labelLength);
}
labelOffset = mLabelBuffer.length - labelLength;
switch (mAxis.getAxisPosition()) {
case AxisY.LEFT_OUTSIDE:
case AxisY.RIGHT_INSIDE:
mLabelTextPaint.setTextAlign(Paint.Align.RIGHT);
separation = -mAxis.getLabelSeparation();
break;
case AxisY.LEFT_INSIDE:
case AxisY.RIGHT_OUTSIDE:
mLabelTextPaint.setTextAlign(Paint.Align.LEFT);
separation = mAxis.getLabelSeparation();
break;
}
float textHeightOffset = (mLabelTextPaint.descent() + mLabelTextPaint.ascent()) / 2;
if (i == 0) { // Bottom
textHeightOffset = mAxis.getLabelSeparation();
} else if (i == labels.length - 1) { // Top
textHeightOffset += textHeightOffset - mAxis.getLabelSeparation();
}
LabelColorSetter colorSetter = ((AxisY) mAxis).getLabelColorSetter();
if (colorSetter != null) {
mLabelTextPaint.setColor(colorSetter.getColorByIndex(i));
}
canvas.drawText(mLabelBuffer, labelOffset, labelLength,
x + separation,
y - i * height - textHeightOffset,
mLabelTextPaint);
}
}
}
public void drawLabels(Canvas canvas) {
if (mAxis.getLabels() == null) {
drawGridLabels(canvas);
} else {
drawAxisLabels(canvas);
}
}
public Axis getAxis() {
return this.mAxis;
}
public void setTypeface(Typeface tf) {
mLabelTextPaint.setTypeface(tf);
}
}
@@ -0,0 +1,248 @@
package cn.jingzhuan.lib.chart.renderer;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Shader;
import android.graphics.Typeface;
import androidx.annotation.NonNull;
import cn.jingzhuan.lib.chart.Viewport;
import cn.jingzhuan.lib.chart.base.Chart;
import cn.jingzhuan.lib.chart.component.AxisY;
import cn.jingzhuan.lib.chart.component.Highlight;
import cn.jingzhuan.lib.chart.data.BarData;
import cn.jingzhuan.lib.chart.data.BarDataSet;
import cn.jingzhuan.lib.chart.data.BarValue;
import cn.jingzhuan.lib.chart.data.ChartData;
import cn.jingzhuan.lib.chart.data.ValueFormatter;
import cn.jingzhuan.lib.chart.event.OnViewportChangeListener;
import cn.jingzhuan.lib.chart.utils.FloatUtils;
import java.util.List;
/**
* Created by Donglua on 17/8/1.
*/
public class BarChartRenderer extends AbstractDataRenderer<BarDataSet> {
private BarData mBarDataSets;
protected final char[] mLabelBuffer = new char[100];
protected Paint mValueTextPaint;
public BarChartRenderer(final Chart chart) {
super(chart);
mValueTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mValueTextPaint.setStyle(Paint.Style.FILL);
chart.setInternalViewportChangeListener(new OnViewportChangeListener() {
@Override
public void onViewportChange(Viewport viewport) {
mViewport.set(viewport);
calcDataSetMinMax();
}
});
final Highlight highlight = new Highlight();
chart.addOnTouchPointChangeListener(new Chart.OnTouchPointChangeListener() {
@Override
public void touch(float x, float y) {
if (chart.isHighlightDisable()) return;
for (BarDataSet dataSet : getDataSet()) {
if (dataSet.isHighlightedVerticalEnable()) {
highlight.setTouchX(x);
highlight.setTouchY(y);
int index = getEntryIndexByCoordinate(x, y) - dataSet.getStartIndexOffset();
if (index < dataSet.getValues().size()) {
BarValue barValue = dataSet.getEntryForIndex(index);
float xPosition = barValue.getX();
float yPosition = barValue.getY();
if (xPosition >= 0 && yPosition >= 0) {
highlight.setX(xPosition);
highlight.setY(yPosition);
highlight.setDataIndex(index);
chart.highlightValue(highlight);
}
}
}
}
}
});
}
@Override protected void renderDataSet(Canvas canvas, ChartData<BarDataSet> chartData) {
for (BarDataSet dataSet : chartData.getDataSets()) {
if (dataSet.isVisible()) {
drawBarDataSet(canvas, dataSet,
chartData.getLeftMax(), chartData.getLeftMin(),
chartData.getRightMax(), chartData.getRightMin());
}
}
}
protected void drawBarDataSet(Canvas canvas, BarDataSet barDataSet,
float lMax, float lMin, float rMax, float rMin) {
mRenderPaint.setStrokeWidth(barDataSet.getStrokeThickness());
mRenderPaint.setStyle(Paint.Style.FILL);
mValueTextPaint.setColor(barDataSet.getValueColor());
mValueTextPaint.setTextSize(barDataSet.getValueTextSize());
int valueCount = barDataSet.getEntryCount();
float min, max;
switch (barDataSet.getAxisDependency()) {
case AxisY.DEPENDENCY_RIGHT:
min = rMin;
max = rMax;
break;
case AxisY.DEPENDENCY_BOTH:
case AxisY.DEPENDENCY_LEFT:
default:
min = lMin;
max = lMax;
break;
}
float width = barDataSet.getBarWidth();
float visibleRange = barDataSet.getVisibleRange(mViewport);
if (barDataSet.isAutoBarWidth() && visibleRange > 0) {
width = mContentRect.width() / visibleRange;
}
final float percent = barDataSet.getBarWidthPercent();
final float scale = 1 / mViewport.width();
final float step = mContentRect.width() * scale / valueCount;
final float startX = mContentRect.left - mViewport.left * mContentRect.width() * scale;
for (int i = 0; i < valueCount && i < barDataSet.getValues().size(); i++) {
BarValue barValue = barDataSet.getEntryForIndex(i);
if (!barValue.isEnable()) continue;
if (barValue.getValues().length < 1 || Float.isNaN(barValue.getValues()[0])) continue;
if (barValue.getColor() != -2) {
mRenderPaint.setColor(barValue.getColor());
} else {
mRenderPaint.setColor(barDataSet.getColor());
}
float x = startX + step * (i + barDataSet.getStartIndexOffset());
float top;
float bottom = calcHeight(0, max, min);
if (barValue.getValueCount() > 0) {
float value = barValue.getValues()[0] * mChartAnimator.getPhaseY();
top = calcHeight(value, max, min) ;
if (barValue.getValueCount() > 1) bottom = calcHeight(barValue.getValues()[1], max, min);
barValue.setCoordinate(x + width * 0.5f, top);
mRenderPaint.setStyle(barValue.getPaintStyle());
float left = x + width * (1 - percent) * 0.5f;
float right = left + width * percent;
if (Math.abs(top - bottom) < 0.0001) {
canvas.drawLine(left, top, right, bottom, mRenderPaint);
} else {
if (barValue.getGradientColors() != null && barValue.getGradientColors().length > 1) {
float centerX = (left + right) * 0.5f;
mRenderPaint.setShader(
new LinearGradient(centerX, top, centerX, bottom,
barValue.getGradientColors()[0],
barValue.getGradientColors()[1], Shader.TileMode.MIRROR));
}
canvas.drawRect(left, top, right, bottom, mRenderPaint);
}
int labelLength;
int labelOffset;
if (barDataSet.isDrawValueEnable()) {
ValueFormatter valueFormatter;
valueFormatter = barDataSet.getValueFormatter();
if (valueFormatter == null) {
labelLength = FloatUtils.formatFloatValue(mLabelBuffer, value, 2);
} else {
char[] labelCharArray = valueFormatter.format(barValue.getValues()[0], i).toCharArray();
labelLength = labelCharArray.length;
System.arraycopy(labelCharArray,
0,
mLabelBuffer,
mLabelBuffer.length - labelLength,
labelLength);
}
labelOffset = mLabelBuffer.length - labelLength;
mValueTextPaint.setTextAlign(Paint.Align.CENTER);
canvas.drawText(mLabelBuffer, labelOffset, labelLength,
x + width * 0.5f,
top - 10, mValueTextPaint);
}
}
mRenderPaint.setShader(null);
}
mRenderPaint.setStyle(Paint.Style.FILL);
}
protected float calcHeight(float value, float max, float min) {
if (Float.compare(max, min) == 0) return 0;
return (max - value) / (max - min) * mContentRect.height();
}
@Override
public void renderHighlighted(Canvas canvas, @NonNull Highlight[] highlights) {
mRenderPaint.setColor(getHighlightColor());
mRenderPaint.setStrokeWidth(1);
mRenderPaint.setStyle(Paint.Style.FILL);
if (mHighlightedDashPathEffect != null) {
mRenderPaint.setPathEffect(mHighlightedDashPathEffect);
}
for (Highlight highlight : highlights) {
canvas.drawLine(
highlight.getX(),
0,
highlight.getX(),
mContentRect.bottom,
mRenderPaint);
}
mRenderPaint.setPathEffect(null);
}
@Override public void removeDataSet(BarDataSet dataSet) {
if (dataSet == null) return;
mBarDataSets.remove(dataSet);
calcDataSetMinMax();
}
@Override public void clearDataSet() {
mBarDataSets.clear();
calcDataSetMinMax();
}
@Override
protected List<BarDataSet> getDataSet() {
return mBarDataSets.getDataSets();
}
@Override public BarData getChartData() {
if (mBarDataSets == null) mBarDataSets = new BarData();
return mBarDataSets;
}
@Override public void setTypeface(Typeface tf) {
mValueTextPaint.setTypeface(tf);
}
}
@@ -0,0 +1,309 @@
package cn.jingzhuan.lib.chart.renderer;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import androidx.annotation.NonNull;
import cn.jingzhuan.lib.chart.base.Chart;
import cn.jingzhuan.lib.chart.Viewport;
import cn.jingzhuan.lib.chart.component.AxisY;
import cn.jingzhuan.lib.chart.component.Highlight;
import cn.jingzhuan.lib.chart.data.CandlestickData;
import cn.jingzhuan.lib.chart.data.CandlestickDataSet;
import cn.jingzhuan.lib.chart.data.CandlestickValue;
import cn.jingzhuan.lib.chart.data.ChartData;
import cn.jingzhuan.lib.chart.event.OnViewportChangeListener;
import java.util.List;
/**
*
* Created by donglua on 8/29/17.
*/
public class CandlestickChartRenderer extends AbstractDataRenderer<CandlestickDataSet> {
private float[] mUpperShadowBuffers = new float[4];
private float[] mLowerShadowBuffers = new float[4];
private float[] mBodyBuffers = new float[4];
private CandlestickData chartData;
public CandlestickChartRenderer(final Chart chart) {
super(chart);
chart.setInternalViewportChangeListener(new OnViewportChangeListener() {
@Override public void onViewportChange(Viewport viewport) {
mViewport.set(viewport);
calcDataSetMinMax();
}
});
final Highlight highlight = new Highlight();
chart.addOnTouchPointChangeListener(new Chart.OnTouchPointChangeListener() {
@Override
public void touch(float x, float y) {
if (chart.isHighlightDisable()) return;
for (CandlestickDataSet dataSet : getDataSet()) {
if (dataSet.isHighlightedVerticalEnable()) {
final int valueCount = dataSet.getEntryCount();
int index;
float xPosition;
float yPosition;
highlight.setTouchX(x);
highlight.setTouchY(y);
if (x >= mContentRect.left) {
index = getEntryIndexByCoordinate(x, y) - dataSet.getStartIndexOffset();
if (index < valueCount && index >= 0 && index < dataSet.getValues().size()) {
final CandlestickValue candlestickValue = dataSet.getEntryForIndex(index);
xPosition = candlestickValue.getX();
yPosition = candlestickValue.getY();
if (xPosition >= 0 && yPosition >= 0) {
highlight.setX(xPosition);
highlight.setY(yPosition);
highlight.setDataIndex(index);
chart.highlightValue(highlight);
}
}
}
}
}
}
});
}
@Override protected void renderDataSet(Canvas canvas, ChartData<CandlestickDataSet> chartData) {
for (CandlestickDataSet dataSet : chartData.getDataSets()) {
if (dataSet.isVisible()) {
drawDataSet(canvas, dataSet,
chartData.getLeftMax(), chartData.getLeftMin(),
chartData.getRightMax(), chartData.getRightMin());
}
}
}
private void drawDataSet(Canvas canvas, CandlestickDataSet candlestickDataSet,
float lMax, float lMin, float rMax, float rMin) {
float min, max;
switch (candlestickDataSet.getAxisDependency()) {
case AxisY.DEPENDENCY_RIGHT:
min = rMin;
max = rMax;
break;
case AxisY.DEPENDENCY_BOTH:
case AxisY.DEPENDENCY_LEFT:
default:
min = lMin;
max = lMax;
break;
}
mRenderPaint.setStrokeWidth(candlestickDataSet.getStrokeThickness());
mRenderPaint.setColor(candlestickDataSet.getColor());
int valueCount = candlestickDataSet.getEntryCount();
final List<CandlestickValue> visibleValues = candlestickDataSet.getVisiblePoints(mViewport);
final float scale = 1 / mViewport.width();
final float step = mContentRect.width() * scale / valueCount;
final float startX = mContentRect.left - mViewport.left * mContentRect.width() * scale;
for (int i = 0; i < valueCount && i < candlestickDataSet.getValues().size(); i++) {
final CandlestickValue candlestick = candlestickDataSet.getEntryForIndex(i);
if (!candlestick.isVisible()) continue;
if (!visibleValues.contains(candlestick)) {
continue;
}
float candleWidth = candlestickDataSet.getCandleWidth();
float xPosition = startX + step * (i + candlestickDataSet.getStartIndexOffset());
if (candlestickDataSet.isAutoWidth()) {
candleWidth = mContentRect.width() / candlestickDataSet.getVisibleRange(mViewport);
}
if (candlestick.getFillBackgroundColor() == CandlestickValue.COLOR_NONE) { // 画背景
mRenderPaint.setColor(candlestickDataSet.getDecreasingColor());
canvas.drawRect(xPosition, 0, xPosition + candleWidth, canvas.getHeight(), mRenderPaint);
}
float highY = (max - candlestick.getHigh()) / (max - min) * mContentRect.height();
float lowY = (max - candlestick.getLow()) / (max - min) * mContentRect.height();
float openY = (max - candlestick.getOpen()) / (max - min) * mContentRect.height();
float closeY = (max - candlestick.getClose()) / (max - min) * mContentRect.height();
float widthPercent = 0.8f;
mBodyBuffers[0] = xPosition + (1 - widthPercent) * 0.5f * candleWidth;
mBodyBuffers[1] = closeY;
mBodyBuffers[2] = mBodyBuffers[0] + candleWidth * widthPercent;
mBodyBuffers[3] = openY;
final float candlestickCenterX = xPosition + candleWidth * 0.5f;
mUpperShadowBuffers[0] = candlestickCenterX;
mUpperShadowBuffers[2] = candlestickCenterX;
mLowerShadowBuffers[0] = candlestickCenterX;
mLowerShadowBuffers[2] = candlestickCenterX;
candlestick.setCoordinate(candlestickCenterX, closeY);
if (Float.compare(candlestick.getOpen(), candlestick.getClose()) > 0) { // 阴线
mUpperShadowBuffers[1] = highY;
mUpperShadowBuffers[3] = openY;
mLowerShadowBuffers[1] = lowY;
mLowerShadowBuffers[3] = closeY;
if (candlestick.getColor() == CandlestickValue.COLOR_NONE) {
mRenderPaint.setColor(candlestickDataSet.getDecreasingColor());
} else {
mRenderPaint.setColor(candlestick.getColor());
}
if (candlestick.getPaintStyle() != null) {
mRenderPaint.setStyle(candlestick.getPaintStyle());
} else {
mRenderPaint.setStyle(candlestickDataSet.getDecreasingPaintStyle());
}
} else if (Float.compare(candlestick.getOpen(), candlestick.getClose()) < 0) { // 阳线
mUpperShadowBuffers[1] = highY;
mUpperShadowBuffers[3] = closeY;
mLowerShadowBuffers[1] = lowY;
mLowerShadowBuffers[3] = openY;
if (candlestick.getColor() == CandlestickValue.COLOR_NONE) {
mRenderPaint.setColor(candlestickDataSet.getIncreasingColor());
} else {
mRenderPaint.setColor(candlestick.getColor());
}
if (candlestick.getPaintStyle() != null) {
mRenderPaint.setStyle(candlestick.getPaintStyle());
} else {
mRenderPaint.setStyle(candlestickDataSet.getIncreasingPaintStyle());
}
} else {
mUpperShadowBuffers[1] = highY;
mUpperShadowBuffers[3] = openY;
mLowerShadowBuffers[1] = lowY;
mLowerShadowBuffers[3] = mUpperShadowBuffers[3];
if (candlestick.getColor() == CandlestickValue.COLOR_NONE) {
mRenderPaint.setColor(candlestickDataSet.getNeutralColor());
} else {
mRenderPaint.setColor(candlestick.getColor());
}
}
if (i > 0) {
final CandlestickValue previousValue = candlestickDataSet.getEntryForIndex(i - 1);
boolean isLimitUp20 = (Float.compare(
candlestick.getClose(),
previousValue.getClose() * 1.2f - 0.01f) > 0) && candlestick.getClose() == candlestick.getHigh();
boolean isLimitUp = (Float.compare(
candlestick.getClose(),
previousValue.getClose() * 1.1f - 0.01f) > 0) && candlestick.getClose() == candlestick.getHigh();
if (candlestickDataSet.getLimitUpColor() != Color.TRANSPARENT) {
if (isLimitUp20) {
mRenderPaint.setColor(candlestickDataSet.getLimitUpColor20());
} else if (isLimitUp) {
mRenderPaint.setColor(candlestickDataSet.getLimitUpColor());
}
}
if (candlestickDataSet.getLimitUpPaintStyle() != null) {
if (isLimitUp || isLimitUp20) {
mRenderPaint.setStyle(candlestickDataSet.getLimitUpPaintStyle());
}
}
}
if (mBodyBuffers[1] == mBodyBuffers[3]) {
canvas.drawLine(mBodyBuffers[0],
mBodyBuffers[1],
mBodyBuffers[2],
mBodyBuffers[3], mRenderPaint);
} else {
canvas.drawRect(mBodyBuffers[0],
mBodyBuffers[1],
mBodyBuffers[2],
mBodyBuffers[3], mRenderPaint);
}
canvas.drawLines(mUpperShadowBuffers, mRenderPaint);
canvas.drawLines(mLowerShadowBuffers, mRenderPaint);
if (candlestickDataSet instanceof CandlestickDataSetArrowDecorator) {
((CandlestickDataSetArrowDecorator) candlestickDataSet).draw(canvas, candlestick,
mContentRect, candleWidth,
xPosition,
highY, lowY);
}
}
if (candlestickDataSet instanceof CandlestickDataSetArrowDecorator) {
((CandlestickDataSetArrowDecorator) candlestickDataSet).reset();
}
}
@Override public void renderHighlighted(Canvas canvas, @NonNull Highlight[] highlights) {
mRenderPaint.setColor(getHighlightColor());
mRenderPaint.setStrokeWidth(1);
mRenderPaint.setStyle(Paint.Style.FILL);
if (mHighlightedDashPathEffect != null) {
mRenderPaint.setPathEffect(mHighlightedDashPathEffect);
}
for (Highlight highlight : highlights) {
for (CandlestickDataSet dataSet : getDataSet()) {
if (dataSet.isHighlightedVerticalEnable()) {
canvas.drawLine(highlight.getX(),
mContentRect.top,
highlight.getX(),
mContentRect.bottom,
mRenderPaint);
}
if (dataSet.isHighlightedHorizontalEnable()) {
canvas.drawLine(mContentRect.left,
highlight.getY(),
mContentRect.right,
highlight.getY(),
mRenderPaint);
}
}
}
mRenderPaint.setPathEffect(null);
}
@Override public void removeDataSet(CandlestickDataSet dataSet) {
getChartData().remove(dataSet);
calcDataSetMinMax();
}
@Override public void clearDataSet() {
getChartData().clear();
getChartData().calcMaxMin(mViewport, mContentRect);
}
@Override public List<CandlestickDataSet> getDataSet() {
return chartData.getDataSets();
}
@Override public ChartData<CandlestickDataSet> getChartData() {
if (chartData == null)
chartData = new CandlestickData();
return chartData;
}
}
@@ -0,0 +1,183 @@
package cn.jingzhuan.lib.chart.renderer;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import cn.jingzhuan.lib.chart.Viewport;
import cn.jingzhuan.lib.chart.data.CandlestickDataSet;
import cn.jingzhuan.lib.chart.data.CandlestickValue;
import cn.jingzhuan.lib.chart.utils.FloatUtils;
import static android.graphics.Paint.ANTI_ALIAS_FLAG;
import static android.graphics.Paint.Align;
/**
* Created by donglua on 2/9/18.
*/
public class CandlestickDataSetArrowDecorator extends CandlestickDataSet {
public final static String ARROW_LEFT = "";
public final static String ARROW_RIGHT = "";
private final Paint mPaint;
private final char[] mLabelBuffer = new char[20];
private final Rect mTextBounds = new Rect();
private float currentMaxValue = -1f;
private float currentMinValue = -1f;
private int textColor = 0xffA1abbb;
private int textSize = 30;
public CandlestickDataSetArrowDecorator(CandlestickDataSet candlestickDataSet) {
super(candlestickDataSet.getValues(), candlestickDataSet.getAxisDependency());
setMinValueCount(candlestickDataSet.getMinValueCount());
setMinVisibleEntryCount(candlestickDataSet.getMinVisibleEntryCount());
setMaxVisibleEntryCount(candlestickDataSet.getMaxVisibleEntryCount());
setDefaultVisibleEntryCount(candlestickDataSet.getDefaultVisibleEntryCount());
setTag(candlestickDataSet.getTag());
setDecreasingPaintStyle(candlestickDataSet.getDecreasingPaintStyle());
setDecreasingColor(candlestickDataSet.getDecreasingColor());
setIncreasingPaintStyle(candlestickDataSet.getIncreasingPaintStyle());
setIncreasingColor(candlestickDataSet.getIncreasingColor());
setNeutralColor(candlestickDataSet.getNeutralColor());
setLimitUpColor(candlestickDataSet.getLimitUpColor());
setLimitUpColor20(candlestickDataSet.getLimitUpColor20());
setGapColor(candlestickDataSet.getGapColor());
setAutoWidth(candlestickDataSet.isAutoWidth());
setCandleWidth(candlestickDataSet.getCandleWidth());
setCandleWidthPercent(candlestickDataSet.getCandleWidthPercent());
setHighlightedHorizontalEnable(candlestickDataSet.isHighlightedHorizontalEnable());
setHighlightedVerticalEnable(candlestickDataSet.isHighlightedVerticalEnable());
setEnable(candlestickDataSet.isEnable());
setEnableGap(candlestickDataSet.isEnableGap());
setStrokeThickness(candlestickDataSet.getStrokeThickness());
setVisible(candlestickDataSet.isVisible());
setTag(candlestickDataSet.getTag());
mPaint = new Paint(ANTI_ALIAS_FLAG);
mPaint.setColor(textColor);
mPaint.setTextSize(textSize);
}
public Paint getPaint() {
return mPaint;
}
@Override
public void calcMinMax(Viewport viewport) {
super.calcMinMax(viewport);
}
public void draw(Canvas canvas, CandlestickValue candlestick, Rect contentRect, float candleWidth, float x, float highY, float lowY) {
draw(canvas, candlestick, contentRect, candleWidth, x, highY, lowY, 2);
}
public void draw(Canvas canvas, CandlestickValue candlestick, Rect contentRect, float candleWidth, float x, float highY, float lowY, int decimalDigitsNumber) {
final float highValue = candlestick.getHigh();
final float lowValue = candlestick.getLow();
float viewportYMax = getViewportYMax();
float viewportYMin = getViewportYMin();
// max + percent * (max - min) = viewportYMax => (1 + percent)max - percent * min = viewportYMax
// min - percent * (max - min) = viewportYMin
// max + min = viewportYMax + viewportYMin => (1 + percent)max + (1 + percent)min = (viewportYMax + viewportYMin) * (1 + percent)
// 推断 (1 + percent + percent)min = (viewportYMax + viewportYMin) * (1 + percent) - viewportYMax
// => min = ((viewportYMax + viewportYMin) * (1 + percent) - viewportYMax) / (1 + percent + percent)
float offsetPercent = getOffsetPercent();
float minValue = ((viewportYMax + viewportYMin) * (1 + offsetPercent) - viewportYMax) / (1 + 2 * offsetPercent);
float maxValue = viewportYMax + viewportYMin - minValue;
float formatMaxValue = FloatUtils.keepPrecision(maxValue, decimalDigitsNumber);
float formatMinValue = FloatUtils.keepPrecision(minValue, decimalDigitsNumber);
float formatHighValue = FloatUtils.keepPrecision(highValue, decimalDigitsNumber);
float formatLowValue = FloatUtils.keepPrecision(lowValue, decimalDigitsNumber);
if (Float.compare(formatHighValue, formatMaxValue) == 0 && currentMaxValue < 0) {
currentMaxValue = highValue;
final int length = FloatUtils.formatFloatValue(mLabelBuffer, highValue, decimalDigitsNumber);
String value = String.valueOf(mLabelBuffer, mLabelBuffer.length - length, length);
if (x < contentRect.width() >> 1) {
mPaint.setTextAlign(Align.LEFT);
String text = ARROW_LEFT + value;
mPaint.getTextBounds(text, 0, text.length(), mTextBounds);
canvas.drawText(text, x + candleWidth * 0.5f, highY + mTextBounds.height() * 0.5f, getPaint());
} else {
mPaint.setTextAlign(Align.RIGHT);
String text = value + ARROW_RIGHT;
mPaint.getTextBounds(text, 0, text.length(), mTextBounds);
canvas.drawText(text, x + candleWidth * 0.5f, highY + mTextBounds.height() * 0.5f, getPaint());
}
}
if (Float.compare(formatLowValue, formatMinValue) == 0 && currentMinValue < 0) {
currentMinValue = lowValue;
final int length = FloatUtils.formatFloatValue(mLabelBuffer, lowValue, decimalDigitsNumber);
String value = String.valueOf(mLabelBuffer, mLabelBuffer.length - length, length);
if (x < contentRect.width() >> 1) {
mPaint.setTextAlign(Align.LEFT);
String text = ARROW_LEFT + value;
mPaint.getTextBounds(text, 0, text.length(), mTextBounds);
canvas.drawText(text, x + candleWidth * 0.5f, lowY + mTextBounds.height() * 0.5f, getPaint());
} else {
mPaint.setTextAlign(Align.RIGHT);
String text = value + ARROW_RIGHT;
mPaint.getTextBounds(text, 0, text.length(), mTextBounds);
canvas.drawText(text, x + candleWidth * 0.5f, lowY + mTextBounds.height() * 0.5f, getPaint());
}
}
}
public void reset() {
currentMinValue = -1f;
currentMaxValue = -1f;
}
public void setTextColor(int textColor) {
this.textColor = textColor;
mPaint.setColor(textColor);
}
public int getTextColor() {
return textColor;
}
public void setTextSize(int textSize) {
this.textSize = textSize;
mPaint.setTextSize(textSize);
}
public int getTextSize() {
return textSize;
}
}
@@ -0,0 +1,208 @@
package cn.jingzhuan.lib.chart.renderer;
import android.graphics.Canvas;
import android.graphics.Typeface;
import androidx.annotation.NonNull;
import cn.jingzhuan.lib.chart.base.Chart;
import cn.jingzhuan.lib.chart.Viewport;
import cn.jingzhuan.lib.chart.component.Highlight;
import cn.jingzhuan.lib.chart.data.AbstractDataSet;
import cn.jingzhuan.lib.chart.data.BarDataSet;
import cn.jingzhuan.lib.chart.data.CandlestickDataSet;
import cn.jingzhuan.lib.chart.data.ChartData;
import cn.jingzhuan.lib.chart.data.CombineData;
import cn.jingzhuan.lib.chart.data.LineDataSet;
import cn.jingzhuan.lib.chart.data.ScatterDataSet;
import cn.jingzhuan.lib.chart.event.OnViewportChangeListener;
import java.util.List;
/**
* Created by Donglua on 17/8/2.
*/
public class CombineChartRenderer extends AbstractDataRenderer {
protected BarChartRenderer barChartRenderer;
protected LineRenderer lineRenderer;
protected CandlestickChartRenderer candlestickChartRenderer;
protected ScatterChartRenderer scatterChartRenderer;
private CombineData combineData;
public CombineChartRenderer(final Chart chart) {
super(chart);
lineRenderer = new LineRenderer(chart);
barChartRenderer = new BarChartRenderer(chart);
candlestickChartRenderer = new CandlestickChartRenderer(chart);
scatterChartRenderer = new ScatterChartRenderer(chart);
chart.setInternalViewportChangeListener(new OnViewportChangeListener() {
@Override public void onViewportChange(Viewport viewport) {
mViewport.set(viewport);
calcDataSetMinMax();
}
});
}
@Override
protected void renderDataSet(Canvas canvas) {
barChartRenderer.renderDataSet(canvas, getChartData().getBarChartData());
candlestickChartRenderer.renderDataSet(canvas, getChartData().getCandlestickChartData());
lineRenderer.renderDataSet(canvas, getChartData().getLineChartData());
scatterChartRenderer.renderDataSet(canvas, getChartData().getScatterChartData());
}
@Override protected void renderDataSet(Canvas canvas, ChartData chartData) {
// ignore
}
@Override
public void setHighlightColor(int highlightColor) {
super.setHighlightColor(highlightColor);
lineRenderer.setHighlightColor(highlightColor);
barChartRenderer.setHighlightColor(highlightColor);
candlestickChartRenderer.setHighlightColor(highlightColor);
scatterChartRenderer.setHighlightColor(highlightColor);
}
@Override
public void renderHighlighted(Canvas canvas, @NonNull Highlight[] highlights) {
if (lineRenderer.getDataSet() != null && !lineRenderer.getDataSet().isEmpty()) {
lineRenderer.renderHighlighted(canvas, highlights);
}
if (barChartRenderer.getDataSet() != null && !barChartRenderer.getDataSet().isEmpty()) {
barChartRenderer.renderHighlighted(canvas, highlights);
}
if (candlestickChartRenderer.getDataSet() != null && !candlestickChartRenderer.getDataSet().isEmpty()) {
candlestickChartRenderer.renderHighlighted(canvas, highlights);
}
if (scatterChartRenderer.getDataSet() != null && !scatterChartRenderer.getDataSet().isEmpty()) {
scatterChartRenderer.renderHighlighted(canvas, highlights);
}
}
@Override
public void addDataSet(AbstractDataSet dataSet) {
getChartData().add(dataSet);
if (dataSet instanceof LineDataSet) {
lineRenderer.addDataSet((LineDataSet) dataSet);
} else if (dataSet instanceof BarDataSet) {
barChartRenderer.addDataSet((BarDataSet) dataSet);
} else if (dataSet instanceof CandlestickDataSet) {
candlestickChartRenderer.addDataSet((CandlestickDataSet) dataSet);
} else if (dataSet instanceof ScatterDataSet) {
scatterChartRenderer.addDataSet((ScatterDataSet) dataSet);
}
calcDataSetMinMax();
}
@Override public void setDefaultVisibleEntryCount(int defaultVisibleEntryCount) {
if (defaultVisibleEntryCount <= 0) return;
barChartRenderer.setDefaultVisibleEntryCount(defaultVisibleEntryCount);
lineRenderer.setDefaultVisibleEntryCount(defaultVisibleEntryCount);
scatterChartRenderer.setDefaultVisibleEntryCount(defaultVisibleEntryCount);
candlestickChartRenderer.setDefaultVisibleEntryCount(defaultVisibleEntryCount);
}
@Override public void setMaxVisibleEntryCount(int maxVisibleEntryCount) {
if (maxVisibleEntryCount <= 0) return;
barChartRenderer.setMaxVisibleEntryCount(maxVisibleEntryCount);
lineRenderer.setMaxVisibleEntryCount(maxVisibleEntryCount);
scatterChartRenderer.setMaxVisibleEntryCount(maxVisibleEntryCount);
candlestickChartRenderer.setMaxVisibleEntryCount(maxVisibleEntryCount);
}
@Override public void setMinVisibleEntryCount(int minVisibleEntryCount) {
if (minVisibleEntryCount <= 0) return;
barChartRenderer.setMinVisibleEntryCount(minVisibleEntryCount);
lineRenderer.setMinVisibleEntryCount(minVisibleEntryCount);
scatterChartRenderer.setMinVisibleEntryCount(minVisibleEntryCount);
candlestickChartRenderer.setMinVisibleEntryCount(minVisibleEntryCount);
}
@Override public void removeDataSet(AbstractDataSet dataSet) {
getChartData().remove(dataSet);
calcDataSetMinMax();
}
@Override public void clearDataSet() {
getChartData().clear();
cleanLineDataSet();
cleanBarDataSet();
cleanCandlestickDataSet();
cleanScatterDataSet();
calcDataSetMinMax();
}
public void cleanLineDataSet() {
lineRenderer.clearDataSet();
getChartData().getLineChartData().clear();
}
public void cleanBarDataSet() {
barChartRenderer.clearDataSet();
getChartData().getBarChartData().clear();
}
public void cleanCandlestickDataSet() {
candlestickChartRenderer.clearDataSet();
getChartData().getCandlestickChartData().clear();
}
public void cleanScatterDataSet() {
scatterChartRenderer.clearDataSet();
getChartData().getScatterChartData().clear();
}
@Override
protected List<AbstractDataSet> getDataSet() {
return combineData.getDataSets();
}
@Override public CombineData getChartData() {
if (combineData == null) combineData = new CombineData();
return combineData;
}
@Override public void enableHighlightDashPathEffect(float[] intervals, float phase) {
super.enableHighlightDashPathEffect(intervals, phase);
this.lineRenderer.enableHighlightDashPathEffect(intervals, phase);
this.barChartRenderer.enableHighlightDashPathEffect(intervals, phase);
this.candlestickChartRenderer.enableHighlightDashPathEffect(intervals, phase);
this.scatterChartRenderer.enableHighlightDashPathEffect(intervals, phase);
}
@Override public void setTypeface(Typeface tf) {
this.lineRenderer.setTypeface(tf);
this.barChartRenderer.setTypeface(tf);
this.candlestickChartRenderer.setTypeface(tf);
this.scatterChartRenderer.setTypeface(tf);
}
@Override public int getEntryIndexByCoordinate(float x, float y) {
if (!getChartData().getCandlestickData().isEmpty()) {
return candlestickChartRenderer.getEntryIndexByCoordinate(x, y);
}
if (!getChartData().getLineData().isEmpty()) {
return lineRenderer.getEntryIndexByCoordinate(x, y);
}
if (!getChartData().getBarData().isEmpty()) {
return barChartRenderer.getEntryIndexByCoordinate(x, y);
}
if (!getChartData().getScatterData().isEmpty()) {
return scatterChartRenderer.getEntryIndexByCoordinate(x, y);
}
return super.getEntryIndexByCoordinate(x, y);
}
}
@@ -0,0 +1,326 @@
package cn.jingzhuan.lib.chart.renderer;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Shader;
import androidx.annotation.NonNull;
import cn.jingzhuan.lib.chart.base.Chart;
import cn.jingzhuan.lib.chart.Viewport;
import cn.jingzhuan.lib.chart.component.AxisY;
import cn.jingzhuan.lib.chart.component.Highlight;
import cn.jingzhuan.lib.chart.data.ChartData;
import cn.jingzhuan.lib.chart.data.LineData;
import cn.jingzhuan.lib.chart.data.LineDataSet;
import cn.jingzhuan.lib.chart.data.PointValue;
import cn.jingzhuan.lib.chart.event.OnViewportChangeListener;
import java.util.ArrayList;
import java.util.List;
/**
* Line Renderer
*
* Created by Donglua on 17/7/19.
*/
public class LineRenderer extends AbstractDataRenderer<LineDataSet> {
private LineData lineData;
private List<Path> shaderPaths;
private List<Shader> shaderPathColors;
private Path linePath;
private Path shaderPath;
public LineRenderer(final Chart chart) {
super(chart);
linePath = new Path();
shaderPath = new Path();
shaderPaths = new ArrayList<>();
shaderPathColors = new ArrayList<>();
chart.setInternalViewportChangeListener(new OnViewportChangeListener() {
@Override
public void onViewportChange(Viewport viewport) {
mViewport.set(viewport);
calcDataSetMinMax();
}
});
final Highlight highlight = new Highlight();
chart.addOnTouchPointChangeListener(new Chart.OnTouchPointChangeListener() {
@Override
public void touch(float x, float y) {
if (chart.isHighlightDisable()) return;
synchronized (chart) {
for (LineDataSet line : getDataSet()) {
if (line.isHighlightedVerticalEnable() && !line.getValues().isEmpty()) {
highlight.setTouchX(x);
highlight.setTouchY(y);
int offset = line.getStartIndexOffset();
int index = getEntryIndexByCoordinate(x, y) - offset;
if (index >= 0 && index < line.getValues().size()) {
final PointValue pointValue = line.getEntryForIndex(index);
float xPosition = pointValue.getX();
float yPosition = pointValue.getY();
if (xPosition >= 0 && yPosition >= 0) {
highlight.setX(xPosition);
highlight.setY(yPosition);
highlight.setDataIndex(index);
chart.highlightValue(highlight);
}
}
}
}
}
}
});
}
@Override
public void renderHighlighted(Canvas canvas, @NonNull Highlight[] highlights) {
mRenderPaint.setStyle(Paint.Style.STROKE);
mRenderPaint.setColor(getHighlightColor());
mRenderPaint.setStrokeWidth(1f);
if (mHighlightedDashPathEffect != null) {
mRenderPaint.setPathEffect(mHighlightedDashPathEffect);
}
for (Highlight highlight : highlights) {
if (highlight != null) {
canvas.drawLine(highlight.getX(),
0,
highlight.getX(),
mContentRect.bottom,
mRenderPaint);
// Horizontal
for (LineDataSet lineDataSet : getDataSet()) {
if (lineDataSet.isHighlightedHorizontalEnable()) {
canvas.drawLine(0,
highlight.getY(),
mContentRect.right,
highlight.getY(),
mRenderPaint);
}
}
}
}
mRenderPaint.setPathEffect(null);
}
@Override public void removeDataSet(LineDataSet dataSet) {
lineData.remove(dataSet);
calcDataSetMinMax();
}
@Override public void clearDataSet() {
lineData.clear();
calcDataSetMinMax();
}
@Override
protected List<LineDataSet> getDataSet() {
return lineData.getDataSets();
}
@Override public ChartData<LineDataSet> getChartData() {
if (lineData == null) lineData = new LineData();
return lineData;
}
@Override protected void renderDataSet(Canvas canvas, ChartData<LineDataSet> chartData) {
for (LineDataSet dataSet : getDataSet()) {
if (dataSet.isVisible()) {
drawDataSet(canvas, dataSet,
chartData.getLeftMax(), chartData.getLeftMin(),
chartData.getRightMax(), chartData.getRightMin());
}
}
}
private void drawDataSet(Canvas canvas, final LineDataSet lineDataSet,
float lMax, float lMin, float rMax, float rMin) {
mRenderPaint.setStrokeWidth(lineDataSet.getLineThickness());
mRenderPaint.setColor(lineDataSet.getColor());
int valueCount = lineDataSet.getEntryCount();
shaderPath.reset();
shaderPaths.clear();
shaderPathColors.clear();
linePath.reset();
boolean isFirst = true;
float min, max;
switch (lineDataSet.getAxisDependency()) {
case AxisY.DEPENDENCY_RIGHT:
min = rMin;
max = rMax;
break;
case AxisY.DEPENDENCY_BOTH:
case AxisY.DEPENDENCY_LEFT:
default:
min = lMin;
max = lMax;
break;
}
final float count = lineDataSet.getVisibleRange(mViewport);
float width = 0;
if (count > 0) {
width = mContentRect.width() / count;
}
int offset = lineDataSet.getStartIndexOffset();
final float scale = 1 / mViewport.width();
final float step = mContentRect.width() * scale / valueCount;
final float startX = mContentRect.left + step * 0.5f - mViewport.left * mContentRect.width() * scale;
PointValue prevValue = null;
boolean shaderSplit = !Float.isNaN(lineDataSet.getShaderBaseValue()) &&
lineDataSet.getShaderBaseValue() < max &&
lineDataSet.getShaderBaseValue() > min;
int lastIndex = 0;
if (mChartAnimator.getPhaseX() > 0) {
lastIndex = (int) (Math.floor(lineDataSet.getValues().size() * mChartAnimator.getPhaseX()) - 1);
}
if (lastIndex >= valueCount) lastIndex = valueCount - 1;
PointValue startPoint = null;
int valuePhaseCount = (int) Math.floor(valueCount * mChartAnimator.getPhaseX());
int i = 0;
for (; i < valuePhaseCount && i < lineDataSet.getValues().size(); i++) {
PointValue point = lineDataSet.getEntryForIndex(i);
if (Float.isNaN(point.getValue())) {
continue;
}
float xPosition = startX + step * (i + lineDataSet.getStartIndexOffset());
float yPosition = (max - point.getValue()) / (max - min) * mContentRect.height();
point.setCoordinate(xPosition, yPosition);
if (isFirst) {
isFirst = false;
linePath.moveTo(xPosition, yPosition);
} else {
linePath.lineTo(xPosition, yPosition);
}
if (shaderSplit) {
float baseValue = lineDataSet.getShaderBaseValue();
float baseValueY = mContentRect.height() / (max - min) * (max - baseValue);
if (startPoint == null) { // isFirst
shaderPath.moveTo(xPosition, yPosition);
startPoint = prevValue == null ? point : prevValue;
} else {
shaderPath.lineTo(xPosition, yPosition);
if (prevValue.getValue() > lineDataSet.getShaderBaseValue()) {
if (point.getValue() <= lineDataSet.getShaderBaseValue()) {
shaderPath.lineTo(point.getX(), baseValueY);
shaderPath.lineTo(startPoint.getX(), baseValueY);
shaderPath.lineTo(startPoint.getX(), startPoint.getY());
shaderPath.close();
shaderPaths.add(new Path(shaderPath));
shaderPathColors.add(lineDataSet.getShaderTop());
shaderPath.reset();
startPoint = null;
}
} else {
if (point.getValue() > lineDataSet.getShaderBaseValue()) {
shaderPath.lineTo(point.getX(), baseValueY);
shaderPath.lineTo(startPoint.getX(), baseValueY);
shaderPath.lineTo(startPoint.getX(), startPoint.getY());
shaderPath.close();
shaderPaths.add(new Path(shaderPath));
shaderPathColors.add(lineDataSet.getShaderBottom());
shaderPath.reset();
startPoint = null;
}
}
}
prevValue = point;
if (lastIndex == i) {
shaderPath.lineTo(lineDataSet.getValues().get(lastIndex).getX(), baseValueY);
shaderPath.lineTo(startPoint.getX(), baseValueY);
shaderPath.lineTo(startPoint.getX(), startPoint.getY());
shaderPath.close();
shaderPaths.add(new Path(shaderPath));
if (prevValue.getValue() > baseValue) {
shaderPathColors.add(lineDataSet.getShaderTop());
} else {
shaderPathColors.add(lineDataSet.getShaderBottom());
}
shaderPath.reset();
startPoint = null;
}
}
}
if (!shaderSplit) {
// draw shader area
if (i > 0 && lineDataSet.getShader() != null && lineDataSet.getValues().size() > 0) {
mRenderPaint.setStyle(Paint.Style.FILL);
if (shaderPath == null) {
shaderPath = new Path(linePath);
} else {
shaderPath.set(linePath);
}
PointValue pointValue = lineDataSet.getEntryForIndex(i - 1);
if (pointValue != null) {
shaderPath.lineTo(pointValue.getX(), mContentRect.bottom);
shaderPath.lineTo(offset * width, mContentRect.bottom);
shaderPath.lineTo(offset * width, lineDataSet.getValues().get(0).getY());
shaderPath.close();
mRenderPaint.setShader(lineDataSet.getShader());
canvas.drawPath(shaderPath, mRenderPaint);
mRenderPaint.setShader(null);
mRenderPaint.setStyle(Paint.Style.STROKE);
}
}
} else {
mRenderPaint.setStyle(Paint.Style.FILL);
for (i = 0; i < shaderPaths.size(); i++) {
Path path = shaderPaths.get(i);
Shader shader = shaderPathColors.get(i);
mRenderPaint.setShader(shader);
canvas.drawPath(path, mRenderPaint);
mRenderPaint.setShader(null);
}
mRenderPaint.setStyle(Paint.Style.STROKE);
}
if (lineDataSet.isLineVisible()) {
canvas.drawPath(linePath, mRenderPaint);
}
}
}
@@ -0,0 +1,12 @@
package cn.jingzhuan.lib.chart.renderer;
import android.graphics.Canvas;
/**
* Created by Donglua on 17/7/17.
*/
public interface Renderer {
void renderer(Canvas canvas);
}
@@ -0,0 +1,154 @@
package cn.jingzhuan.lib.chart.renderer;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.RectF;
import org.jetbrains.annotations.NotNull;
import cn.jingzhuan.lib.chart.base.Chart;
import cn.jingzhuan.lib.chart.component.AxisY;
import cn.jingzhuan.lib.chart.component.Highlight;
import cn.jingzhuan.lib.chart.data.ChartData;
import cn.jingzhuan.lib.chart.data.ScatterData;
import cn.jingzhuan.lib.chart.data.ScatterDataSet;
import cn.jingzhuan.lib.chart.data.ScatterValue;
import java.util.List;
/**
* Scatter Chart Renderer
*
* Created by donglua on 10/19/17.
*/
public class ScatterChartRenderer extends AbstractDataRenderer<ScatterDataSet> {
private ScatterData scatterData;
public ScatterChartRenderer(Chart chart) {
super(chart);
}
@Override protected void renderDataSet(Canvas canvas, ChartData<ScatterDataSet> chartData) {
for (ScatterDataSet dataSet : getDataSet()) {
if (dataSet.isVisible()) {
drawDataSet(canvas, dataSet,
chartData.getLeftMax(), chartData.getLeftMin(),
chartData.getRightMax(), chartData.getRightMin());
}
}
}
private void drawDataSet(Canvas canvas, final ScatterDataSet dataSet,
float leftMax, float leftMin, float rightMax, float rightMin) {
mRenderPaint.setStrokeWidth(2);
mRenderPaint.setColor(dataSet.getColor());
int valueCount = dataSet.getEntryCount();
float min, max;
switch (dataSet.getAxisDependency()) {
case AxisY.DEPENDENCY_RIGHT:
min = rightMin;
max = rightMax;
break;
case AxisY.DEPENDENCY_BOTH:
case AxisY.DEPENDENCY_LEFT:
default:
min = leftMin;
max = leftMax;
break;
}
final float width = (mContentRect.width() - dataSet.getStartXOffset() - dataSet.getEndXOffset())
/ dataSet.getVisibleRange(mViewport) + 1;
float shapeWidth = dataSet.getShape().getIntrinsicWidth();
float shapeHeight = dataSet.getShape().getIntrinsicHeight();
if (dataSet.isAutoWidth()) {
shapeWidth = width * 0.8f;
shapeHeight = shapeWidth * shapeHeight / ((float) dataSet.getShape().getIntrinsicWidth());
}
for (int i = 0; i < valueCount && i < dataSet.getValues().size() && dataSet.getShape() != null; i++) {
ScatterValue point = dataSet.getEntryForIndex(i);
if (!point.isVisible()) continue;
float xPosition = dataSet.getStartXOffset() + width * 0.5f
+ getDrawX((i + dataSet.getStartIndexOffset()) / ((float) valueCount)) - shapeWidth * 0.5f;
float yPosition = (max - point.getValue()) / (max - min) * mContentRect.height() - shapeHeight * 0.5f;
point.setCoordinate(xPosition, yPosition);
int x = (int) (xPosition + dataSet.getDrawOffsetX());
int y = (int) (yPosition + dataSet.getDrawOffsetY());
if (point.getColor() != Color.TRANSPARENT) {
dataSet.getShape().setColorFilter(point.getColor(), PorterDuff.Mode.SRC_OVER);
}
dataSet.getShape().setBounds(x,
y,
(int) (x + shapeWidth),
(int) (y + shapeHeight));
int saveId = canvas.save();
dataSet.getShape().draw(canvas);
canvas.restoreToCount(saveId);
if (dataSet.getTextValueRenderers() != null) {
for (TextValueRenderer textValueRenderer : dataSet.getTextValueRenderers()) {
textValueRenderer.render(canvas, i,
x + shapeWidth * 0.5f, y + shapeHeight * 0.5f);
}
}
}
}
@Override public int getEntryIndexByCoordinate(float x, float y) {
int index = -1;
if (scatterData.getDataSets().size() > 0) {
ScatterDataSet dataSet = scatterData.getDataSets().get(0);
RectF rect = new RectF();
float shapeWidth = dataSet.getShape().getIntrinsicWidth();
float shapeHeight = dataSet.getShape().getIntrinsicHeight();
for (int i = 0; i < dataSet.getValues().size(); i++) {
final ScatterValue value = dataSet.getEntryForIndex(i);
float pX = value.getX();
float pY = value.getY();
rect.set(pX, pY, pX + shapeWidth, pY + shapeHeight);
if (rect.contains(x, y)) {
index = i;
break;
}
}
return index;
}
return super.getEntryIndexByCoordinate(x, y);
}
@Override public void renderHighlighted(Canvas canvas, @NotNull Highlight[] highlights) {
}
@Override public void removeDataSet(ScatterDataSet dataSet) {
getChartData().remove(dataSet);
calcDataSetMinMax();
}
@Override public void clearDataSet() {
getChartData().clear();
}
@Override protected List<ScatterDataSet> getDataSet() {
return getChartData().getDataSets();
}
@Override public ChartData<ScatterDataSet> getChartData() {
if (scatterData == null) {
scatterData = new ScatterData();
}
return scatterData;
}
}
@@ -0,0 +1,30 @@
package cn.jingzhuan.lib.chart.renderer;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
/**
* DataSet Values Text Renderer
*
* Created by donglua on 11/23/17.
*/
public abstract class TextValueRenderer {
private final Paint mValueTextPaint;
public TextValueRenderer() {
mValueTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mValueTextPaint.setStyle(Paint.Style.FILL);
mValueTextPaint.setColor(Color.WHITE);
mValueTextPaint.setTextAlign(Paint.Align.CENTER);
}
public void render(Canvas canvas, int index, float x, float y) {
this.render(canvas, mValueTextPaint, index, x, y);
}
public abstract void render(Canvas canvas, Paint textPaint, int index, float x, float y);
}
@@ -0,0 +1,221 @@
package cn.jingzhuan.lib.chart.utils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import cn.jingzhuan.lib.chart.AxisAutoValues;
public class FloatUtils {
public static final int POW10[] = {1, 10, 100, 1000, 10000, 100000, 1000000};
/**
* Returns next bigger float value considering precision of the argument.
*/
public static float nextUpF(float f) {
if (Float.isNaN(f) || f == Float.POSITIVE_INFINITY) {
return f;
} else {
f += 0.0f;
return Float.intBitsToFloat(Float.floatToRawIntBits(f) + ((f >= 0.0f) ? +1 : -1));
}
}
/**
* Returns next smaller float value considering precision of the argument.
*/
public static float nextDownF(float f) {
if (Float.isNaN(f) || f == Float.NEGATIVE_INFINITY) {
return f;
} else {
if (f == 0.0f) {
return -Float.MIN_VALUE;
} else {
return Float.intBitsToFloat(Float.floatToRawIntBits(f) + ((f > 0.0f) ? -1 : +1));
}
}
}
/**
* Returns next bigger double value considering precision of the argument.
*/
public static double nextUp(double d) {
if (Double.isNaN(d) || d == Double.POSITIVE_INFINITY) {
return d;
} else {
d += 0.0;
return Double.longBitsToDouble(Double.doubleToRawLongBits(d) + ((d >= 0.0) ? +1 : -1));
}
}
/**
* Returns next smaller float value considering precision of the argument.
*/
public static double nextDown(double d) {
if (Double.isNaN(d) || d == Double.NEGATIVE_INFINITY) {
return d;
} else {
if (d == 0.0f) {
return -Float.MIN_VALUE;
} else {
return Double.longBitsToDouble(Double.doubleToRawLongBits(d) + ((d > 0.0f) ? -1 : +1));
}
}
}
/**
* Method checks if two float numbers are similar.
*/
public static boolean almostEqual(float a, float b, float absoluteDiff, float relativeDiff) {
float diff = Math.abs(a - b);
if (diff <= absoluteDiff) {
return true;
}
a = Math.abs(a);
b = Math.abs(b);
float largest = (a > b) ? a : b;
if (diff <= largest * relativeDiff) {
return true;
}
return false;
}
/**
* Rounds the given number to the given number of significant digits. Based on an answer on <a
* href="http://stackoverflow.com/questions/202302">Stack Overflow</a>.
*/
public static float roundToOneSignificantFigure(double num) {
final float d = (float) Math.ceil((float) Math.log10(num < 0 ? -num : num));
final int power = 1 - (int) d;
final float magnitude = (float) Math.pow(10, power);
final long shifted = Math.round(num * magnitude);
return shifted / magnitude;
}
/**
* Formats a float value to the given number of decimals. Returns the length of the string. The string begins at
* [endIndex] - [return value] and ends at [endIndex]. It's up to you to check indexes correctness.
* Parameter [endIndex] can be helpful when you want to append some text to formatted value.
*
* @return number of characters of formatted value
*/
public static int formatFloat(final char[] formattedValue, float value, int endIndex, int digits, char separator) {
if (digits >= POW10.length) {
formattedValue[endIndex - 1] = '.';
return 1;
}
boolean negative = false;
if (value == 0) {
formattedValue[endIndex - 1] = '0';
return 1;
}
if (value < 0) {
negative = true;
value = -value;
}
if (digits > POW10.length) {
digits = POW10.length - 1;
}
value *= POW10[digits];
long lval = Math.round(value);
int index = endIndex - 1;
int charsNumber = 0;
while (lval != 0 || charsNumber < (digits + 1)) {
int digit = (int) (lval % 10);
lval = lval / 10;
formattedValue[index--] = (char) (digit + '0');
charsNumber++;
if (charsNumber == digits) {
formattedValue[index--] = separator;
charsNumber++;
}
}
if (formattedValue[index + 1] == separator) {
formattedValue[index--] = '0';
charsNumber++;
}
if (negative) {
formattedValue[index--] = '-';
charsNumber++;
}
return charsNumber;
}
public static int formatFloatValue(char[] formattedValue, float value, int decimalDigitsNumber) {
return FloatUtils.formatFloat(formattedValue, value, formattedValue.length,
decimalDigitsNumber,
'.');
}
/**
* Computes the set of axis labels to show given start and stop boundaries and an ideal number of values between
* these boundaries.
*
* @param start The minimum extreme (e.g. the left edge) for the axis.
* @param stop The maximum extreme (e.g. the right edge) for the axis.
* @param steps The ideal number of values to create. This should be based on available screen space; the more
* space
* there is, the more values should be shown.
* @param outValues The destination {@link AxisAutoValues} object to populate.
*/
public static void computeAutoGeneratedAxisValues(float start, float stop, int steps, AxisAutoValues outValues) {
double range = stop - start;
if (steps == 0 || range <= 0) {
outValues.values = new float[]{};
outValues.number = 0;
return;
}
double rawInterval = range / steps;
double interval = roundToOneSignificantFigure(rawInterval);
double intervalMagnitude = Math.pow(10, (int) Math.log10(interval));
int intervalSigDigit = (int) (interval / intervalMagnitude);
if (intervalSigDigit > 5) {
// Use one order of magnitude higher, to avoid intervals like 0.9 or 90
interval = Math.floor(10 * intervalMagnitude);
}
double first = Math.ceil(start / interval) * interval;
double last = nextUp(Math.floor(stop / interval) * interval);
double intervalValue;
int valueIndex;
int valuesNum = 0;
for (intervalValue = first; intervalValue <= last; intervalValue += interval) {
++valuesNum;
}
outValues.number = valuesNum;
if (outValues.values.length < valuesNum) {
// Ensure values contains at least number elements.
outValues.values = new float[valuesNum];
}
for (intervalValue = first, valueIndex = 0; valueIndex < valuesNum; intervalValue += interval, ++valueIndex) {
outValues.values[valueIndex] = (float) intervalValue;
}
if (interval < 1) {
outValues.decimals = (int) Math.ceil(-Math.log10(interval));
} else {
outValues.decimals = 0;
}
}
/**
* 对float类型的数值保留指定位数的小数<br>
* 该方法舍入模式最接近的数字舍入如果与两个相邻数字的距离相等则为向上舍入的舍入模式<br>
* <b>注意</b>如果精度要求比较精确请使用 keepPrecision(String number, int precision)方法
*
* @param number 要保留小数的数字
* @param precision 小数位数
* @return float 如果数值较大则使用科学计数法表示
*/
public static float keepPrecision(float number, int precision) {
BigDecimal bg = new BigDecimal(number);
return bg.setScale(precision, RoundingMode.HALF_UP).floatValue();
}
}
@@ -0,0 +1,21 @@
package cn.jingzhuan.lib.chart.utils;
import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Created by donglua on 9/4/17.
*/
public interface ForceAlign {
int LEFT = 21;
int RIGHT = 22;
int CENTER = 23;
@Retention(RetentionPolicy.SOURCE)
@IntDef({ LEFT, RIGHT, CENTER })
@interface XForce {}
}
@@ -0,0 +1,14 @@
package cn.jingzhuan.lib.chart.utils;
public class RequestDataType {
public static int DATA_TYPE = -1;
//默认加载500条数据
public static int DATA_TYPE_DEFAULT = -1;
//增量请求
public static int DATA_TYPE_INCREMENT = 0;
//动态加载500条数据
public static int DATA_TYPE_RANGE = 1;
}
@@ -0,0 +1,57 @@
package cn.jingzhuan.lib.chart.widget;
import android.content.Context;
import android.os.Build;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.animation.Animation;
import cn.jingzhuan.lib.chart.base.BaseChart;
import cn.jingzhuan.lib.chart.renderer.BarChartRenderer;
import cn.jingzhuan.lib.chart.data.BarDataSet;
/**
* Created by Donglua on 17/8/2.
*/
public class BarChart extends BaseChart {
public BarChart(Context context) {
super(context);
}
public BarChart(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public BarChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public BarChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public void initChart() {
super.initChart();
mRenderer = new BarChartRenderer(this);
}
@SuppressWarnings("unchecked")
public void addDataSet(BarDataSet barDataSet) {
mRenderer.addDataSet(barDataSet);
}
public void setDataSet(BarDataSet barDataSet) {
mRenderer.clearDataSet();
addDataSet(barDataSet);
}
@Override public void startAnimation(Animation animation) {
super.startAnimation(animation);
}
}
@@ -0,0 +1,53 @@
package cn.jingzhuan.lib.chart.widget;
import android.content.Context;
import android.os.Build;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import android.util.AttributeSet;
import cn.jingzhuan.lib.chart.base.BaseChart;
import cn.jingzhuan.lib.chart.data.CandlestickDataSet;
import cn.jingzhuan.lib.chart.renderer.CandlestickChartRenderer;
/**
* K线图
* Created by donglua on 8/29/17.
*/
public class CandlestickChart extends BaseChart {
public CandlestickChart(Context context) {
super(context);
}
public CandlestickChart(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CandlestickChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public CandlestickChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override public void initChart() {
super.initChart();
mRenderer = new CandlestickChartRenderer(this);
}
public void addDataSet(CandlestickDataSet candlestickDataSet) {
mRenderer.addDataSet(candlestickDataSet);
}
public void setDataSet(CandlestickDataSet candlestickDataSet) {
mRenderer.clearDataSet();
addDataSet(candlestickDataSet);
}
}
@@ -0,0 +1,130 @@
package cn.jingzhuan.lib.chart.widget;
import android.content.Context;
import android.graphics.Typeface;
import android.os.Build;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import android.util.AttributeSet;
import cn.jingzhuan.lib.chart.data.AbstractDataSet;
import cn.jingzhuan.lib.chart.data.CandlestickDataSet;
import cn.jingzhuan.lib.chart.data.ScatterDataSet;
import java.util.List;
import cn.jingzhuan.lib.chart.base.BaseChart;
import cn.jingzhuan.lib.chart.data.BarDataSet;
import cn.jingzhuan.lib.chart.data.CombineData;
import cn.jingzhuan.lib.chart.data.LineDataSet;
import cn.jingzhuan.lib.chart.renderer.CombineChartRenderer;
/**
* Created by Donglua on 17/8/2.
*/
public class CombineChart extends BaseChart {
public CombineChart(Context context) {
super(context);
}
public CombineChart(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CombineChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public CombineChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public void initChart() {
super.initChart();
mRenderer = new CombineChartRenderer(this);
}
public void addDataSet(AbstractDataSet abstractDataSet) {
getRenderer().addDataSet(abstractDataSet);
}
public void setDataSet(AbstractDataSet dataSet) {
cleanAllDataSet();
addDataSet(dataSet);
}
public void setCombineData(final CombineData combineData) {
cleanAllDataSet();
for (LineDataSet lineDataSet : combineData.getLineData()) {
addDataSet(lineDataSet);
}
for (BarDataSet barDataSet : combineData.getBarData()) {
addDataSet(barDataSet);
}
for (CandlestickDataSet candlestickDataSet : combineData.getCandlestickData()) {
addDataSet(candlestickDataSet);
}
for (ScatterDataSet scatterDataSet : combineData.getScatterData()) {
addDataSet(scatterDataSet);
}
}
public <T extends AbstractDataSet> void setData(List<T> data) {
cleanAllDataSet();
for (T datum : data) {
addDataSet(datum);
}
}
public List<LineDataSet> getLineDataSet() {
return getRenderer().getChartData().getLineData();
}
public List<BarDataSet> getBarDataSet() {
return getRenderer().getChartData().getBarData();
}
public List<CandlestickDataSet> getCandlestickDataSet() {
return getRenderer().getChartData().getCandlestickData();
}
public List<ScatterDataSet> getScatterDataSet() {
return getRenderer().getChartData().getScatterData();
}
public CombineChartRenderer getRenderer() {
return (CombineChartRenderer) mRenderer;
}
@Override public void setTypeface(Typeface tf) {
mRenderer.setTypeface(tf);
super.setTypeface(tf);
}
public void cleanLineDataSet() {
getRenderer().cleanLineDataSet();
}
public void cleanBarDataSet() {
getRenderer().cleanBarDataSet();
}
public void cleanCandlestickDataSet() {
getRenderer().cleanCandlestickDataSet();
}
public void cleanScatterDataSet() {
getRenderer().cleanScatterDataSet();
}
public void cleanAllDataSet() {
getRenderer().clearDataSet();
}
}
@@ -0,0 +1,51 @@
package cn.jingzhuan.lib.chart.widget;
import android.content.Context;
import android.os.Build;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import android.util.AttributeSet;
import cn.jingzhuan.lib.chart.base.BaseChart;
import cn.jingzhuan.lib.chart.renderer.LineRenderer;
import cn.jingzhuan.lib.chart.data.LineDataSet;
/**
* Created by Donglua on 17/7/19.
*/
public class LineChart extends BaseChart {
public LineChart(Context context) {
super(context);
}
public LineChart(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public LineChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public LineChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public void initChart() {
super.initChart();
mRenderer = new LineRenderer(this);
}
public void addLine(LineDataSet lineDataSet) {
mRenderer.addDataSet(lineDataSet);
}
public void setLine(LineDataSet lineDataSet) {
mRenderer.clearDataSet();
addLine(lineDataSet);
}
}
@@ -0,0 +1,35 @@
package cn.jingzhuan.lib.chart2;
import android.annotation.SuppressLint;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import cn.jingzhuan.lib.chart.utils.FloatUtils;
public class TimeUtil {
/**
*传入的时间格式要和格式化时间对应一致 例如start为2019-7-12 13:35:10 则pattern为yyyy-MM-dd HH:mm:ss 以此类推
*/
public static boolean isInTime() {
try {
String date = longToDate();
@SuppressLint("SimpleDateFormat") SimpleDateFormat format = new SimpleDateFormat( "yyyy-MM-dd HH:mm");
Date startTime = format.parse( date + " 09:30");
Date endTime = format.parse(date + " 14:00");
return startTime.before(new Date()) && endTime.after(new Date());
} catch (ParseException e) {
e.printStackTrace();
}
return false;
}
public static String longToDate(){
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.format(System.currentTimeMillis());
}
}
@@ -0,0 +1,568 @@
package cn.jingzhuan.lib.chart2.base;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.os.Build;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import cn.jingzhuan.lib.chart.R;
import cn.jingzhuan.lib.chart.animation.ChartAnimator;
import cn.jingzhuan.lib.chart.component.Highlight;
import cn.jingzhuan.lib.chart.data.ChartData;
import cn.jingzhuan.lib.chart.data.ValueFormatter;
import cn.jingzhuan.lib.chart.data.ValueIndexFormatter;
import cn.jingzhuan.lib.chart.event.HighlightStatusChangeListener;
import cn.jingzhuan.lib.chart.event.OnHighlightListener;
import cn.jingzhuan.lib.chart.renderer.AxisRenderer;
import cn.jingzhuan.lib.chart.utils.FloatUtils;
import cn.jingzhuan.lib.chart2.renderer.AbstractDataRenderer;
import java.util.ArrayList;
import java.util.List;
import static cn.jingzhuan.lib.chart.animation.Easing.EasingFunction;
/**
* Created by Donglua on 17/7/17.
*/
public class BaseChart extends Chart {
protected AbstractDataRenderer mRenderer;
protected List<AxisRenderer> mAxisRenderers;
private HighlightStatusChangeListener mHighlightStatusChangeListener;
private OnHighlightListener mHighlightListener;
private ChartAnimator mChartAnimator;
private final Paint waterMarkPaint = new Paint();
protected Paint mHighlightTextPaint;
protected Paint mHighlightBgPaint;
public BaseChart(Context context) {
super(context);
}
public BaseChart(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public BaseChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public BaseChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public void initChart() {
initHighlightPaint();
mAxisRenderers = new ArrayList<>(4);
mAxisRenderers.add(new AxisRenderer(this, mAxisTop));
mAxisRenderers.add(new AxisRenderer(this, mAxisBottom));
mAxisRenderers.add(new AxisRenderer(this, mAxisLeft));
mAxisRenderers.add(new AxisRenderer(this, mAxisRight));
mChartAnimator = new ChartAnimator(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
postInvalidate();
}
});
}
protected void initHighlightPaint() {
mHighlightTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mHighlightTextPaint.setStyle(Paint.Style.FILL);
mHighlightTextPaint.setTextSize(getHighlightTextSize());
mHighlightTextPaint.setColor(getHighlightTextColor());
mHighlightTextPaint.setTextAlign(Paint.Align.CENTER);
mHighlightBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mHighlightBgPaint.setStyle(Paint.Style.FILL);
mHighlightBgPaint.setColor(getHighlightTextBgColor());
}
@Override
public Paint getRenderPaint() {
return mRenderer.getRenderPaint();
}
@Override
public Canvas getBitmapCanvas() {
return mBitmapCanvas;
}
@Override
public void drawAxis(Canvas canvas) {
for (AxisRenderer axisRenderer : mAxisRenderers) {
axisRenderer.renderer(canvas);
}
}
@Override
public void drawGridLine(Canvas canvas) {
for (AxisRenderer axisRenderer : mAxisRenderers) {
axisRenderer.drawGridLines(canvas);
}
}
@Override
public void drawLabels(Canvas canvas) {
for (AxisRenderer axisRenderer : mAxisRenderers) {
axisRenderer.drawLabels(canvas);
}
}
@Override
public void drawWaterMark(Canvas canvas) {
if (isShowWaterMark()) {
int padding = getResources().getDimensionPixelSize(R.dimen.jz_chart_water_mark_padding);
Bitmap waterMarkBitmap = BitmapFactory.decodeResource(
this.getResources(), isNightMode() ? R.drawable.ico_water_mark_night : R.drawable.ico_water_mark);
int left = getWidth() - padding - waterMarkBitmap.getWidth() - getPaddingRight();
canvas.drawBitmap(waterMarkBitmap, (float) left, (float) padding, waterMarkPaint);
}
}
@Override
public void onTouchPoint(MotionEvent e) {
if (e.getPointerCount() == 1 && isTouchPointEnable()) {
for (OnTouchPointChangeListener touchPointChangeListener : mTouchPointChangeListeners) {
touchPointChangeListener.touch(e.getX(), e.getY());
}
}
}
@Override
public void onTouchHighlight(MotionEvent e) {
if (e.getPointerCount() == 1) {
for (OnTouchHighlightChangeListener touchHighlightChangeListener : mTouchHighlightChangeListeners) {
touchHighlightChangeListener.highlight(e.getX(), e.getY());
}
}
}
@Override
public void highlightValue(Highlight highlight) {
if (highlight == null) return;
final Highlight[] highlights = new Highlight[]{highlight};
if (mHighlightStatusChangeListener != null) {
mHighlightStatusChangeListener.onHighlightShow(highlights);
}
if (mHighlightListener != null) {
mHighlightListener.highlight(highlights);
}
mHighlights = highlights;
mIsHighlight = true;
postInvalidate();
}
@Override
public void cleanHighlight() {
mHighlights = null;
if (mHighlightStatusChangeListener != null)
mHighlightStatusChangeListener.onHighlightHide();
mFocusIndex = -1;
mIsHighlight = false;
invalidate();
}
public void setRenderer(AbstractDataRenderer renderer) {
this.mRenderer = renderer;
}
@Override
public final void render(final Canvas canvas) {
if (mRenderer != null) {
mRenderer.renderer(canvas);
}
drawEdgeEffectsUnclipped(canvas);
renderHighlighted(canvas);
}
public void renderHighlighted(Canvas canvas) {
if (mRenderer != null && getHighlights() != null) {
mRenderer.renderHighlighted(canvas, getHighlights());
if (isEnableVerticalHighlight() && !isHideVerticalHighlight()) {
drawHighlightVertical(canvas);
}
if (isEnableHorizontalHighlight() && !isHideHorizontalHighlight()) {
drawHighlightHorizontal(canvas);
}
}
}
/**
* 画十字光标垂直线
* @param canvas
*/
private void drawHighlightVertical(Canvas canvas) {
if (getHighlights() == null) return;
Highlight highlight = getHighlights()[0];
float x = highlight.getX();
if (Float.isNaN(x)) return;
float thickness = mRenderer.getHighlightThickness();
if (x < mContentRect.left + thickness * 0.5f) {
x = mContentRect.left + thickness * 0.5f;
}
if (x > mContentRect.right - thickness * 0.5f){
x = mContentRect.right - thickness * 0.5f;
}
canvas.drawLine(x, mContentRect.top, x, mContentRect.bottom, mRenderer.getHighlightLinePaint());
}
/**
* 画十字光标水平线
* @param canvas
*/
private void drawHighlightHorizontal(Canvas canvas) {
if (getHighlights() == null) return;
Highlight highlight = getHighlights()[0];
float y = highlight.getY();
if (Float.isNaN(y)) return;
float thickness = mRenderer.getHighlightThickness();
if (y < mContentRect.top + thickness * 0.5f) {
y = mContentRect.top + thickness * 0.5f;
}
if (y > mContentRect.bottom - thickness * 0.5f){
y = mContentRect.bottom - thickness * 0.5f;
}
canvas.drawLine(0, y, mContentRect.right, y, mRenderer.getHighlightLinePaint());
}
/**
* 画十字光标左侧文本
* @param canvas
*/
@Override
public void drawHighlightLeft(Canvas canvas) {
if (isEnableHorizontalHighlight() && isEnableHighlightLeftText() && !isHideHorizontalHighlight()) {
if (getHighlights() == null) return;
Highlight highlight = getHighlights()[0];
Rect contentRect = getContentRect();
float y = highlight.getY();
if (Float.isNaN(y)) return;
// if (y > contentRect.bottom || y < contentRect.top) {
// return;
// }
int textHeight = getHighlightTextBgHeight();
float top = y - textHeight * 0.5f;
float bottom = y + textHeight * 0.5f;
if (y < textHeight * 0.5f) {
top = contentRect.top;
bottom = top + textHeight;
}
if (y > contentRect.bottom - textHeight * 0.5f){
bottom = contentRect.bottom;
top = bottom - textHeight;
}
// Log.d("drawHighlightLeft", "y="+ y + "top=" + top + "bottom: "+bottom);
ChartData chartData = mRenderer.getChartData();
float leftMax = chartData.getLeftMax();
float leftMin = chartData.getLeftMin();
float price = getTouchPriceByY(y, leftMax, leftMin);
ValueFormatter valueFormatter = getAxisLeft().getLabelValueFormatter();
String text;
if (valueFormatter == null) {
text = price == -1 ? "--" : String.valueOf(FloatUtils.keepPrecision(price, 2));
} else {
text = valueFormatter.format(price, 0);
}
if (text.isEmpty()) return;
int width = calculateWidth(text);
// 画背景
Rect bgRect = new Rect(0, (int) top, width, (int) bottom);
canvas.drawRect(bgRect, mHighlightBgPaint);
// 画文本
Paint.FontMetrics fontMetrics = mHighlightTextPaint.getFontMetrics();
float distance = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;
float baseline = bgRect.centerY() + distance;
canvas.drawText(text, bgRect.centerX(), baseline, mHighlightTextPaint);
}
}
/**
* 画十字光标右侧文本
* @param canvas
*/
@Override
public void drawHighlightRight(Canvas canvas) {
if (isEnableHorizontalHighlight() && isEnableHighlightRightText() && !isHideHorizontalHighlight()) {
if (getHighlights() == null) return;
Highlight highlight = getHighlights()[0];
Rect contentRect = getContentRect();
float y = highlight.getY();
if (Float.isNaN(y)) return;
// if (y > contentRect.bottom || y < contentRect.top) {
// return;
// }
int textHeight = getHighlightTextBgHeight();
float top = y - textHeight * 0.5f;
float bottom = y + textHeight * 0.5f;
if (y < textHeight * 0.5f) {
top = contentRect.top;
bottom = top + textHeight;
}
if (y > contentRect.bottom - textHeight * 0.5f){
bottom = contentRect.bottom;
top = bottom - textHeight;
}
Log.d("drawHighlightRight", "y="+ y + "top=" + top + "bottom: "+bottom);
ChartData chartData = mRenderer.getChartData();
float rightMax = chartData.getRightMax();
float rightMin = chartData.getRightMin();
float price = getTouchPriceByY(y, rightMax, rightMin);
ValueFormatter valueFormatter = getAxisRight().getLabelValueFormatter();
String text;
if (valueFormatter == null) {
text = price == -1 ? "--" : String.valueOf(FloatUtils.keepPrecision(price, 2));
} else {
text = valueFormatter.format(price, -1);
}
if (text.isEmpty()) return;
int width = calculateWidth(text);
// 画背景
Rect bgRect = new Rect(mContentRect.right - width, (int) top, mContentRect.right, (int) bottom);
canvas.drawRect(bgRect, mHighlightBgPaint);
// 画文本
Paint.FontMetrics fontMetrics = mHighlightTextPaint.getFontMetrics();
float distance = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;
float baseline = bgRect.centerY() + distance;
canvas.drawText(text, bgRect.centerX(), baseline, mHighlightTextPaint);
}
}
/**
* 画十字光标底部文本
* @param canvas
*/
@Override
public void drawHighlightBottom(Canvas canvas) {
if (isEnableVerticalHighlight() && !isHideVerticalHighlight() && isEnableHighlightBottomText()) {
if (getHighlights() == null) return;
Highlight highlight = getHighlights()[0];
float x = highlight.getX();
if (Float.isNaN(x)) return;
ValueIndexFormatter formatter = getAxisBottom().getValueIndexFormatter();
String text = "";
if (formatter != null) {
text = formatter.format( highlight.getDataIndex());
}
if (text.isEmpty()) return;
int width = calculateWidth(text);
float left = x - width * 0.5f;
float right = x + width * 0.5f;
if (x < width * 0.5f) {
left = mBottomRect.left;
right = left + width;
}
if (x > mBottomRect.right - width * 0.5f){
right = mBottomRect.right;
left = right - width;
}
int top = mBottomRect.top;
int bottom = mBottomRect.bottom;
if (mBottomRect.height() == 0) {
int textHeight = getHighlightTextBgHeight();
Rect contentRect = getContentRect();
top = contentRect.bottom - textHeight;
bottom = contentRect.bottom;
}
// 画背景
Rect bgRect = new Rect((int) left, top, (int) right, bottom);
canvas.drawRect(bgRect, mHighlightBgPaint);
// 画文本
Paint.FontMetrics fontMetrics = mHighlightTextPaint.getFontMetrics();
float distance = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;
float baseline = bgRect.centerY() + distance;
canvas.drawText(text, bgRect.centerX(), baseline, mHighlightTextPaint);
}
}
/**
* 计算文本长度
*
* @return
*/
private int calculateWidth(String text) {
Rect rect = new Rect();
int padding = getResources().getDimensionPixelSize(R.dimen.jz_chart_highlight_text_padding);
mHighlightTextPaint.getTextBounds(text, 0, text.length(), rect);
return rect.width() + padding * 2;
}
private float getTouchPriceByY(float touchY, float viewportMax, float viewportMin) {
if (viewportMax > viewportMin && viewportMax > 0) {
Rect contentRect = getContentRect();
var price = viewportMin + (viewportMax - viewportMin) / contentRect.height() * (contentRect.height() - touchY);
if (price > viewportMax) price = viewportMax;
if (price < viewportMin) price = viewportMin;
return price;
} else if (viewportMax == viewportMin) {
return viewportMax;
}
return -1f;
}
public void setHighlightColor(int color) {
mRenderer.setHighlightColor(color);
}
public int getHighlightColor() {
return mRenderer.getHighlightColor();
}
public Highlight[] getHighlights() {
return mHighlights;
}
public void setHighlights(Highlight[] highlights) {
this.mHighlights = highlights;
}
public void setOnHighlightStatusChangeListener(HighlightStatusChangeListener mHighlightStatusChangeListener) {
this.mHighlightStatusChangeListener = mHighlightStatusChangeListener;
}
public HighlightStatusChangeListener getOnHighlightStatusChangeListener() {
return mHighlightStatusChangeListener;
}
public void setOnHighlightListener(OnHighlightListener highlightListener) {
this.mHighlightListener = highlightListener;
}
public void enableHighlightDashPathEffect(float[] intervals, float phase) {
this.mRenderer.enableHighlightDashPathEffect(intervals, phase);
}
@Override
public void setMinVisibleEntryCount(int minVisibleEntryCount) {
super.setMinVisibleEntryCount(minVisibleEntryCount);
mRenderer.setMinVisibleEntryCount(minVisibleEntryCount);
}
@Override
public void setMaxVisibleEntryCount(int maxVisibleEntryCount) {
super.setMaxVisibleEntryCount(maxVisibleEntryCount);
mRenderer.setMaxVisibleEntryCount(maxVisibleEntryCount);
}
@Override
public void setDefaultVisibleEntryCount(int defaultVisibleEntryCount) {
super.setDefaultVisibleEntryCount(defaultVisibleEntryCount);
mRenderer.setDefaultVisibleEntryCount(defaultVisibleEntryCount);
}
@Override
public int getEntryIndexByCoordinate(float x, float y) {
return mRenderer.getEntryIndexByCoordinate(x, y);
}
@Override
public float getEntryCoordinateByIndex(int index) {
return mRenderer.getEntryCoordinateByIndex(index);
}
public void setTypeface(Typeface tf) {
for (AxisRenderer mAxisRenderer : mAxisRenderers) {
mAxisRenderer.setTypeface(tf);
}
postInvalidate();
}
public ChartAnimator getChartAnimator() {
return mChartAnimator;
}
public void animateXY(int durationMillisX, int durationMillisY, EasingFunction easingX,
EasingFunction easingY) {
mChartAnimator.animateXY(durationMillisX, durationMillisY, easingX, easingY);
}
public void animateXY(int durationMillisX, int durationMillisY, EasingFunction easing) {
mChartAnimator.animateXY(durationMillisX, durationMillisY, easing);
}
public void animateX(int durationMillis, EasingFunction easing) {
mChartAnimator.animateX(durationMillis, easing);
}
public void animateY(int durationMillis, EasingFunction easing) {
mChartAnimator.animateY(durationMillis, easing);
}
public void animateX(int durationMillis) {
mChartAnimator.animateX(durationMillis);
}
public void animateY(int durationMillis) {
mChartAnimator.animateY(durationMillis);
}
public void animateXY(int durationMillisX, int durationMillisY) {
mChartAnimator.animateXY(durationMillisX, durationMillisY);
}
}
@@ -0,0 +1,165 @@
package cn.jingzhuan.lib.chart2.base;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Rect;
import android.os.Build;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.View;
import cn.jingzhuan.lib.chart.Viewport;;
import java.lang.ref.WeakReference;
public abstract class BitmapCachedChart extends View implements IChart {
protected WeakReference<Bitmap> mDrawBitmap;
protected Canvas mBitmapCanvas;
protected Bitmap.Config mBitmapConfig = Bitmap.Config.ARGB_8888;
/**
* The current viewport. This rectangle represents the currently visible lib domain
* and range. The currently visible lib X values are from this rectangle's left to its right.
* The currently visible lib Y values are from this rectangle's top to its bottom.
* <p>
* Note that this rectangle's top is actually the smaller Y value, and its bottom is the larger
* Y value. Since the lib is drawn onscreen in such a way that lib Y values increase
* towards the top of the screen (decreasing pixel Y positions), this rectangle's "top" is drawn
* above this rectangle's "bottom" value.
*
* @see #mContentRect
*/
protected Viewport mCurrentViewport = new Viewport();
/**
* The current destination rectangle (in pixel coordinates) into which the lib data should
* be drawn. Chart labels are drawn outside this area.
*
* @see #mCurrentViewport
*/
protected Rect mContentRect = new Rect();
protected Rect mBottomRect = new Rect();
public BitmapCachedChart(Context context) {
super(context);
}
public BitmapCachedChart(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public BitmapCachedChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public BitmapCachedChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public Rect getContentRect() {
return mContentRect;
}
public Rect getBottomRect() {
return mBottomRect;
}
public Viewport getCurrentViewport() {
return mCurrentViewport;
}
public Canvas getBitmapCanvas() {
return mBitmapCanvas;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Clips the next few drawing operations to the content area
int clipRestoreCount = canvas.save();
canvas.clipRect(mContentRect);
canvas.drawColor(getBackgroundColor());
drawWaterMark(canvas);
// 坐标轴刻度在最底层
drawAxis(canvas);
drawGridLine(canvas);
if (getDrawLabelsInBottom()) drawLabels(canvas);
createBitmapCache(canvas);
if (getBitmapCanvas() != null) {
render(getBitmapCanvas());
canvas.drawBitmap(getDrawBitmap(), 0, 0, getRenderPaint());
}
// Removes clipping rectangle
canvas.restoreToCount(clipRestoreCount);
if (getDrawLabelsInBottom()) {
drawHighlightLeft(canvas);
drawHighlightRight(canvas);
canvas.clipRect(mBottomRect);
drawLabels(canvas);
} else {
drawLabels(canvas);
drawHighlightLeft(canvas);
drawHighlightRight(canvas);
}
drawHighlightBottom(canvas);
}
protected void createBitmapCache(Canvas canvas) {
int width = getContentRect().width() + getContentRect().left;
int height = getContentRect().height();
if (mDrawBitmap == null
|| (mDrawBitmap.get() == null)
|| (mDrawBitmap.get().getWidth() != width)
|| (mDrawBitmap.get().getHeight() != height)) {
if (width > 0 && height > 0) {
mDrawBitmap = new WeakReference<>(Bitmap.createBitmap(getResources().getDisplayMetrics(), width, height, mBitmapConfig));
mBitmapCanvas = new Canvas(mDrawBitmap.get());
} else
return;
}
mDrawBitmap.get().eraseColor(Color.TRANSPARENT);
}
protected Bitmap getDrawBitmap() {
return mDrawBitmap.get();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
releaseBitmap();
}
public void releaseBitmap() {
if (mBitmapCanvas != null) {
mBitmapCanvas.setBitmap(null);
mBitmapCanvas = null;
}
if (mDrawBitmap != null) {
if (mDrawBitmap.get() != null) mDrawBitmap.get().recycle();
mDrawBitmap.clear();
mDrawBitmap = null;
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,79 @@
package cn.jingzhuan.lib.chart2.base;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.view.MotionEvent;
import cn.jingzhuan.lib.chart.component.Highlight;
public interface IChart {
/**
* 初始化 RendererAnimator
*/
void initChart();
/**
* 画坐标轴
*/
void drawAxis(Canvas canvas);
/**
* 画坐标轴文本
*/
void drawLabels(Canvas canvas);
/**
* 画网格线
*/
void drawGridLine(Canvas canvas);
/**
* 画水印
*/
void drawWaterMark(Canvas canvas);
void drawHighlightLeft(Canvas canvas);
void drawHighlightRight(Canvas canvas);
void drawHighlightBottom(Canvas canvas);
/**
* 渲染图表
*/
void render(Canvas canvas);
/**
* 获取当前Renderer的Painter
*/
Paint getRenderPaint();
/**
* 获取当前图表设置的背景颜色
*/
int getBackgroundColor();
/**
* 十字光标选中 信息更新
*/
void highlightValue(Highlight highlight);
/**
* 清除十字光标
*/
void cleanHighlight();
/**
* 根据当前点获取下标
*/
int getEntryIndexByCoordinate(float x, float y);
float getEntryCoordinateByIndex(int index);
void onTouchPoint(MotionEvent e);
void onTouchHighlight(MotionEvent e);
boolean getDrawLabelsInBottom();
}
@@ -0,0 +1,182 @@
package cn.jingzhuan.lib.chart2.renderer;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.util.Log;
import androidx.annotation.NonNull;
import cn.jingzhuan.lib.chart.Viewport;;
import cn.jingzhuan.lib.chart.data.AbstractDataSet;
import cn.jingzhuan.lib.chart.animation.ChartAnimator;
import cn.jingzhuan.lib.chart.renderer.Renderer;
import cn.jingzhuan.lib.chart2.base.BaseChart;
import cn.jingzhuan.lib.chart2.base.Chart;
import cn.jingzhuan.lib.chart.component.Highlight;
import cn.jingzhuan.lib.chart.data.ChartData;
import java.util.List;
/**
* Created by Donglua on 17/7/19.
*/
public abstract class AbstractDataRenderer<T extends AbstractDataSet> implements Renderer {
protected Viewport mViewport;
protected Rect mContentRect;
protected Paint mRenderPaint;
protected DashPathEffect mHighlightedDashPathEffect;
private int mHighlightColor = Color.WHITE;
protected ChartAnimator mChartAnimator;
private float mHighlightThickness = 3;
protected Paint mHighlightLinePaint;
public AbstractDataRenderer(Chart chart) {
this.mViewport = chart.getCurrentViewport();
this.mContentRect = chart.getContentRect();
if (chart instanceof BaseChart) {
this.mChartAnimator = ((BaseChart) chart).getChartAnimator();
}
getChartData().setChart(chart);
mRenderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mRenderPaint.setStyle(Paint.Style.STROKE);
mHighlightLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mHighlightLinePaint.setStyle(Paint.Style.FILL);
mHighlightLinePaint.setStrokeWidth(mHighlightThickness);
}
@Override
public final void renderer(Canvas canvas) {
renderDataSet(canvas);
}
protected void renderDataSet(Canvas canvas) {
renderDataSet(canvas, getChartData());
}
protected abstract void renderDataSet(Canvas canvas, ChartData<T> chartData);
protected abstract void renderDataSet(Canvas canvas, ChartData<T> chartData, T dataSet);
public abstract void renderHighlighted(Canvas canvas, @NonNull Highlight[] highlights);
/**
* Computes the pixel offset for the given X lib value. This may be outside the view bounds.
*/
protected float getDrawX(float x) {
return mContentRect.left
+ mContentRect.width() * (x - mViewport.left) / mViewport.width();
}
/**
* Computes the pixel offset for the given Y lib value. This may be outside the view bounds.
*/
protected float getDrawY(float y) {
return mContentRect.bottom
- mContentRect.height() * (y - mViewport.top) / mViewport.height();
}
public void addDataSet(T dataSet) {
if (dataSet == null) return;
getChartData().add(dataSet);
calcDataSetMinMax();
}
public abstract void removeDataSet(T dataSet);
public abstract void clearDataSet();
protected abstract List<T> getDataSet();
public abstract ChartData<T> getChartData();
public int getEntryIndexByCoordinate(float x, float y) {
float valueCount = getChartData().getEntryCount();
int index =
(int) (((x - mContentRect.left) * mViewport.width() / mContentRect.width() + mViewport.left) * valueCount);
if (index >= getChartData().getEntryCount()) index = getChartData().getEntryCount() - 1;
if (index < 0) index = 0;
return index;
}
public float getEntryCoordinateByIndex(int index) {
float valueCount = getChartData().getEntryCount();
float x = mContentRect.left + ((index / valueCount - mViewport.left) / mViewport.width()) * mContentRect.width();
if (x > mContentRect.right) x = mContentRect.right;
if (x < mContentRect.left) x = mContentRect.left;
return x;
}
protected void calcDataSetMinMax() {
getChartData().calcMaxMin(mViewport, mContentRect);
}
public void setHighlightColor(int highlightColor) {
this.mHighlightColor = highlightColor;
mHighlightLinePaint.setColor(mHighlightColor);
}
public int getHighlightColor() {
return mHighlightColor;
}
public void enableHighlightDashPathEffect(float[] intervals, float phase) {
mHighlightedDashPathEffect = new DashPathEffect(intervals, phase);
mHighlightLinePaint.setPathEffect(mHighlightedDashPathEffect);
}
public DashPathEffect getHighlightDashPathEffect() {
return this.mHighlightedDashPathEffect;
}
public void setMaxVisibleEntryCount(int maxVisibleEntryCount) {
if (maxVisibleEntryCount <= 0) return;
getChartData().setMaxVisibleEntryCount(maxVisibleEntryCount);
}
public void setMinVisibleEntryCount(int minVisibleEntryCount) {
if (minVisibleEntryCount <= 0) return;
getChartData().setMinVisibleEntryCount(minVisibleEntryCount);
}
public void setDefaultVisibleEntryCount(int defaultVisibleEntryCount) {
if (defaultVisibleEntryCount <= 0) return;
getChartData().setDefaultVisibleEntryCount(defaultVisibleEntryCount);
}
public Paint getRenderPaint() {
return mRenderPaint;
}
public ChartAnimator getChartAnimator() {
return mChartAnimator;
}
public float getHighlightThickness() {
return mHighlightThickness;
}
public void setHighlightThickness(float highlightThickness) {
this.mHighlightThickness = highlightThickness;
mHighlightLinePaint.setStrokeWidth(mHighlightThickness);
}
public Paint getHighlightLinePaint() {
return mHighlightLinePaint;
}
public void setTypeface(Typeface tf) {
}
}
@@ -0,0 +1,311 @@
package cn.jingzhuan.lib.chart2.renderer;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Shader;
import android.graphics.Typeface;
import androidx.annotation.NonNull;
import cn.jingzhuan.lib.chart.data.BarDataSet;
import cn.jingzhuan.lib.chart.data.BarValue;
import cn.jingzhuan.lib.chart.Viewport;
import cn.jingzhuan.lib.chart2.base.Chart;
import cn.jingzhuan.lib.chart.component.AxisY;
import cn.jingzhuan.lib.chart.component.Highlight;
import cn.jingzhuan.lib.chart.data.BarData;
import cn.jingzhuan.lib.chart.data.ChartData;
import cn.jingzhuan.lib.chart.data.ValueFormatter;
import cn.jingzhuan.lib.chart.event.OnViewportChangeListener;
import cn.jingzhuan.lib.chart.utils.FloatUtils;
import java.util.List;
/**
* Created by Donglua on 17/8/1.
*/
public class BarChartRenderer extends AbstractDataRenderer<BarDataSet> {
private BarData mBarDataSets;
protected final char[] mLabelBuffer = new char[100];
protected Paint mValueTextPaint;
private final Chart chart;
public BarChartRenderer(final Chart chart) {
super(chart);
this.chart = chart;
mValueTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mValueTextPaint.setStyle(Paint.Style.FILL);
chart.setInternalViewportChangeListener(new OnViewportChangeListener() {
@Override
public void onViewportChange(Viewport viewport) {
mViewport.set(viewport);
calcDataSetMinMax();
}
});
final Highlight highlight = new Highlight();
chart.addOnTouchPointChangeListener(new Chart.OnTouchPointChangeListener() {
@Override
public void touch(float x, float y) {
if (chart.isHighlightDisable()) return;
for (BarDataSet dataSet : getDataSet()) {
if (dataSet.isHighlightedVerticalEnable()) {
highlight.setTouchX(x);
highlight.setTouchY(y);
int index = getEntryIndexByCoordinate(x, y) - dataSet.getStartIndexOffset();
if (index < dataSet.getValues().size()) {
BarValue barValue = dataSet.getEntryForIndex(index);
float xPosition = Math.max(mContentRect.left, barValue.getX());
float yPosition = barValue.getY();
if (xPosition >= 0 && yPosition >= 0) {
highlight.setX(xPosition);
highlight.setY(yPosition);
highlight.setDataIndex(index);
chart.highlightValue(highlight);
}
}
}
}
}
});
}
@Override
protected void renderDataSet(Canvas canvas, ChartData<BarDataSet> chartData) {
for (BarDataSet dataSet : chartData.getDataSets()) {
renderDataSet(canvas, chartData, dataSet);
}
}
@Override
protected void renderDataSet(Canvas canvas, ChartData<BarDataSet> chartData, BarDataSet dataSet) {
if (dataSet.isVisible()) {
drawBarDataSet(canvas, dataSet,
chartData.getLeftMax(), chartData.getLeftMin(),
chartData.getRightMax(), chartData.getRightMin());
}
}
private void drawBarDataSet(Canvas canvas, BarDataSet barDataSet,
float lMax, float lMin, float rMax, float rMin) {
float strokeThickness = barDataSet.getStrokeThickness();
mRenderPaint.setStrokeWidth(strokeThickness);
mRenderPaint.setStyle(Paint.Style.FILL);
mValueTextPaint.setColor(barDataSet.getValueColor());
mValueTextPaint.setTextSize(barDataSet.getValueTextSize());
int valueCount = barDataSet.getEntryCount();
float min, max;
switch (barDataSet.getAxisDependency()) {
case AxisY.DEPENDENCY_RIGHT:
min = rMin;
max = rMax;
break;
case AxisY.DEPENDENCY_BOTH:
case AxisY.DEPENDENCY_LEFT:
default:
min = lMin;
max = lMax;
break;
}
// float strokeThicknessPercent = (max - min) * (strokeThickness / mContentRect.height());
// min = min - strokeThicknessPercent;
// max = max + strokeThicknessPercent;
float width = barDataSet.getBarWidth();
float visibleRange = barDataSet.getVisibleRange(mViewport);
if (barDataSet.isAutoBarWidth() && visibleRange > 0) {
width = mContentRect.width() / visibleRange;
if (barDataSet.getMinBarWidth() > 0.0f && width < barDataSet.getMinBarWidth()) {
width = barDataSet.getMinBarWidth();
}
}
final float percent = barDataSet.getBarWidthPercent();
final float scale = 1 / mViewport.width();
final float step = mContentRect.width() * scale / valueCount;
final float startX = mContentRect.left - mViewport.left * mContentRect.width() * scale;
for (int i = 0; i < valueCount && i < barDataSet.getValues().size(); i++) {
BarValue barValue = barDataSet.getEntryForIndex(i);
if (!barValue.isEnable()) continue;
if (barValue.getValues().length < 1 || Float.isNaN(barValue.getValues()[0])) continue;
if (barValue.getColor() != -2) {
mRenderPaint.setColor(barValue.getColor());
} else {
mRenderPaint.setColor(barDataSet.getColor());
}
float x = startX + step * (i + barDataSet.getStartIndexOffset());
float top;
float bottom = calcHeight(0, max, min);
if (barValue.getValueCount() > 0) {
float value = barValue.getValues()[0] * mChartAnimator.getPhaseY();
Paint.Style style = barValue.getPaintStyle();
top = calcHeight(value, max, min);
if (top == 0 && (style == Paint.Style.STROKE)) {
top = top + strokeThickness;
}
if (width < strokeThickness * 2 && (style == Paint.Style.STROKE)) {
style = Paint.Style.FILL;
}
if (barValue.getValueCount() > 1)
bottom = calcHeight(barValue.getValues()[1], max, min);
int roundBottom = Math.round(bottom);
if (roundBottom == mContentRect.height()) {
bottom = mContentRect.height() - strokeThickness;
if (style == Paint.Style.FILL) {
bottom = bottom + strokeThickness / 2;
}
}
barValue.setCoordinate(x + width * 0.5f, top);
mRenderPaint.setStyle(style);
float left = x + width * (1 - percent) * 0.5f;
float right = left + width * percent;
if (Math.abs(top - bottom) < 0.0001) {
canvas.drawLine(left, top, right, bottom, mRenderPaint);
} else {
if (barValue.getGradientColors() != null && barValue.getGradientColors().length > 1) {
float centerX = (left + right) * 0.5f;
mRenderPaint.setShader(
new LinearGradient(centerX, top, centerX, bottom,
barValue.getGradientColors()[0],
barValue.getGradientColors()[1], Shader.TileMode.MIRROR));
}
canvas.drawRect(left, top, right, bottom, mRenderPaint);
}
int labelLength;
int labelOffset;
if (barDataSet.isDrawValueEnable()) {
ValueFormatter valueFormatter;
valueFormatter = barDataSet.getValueFormatter();
if (valueFormatter == null) {
labelLength = FloatUtils.formatFloatValue(mLabelBuffer, value, 2);
} else {
char[] labelCharArray = valueFormatter.format(barValue.getValues()[0], i).toCharArray();
labelLength = labelCharArray.length;
System.arraycopy(labelCharArray,
0,
mLabelBuffer,
mLabelBuffer.length - labelLength,
labelLength);
}
labelOffset = mLabelBuffer.length - labelLength;
mValueTextPaint.setTextAlign(Paint.Align.CENTER);
canvas.drawText(mLabelBuffer, labelOffset, labelLength,
x + width * 0.5f,
top - 10, mValueTextPaint);
}
}
mRenderPaint.setShader(null);
}
mRenderPaint.setStyle(Paint.Style.FILL);
}
protected float calcHeight(float value, float max, float min) {
if (Float.compare(max, min) == 0) return 0;
return (max - value) / (max - min) * mContentRect.height();
}
@Override
public void renderHighlighted(Canvas canvas, @NonNull Highlight[] highlights) {
mRenderPaint.setColor(getHighlightColor());
mRenderPaint.setStrokeWidth(getHighlightThickness());
mRenderPaint.setStyle(Paint.Style.STROKE);
if (mHighlightedDashPathEffect != null) {
mRenderPaint.setPathEffect(mHighlightedDashPathEffect);
}
for (Highlight highlight : highlights) {
if (highlight != null) {
if (!chart.isEnableVerticalHighlight()) {
canvas.drawLine(highlight.getX(),
0,
highlight.getX(),
mContentRect.bottom,
mRenderPaint);
}
// Horizontal
if (!Float.isNaN(highlight.getY())) {
for (BarDataSet barDataSet : getDataSet()) {
if (barDataSet.isHighlightedHorizontalEnable() && !chart.isEnableHorizontalHighlight()) {
float y = highlight.getY();
if (y < mContentRect.top + getHighlightThickness() * 0.5f) {
y = mContentRect.top + getHighlightThickness() * 0.5f;
}
if (y > mContentRect.bottom - getHighlightThickness() * 0.5f){
y = mContentRect.bottom - getHighlightThickness() * 0.5f;
}
canvas.drawLine(0,
y,
mContentRect.right,
y,
mRenderPaint);
}
}
}
}
}
mRenderPaint.setPathEffect(null);
}
@Override
public void removeDataSet(BarDataSet dataSet) {
if (dataSet == null) return;
mBarDataSets.remove(dataSet);
calcDataSetMinMax();
}
@Override
public void clearDataSet() {
mBarDataSets.clear();
calcDataSetMinMax();
}
@Override
protected List<BarDataSet> getDataSet() {
return mBarDataSets.getDataSets();
}
@Override
public BarData getChartData() {
if (mBarDataSets == null) mBarDataSets = new BarData();
return mBarDataSets;
}
@Override
public void setTypeface(Typeface tf) {
mValueTextPaint.setTypeface(tf);
}
}
@@ -0,0 +1,365 @@
package cn.jingzhuan.lib.chart2.renderer;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import androidx.annotation.NonNull;
import android.util.Pair;
import cn.jingzhuan.lib.chart.Viewport;
import cn.jingzhuan.lib.chart.data.CandlestickDataSet;
import cn.jingzhuan.lib.chart.data.CandlestickValue;
import cn.jingzhuan.lib.chart.renderer.CandlestickDataSetArrowDecorator;
import cn.jingzhuan.lib.chart2.base.Chart;
import cn.jingzhuan.lib.chart.component.AxisY;
import cn.jingzhuan.lib.chart.component.Highlight;
import cn.jingzhuan.lib.chart.data.CandlestickData;
import cn.jingzhuan.lib.chart.event.OnViewportChangeListener;
import cn.jingzhuan.lib.chart.data.ChartData;
import java.util.List;
/**
* Created by donglua on 8/29/17.
*/
public class CandlestickChartRenderer extends AbstractDataRenderer<CandlestickDataSet> {
private final float[] mUpperShadowBuffers = new float[4];
private final float[] mLowerShadowBuffers = new float[4];
private final float[] mBodyBuffers = new float[4];
private CandlestickData chartData;
protected Paint mHighlightRenderPaint;
private final Chart chart;
public CandlestickChartRenderer(final Chart chart) {
super(chart);
this.chart = chart;
chart.setInternalViewportChangeListener(new OnViewportChangeListener() {
@Override
public void onViewportChange(Viewport viewport) {
mViewport.set(viewport);
calcDataSetMinMax();
}
});
final Highlight highlight = new Highlight();
chart.addOnTouchPointChangeListener((x, y) -> {
if (chart.isHighlightDisable()) return;
for (CandlestickDataSet dataSet : getDataSet()) {
if (dataSet.isHighlightedVerticalEnable()) {
final int valueCount = dataSet.getEntryCount();
float xPosition;
float yPosition;
highlight.setTouchX(x);
highlight.setTouchY(y);
int index = getEntryIndexByCoordinate(x, y) - dataSet.getStartIndexOffset();
if (index < valueCount && index >= 0 && index < dataSet.getValues().size()) {
final CandlestickValue candlestickValue = dataSet.getEntryForIndex(index);
xPosition = Math.max(mContentRect.left, candlestickValue.getX());
yPosition = candlestickValue.getY();
if (xPosition >= 0 && yPosition >= 0) {
highlight.setX(xPosition);
highlight.setY(yPosition);
highlight.setDataIndex(index);
chart.highlightValue(highlight);
}
}
}
}
});
mHighlightRenderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mHighlightRenderPaint.setStyle(Paint.Style.FILL);
}
@Override
protected void renderDataSet(Canvas canvas, ChartData<CandlestickDataSet> chartData) {
for (CandlestickDataSet dataSet : chartData.getDataSets()) {
renderDataSet(canvas, chartData, dataSet);
}
}
@Override
protected void renderDataSet(Canvas canvas, ChartData<CandlestickDataSet> chartData, CandlestickDataSet dataSet) {
if (dataSet.isVisible()) {
drawDataSet(canvas, dataSet,
chartData.getLeftMax(), chartData.getLeftMin(),
chartData.getRightMax(), chartData.getRightMin());
}
}
private void drawDataSet(Canvas canvas, CandlestickDataSet candlestickDataSet,
float lMax, float lMin, float rMax, float rMin) {
float min, max;
switch (candlestickDataSet.getAxisDependency()) {
case AxisY.DEPENDENCY_RIGHT:
min = rMin;
max = rMax;
break;
case AxisY.DEPENDENCY_BOTH:
case AxisY.DEPENDENCY_LEFT:
default:
min = lMin;
max = lMax;
break;
}
mRenderPaint.setStrokeWidth(candlestickDataSet.getStrokeThickness());
mRenderPaint.setColor(candlestickDataSet.getColor());
int valueCount = candlestickDataSet.getEntryCount();
final List<CandlestickValue> visibleValues = candlestickDataSet.getVisiblePoints(mViewport);
final float visibleRange = candlestickDataSet.getVisibleRange(mViewport);
final double scale = 1.0 / mViewport.width();
final double step = mContentRect.width() * scale / valueCount;
final double startX = mContentRect.left - mViewport.left * mContentRect.width() * scale;
double candleWidth = candlestickDataSet.getCandleWidth();
if (candlestickDataSet.isAutoWidth()) {
candleWidth = mContentRect.width() / Math.max(visibleRange, candlestickDataSet.getMinValueCount());
}
float widthPercent = candlestickDataSet.getCandleWidthPercent();
for (int i = 0; i < valueCount && i < candlestickDataSet.getValues().size(); i++) {
CandlestickValue candlestick = candlestickDataSet.getEntryForIndex(i);
if (!candlestick.isVisible()) continue;
if (!visibleValues.contains(candlestick)) {
continue;
}
double xPosition = startX + step * (i + candlestickDataSet.getStartIndexOffset());
final double candlestickCenterX = xPosition + candleWidth * 0.5;
if (candlestickDataSet.isEnableGap()) {
mRenderPaint.setColor(candlestickDataSet.getGapColor());
mRenderPaint.setStyle(Paint.Style.FILL);
if (candlestickDataSet.getLowGaps().size() > 0) {
// 缺口
final Pair<Float, Float> gap = candlestickDataSet.getLowGaps().get(i, null);
if (gap != null) {
float y1 = (max - gap.first) / (max - min) * mContentRect.height();
float y2 = (max - gap.second) / (max - min) * mContentRect.height();
canvas.drawRect((float) xPosition,
y1,
mContentRect.right,
y2, mRenderPaint);
}
}
if (candlestickDataSet.getHighGaps().size() > 0) {
// 缺口
final Pair<Float, Float> gap = candlestickDataSet.getHighGaps().get(i, null);
if (gap != null) {
float y1 = (max - gap.first) / (max - min) * mContentRect.height();
float y2 = (max - gap.second) / (max - min) * mContentRect.height();
canvas.drawRect((float) candlestickCenterX,
y1,
mContentRect.right,
y2, mRenderPaint);
}
}
}
if (candlestick.getFillBackgroundColor() != CandlestickValue.COLOR_NONE) { // 画背景
mRenderPaint.setColor(candlestick.getFillBackgroundColor());
mRenderPaint.setStyle(Paint.Style.FILL);
canvas.drawRect((float) xPosition, 0,
(float) (xPosition + candleWidth), canvas.getHeight(), mRenderPaint);
}
float highY = (max - candlestick.getHigh()) / (max - min) * mContentRect.height();
float lowY = (max - candlestick.getLow()) / (max - min) * mContentRect.height();
float openY = (max - candlestick.getOpen()) / (max - min) * mContentRect.height();
float closeY = (max - candlestick.getClose()) / (max - min) * mContentRect.height();
mBodyBuffers[0] = (float) (xPosition + (1 - widthPercent) * 0.5 * candleWidth);
mBodyBuffers[1] = closeY;
mBodyBuffers[2] = (float) (mBodyBuffers[0] + candleWidth * widthPercent);
mBodyBuffers[3] = openY;
mUpperShadowBuffers[0] = (float) candlestickCenterX;
mUpperShadowBuffers[2] = (float) candlestickCenterX;
mLowerShadowBuffers[0] = (float) candlestickCenterX;
mLowerShadowBuffers[2] = (float) candlestickCenterX;
candlestick.setCoordinate((float) candlestickCenterX, closeY);
if (Float.compare(candlestick.getOpen(), candlestick.getClose()) > 0) { // 阴线
mUpperShadowBuffers[1] = highY;
mUpperShadowBuffers[3] = openY;
mLowerShadowBuffers[1] = lowY;
mLowerShadowBuffers[3] = closeY;
if (candlestick.getColor() == CandlestickValue.COLOR_NONE) {
mRenderPaint.setColor(candlestickDataSet.getDecreasingColor());
} else {
mRenderPaint.setColor(candlestick.getColor());
}
if (candlestick.getPaintStyle() != null) {
mRenderPaint.setStyle(candlestick.getPaintStyle());
} else {
mRenderPaint.setStyle(candlestickDataSet.getDecreasingPaintStyle());
}
} else if (Float.compare(candlestick.getOpen(), candlestick.getClose()) < 0) { // 阳线
mUpperShadowBuffers[1] = highY;
mUpperShadowBuffers[3] = closeY;
mLowerShadowBuffers[1] = lowY;
mLowerShadowBuffers[3] = openY;
if (candlestick.getColor() == CandlestickValue.COLOR_NONE) {
mRenderPaint.setColor(candlestickDataSet.getIncreasingColor());
} else {
mRenderPaint.setColor(candlestick.getColor());
}
if (candlestick.getPaintStyle() != null) {
mRenderPaint.setStyle(candlestick.getPaintStyle());
} else {
mRenderPaint.setStyle(candlestickDataSet.getIncreasingPaintStyle());
}
} else {
mUpperShadowBuffers[1] = highY;
mUpperShadowBuffers[3] = openY;
mLowerShadowBuffers[1] = lowY;
mLowerShadowBuffers[3] = mUpperShadowBuffers[3];
if (candlestick.getColor() == CandlestickValue.COLOR_NONE) {
mRenderPaint.setColor(candlestickDataSet.getNeutralColor());
} else {
mRenderPaint.setColor(candlestick.getColor());
}
}
if (i > 0) {
final CandlestickValue previousValue = candlestickDataSet.getEntryForIndex(i - 1);
boolean isLimitUp20 = (Float.compare(
candlestick.getClose(),
previousValue.getClose() * 1.2f - 0.01f) > 0) && candlestick.getClose() == candlestick.getHigh();
boolean isLimitUp = (Float.compare(
candlestick.getClose(),
previousValue.getClose() * 1.1f - 0.01f) > 0) && candlestick.getClose() == candlestick.getHigh();
if (candlestickDataSet.getLimitUpColor() != Color.TRANSPARENT) {
if (isLimitUp20) {
mRenderPaint.setColor(candlestickDataSet.getLimitUpColor20());
} else if (isLimitUp) {
mRenderPaint.setColor(candlestickDataSet.getLimitUpColor());
}
}
if (candlestickDataSet.getLimitUpPaintStyle() != null) {
if (isLimitUp || isLimitUp20) {
mRenderPaint.setStyle(candlestickDataSet.getLimitUpPaintStyle());
}
}
}
if (mBodyBuffers[1] == mBodyBuffers[3]) {
canvas.drawLine(mBodyBuffers[0],
mBodyBuffers[1],
mBodyBuffers[2],
mBodyBuffers[3], mRenderPaint);
} else {
canvas.drawRect(mBodyBuffers[0],
mBodyBuffers[1],
mBodyBuffers[2],
mBodyBuffers[3], mRenderPaint);
}
canvas.drawLines(mUpperShadowBuffers, mRenderPaint);
canvas.drawLines(mLowerShadowBuffers, mRenderPaint);
if (candlestickDataSet instanceof CandlestickDataSetArrowDecorator) {
int decimalDigitsNumber = chart.getDecimalDigitsNumber();
CandlestickDataSetArrowDecorator arrowDecorator = (CandlestickDataSetArrowDecorator) candlestickDataSet;
arrowDecorator.draw(canvas, candlestick, mContentRect, (float) candleWidth, (float) xPosition, highY, lowY, decimalDigitsNumber);
}
}
if (candlestickDataSet instanceof CandlestickDataSetArrowDecorator) {
((CandlestickDataSetArrowDecorator) candlestickDataSet).reset();
}
}
@Override
public void renderHighlighted(Canvas canvas, @NonNull Highlight[] highlights) {
mHighlightRenderPaint.setColor(getHighlightColor());
if (mHighlightedDashPathEffect != null) {
mHighlightRenderPaint.setPathEffect(mHighlightedDashPathEffect);
}
mHighlightRenderPaint.setStrokeWidth(getHighlightThickness());
mHighlightRenderPaint.setStyle(Paint.Style.FILL);
for (Highlight highlight : highlights) {
for (CandlestickDataSet dataSet : getDataSet()) {
if (dataSet.isHighlightedVerticalEnable() && !chart.isEnableVerticalHighlight()) {
canvas.drawLine(highlight.getX(),
mContentRect.top,
highlight.getX(),
mContentRect.bottom,
mHighlightRenderPaint);
}
if (dataSet.isHighlightedHorizontalEnable() && !chart.isEnableHorizontalHighlight()) {
float y = highlight.getY();
if (y < mContentRect.top + getHighlightThickness() * 0.5f) {
y = mContentRect.top + getHighlightThickness() * 0.5f;
}
if (y > mContentRect.bottom - getHighlightThickness() * 0.5f){
y = mContentRect.bottom - getHighlightThickness() * 0.5f;
}
canvas.drawLine(mContentRect.left,
y,
mContentRect.right,
y,
mHighlightRenderPaint);
}
}
}
mHighlightRenderPaint.setPathEffect(null);
}
@Override
public void removeDataSet(CandlestickDataSet dataSet) {
getChartData().remove(dataSet);
calcDataSetMinMax();
}
@Override
public void clearDataSet() {
getChartData().clear();
getChartData().calcMaxMin(mViewport, mContentRect);
}
@Override
public List<CandlestickDataSet> getDataSet() {
return chartData.getDataSets();
}
@Override
public ChartData<CandlestickDataSet> getChartData() {
if (chartData == null)
chartData = new CandlestickData();
return chartData;
}
}
@@ -0,0 +1,434 @@
package cn.jingzhuan.lib.chart2.renderer;
import android.graphics.Canvas;
import android.graphics.Typeface;
import androidx.annotation.NonNull;
import org.jetbrains.annotations.NotNull;
import cn.jingzhuan.lib.chart.Viewport;
import cn.jingzhuan.lib.chart.data.AbstractDataSet;
import cn.jingzhuan.lib.chart.data.BarDataSet;
import cn.jingzhuan.lib.chart.data.CandlestickDataSet;
import cn.jingzhuan.lib.chart.data.LineDataSet;
import cn.jingzhuan.lib.chart.data.PointLineDataSet;
import cn.jingzhuan.lib.chart.data.ScatterDataSet;
import cn.jingzhuan.lib.chart.data.ScatterTextDataSet;
import cn.jingzhuan.lib.chart.data.TreeDataSet;
import cn.jingzhuan.lib.chart.utils.RequestDataType;
import cn.jingzhuan.lib.chart2.base.Chart;
import cn.jingzhuan.lib.chart.component.Highlight;
import cn.jingzhuan.lib.chart.data.ChartData;
import cn.jingzhuan.lib.chart.data.CombineData;
import cn.jingzhuan.lib.chart.event.OnViewportChangeListener;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Donglua on 17/8/2.
*/
public class CombineChartRenderer extends AbstractDataRenderer {
protected TreeChartRenderer treeChartRenderer;
protected BarChartRenderer barChartRenderer;
protected LineRenderer lineRenderer;
protected CandlestickChartRenderer candlestickChartRenderer;
protected ScatterChartRenderer scatterChartRenderer;
//K线区域选择的renderer
public RangeRenderer rangeRenderer;
protected PointLineRenderer pointLineRenderer;
protected ScatterTextRenderer scatterTextRenderer;
private final Chart chart;
private CombineData combineData;
private int lastDataSize = 0;
public CombineChartRenderer(final Chart chart) {
super(chart);
treeChartRenderer = initTreeChartRenderer(chart);
lineRenderer = initLineRenderer(chart);
barChartRenderer = initBarChartRenderer(chart);
candlestickChartRenderer = initCandlestickChartRenderer(chart);
scatterChartRenderer = initScatterChartRenderer(chart);
rangeRenderer = initRangeChartRenderer(chart);
pointLineRenderer = initPointLineRenderer(chart);
scatterTextRenderer = initScatterTextRenderer(chart);
this.chart = chart;
chart.setInternalViewportChangeListener(new OnViewportChangeListener() {
@Override
public void onViewportChange(Viewport viewport) {
mViewport.set(viewport);
calcDataSetMinMax();
}
});
}
private TreeChartRenderer initTreeChartRenderer(Chart chart) {
return new TreeChartRenderer(chart);
}
private ScatterTextRenderer initScatterTextRenderer(Chart chart) {
return new ScatterTextRenderer(chart);
}
private PointLineRenderer initPointLineRenderer(Chart chart) {
return new PointLineRenderer(chart);
}
private RangeRenderer initRangeChartRenderer(Chart chart) {
return new RangeRenderer(chart);
}
@NotNull
private ScatterChartRenderer initScatterChartRenderer(Chart chart) {
return new ScatterChartRenderer(chart);
}
@NotNull
protected CandlestickChartRenderer initCandlestickChartRenderer(Chart chart) {
return new CandlestickChartRenderer(chart);
}
@NotNull
protected BarChartRenderer initBarChartRenderer(Chart chart) {
return new BarChartRenderer(chart);
}
@NotNull
protected LineRenderer initLineRenderer(Chart chart) {
return new LineRenderer(chart);
}
@Override
protected void renderDataSet(Canvas canvas) {
CombineData combineData = getChartData();
List<AbstractDataSet<?>> sortedDataSets = combineData.getAllDataSet();
List<AbstractDataSet<?>> candlestickDataSets = new ArrayList<>();
// 按云引擎指定顺序绘制dataSet
for (int i = 0; i < sortedDataSets.size(); i++) {
AbstractDataSet<?> dataSet = sortedDataSets.get(i);
if (dataSet instanceof TreeDataSet) {
treeChartRenderer.renderDataSet(canvas, combineData.getTreeChartData(), (TreeDataSet) dataSet);
}
if (dataSet instanceof CandlestickDataSet) {
candlestickChartRenderer.renderDataSet(canvas, combineData.getCandlestickChartData(), (CandlestickDataSet) dataSet);
candlestickDataSets.add(dataSet);
}
if (dataSet instanceof LineDataSet) {
lineRenderer.renderDataSet(canvas, combineData.getLineChartData(), (LineDataSet) dataSet);
}
if (dataSet instanceof BarDataSet) {
barChartRenderer.renderDataSet(canvas, combineData.getBarChartData(), (BarDataSet) dataSet);
}
if (dataSet instanceof ScatterDataSet) {
scatterChartRenderer.renderDataSet(canvas, combineData.getScatterChartData(), (ScatterDataSet) dataSet);
}
if (dataSet instanceof PointLineDataSet) {
pointLineRenderer.renderDataSet(canvas, combineData.getPointLineChartData(), (PointLineDataSet) dataSet);
}
if (dataSet instanceof ScatterTextDataSet) {
scatterTextRenderer.renderDataSet(canvas, combineData.getScatterTextChartData(), (ScatterTextDataSet) dataSet);
}
}
if (!candlestickDataSets.isEmpty()) {
// k线叠加时 区间统计只画一次
CandlestickDataSet candlestickDataSet = (CandlestickDataSet) candlestickDataSets.get(0);
if (chart.getRangeEnable()) {
rangeRenderer.renderDataSet(canvas, combineData.getCandlestickChartData(), candlestickDataSet);
}
}
}
@Override
protected void renderDataSet(Canvas canvas, ChartData chartData) {
// ignore
}
@Override
protected void renderDataSet(Canvas canvas, ChartData chartData, AbstractDataSet dataSet) {
// ignore
}
@Override
public void setHighlightColor(int highlightColor) {
super.setHighlightColor(highlightColor);
treeChartRenderer.setHighlightColor(highlightColor);
lineRenderer.setHighlightColor(highlightColor);
barChartRenderer.setHighlightColor(highlightColor);
candlestickChartRenderer.setHighlightColor(highlightColor);
scatterChartRenderer.setHighlightColor(highlightColor);
rangeRenderer.setHighlightColor(highlightColor);
pointLineRenderer.setHighlightColor(highlightColor);
}
@Override
public void renderHighlighted(Canvas canvas, @NonNull Highlight[] highlights) {
if (treeChartRenderer.getDataSet() != null && !treeChartRenderer.getDataSet().isEmpty()) {
treeChartRenderer.renderHighlighted(canvas, highlights);
}
if (lineRenderer.getDataSet() != null && !lineRenderer.getDataSet().isEmpty()) {
lineRenderer.renderHighlighted(canvas, highlights);
}
if (barChartRenderer.getDataSet() != null && !barChartRenderer.getDataSet().isEmpty()) {
barChartRenderer.renderHighlighted(canvas, highlights);
}
if (candlestickChartRenderer.getDataSet() != null && !candlestickChartRenderer.getDataSet().isEmpty()) {
candlestickChartRenderer.renderHighlighted(canvas, highlights);
}
if (scatterChartRenderer.getDataSet() != null && !scatterChartRenderer.getDataSet().isEmpty()) {
scatterChartRenderer.renderHighlighted(canvas, highlights);
}
if (rangeRenderer.getDataSet() != null && !rangeRenderer.getDataSet().isEmpty()) {
rangeRenderer.renderHighlighted(canvas, highlights);
}
if (pointLineRenderer.getDataSet() != null && !pointLineRenderer.getDataSet().isEmpty()) {
pointLineRenderer.renderHighlighted(canvas, highlights);
}
}
@Override
public void addDataSet(AbstractDataSet dataSet) {
if (dataSet == null) return;
getChartData().add(dataSet);
int dataSize = dataSet.getValues().size();
if (lastDataSize == 0) lastDataSize = dataSize;
if (RequestDataType.DATA_TYPE == RequestDataType.DATA_TYPE_RANGE) {
if (lastDataSize != dataSize) {
int from = Math.round(mViewport.left * lastDataSize) + (dataSize - lastDataSize);
int to = Math.round(mViewport.right * lastDataSize) + (dataSize - lastDataSize);
mViewport.left = from / (float) dataSize;
mViewport.right = to / (float) dataSize;
chart.setCurrentViewport(mViewport);
lastDataSize = dataSize;
}
}
if (dataSet instanceof TreeDataSet) {
treeChartRenderer.addDataSet((TreeDataSet) dataSet);
} else if (dataSet instanceof LineDataSet) {
lineRenderer.addDataSet((LineDataSet) dataSet);
} else if (dataSet instanceof BarDataSet) {
barChartRenderer.addDataSet((BarDataSet) dataSet);
} else if (dataSet instanceof CandlestickDataSet) {
candlestickChartRenderer.addDataSet((CandlestickDataSet) dataSet);
rangeRenderer.addDataSet((CandlestickDataSet) dataSet);
} else if (dataSet instanceof ScatterDataSet) {
scatterChartRenderer.addDataSet((ScatterDataSet) dataSet);
} else if (dataSet instanceof PointLineDataSet) {
pointLineRenderer.addDataSet((PointLineDataSet) dataSet);
} else if (dataSet instanceof ScatterTextDataSet) {
scatterTextRenderer.addDataSet((ScatterTextDataSet) dataSet);
}
calcDataSetMinMax();
}
@Override
public void setDefaultVisibleEntryCount(int defaultVisibleEntryCount) {
if (defaultVisibleEntryCount <= 0) return;
treeChartRenderer.setDefaultVisibleEntryCount(defaultVisibleEntryCount);
barChartRenderer.setDefaultVisibleEntryCount(defaultVisibleEntryCount);
lineRenderer.setDefaultVisibleEntryCount(defaultVisibleEntryCount);
scatterChartRenderer.setDefaultVisibleEntryCount(defaultVisibleEntryCount);
candlestickChartRenderer.setDefaultVisibleEntryCount(defaultVisibleEntryCount);
rangeRenderer.setDefaultVisibleEntryCount(defaultVisibleEntryCount);
pointLineRenderer.setDefaultVisibleEntryCount(defaultVisibleEntryCount);
scatterTextRenderer.setDefaultVisibleEntryCount(defaultVisibleEntryCount);
}
@Override
public void setMaxVisibleEntryCount(int maxVisibleEntryCount) {
if (maxVisibleEntryCount <= 0) return;
treeChartRenderer.setMaxVisibleEntryCount(maxVisibleEntryCount);
barChartRenderer.setMaxVisibleEntryCount(maxVisibleEntryCount);
lineRenderer.setMaxVisibleEntryCount(maxVisibleEntryCount);
scatterChartRenderer.setMaxVisibleEntryCount(maxVisibleEntryCount);
candlestickChartRenderer.setMaxVisibleEntryCount(maxVisibleEntryCount);
rangeRenderer.setMaxVisibleEntryCount(maxVisibleEntryCount);
pointLineRenderer.setMaxVisibleEntryCount(maxVisibleEntryCount);
scatterTextRenderer.setMaxVisibleEntryCount(maxVisibleEntryCount);
}
@Override
public void setMinVisibleEntryCount(int minVisibleEntryCount) {
if (minVisibleEntryCount <= 0) return;
treeChartRenderer.setMinVisibleEntryCount(minVisibleEntryCount);
barChartRenderer.setMinVisibleEntryCount(minVisibleEntryCount);
lineRenderer.setMinVisibleEntryCount(minVisibleEntryCount);
scatterChartRenderer.setMinVisibleEntryCount(minVisibleEntryCount);
candlestickChartRenderer.setMinVisibleEntryCount(minVisibleEntryCount);
rangeRenderer.setMinVisibleEntryCount(minVisibleEntryCount);
pointLineRenderer.setMinVisibleEntryCount(minVisibleEntryCount);
scatterTextRenderer.setMinVisibleEntryCount(minVisibleEntryCount);
}
@Override
public void removeDataSet(AbstractDataSet dataSet) {
getChartData().remove(dataSet);
calcDataSetMinMax();
}
@Override
public void clearDataSet() {
getChartData().clear();
cleanTreeDataSet();
cleanLineDataSet();
cleanBarDataSet();
cleanCandlestickDataSet();
cleanScatterDataSet();
cleanPointLineDataSet();
cleanScatterTextDataSet();
cleanRangeDataSet();
calcDataSetMinMax();
}
public void cleanLineDataSet() {
lineRenderer.clearDataSet();
getChartData().getLineChartData().clear();
}
public void cleanBarDataSet() {
barChartRenderer.clearDataSet();
getChartData().getBarChartData().clear();
}
public void cleanCandlestickDataSet() {
candlestickChartRenderer.clearDataSet();
getChartData().getCandlestickChartData().clear();
}
public void cleanScatterDataSet() {
scatterChartRenderer.clearDataSet();
getChartData().getScatterChartData().clear();
}
public void cleanPointLineDataSet() {
pointLineRenderer.clearDataSet();
getChartData().getPointLineChartData().clear();
}
public void cleanScatterTextDataSet() {
scatterTextRenderer.clearDataSet();
getChartData().getScatterTextChartData().clear();
}
public void cleanTreeDataSet() {
treeChartRenderer.clearDataSet();
getChartData().getTreeChartData().clear();
}
public void cleanRangeDataSet() {
rangeRenderer.clearDataSet();
}
@Override
protected List<AbstractDataSet> getDataSet() {
return combineData.getDataSets();
}
@Override
public CombineData getChartData() {
if (combineData == null) combineData = new CombineData();
return combineData;
}
@Override
public void enableHighlightDashPathEffect(float[] intervals, float phase) {
super.enableHighlightDashPathEffect(intervals, phase);
this.treeChartRenderer.enableHighlightDashPathEffect(intervals, phase);
this.lineRenderer.enableHighlightDashPathEffect(intervals, phase);
this.barChartRenderer.enableHighlightDashPathEffect(intervals, phase);
this.candlestickChartRenderer.enableHighlightDashPathEffect(intervals, phase);
this.scatterChartRenderer.enableHighlightDashPathEffect(intervals, phase);
this.rangeRenderer.enableHighlightDashPathEffect(intervals, phase);
this.pointLineRenderer.enableHighlightDashPathEffect(intervals, phase);
this.scatterTextRenderer.enableHighlightDashPathEffect(intervals, phase);
}
@Override
public void setTypeface(Typeface tf) {
this.treeChartRenderer.setTypeface(tf);
this.lineRenderer.setTypeface(tf);
this.barChartRenderer.setTypeface(tf);
this.candlestickChartRenderer.setTypeface(tf);
this.scatterChartRenderer.setTypeface(tf);
this.rangeRenderer.setTypeface(tf);
this.pointLineRenderer.setTypeface(tf);
this.scatterTextRenderer.setTypeface(tf);
}
@Override
public int getEntryIndexByCoordinate(float x, float y) {
if (!getChartData().getCandlestickData().isEmpty()) {
return candlestickChartRenderer.getEntryIndexByCoordinate(x, y);
}
if (!getChartData().getLineData().isEmpty()) {
return lineRenderer.getEntryIndexByCoordinate(x, y);
}
if (!getChartData().getBarData().isEmpty()) {
return barChartRenderer.getEntryIndexByCoordinate(x, y);
}
if (!getChartData().getScatterData().isEmpty()) {
return scatterChartRenderer.getEntryIndexByCoordinate(x, y);
}
if (!getChartData().getPointLineData().isEmpty()) {
return pointLineRenderer.getEntryIndexByCoordinate(x, y);
}
if (!getChartData().getScatterTextData().isEmpty()) {
return scatterTextRenderer.getEntryIndexByCoordinate(x, y);
}
return super.getEntryIndexByCoordinate(x, y);
}
@Override
public float getEntryCoordinateByIndex(int index) {
if (!getChartData().getCandlestickData().isEmpty()) {
return candlestickChartRenderer.getEntryCoordinateByIndex(index);
}
if (!getChartData().getLineData().isEmpty()) {
return lineRenderer.getEntryCoordinateByIndex(index);
}
if (!getChartData().getBarData().isEmpty()) {
return barChartRenderer.getEntryCoordinateByIndex(index);
}
if (!getChartData().getScatterData().isEmpty()) {
return scatterChartRenderer.getEntryCoordinateByIndex(index);
}
if (!getChartData().getPointLineData().isEmpty()) {
return pointLineRenderer.getEntryCoordinateByIndex(index);
}
if (!getChartData().getScatterTextData().isEmpty()) {
return scatterTextRenderer.getEntryCoordinateByIndex(index);
}
return super.getEntryCoordinateByIndex(index);
}
@Override
public void setHighlightThickness(float highlightThickness) {
super.setHighlightThickness(highlightThickness);
barChartRenderer.setHighlightThickness(highlightThickness);
lineRenderer.setHighlightThickness(highlightThickness);
candlestickChartRenderer.setHighlightThickness(highlightThickness);
}
}
@@ -0,0 +1,707 @@
package cn.jingzhuan.lib.chart2.renderer;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.Shader;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.util.ArrayList;
import java.util.List;
import cn.jingzhuan.lib.chart.Viewport;
import cn.jingzhuan.lib.chart.component.AxisY;
import cn.jingzhuan.lib.chart.component.Highlight;
import cn.jingzhuan.lib.chart.data.ChartData;
import cn.jingzhuan.lib.chart.data.LineData;
import cn.jingzhuan.lib.chart.data.LineDataSet;
import cn.jingzhuan.lib.chart.data.PartLineData;
import cn.jingzhuan.lib.chart.data.PointValue;
import cn.jingzhuan.lib.chart.event.OnViewportChangeListener;
import cn.jingzhuan.lib.chart2.TimeUtil;
import cn.jingzhuan.lib.chart2.base.BaseChart;
import cn.jingzhuan.lib.chart2.base.Chart;
import cn.jingzhuan.lib.chart2.widget.LineChart;
/**
* Line Renderer
* <p>
* Created by Donglua on 17/7/19.
*/
public class LineRenderer extends AbstractDataRenderer<LineDataSet> {
private LineData lineData;
private final List<Path> shaderPaths;
private final List<Shader> shaderPathColors;
private final List<Path> linePaths;
private Path shaderPath;
private final List<PartLineData> partLineDatas;
private boolean onlyLines = false;
private boolean isDrawHighLight = false;
private Paint mTextPaint;
private final Chart chart;
public LineRenderer(final Chart chart) {
super(chart);
this.chart = chart;
linePaths = new ArrayList<>();
shaderPath = new Path();
shaderPaths = new ArrayList<>();
shaderPathColors = new ArrayList<>();
partLineDatas = new ArrayList<>();
if (chart instanceof LineChart) {
onlyLines = true;
}
initPaint();
chart.setInternalViewportChangeListener(new OnViewportChangeListener() {
@Override
public void onViewportChange(Viewport viewport) {
mViewport.set(viewport);
calcDataSetMinMax();
}
});
final Highlight highlight = new Highlight();
chart.addOnTouchPointChangeListener(new Chart.OnTouchPointChangeListener() {
@Override
public void touch(float x, float y) {
if (chart.isHighlightDisable()) return;
synchronized (chart) {
for (LineDataSet line : getDataSet()) {
if (line.isHighlightedVerticalEnable() && !line.getValues().isEmpty()) {
highlight.setTouchX(x);
highlight.setTouchY(y);
int offset = line.getStartIndexOffset();
int index = getEntryIndexByCoordinate(x, y) - offset;
index = Math.max(index, 0);
index = Math.min(index, line.getValues().size() - 1);
final PointValue pointValue = line.getEntryForIndex(index);
float xPosition = Math.max(mContentRect.left, pointValue.getX());
float yPosition = pointValue.getY();
if (xPosition >= 0 && yPosition >= 0) {
highlight.setX(xPosition);
highlight.setY(yPosition);
highlight.setDataIndex(index);
chart.highlightValue(highlight);
}
}
}
}
}
});
}
private void initPaint() {
mTextPaint = new Paint();
mTextPaint.setStyle(Paint.Style.FILL);
mTextPaint.setStrokeWidth(8);
mTextPaint.setAntiAlias(true);
mTextPaint.setTextAlign(Paint.Align.CENTER);
mTextPaint.setColor(Color.WHITE);
mTextPaint.setTextSize(25f);
}
@Override
public void renderHighlighted(Canvas canvas, @NonNull Highlight[] highlights) {
mRenderPaint.setStyle(Paint.Style.FILL);
mRenderPaint.setStrokeWidth(getHighlightThickness());
mRenderPaint.setColor(getHighlightColor());
for (Highlight highlight : highlights) {
if (highlight != null) {
if (!chart.isEnableVerticalHighlight()) {
canvas.drawLine(highlight.getX(),
0,
highlight.getX(),
mContentRect.bottom,
mRenderPaint);
}
// Horizontal
if (!Float.isNaN(highlight.getY())) {
for (LineDataSet lineDataSet : getDataSet()) {
if (lineDataSet.isHighlightedHorizontalEnable() && !chart.isEnableHorizontalHighlight()) {
float y = highlight.getY();
if (y < mContentRect.top + getHighlightThickness() * 0.5f) {
y = mContentRect.top + getHighlightThickness() * 0.5f;
}
if (y > mContentRect.bottom - getHighlightThickness() * 0.5f){
y = mContentRect.bottom - getHighlightThickness() * 0.5f;
}
canvas.drawLine(0,
y,
mContentRect.right,
y,
mRenderPaint);
}
}
}
}
}
mRenderPaint.setPathEffect(null);
}
@Override
public void removeDataSet(LineDataSet dataSet) {
lineData.remove(dataSet);
calcDataSetMinMax();
}
@Override
public void clearDataSet() {
lineData.clear();
calcDataSetMinMax();
}
@Override
protected List<LineDataSet> getDataSet() {
return lineData.getDataSets();
}
@Override
public ChartData<LineDataSet> getChartData() {
if (lineData == null) lineData = new LineData();
return lineData;
}
@Override
protected void renderDataSet(Canvas canvas, ChartData<LineDataSet> chartData) {
for (LineDataSet dataSet : getDataSet()) {
renderDataSet(canvas, chartData, dataSet);
}
}
@Override
protected void renderDataSet(Canvas canvas, ChartData<LineDataSet> chartData, LineDataSet dataSet) {
if (dataSet.isVisible()) {
drawDataSet(canvas, dataSet,
chartData.getLeftMax(), chartData.getLeftMin(),
chartData.getRightMax(), chartData.getRightMin());
}
}
private void drawDataSet(Canvas canvas, final LineDataSet lineDataSet,
float lMax, float lMin, float rMax, float rMin) {
mRenderPaint.setStyle(Paint.Style.STROKE);
mRenderPaint.setStrokeWidth(lineDataSet.getLineThickness());
mRenderPaint.setColor(lineDataSet.getColor());
int valueCount = lineDataSet.getEntryCount();
shaderPath.reset();
shaderPaths.clear();
shaderPathColors.clear();
linePaths.clear();
partLineDatas.clear();
boolean isFirst = true;
float min, max;
switch (lineDataSet.getAxisDependency()) {
case AxisY.DEPENDENCY_RIGHT:
min = rMin;
max = rMax;
break;
case AxisY.DEPENDENCY_BOTH:
case AxisY.DEPENDENCY_LEFT:
default:
min = lMin;
max = lMax;
break;
}
final float count = lineDataSet.getVisibleRange(mViewport);
final float width = count > 0 ? (mContentRect.width() / count) : 0;
final int offset = lineDataSet.getStartIndexOffset();
final float scale = 1 / mViewport.width();
final float step = (valueCount > 1 && onlyLines) ?
(mContentRect.width() * scale / (valueCount - 1)) : (mContentRect.width() * scale / valueCount);
final float startX = mContentRect.left + (onlyLines ? 0f : step * 0.5f) - mViewport.left * mContentRect.width() * scale;
PointValue prevValue = null;
boolean shaderSplit = !Float.isNaN(lineDataSet.getShaderBaseValue()) &&
lineDataSet.getShaderBaseValue() < max &&
lineDataSet.getShaderBaseValue() > min;
int lastIndex = 0;
if (mChartAnimator.getPhaseX() > 0) {
lastIndex = (int) (Math.floor(lineDataSet.getValues().size() * mChartAnimator.getPhaseX()) - 1);
}
if (lastIndex >= valueCount) lastIndex = valueCount - 1;
int valuePhaseCount = (int) Math.floor(valueCount * mChartAnimator.getPhaseX());
Path linePath = new Path();
float splitStartBaseX = 0;
int i = 0;
float preBaseX = Float.NaN;
if (lineDataSet.isDrawBand()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
try {
// 这里有潜在的崩溃问题先catch后续重写
drawBand(canvas, lineDataSet, valuePhaseCount, startX, step, offset, max, min);
} catch (Exception e) {
e.printStackTrace();
}
return;
}
}
if (lineDataSet.isHorizontalLine()) {
drawHorizontalLine(canvas, lineDataSet, linePath, max, min);
return;
}
int lineThickness = lineDataSet.getLineThickness();
PointValue headPoint = lineDataSet.getHeadPoint();
if (headPoint != null && !headPoint.isValueNaN()) {
float headXPosition = startX;
// 垂直方向绘制范围收缩至能容下线条的宽度
float headYPosition = (max - headPoint.getValue()) / (max - min) * (mContentRect.height() - 2 * lineThickness) + lineThickness * 0.5f;
linePath.moveTo(headXPosition, headYPosition);
PointValue firstPoint = null;
float firstXPosition = startX;
for (; i < valuePhaseCount && i < lineDataSet.getValues().size(); i++) {
PointValue point = lineDataSet.getEntryForIndex(i);
if (!point.isValueNaN()) {
firstPoint = point;
firstXPosition = startX + step * (i + offset);
break;
}
}
if (firstPoint != null) {
float firstYPosition = (max - firstPoint.getValue()) / (max - min) * (mContentRect.height() - 2 * lineThickness) + lineThickness * 0.5f;
linePath.lineTo(firstXPosition, firstYPosition);
}
}
for (; i < valuePhaseCount && i < lineDataSet.getValues().size(); i++) {
PointValue point = lineDataSet.getEntryForIndex(i);
if (point.isValueNaN()) {
continue;
}
float xPosition = startX + step * (i + offset);
float yPosition = (max - point.getValue()) / (max - min) * (mContentRect.height() - 2 * lineThickness) + lineThickness * 0.5f;
point.setCoordinate(xPosition, yPosition);
//分段线条
if (i > 1 && lineDataSet.isPartLine()) {
PointValue lastPoint = lineDataSet.getEntryForIndex(i - 1);
boolean split = point.getPathColor() != lastPoint.getPathColor();
if (split) {
partLineDatas.add(new PartLineData(linePath, lastPoint.getPathColor()));
linePath = new Path();
linePath.moveTo(lastPoint.getX(), lastPoint.getY());
}
if (i == lineDataSet.getValues().size() - 1) {
partLineDatas.add(new PartLineData(linePath, point.getPathColor()));
}
}
//普通线条
if (isFirst) {
if (!point.isPathEnd()) {
isFirst = false;
linePath.moveTo(xPosition, yPosition);
}
} else {
linePath.lineTo(xPosition, yPosition);
if (point.isPathEnd()) {
linePaths.add(linePath);
linePath = new Path();
isFirst = true;
}
}
//阴影
if (shaderSplit) {
float baseValue = lineDataSet.getShaderBaseValue();
float baseValueY = mContentRect.height() / (max - min) * (max - baseValue);
if (prevValue == null) {
preBaseX = point.getX();
shaderPath.moveTo(preBaseX, yPosition);
} else if (prevValue.getValue() > lineDataSet.getShaderBaseValue()) {
if (point.getValue() <= lineDataSet.getShaderBaseValue()) { // 跨越颜色区域
float nextBaseX = getBaseX(prevValue, point, baseValueY);
shaderPath.lineTo(nextBaseX, baseValueY);
shaderPath.lineTo(preBaseX, baseValueY);
shaderPath.close();
shaderPaths.add(new Path(shaderPath));
shaderPathColors.add(lineDataSet.getShaderTop());
shaderPath.reset();
shaderPath.moveTo(nextBaseX, baseValueY);
shaderPath.lineTo(xPosition, yPosition);
preBaseX = nextBaseX;
} else {
shaderPath.lineTo(xPosition, yPosition); // 当前值坐标
}
} else if (point.getValue() > lineDataSet.getShaderBaseValue()) {
float nextBaseX = getBaseX(prevValue, point, baseValueY);
shaderPath.lineTo(nextBaseX, baseValueY);
shaderPath.lineTo(preBaseX, baseValueY);
shaderPath.close();
shaderPaths.add(new Path(shaderPath));
shaderPathColors.add(lineDataSet.getShaderBottom());
shaderPath.reset();
shaderPath.moveTo(nextBaseX, baseValueY);
shaderPath.lineTo(xPosition, yPosition);
preBaseX = nextBaseX;
} else {
shaderPath.lineTo(xPosition, yPosition); // 当前值坐标
}
prevValue = point;
if (lastIndex == i) {
shaderPath.lineTo(xPosition, baseValueY);
shaderPath.lineTo(preBaseX, baseValueY);
shaderPath.close();
shaderPaths.add(new Path(shaderPath));
if (prevValue.getValue() > baseValue) {
shaderPathColors.add(lineDataSet.getShaderTop());
} else {
shaderPathColors.add(lineDataSet.getShaderBottom());
}
shaderPath.reset();
}
}
} // end for.
if (!isFirst) {
linePaths.add(linePath);
}
if (!shaderSplit) { // 不区分颜色分段
// draw shader area
if (i > 0 && lineDataSet.getShader() != null && lineDataSet.getValues().size() > 0) {
mRenderPaint.setStyle(Paint.Style.FILL);
if (shaderPath == null) {
shaderPath = new Path(linePath);
} else {
shaderPath.set(linePath);
}
PointValue pointValue = lineDataSet.getEntryForIndex(i - 1);
if (pointValue != null) {
shaderPath.lineTo(pointValue.getX(), mContentRect.bottom);
shaderPath.lineTo(startX + offset * width, mContentRect.bottom);
shaderPath.lineTo(startX + offset * width, lineDataSet.getValues().get(0).getY());
shaderPath.close();
mRenderPaint.setShader(lineDataSet.getShader());
canvas.drawPath(shaderPath, mRenderPaint);
mRenderPaint.setShader(null);
mRenderPaint.setStyle(Paint.Style.STROKE);
}
}
} else {
mRenderPaint.setStyle(Paint.Style.FILL);
for (i = 0; i < shaderPaths.size(); i++) {
Path path = shaderPaths.get(i);
Shader shader = shaderPathColors.get(i);
mRenderPaint.setShader(shader);
canvas.drawPath(path, mRenderPaint);
mRenderPaint.setShader(null);
}
mRenderPaint.setStyle(Paint.Style.STROKE);
}
if (lineDataSet.isLineVisible()) {
if (lineDataSet.isPartLine()) {
for (PartLineData partLineData : partLineDatas) {
mRenderPaint.setColor(partLineData.getColor());
canvas.drawPath(partLineData.getPath(), mRenderPaint);
}
} else {
for (Path path : linePaths) {
canvas.drawPath(path, mRenderPaint);
}
}
}
}
private void drawHorizontalLine(Canvas canvas, LineDataSet lineDataSet, Path linePath, float max, float min) {
if (chart instanceof BaseChart){
isDrawHighLight = ((BaseChart)chart).getHighlights() != null;
}
mRenderPaint.setPathEffect(new DashPathEffect(new float[]{5, 5, 5, 5}, 0));
mRenderPaint.setColor(lineDataSet.getColor());
PointValue value = lineDataSet.getEntryForIndex(lineDataSet.getValues().size() - 1);
float yPosition = (max - value.getValue()) / (max - min) * mContentRect.height();
linePath.moveTo(0, yPosition);
linePath.lineTo(mContentRect.width(), yPosition);
canvas.drawPath(linePath, mRenderPaint);
mRenderPaint.setPathEffect(null);
mRenderPaint.setStyle(Paint.Style.FILL);
mRenderPaint.setStrokeWidth(2f);
mRenderPaint.setTextSize(25f);
String text = "";
if (isDrawHighLight)
text = value.getValue() + "";
else
text = lineDataSet.getTag();
Rect textBound = new Rect();
mRenderPaint.getTextBounds(text, 0, text.length(), textBound);
int padding = 10;
Rect textRect = new Rect();
textRect.left = 0;
textRect.top = (int) (yPosition - textBound.height());
textRect.right = textBound.width() + padding * 2;
textRect.bottom = (int) (yPosition + textBound.height());
if (TimeUtil.isInTime()){
textRect.left = mContentRect.width() - textBound.width() - padding * 2;
textRect.right = mContentRect.width();
}
if (textRect.top < 0 && yPosition >=0){
int height = textRect.height();
textRect.top = 0;
textRect.bottom = height;
}
if (textRect.bottom > mContentRect.height() && yPosition <= mContentRect.height()){
int height = textRect.height();
textRect.bottom = mContentRect.height();
textRect.top = mContentRect.height() - height;
}
if (value.getValue() <= max && value.getValue() >= min) {
canvas.drawRect(textRect, mRenderPaint);
}
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
float distance = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;
float baseline = textRect.centerY() + distance;
if (value.getValue() <= max && value.getValue() >= min) {
canvas.drawText(text, textRect.centerX(), baseline, mTextPaint);
}
}
/**
* 绘制带状线
*/
@Deprecated
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private void drawBand(Canvas canvas, LineDataSet lineDataSet, int valuePhaseCount, float startX, float step, int offset, float max, float min) {
int i = 0;
Path cache1 = new Path();
Path cache2 = new Path();
//当数据1 > 数据2 将cache1连接控件底部形成不规则封闭图形 cache2连接控件顶部 取交集部分绘制
boolean isCloseToBottom;
boolean preStatus = false;
boolean newPath = true;
float pathStartX = 0;
ArrayList<PartLineData> paths = new ArrayList<>();
for (; i < valuePhaseCount && i < lineDataSet.getValues().size(); i++) {
PointValue point = lineDataSet.getEntryForIndex(i);
if (point.isValueNaN()) {
continue;
}
isCloseToBottom = point.getValue() > point.getSecondValue();
//
float xPosition = startX + step * (i + offset);
float yPosition = (max - point.getValue()) / (max - min) * mContentRect.height();
float secondYPosition = (max - point.getSecondValue()) / (max - min) * mContentRect.height();
point.setCoordinate(xPosition, yPosition);
point.setSecondY(secondYPosition);
if (newPath) {
cache1.moveTo(xPosition, yPosition);
cache2.moveTo(xPosition, secondYPosition);
newPath = false;
pathStartX = xPosition;
} else {
cache1.lineTo(xPosition, yPosition);
cache2.lineTo(xPosition, secondYPosition);
}
if (i > 0 && i == lineDataSet.getValues().size() - 1) { //结尾部分
//找到上一个点 做为起点
PointValue prePoint = lineDataSet.getEntryForIndex(i - 1);
boolean special = prePoint.getValue() > prePoint.getSecondValue();
if (special != isCloseToBottom) {
if (special) {
cache1.lineTo(xPosition, mContentRect.height());
cache1.lineTo(pathStartX, mContentRect.height());
cache2.lineTo(xPosition, 0);
cache2.lineTo(pathStartX, 0);
} else {
cache2.lineTo(xPosition, mContentRect.height());
cache2.lineTo(pathStartX, mContentRect.height());
cache1.lineTo(xPosition, 0);
cache1.lineTo(pathStartX, 0);
}
cache1.close();
cache2.close();
cache1.op(cache2, Path.Op.INTERSECT);
paths.add(new PartLineData(cache1, prePoint.getPathColor()));
cache1 = new Path();
cache2 = new Path();
pathStartX = prePoint.getX();
//相交代表 本来数据比较高的线变成低 低的线变成高
cache1.moveTo(prePoint.getX(), prePoint.getY());
cache1.lineTo(xPosition, yPosition);
cache2.moveTo(prePoint.getX(), prePoint.getSecondY());
cache2.lineTo(xPosition, secondYPosition);
}
if (isCloseToBottom) {
cache1.lineTo(xPosition, mContentRect.height());
cache1.lineTo(pathStartX, mContentRect.height());
cache2.lineTo(xPosition, 0);
cache2.lineTo(pathStartX, 0);
} else {
cache2.lineTo(xPosition, mContentRect.height());
cache2.lineTo(pathStartX, mContentRect.height());
cache1.lineTo(xPosition, 0);
cache1.lineTo(pathStartX, 0);
}
cache1.close();
cache2.close();
cache1.op(cache2, Path.Op.INTERSECT);
paths.add(new PartLineData(cache1, point.getPathColor()));
} else {
if (preStatus != isCloseToBottom && i > 0) { //相交 两个值 高的连接底部 低的连接顶部
if (isCloseToBottom) {
cache2.lineTo(xPosition, mContentRect.height());
cache2.lineTo(pathStartX, mContentRect.height());
cache1.lineTo(xPosition, 0);
cache1.lineTo(pathStartX, 0);
} else {
cache1.lineTo(xPosition, mContentRect.height());
cache1.lineTo(pathStartX, mContentRect.height());
cache2.lineTo(xPosition, 0);
cache2.lineTo(pathStartX, 0);
}
cache1.close();
cache2.close();
cache1.op(cache2, Path.Op.INTERSECT);
//找到上一个点 做为起点
PointValue prePoint = lineDataSet.getEntryForIndex(i - 1);
paths.add(new PartLineData(cache1, prePoint.getPathColor()));
cache1 = new Path();
cache2 = new Path();
pathStartX = prePoint.getX();
//相交代表 本来数据比较高的线变成低 低的线变成高
cache1.moveTo(prePoint.getX(), prePoint.getY());
cache1.lineTo(xPosition, yPosition);
cache2.moveTo(prePoint.getX(), prePoint.getSecondY());
cache2.lineTo(xPosition, secondYPosition);
}
}
preStatus = isCloseToBottom;
}
if (!paths.isEmpty()) {
for (PartLineData pathData : paths) {
mRenderPaint.setColor(pathData.getColor());
mRenderPaint.setStyle(Paint.Style.FILL);
canvas.drawPath(pathData.getPath(), mRenderPaint);
}
}
}
private float getBaseX(PointValue p1, PointValue p2, float baseY) {
float x1 = p1.getX();
float x2 = p2.getX();
float y1 = Math.abs(p1.getY() - baseY);
float y2 = Math.abs(p2.getY() - baseY);
return (y1 * x2 + x1 * y2) / (y2 + y1);
}
}
@@ -0,0 +1,240 @@
package cn.jingzhuan.lib.chart2.renderer;
import android.graphics.Canvas;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import androidx.annotation.NonNull;
import java.util.List;
import cn.jingzhuan.lib.chart.Viewport;
import cn.jingzhuan.lib.chart.component.AxisY;
import cn.jingzhuan.lib.chart.component.Highlight;
import cn.jingzhuan.lib.chart.data.ChartData;
import cn.jingzhuan.lib.chart.data.PointLineData;
import cn.jingzhuan.lib.chart.data.PointLineDataSet;
import cn.jingzhuan.lib.chart.data.PointValue;
import cn.jingzhuan.lib.chart.event.OnViewportChangeListener;
import cn.jingzhuan.lib.chart2.base.Chart;
/**
* 虚线加点
*/
public class PointLineRenderer extends AbstractDataRenderer<PointLineDataSet> {
private PointLineData pointLineData;
private Path mPath;
//圆点画笔
private final Paint mPointPaint;
private final Chart chart;
public PointLineRenderer(Chart chart) {
super(chart);
this.chart = chart;
mPath = new Path();
mPointPaint = new Paint();
mPointPaint.setStyle(Paint.Style.FILL);
chart.setInternalViewportChangeListener(new OnViewportChangeListener() {
@Override
public void onViewportChange(Viewport viewport) {
mViewport.set(viewport);
calcDataSetMinMax();
}
});
final Highlight highlight = new Highlight();
chart.addOnTouchPointChangeListener(new Chart.OnTouchPointChangeListener() {
@Override
public void touch(float x, float y) {
if (chart.isHighlightDisable()) return;
synchronized (chart) {
for (PointLineDataSet line : getDataSet()) {
if (line.isHighlightedVerticalEnable() && !line.getValues().isEmpty()) {
highlight.setTouchX(x);
highlight.setTouchY(y);
int offset = line.getStartIndexOffset();
int index = getEntryIndexByCoordinate(x, y) - offset;
index = Math.max(index, 0);
index = Math.min(index, line.getValues().size() - 1);
final PointValue pointValue = line.getEntryForIndex(index);
float xPosition = Math.max(mContentRect.left, pointValue.getX());
float yPosition = pointValue.getY();
if (xPosition >= 0 && yPosition >= 0) {
highlight.setX(xPosition);
highlight.setY(yPosition);
highlight.setDataIndex(index);
chart.highlightValue(highlight);
}
}
}
}
}
});
}
private void drawDataSet(Canvas canvas, final PointLineDataSet lineDataSet,
float lMax, float lMin, float rMax, float rMin){
mRenderPaint.setStyle(Paint.Style.STROKE);
mRenderPaint.setStrokeWidth(lineDataSet.getLineThickness());
mRenderPaint.setColor(lineDataSet.getColor());
float interval = lineDataSet.getInterval();
if (interval != 0f)
mRenderPaint.setPathEffect(new DashPathEffect(new float[]{interval,interval},lineDataSet.getPhase()));
mRenderPaint.setAntiAlias(true);
mPointPaint.setColor(lineDataSet.getColor());
mPointPaint.setAntiAlias(true);
int valueCount = lineDataSet.getEntryCount();
float min, max;
switch (lineDataSet.getAxisDependency()) {
case AxisY.DEPENDENCY_RIGHT:
min = rMin;
max = rMax;
break;
case AxisY.DEPENDENCY_BOTH:
case AxisY.DEPENDENCY_LEFT:
default:
min = lMin;
max = lMax;
break;
}
final int offset = lineDataSet.getStartIndexOffset();
final float scale = 1 / mViewport.width();
final float step = (mContentRect.width() * scale / valueCount);
final float startX = mContentRect.left - mViewport.left * mContentRect.width() * scale;
int valuePhaseCount = (int) Math.floor(valueCount * mChartAnimator.getPhaseX());
mPath.reset();
mPath = new Path();
boolean isFirst = true;
int i = 0;
final float visibleRange = lineDataSet.getVisibleRange(mViewport);
double candleWidth = mContentRect.width() / Math.max(visibleRange, lineDataSet.getMinValueCount());
for (; i < valuePhaseCount && i < lineDataSet.getValues().size(); i++) {
PointValue point = lineDataSet.getEntryForIndex(i);
if (point.isValueNaN()) {
continue;
}
float xPosition = startX + step * (i + offset);
float yPosition = (max - point.getValue()) / (max - min) * mContentRect.height();
final float candlestickCenterX = (float) (xPosition + candleWidth * 0.5);
point.setCoordinate(candlestickCenterX, yPosition);
if ((yPosition + lineDataSet.getRadius() )> mContentRect.bottom){
yPosition -= lineDataSet.getRadius();
}
if ((yPosition - lineDataSet.getRadius()) < mContentRect.top)
yPosition += lineDataSet.getRadius();
if (isFirst){
isFirst = false;
mPath.moveTo(candlestickCenterX, yPosition);
}else {
mPath.lineTo(candlestickCenterX,yPosition);
}
if (point.isDrawCircle()) {
canvas.drawCircle(candlestickCenterX,yPosition,lineDataSet.getRadius(),mPointPaint);
}
}//for
if (lineDataSet.isLineVisible()) {
canvas.drawPath(mPath, mRenderPaint);
}
}
@Override
protected void renderDataSet(Canvas canvas, ChartData<PointLineDataSet> chartData) {
for (PointLineDataSet dataSet : getDataSet()) {
renderDataSet(canvas, chartData, dataSet);
}
}
@Override
protected void renderDataSet(Canvas canvas, ChartData<PointLineDataSet> chartData, PointLineDataSet dataSet) {
if (dataSet.isVisible()) {
drawDataSet(canvas, dataSet,
chartData.getLeftMax(), chartData.getLeftMin(),
chartData.getRightMax(), chartData.getRightMin());
}
}
@Override
public void renderHighlighted(Canvas canvas, @NonNull Highlight[] highlights) {
mRenderPaint.setStyle(Paint.Style.FILL);
mRenderPaint.setStrokeWidth(getHighlightThickness());
mRenderPaint.setColor(getHighlightColor());
for (Highlight highlight : highlights) {
if (highlight != null) {
if (!chart.isEnableVerticalHighlight()) {
canvas.drawLine(highlight.getX(),
0,
highlight.getX(),
mContentRect.bottom,
mRenderPaint);
}
// Horizontal
if (!Float.isNaN(highlight.getY())) {
for (PointLineDataSet lineDataSet : getDataSet()) {
if (lineDataSet.isHighlightedHorizontalEnable() && !chart.isEnableHorizontalHighlight()) {
float y = highlight.getY();
if (y < mContentRect.top + getHighlightThickness() * 0.5f) {
y = mContentRect.top + getHighlightThickness() * 0.5f;
}
if (y > mContentRect.bottom - getHighlightThickness() * 0.5f){
y = mContentRect.bottom - getHighlightThickness() * 0.5f;
}
canvas.drawLine(0,
y,
mContentRect.right,
y,
mRenderPaint);
}
}
}
}
}
mRenderPaint.setPathEffect(null);
}
@Override
public void removeDataSet(PointLineDataSet dataSet) {
pointLineData.remove(dataSet);
calcDataSetMinMax();
}
@Override
public void clearDataSet() {
pointLineData.clear();
calcDataSetMinMax();
}
@Override
protected List<PointLineDataSet> getDataSet() {
return pointLineData.getDataSets();
}
@Override
public ChartData<PointLineDataSet> getChartData() {
if (pointLineData == null) pointLineData = new PointLineData();
return pointLineData;
}
}
@@ -0,0 +1,758 @@
package cn.jingzhuan.lib.chart2.renderer;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import androidx.annotation.NonNull;
import android.view.MotionEvent;
import android.view.ViewTreeObserver;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import cn.jingzhuan.lib.chart.R;
import cn.jingzhuan.lib.chart.component.Highlight;
import cn.jingzhuan.lib.chart.data.CandlestickData;
import cn.jingzhuan.lib.chart.data.CandlestickDataSet;
import cn.jingzhuan.lib.chart.data.CandlestickValue;
import cn.jingzhuan.lib.chart.data.ChartData;
import cn.jingzhuan.lib.chart2.base.Chart;
/**
* 区间统计Renderer
*/
public class RangeRenderer extends AbstractDataRenderer<CandlestickDataSet> {
private CandlestickData chartData;
/**
* 最小间隔
*/
private final int MAX_DIFF_ENTRY = 1;
/**
* 周期内 的起点和终点的下标
*/
private int mStartIndex = 0;
private int mEndIndex = 0;
/**
* 右两边icon x的坐标
*/
private float mStartX = 0;
private float mEndX = 0;
/**
* 当前chart的左右边界
*/
private int chartLeft = 0;
private int chartRight = 0;
/**
* 右两边touch bitmap
*/
private Bitmap leftTouchBitmap;
private Bitmap rightTouchBitmap;
/**
* 右两边 touch的矩形区域
*/
private RectF leftTouchRect;
private RectF rightTouchRect;
/**
* 上一次触摸的xy坐标
*/
private float lastPreX = 0;
// private float lastPreY = 0;
/**
* 右两边touch的矩形区域中间 线的颜色
* 默认为 #FD263F的 %8透明度
*/
private int mLineColor = Color.parseColor("#FD263F");
/**
* 区间的颜色
* 默认为 #FD263F的 %8透明度
*/
private int mRangeColor = Color.parseColor("#FD263F");
/**
* 当前区间内按下的状态 左边右边公共区域
*/
private TouchDirection touchDirection = TouchDirection.none;
/**
* 用于划线
*/
private final Paint linePaint = new Paint();
/**
* 用于画bitmap
*/
private final Paint btPaint = new Paint();
/**
* 中间阴影
*/
private final Paint shadowPaint = new Paint();
/**
* 当前chart
*/
private final Chart chart;
private OnRangeListener mOnRangeListener;
private OnRangeKLineListener mOnRangeKLineListener;
public RangeRenderer(Chart chart) {
super(chart);
this.chart = chart;
initPaint();
initMeasure();
chart.setInternalViewportChangeListener(viewport -> {
mViewport.set(viewport);
calcDataSetMinMax();
});
chart.addOnViewportChangeListener(viewport -> {
mViewport.set(viewport);
initChartBoundary();
if (viewport.width() == 1.0f) return;
if(chart.getRangeEnable() && (mStartX != 0 && mEndX != 0)) {
int start = getEntryIndexByCoordinate(mStartX, 0);
int end = getEntryIndexByCoordinate(mEndX, 0);
if(chart.isScaling() || chart.getZoomer().computeZoom()) {
final CandlestickDataSet candlestickDataSet = dataValid();
int listSize = candlestickDataSet.getValues().size();
int chartLeftIndex = Math.round(mViewport.left * listSize);
int chartRightIndex = Math.min(listSize - 1, Math.round(mViewport.right * listSize - 1));
if(mStartIndex < chartLeftIndex) {
mEndIndex = chartLeftIndex + mEndIndex - mStartIndex;
if(mEndIndex > chartRightIndex) {
mEndIndex = chartRightIndex;
}
mStartIndex = chartLeftIndex;
}
if(mEndIndex > chartRightIndex) {
mEndIndex = chartRightIndex;
}
mStartX = getScaleCoordinateByIndex(mStartIndex);
mEndX = getScaleCoordinateByIndex(mEndIndex);
}
if((start != 0 && end != 0) && (mStartIndex != start && mEndIndex != end)) {
mStartIndex = start;
mEndIndex = end;
touchDirection = TouchDirection.none;
}
}
});
}
@Override
protected void renderDataSet(Canvas canvas, ChartData<CandlestickDataSet> chartData) {
drawCanvas(canvas);
}
@Override
protected void renderDataSet(Canvas canvas, ChartData<CandlestickDataSet> chartData, CandlestickDataSet dataSet) {
drawCanvas(canvas);
}
private void initMeasure() {
chart.getViewTreeObserver()
.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
chart.getViewTreeObserver().removeOnGlobalLayoutListener(this);
initChartBoundary();
}
});
}
private void initChartBoundary() {
if(chartLeft != chart.getContentRect().left)
chartLeft = chart.getContentRect().left;
if(chartRight != chart.getContentRect().right)
chartRight = chart.getContentRect().right;
}
public void initPaint() {
btPaint.setAntiAlias(true);
linePaint.setAntiAlias(true);
linePaint.setColor(getLineColor());
linePaint.setSubpixelText(true);
linePaint.setStrokeWidth(3);
linePaint.setAlpha(20);
shadowPaint.setAntiAlias(true);
shadowPaint.setColor(getRangeColor());
shadowPaint.setStyle(Paint.Style.FILL);
shadowPaint.setAlpha(20);
}
@Override
public void renderHighlighted(Canvas canvas, @NonNull @NotNull Highlight[] highlights) {
if(highlights.length > 0) {
initChartBoundary();
CandlestickDataSet candlestickDataSet = dataValid();
if (candlestickDataSet == null) return;
mStartX = highlights[0].getX();
mStartIndex = getEntryIndexByCoordinate(mStartX, 0);
int listSize = candlestickDataSet.getValues().size();
mEndIndex = Math.min(listSize - 1, Math.round(mViewport.right * listSize - 1));
mEndX = getScaleCoordinateByIndex(mEndIndex);
touchDirection = TouchDirection.none;
}
}
public void drawCanvas(Canvas canvas) {
CandlestickDataSet candlestickDataSet = dataValid();
if (candlestickDataSet == null) return;
//默认选中当前屏幕内全部
if (mStartIndex == 0 && mEndIndex == 0) {
int listSize = candlestickDataSet.getValues().size();
mStartIndex = Math.round(mViewport.left * listSize);
mEndIndex = Math.min(listSize - 1, Math.round(mViewport.right * listSize - 1));
int chartSize = candlestickDataSet.getValues().size();
if(mEndIndex > chartSize - 1) mEndIndex = chartSize - 1;
// 获取区间统计开始的K线坐标
mStartX = getScaleCoordinateByIndex(mStartIndex);
// 获取区间统计结束的K线坐标
mEndX = getScaleCoordinateByIndex(mEndIndex);
touchDirection = TouchDirection.none;
}
if (mOnRangeListener != null)
mOnRangeListener.onRange(mStartX, mEndX, touchDirection);
if(mStartX > mEndX) mStartX = mEndX;
if(mStartX < chartLeft || mStartX > chartRight) return;
if(mEndX < chartLeft || mEndX > chartRight ) return;
RectF rect = new RectF(mStartX, 0, mEndX, chart.getContentRect().height());
//绘制区间统计的选择区域
canvas.drawRect(rect, shadowPaint);
/*
* 根据K线数据的中点绘制线条
*/
canvas.drawLine(mStartX, 0, mStartX, chart.getContentRect().height(), linePaint);
canvas.drawLine(mEndX, 0, mEndX, chart.getContentRect().height(), linePaint);
// 如果leftTouchBitmap和rightTouchBitmap 没有设置 用默认的
if(leftTouchBitmap == null)
leftTouchBitmap = BitmapFactory.decodeResource(this.chart.getResources(), R.drawable.ico_range_touch_left);
if(rightTouchBitmap == null)
rightTouchBitmap = BitmapFactory.decodeResource(this.chart.getResources(), R.drawable.ico_range_touch_right);
// 绘制左右touch icon
float bitmapSpanX = leftTouchBitmap.getWidth() / 2f;
float bitmapSpanY = leftTouchBitmap.getHeight() / 2f;
if(rightTouchBitmap != null)
canvas.drawBitmap(rightTouchBitmap, mEndX - bitmapSpanX, chart.getContentRect().height() / 2f - bitmapSpanY, btPaint);
if(leftTouchBitmap != null)
canvas.drawBitmap(leftTouchBitmap, mStartX - bitmapSpanX, chart.getContentRect().height() / 2f - bitmapSpanY, btPaint);
/*
* 创建开始&结束矩阵 用于判断触摸点是否在该区域内
* true -> 改变区间统计范围
* false -> 不作反应
*/
leftTouchRect = new RectF(
mStartX - bitmapSpanX * 3f , chart.getContentRect().top,
mStartX + bitmapSpanX * 3f,
chart.getContentRect().bottom);
rightTouchRect = new RectF(
mEndX - bitmapSpanX * 3f,
chart.getContentRect().top,
mEndX + bitmapSpanX * 3f,
chart.getContentRect().bottom);
if (mOnRangeKLineListener!=null) mOnRangeKLineListener.onRangeKLine(mStartIndex,mEndIndex);
}
private boolean touchToLeft(float currentX) {
final CandlestickDataSet candlestickDataSet = dataValid();
if (candlestickDataSet == null) {
return false;
}
int leftIndex = getEntryIndexByCoordinate(currentX, 0);
int rightIndex = getEntryIndexByCoordinate(mEndX, 0);
int listSize = candlestickDataSet.getValues().size();
int chartLeftIndex = Math.round(mViewport.left * listSize);
int chartRightIndex = Math.min(listSize - 1, Math.round(mViewport.right * listSize - 1));
int chartSize = candlestickDataSet.getValues().size();
if(chartRightIndex > chartSize - 1) chartRightIndex = chartSize - 1;
float newStartX = currentX - lastPreX + mStartX;
if(leftIndex != mStartIndex && leftIndex >= chartLeftIndex && leftIndex < chartRightIndex){
if (rightIndex - leftIndex <= MAX_DIFF_ENTRY && newStartX >= mStartX) {
mStartIndex = leftIndex;
mEndIndex = leftIndex + 1;
} else {
mStartIndex = leftIndex;
mEndIndex = rightIndex;
}
mStartX = getScaleCoordinateByIndex(mStartIndex);
mEndX = getScaleCoordinateByIndex(mEndIndex);
chart.invalidate();
}
return true;
}
private boolean touchToRight(float currentX) {
final CandlestickDataSet candlestickDataSet = dataValid();
if (candlestickDataSet == null) {
return false;
}
int leftIndex = getEntryIndexByCoordinate(mStartX, 0);
int rightIndex = getEntryIndexByCoordinate(currentX, 0);
int listSize = candlestickDataSet.getValues().size();
int chartLeftIndex = Math.round(mViewport.left * listSize);
int chartRightIndex = Math.min(listSize - 1, Math.round(mViewport.right * listSize - 1));
int chartSize = candlestickDataSet.getValues().size();
if(chartRightIndex > chartSize - 1) chartRightIndex = chartSize - 1;
float newEndX = currentX - lastPreX + mEndX;
if(rightIndex != mEndIndex && rightIndex > chartLeftIndex && rightIndex <= chartRightIndex){
mEndIndex = rightIndex;
if (rightIndex - leftIndex <= MAX_DIFF_ENTRY && newEndX < mEndX) {
mStartIndex = rightIndex - 1;
} else {
mStartIndex = leftIndex;
}
mStartX = getScaleCoordinateByIndex(mStartIndex);
mEndX = getScaleCoordinateByIndex(mEndIndex);
chart.invalidate();
}
return true;
}
private boolean touchBothToLeftOrRight(float currentX) {
final CandlestickDataSet candlestickDataSet = dataValid();
if (candlestickDataSet == null) {
return false;
}
int currentIndex = getEntryIndexByCoordinate(currentX, 0);
int lastPreIndex = getEntryIndexByCoordinate(lastPreX, 0);
int deltaIndex = currentIndex - lastPreIndex;
if(deltaIndex == 0) return true;
int listSize = candlestickDataSet.getValues().size();
int chartLeftIndex = Math.round(mViewport.left * listSize);
int chartRightIndex = Math.min(listSize - 1, Math.round(mViewport.right * listSize - 1));
int chartSize = candlestickDataSet.getValues().size();
if(chartRightIndex > chartSize - 1) chartRightIndex = chartSize - 1;
int leftIndex = mStartIndex + deltaIndex;
int rightIndex = mEndIndex + deltaIndex;
if(leftIndex >= chartLeftIndex && rightIndex <= chartRightIndex){
mStartIndex = leftIndex;
mEndIndex = rightIndex;
mStartX = getScaleCoordinateByIndex(mStartIndex);
mEndX = getScaleCoordinateByIndex(mEndIndex);
chart.invalidate();
}
return true;
}
// //这里暂时注释 这个是平滑移动 不以蜡烛宽度为每次滑动的间隔
// private boolean touchToLeft(float deltaX) {
// final CandlestickDataSet candlestickDataSet = dataVaild();
// if (candlestickDataSet == null) {
// return false;
// }
// if (deltaX + mStartX < chartLeft) {
// deltaX = chartLeft - mStartX;
// }
// float tempLeftX = mStartX + deltaX;
//
// if(tempLeftX <= chartLeft) {
// tempLeftX = chartLeft;
// }
// if (tempLeftX >= chartRight) {
// tempLeftX = chartRight;
// }
// int leftIndex = getEntryIndexByCoordinate(tempLeftX, 0);
// int rightIndex = getEntryIndexByCoordinate(mEndX, 0);
// mStartIndex = leftIndex;
// mEndIndex = rightIndex;
//
// if (mEndIndex - mStartIndex <= MAX_DIFF_ENTRY && tempLeftX > mStartX) {
// mEndIndex = mEndIndex + MAX_DIFF_ENTRY;
// mStartIndex = mEndIndex - MAX_DIFF_ENTRY;
// if (tempLeftX >= chartRight - getItemWidth()) {
// return true;
// }
// mStartX = tempLeftX;
// mEndX = tempLeftX + getItemWidth();
// chart.invalidate();
// } else {
// mStartX = tempLeftX;
// chart.invalidate();
// }
// return true;
// }
//
// private boolean touchToRight(float deltaX) {
// final CandlestickDataSet candlestickDataSet = dataVaild();
// if (candlestickDataSet == null) {
// return false;
// }
// if (deltaX + mEndX > chartRight) {
// deltaX = chartRight - mEndX;
// }
// float tempRightX = deltaX + mEndX;
// if (tempRightX <= chartLeft) {
// tempRightX = chartLeft;
// }
// if(tempRightX >= chartRight) {
// tempRightX = chartRight;
// }
//
// int leftIndex = getEntryIndexByCoordinate(mStartX, 0);
// int rightIndex = getEntryIndexByCoordinate(tempRightX, 0);
// mStartIndex = leftIndex;
// mEndIndex = rightIndex;
//
// if (mEndIndex - mStartIndex <= MAX_DIFF_ENTRY && tempRightX < mEndX) {
// mStartIndex = mStartIndex - MAX_DIFF_ENTRY;
// mEndIndex = mStartIndex + MAX_DIFF_ENTRY;
// if (tempRightX <= chartLeft + getItemWidth()) {
// return true;
// }
// mEndX = tempRightX;
// mStartX = mEndX - getItemWidth();
// chart.invalidate();
// } else {
// mEndX = tempRightX;
// chart.invalidate();
// }
// return true;
// }
// private boolean touchBothToLeftOrRight(float deltaX) {
// final CandlestickDataSet candlestickDataSet = dataVaild();
// if (candlestickDataSet == null) {
// return false;
// }
// if (deltaX < chartLeft - mStartX) {
// deltaX = chartLeft - mStartX;
// }
//
// if (deltaX > chartRight - mEndX) {
// deltaX = chartRight - mEndX;
// }
//
// float tempLeftX = deltaX + mStartX;
// float tempRightX = deltaX + mEndX;
//
// if (tempRightX <= chartRight && tempLeftX >= chartLeft) {
// mStartX = tempLeftX;
// mEndX = tempRightX;
//
// mStartIndex = getEntryIndexByCoordinate(tempLeftX, 0);
// mEndIndex = getEntryIndexByCoordinate(tempRightX, 0);
// chart.invalidate();
// } else {
// return false;
// }
// return true;
// }
public boolean onTouchEvent(@NotNull MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
touchDirection = TouchDirection.none;
float currentX = event.getX();
float currentY = event.getY();
lastPreX = currentX;
// lastPreY = currentY;
if((currentX > leftTouchRect.right && currentX < rightTouchRect.left)) {
// 在左右touch之内 认为是同时滑动
touchDirection = TouchDirection.both;
return true;
}else if (leftTouchRect.contains(currentX, currentY)) {
touchDirection = TouchDirection.left;
return true;
} else if (rightTouchRect.contains(currentX, currentY)) {
touchDirection = TouchDirection.right;
return true;
} else {
return false;
}
}
case MotionEvent.ACTION_MOVE: {
float currentX = event.getX();
float currentY = event.getY();
float deltaX = currentX - lastPreX;
try {
if (touchDirection == TouchDirection.both) {
touchBothToLeftOrRight(currentX);
} else if (touchDirection == TouchDirection.left) {
touchToLeft(currentX);
} else if(touchDirection == TouchDirection.right){
touchToRight(currentX);
} else {
return false;
}
} finally {
lastPreX = currentX;
// lastPreY = currentY;
}
return true;
}
case MotionEvent.ACTION_UP:
touchDirection = TouchDirection.none;
if (mOnRangeListener != null)
mOnRangeListener.onRange(mStartX, mEndX, touchDirection);
break;
case MotionEvent.ACTION_CANCEL:
}
return false;
}
/**
* 根据子当前Kline在数据集合的index获得缩放后对应的X轴坐标
* @param index 数据集合的index
* @return X轴坐标
*/
public float getScaleCoordinateByIndex(int index) {
CandlestickDataSet candlestickDataSet = dataValid();
int valueCount = candlestickDataSet.getEntryCount();
final float scale = 1.0f / mViewport.width();
float candleWidth = candlestickDataSet.getCandleWidth();
final float visibleRange = candlestickDataSet.getVisibleRange(mViewport);
if (candlestickDataSet.isAutoWidth()) {
candleWidth = mContentRect.width() / Math.max(visibleRange, candlestickDataSet.getMinValueCount());
}
final float step = mContentRect.width() * scale / valueCount;
final float startX = mContentRect.left - mViewport.left * mContentRect.width() * scale;
float startXPosition = startX + step * (index + candlestickDataSet.getStartIndexOffset());
return startXPosition + candleWidth * 0.5f;
}
private float getItemWidth() {
if (getDataSet() == null || getDataSet().size() == 0) {
return 0;
}
CandlestickDataSet candlestickDataSet = getDataSet().get(0);
float candleWidth = candlestickDataSet.getCandleWidth();
final List<CandlestickValue> visibleValues = candlestickDataSet.getVisiblePoints(mViewport);
if (candlestickDataSet.isAutoWidth()) {
candleWidth = (mContentRect.width() + 0f) / Math.max(visibleValues.size(),
candlestickDataSet.getMinValueCount());
}
return candleWidth;
}
/**
* 数据有效且数据量大于2那么返回对应的数据集合否则不返回
*/
public CandlestickDataSet dataValid() {
List<CandlestickDataSet> dataSet = getDataSet();
if (getChartData() == null
|| dataSet == null
|| dataSet.size() <= 0) {
return null;
} else {
CandlestickDataSet candlestickDataSet = dataSet.get(dataSet.size() - 1);
if (candlestickDataSet.getValues().size() < 2) {
return null;
} else {
return candlestickDataSet;
}
}
}
@Override
public void removeDataSet(CandlestickDataSet dataSet) {
getChartData().remove(dataSet);
calcDataSetMinMax();
}
@Override
public void clearDataSet() {
getChartData().clear();
getChartData().calcMaxMin(mViewport, mContentRect);
}
@Override
protected List<CandlestickDataSet> getDataSet() {
return chartData.getDataSets();
}
@Override
public ChartData<CandlestickDataSet> getChartData() {
if (chartData == null)
chartData = new CandlestickData();
return chartData;
}
/**
* 重置数据
*/
public void resetData() {
if (mStartIndex != 0 || mEndIndex != 0) {
mStartIndex = mEndIndex = 0;
mStartX = mEndX = 0;
}
}
public Boolean isRangeMoving() {
return touchDirection != TouchDirection.none;
}
/**
* @return 线条颜色
*/
public int getLineColor() {
return mLineColor;
}
/**
* @param mLineColor 设置线条指定的颜色
*/
public void setLineColor(int mLineColor) {
this.mLineColor = mLineColor;
}
/**
* @return 区间统计颜色
*/
public int getRangeColor() {
return mRangeColor;
}
/**
* @param mRangeColor 设置区间统计指定的颜色
*/
public void setRangeColor(int mRangeColor) {
this.mRangeColor = mRangeColor;
}
/**
* 设置区间统计左边touch icon图标
* @param icon 设置指定的Bitmap
*/
public void setRangeLeftBitmap(Bitmap icon) {
this.leftTouchBitmap = icon;
}
/**
* 设置区间统计右边touch icon图标
* @param icon 设置指定的Bitmap
*/
public void setRangeRightBitmap(Bitmap icon) {
this.rightTouchBitmap = icon;
}
/**
* 设置区间统计左边Index
*/
public void setRangeLeftIndex(int index) {
this.mStartIndex = index;
this.mStartX = getScaleCoordinateByIndex(index);
}
/**
* 设置区间统计右边Index
*/
public void setRangeRightIndex(int index) {
this.mEndIndex = index;
this.mEndX = getScaleCoordinateByIndex(index);
}
/**
* 区间统计监听器
*/
public interface OnRangeListener {
/**
* 区间统计坐标
*
* @param startX 开始的X坐标
* @param endX 结束的X坐标
*/
void onRange(float startX, float endX, TouchDirection direction);
}
/**
* 设置区间统计范围的监听器 用于更新关闭按钮的所在位置(X轴坐标)
*
* @param listener 监听器
*/
public void setOnRangeListener(OnRangeListener listener) {
this.mOnRangeListener = listener;
}
/**
* 监听K线是否可见
*/
public interface OnRangeKLineVisibleListener {
/**
* @param visible true为可见
*/
void onRangeKLineVisible(boolean visible);
}
/**
* 监听K线是否可见
*/
public interface OnRangeKLineListener {
/**
*
* @param startIndex 开始的index
* @param endIndex 结束的index
*/
void onRangeKLine(int startIndex , int endIndex);
}
/**
* 设置区间统计可见KLine监听器
*
* @param listener 监听器
*/
public void setOnRangeKLineListener(OnRangeKLineListener listener) {
this.mOnRangeKLineListener = listener;
}
}
@@ -0,0 +1,243 @@
package cn.jingzhuan.lib.chart2.renderer;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import cn.jingzhuan.lib.chart.data.ScatterDataSet;
import cn.jingzhuan.lib.chart.data.ScatterValue;
import cn.jingzhuan.lib.chart.renderer.TextValueRenderer;
import cn.jingzhuan.lib.chart2.base.Chart;
import cn.jingzhuan.lib.chart.component.AxisY;
import cn.jingzhuan.lib.chart.component.Highlight;
import cn.jingzhuan.lib.chart.data.ChartData;
import cn.jingzhuan.lib.chart.data.ScatterData;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Scatter Chart Renderer
* <p>
* Created by donglua on 10/19/17.
*/
public class ScatterChartRenderer extends AbstractDataRenderer<ScatterDataSet> {
private ScatterData scatterData;
private final Map<String, Float> alignTopHeights = new HashMap<>();
private final Map<String, Float> alignBottomHeights = new HashMap<>();
private final Map<String, Float> alignParentTopHeights = new HashMap<>();
private final Map<String, Float> alignParentBottomHeights = new HashMap<>();
public ScatterChartRenderer(Chart chart) {
super(chart);
}
@Override
protected void renderDataSet(Canvas canvas, ChartData<ScatterDataSet> chartData) {
clearTemporaryData();
for (ScatterDataSet dataSet : getDataSet()) {
renderDataSet(canvas, chartData, dataSet);
}
}
@Override
protected void renderDataSet(Canvas canvas, ChartData<ScatterDataSet> chartData, ScatterDataSet dataSet) {
clearTemporaryData();
for (ScatterDataSet scatterDataSet : getDataSet()) {
drawDataSet(canvas, scatterDataSet, chartData.getLeftMax(), chartData.getLeftMin(), chartData.getRightMax(), chartData.getRightMin());
}
// if (dataSet.isVisible()) {
// drawDataSet(canvas, dataSet,
// chartData.getLeftMax(), chartData.getLeftMin(),
// chartData.getRightMax(), chartData.getRightMin());
// }
}
private void drawDataSet(Canvas canvas, final ScatterDataSet dataSet, float leftMax, float leftMin, float rightMax, float rightMin) {
mRenderPaint.setStrokeWidth(1);
mRenderPaint.setColor(dataSet.getColor());
int valueCount = dataSet.getEntryCount();
float min, max;
switch (dataSet.getAxisDependency()) {
case AxisY.DEPENDENCY_RIGHT:
min = rightMin;
max = rightMax;
break;
case AxisY.DEPENDENCY_BOTH:
case AxisY.DEPENDENCY_LEFT:
default:
min = leftMin;
max = leftMax;
break;
}
final float width = (mContentRect.width() - dataSet.getStartXOffset() - dataSet.getEndXOffset())
/ dataSet.getVisibleRange(mViewport) + 1;
float shapeWidth = dataSet.getShape().getIntrinsicWidth();
float shapeHeight = dataSet.getShape().getIntrinsicHeight();
if (dataSet.isAutoWidth()) {
shapeWidth = Math.max(width * 0.8f, dataSet.getShapeMinWidth());
if (!Float.isNaN(dataSet.getShapeMaxWidth())) {
shapeWidth = Math.min(shapeWidth, dataSet.getShapeMaxWidth());
}
shapeHeight = shapeWidth * shapeHeight / ((float) dataSet.getShape().getIntrinsicWidth());
}
float yOffset;
if (dataSet.getShapeAlign() == ScatterDataSet.SHAPE_ALIGN_CENTER) {
yOffset = shapeHeight * 0.5f;
} else {
yOffset = 0f;
}
Drawable shape = dataSet.getShape();
for (int i = 0; i < valueCount && i < dataSet.getValues().size() && shape != null; i++) {
ScatterValue point = dataSet.getEntryForIndex(i);
if (!point.isVisible()) continue;
float xPosition = dataSet.getStartXOffset() + width * 0.5f
+ getDrawX((i + dataSet.getStartIndexOffset()) / ((float) valueCount)) - shapeWidth * 0.5f;
float yPosition;
String heightIndexKey = String.valueOf(i);
if (dataSet.getShapeAlign() == ScatterDataSet.SHAPE_ALIGN_PARENT_BOTTOM) {
float offset = shapeHeight;
if(!Float.isNaN(point.getValue())) {
float lastOffset = alignParentBottomHeights.get(heightIndexKey) == null ? 0f : alignParentBottomHeights.get(heightIndexKey);
offset = offset + lastOffset;
alignParentBottomHeights.put(heightIndexKey, offset);
}
yPosition = mContentRect.height() - offset;
} else if (dataSet.getShapeAlign() == ScatterDataSet.SHAPE_ALIGN_PARENT_TOP) {
float offset = alignParentTopHeights.get(heightIndexKey) == null ? 0f : alignParentTopHeights.get(heightIndexKey);
yPosition = mContentRect.top + offset;
if(!Float.isNaN(point.getValue())) {
alignParentTopHeights.put(heightIndexKey, offset + shapeHeight);
}
} else if (dataSet.getShapeAlign() == ScatterDataSet.SHAPE_ALIGN_BOTTOM) {
float offset = shapeHeight;
if(!Float.isNaN(point.getValue())) {
float lastOffset = alignBottomHeights.get(heightIndexKey) == null ? 0f : alignBottomHeights.get(heightIndexKey);
offset = offset + lastOffset;
alignBottomHeights.put(heightIndexKey, offset);
}
yPosition = (max - point.getValue()) / (max - min) * mContentRect.height() - offset;
} else if (dataSet.getShapeAlign() == ScatterDataSet.SHAPE_ALIGN_TOP) {
float offset = alignTopHeights.get(heightIndexKey) == null ? 0f : alignTopHeights.get(heightIndexKey);
yPosition = (max - point.getValue()) / (max - min) * mContentRect.height() + offset;
if(!Float.isNaN(point.getValue())) {
alignTopHeights.put(heightIndexKey, offset + shapeHeight);
}
} else {
yPosition = (max - point.getValue()) / (max - min) * mContentRect.height() - yOffset;
}
shape = dataSet.getShape();
if (point.getShape() != null) {
shape = point.getShape();
}
point.setCoordinate(xPosition, yPosition);
int x = (int) (xPosition + dataSet.getDrawOffsetX());
int y = (int) (yPosition + dataSet.getDrawOffsetY());
if (point.getColor() != Color.TRANSPARENT) {
shape.setColorFilter(point.getColor(), PorterDuff.Mode.SRC_OVER);
}
shape.setBounds(x,
y,
(int) (x + shapeWidth),
(int) (y + shapeHeight));
int saveId = canvas.save();
shape.draw(canvas);
canvas.restoreToCount(saveId);
if (dataSet.getTextValueRenderers() != null) {
for (TextValueRenderer textValueRenderer : dataSet.getTextValueRenderers()) {
textValueRenderer.render(canvas, i,
x + shapeWidth * 0.5f, y + shapeHeight * 0.5f);
}
}
}
}
@Override
public int getEntryIndexByCoordinate(float x, float y) {
int index = -1;
if (scatterData.getDataSets().size() > 0) {
ScatterDataSet dataSet = scatterData.getDataSets().get(0);
RectF rect = new RectF();
Drawable shape = dataSet.getShape();
float shapeWidth = shape.getIntrinsicWidth();
float shapeHeight = shape.getIntrinsicHeight();
for (int i = 0; i < dataSet.getValues().size(); i++) {
final ScatterValue value = dataSet.getEntryForIndex(i);
float pX = value.getX();
float pY = value.getY();
rect.set(pX, pY, pX + shapeWidth, pY + shapeHeight);
if (rect.contains(x, y)) {
index = i;
break;
}
}
return index;
}
return super.getEntryIndexByCoordinate(x, y);
}
@Override
public void renderHighlighted(Canvas canvas, @NonNull Highlight[] highlights) {
}
@Override
public void removeDataSet(ScatterDataSet dataSet) {
getChartData().remove(dataSet);
calcDataSetMinMax();
}
@Override
public void clearDataSet() {
getChartData().clear();
clearTemporaryData();
}
@Override
protected List<ScatterDataSet> getDataSet() {
return getChartData().getDataSets();
}
@Override
public ChartData<ScatterDataSet> getChartData() {
if (scatterData == null) {
scatterData = new ScatterData();
}
return scatterData;
}
private void clearTemporaryData() {
alignTopHeights.clear();
alignBottomHeights.clear();
alignParentTopHeights.clear();
alignParentBottomHeights.clear();
}
}
@@ -0,0 +1,370 @@
package cn.jingzhuan.lib.chart2.renderer;
import static cn.jingzhuan.lib.chart.data.ScatterTextDataSet.ALIGN_BOTTOM;
import static cn.jingzhuan.lib.chart.data.ScatterTextDataSet.ALIGN_TOP;
import android.graphics.Canvas;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.Log;
import androidx.annotation.NonNull;
import java.util.List;
import cn.jingzhuan.lib.chart.component.AxisY;
import cn.jingzhuan.lib.chart.component.Highlight;
import cn.jingzhuan.lib.chart.data.ChartData;
import cn.jingzhuan.lib.chart.data.ScatterTextData;
import cn.jingzhuan.lib.chart.data.ScatterTextDataSet;
import cn.jingzhuan.lib.chart.data.ScatterTextValue;
import cn.jingzhuan.lib.chart2.base.Chart;
public class ScatterTextRenderer extends AbstractDataRenderer<ScatterTextDataSet> {
// 顶部和底部给个边距
public final static int margin = 2;
ScatterTextData scatterTextData;
private final Paint mTextPaint;
public ScatterTextRenderer(Chart chart) {
super(chart);
chart.setInternalViewportChangeListener(viewport -> {
mViewport.set(viewport);
calcDataSetMinMax();
});
mTextPaint = new Paint();
mTextPaint.setStyle(Paint.Style.FILL);
mTextPaint.setStrokeWidth(8);
mTextPaint.setAntiAlias(true);
mTextPaint.setTextAlign(Paint.Align.CENTER);
}
@Override
protected void renderDataSet(Canvas canvas, ChartData<ScatterTextDataSet> chartData) {
List<ScatterTextDataSet> dataSets = getDataSet();
for (int i = dataSets.size() - 1; i >= 0; i--) {
ScatterTextDataSet dataSet = dataSets.get(i);
renderDataSet(canvas, chartData, dataSet);
}
// for (int i = dataSets.size() - 1; i >= 0; i--) {
// ScatterTextDataSet dataSet = dataSets.get(i);
// if (dataSet.isVisible()) {
// cal(canvas, dataSet, i,
// chartData.getLeftMax(), chartData.getLeftMin(),
// chartData.getRightMax(), chartData.getRightMin());
// }
// }
// for (int i = dataSets.size() - 1; i >= 0; i--) {
// ScatterTextDataSet dataSet = dataSets.get(i);
// if (dataSet.isVisible()) {
// drawDataSet2(canvas, dataSet, i,
// chartData.getLeftMax(), chartData.getLeftMin(),
// chartData.getRightMax(), chartData.getRightMin());
// }
// }
}
@Override
protected void renderDataSet(Canvas canvas, ChartData<ScatterTextDataSet> chartData, ScatterTextDataSet dataSet) {
List<ScatterTextDataSet> dataSets = getDataSet();
int i = dataSets.indexOf(dataSet);
if (dataSet.isVisible()) {
drawDataSet(canvas, dataSet, i,
chartData.getLeftMax(), chartData.getLeftMin(),
chartData.getRightMax(), chartData.getRightMin());
}
}
private void drawDataSet(Canvas canvas, ScatterTextDataSet dataSet, int index, float leftMax, float leftMin, float rightMax, float rightMin) {
mRenderPaint.setStrokeWidth(2);
mRenderPaint.setColor(dataSet.getLineColor());
mRenderPaint.setTextSize(dataSet.getTextSize());
mTextPaint.setColor(dataSet.getTextColor());
mTextPaint.setTextSize(dataSet.getTextSize());
String text = dataSet.getText();
int bgColor = dataSet.getTextBgColor();
int lineColor = dataSet.getLineColor();
int frameColor = dataSet.getFrameColor();
int axisDependency = dataSet.getAxisDependency();
int dashHeight = dataSet.getLineDashHeight();
int textPadding = dataSet.getTextPadding();
int dashLength;
Rect textBound = new Rect();
mRenderPaint.getTextBounds(text, 0, text.length(), textBound);
int textRectHeight = textBound.height();
int textRectWidth = textBound.width();
int valueCount = dataSet.getEntryCount();
float min, max;
switch (dataSet.getAxisDependency()) {
case AxisY.DEPENDENCY_RIGHT:
min = rightMin;
max = rightMax;
break;
case AxisY.DEPENDENCY_BOTH:
case AxisY.DEPENDENCY_LEFT:
default:
min = leftMin;
max = leftMax;
break;
}
final float scale = 1 / mViewport.width();
final float step = mContentRect.width() * scale / valueCount;
final float startX = mContentRect.left - mViewport.left * mContentRect.width() * scale;
final float visibleRange = dataSet.getVisibleRange(mViewport);
int i = 0;
int valuePhaseCount = (int) Math.floor(valueCount * mChartAnimator.getPhaseX());
double candleWidth = mContentRect.width() / Math.max(visibleRange, dataSet.getMinValueCount());
for (; i < valuePhaseCount && i < dataSet.getValues().size(); i++) {
ScatterTextValue value = dataSet.getEntryForIndex(i);
if (!value.isVisible() || Float.isNaN(value.getHigh()) || Float.isNaN(value.getLow()))
continue;
int offset = calOffset(index, i, textPadding);
dashLength = dashHeight + offset;
// Log.d("ScatterTextRenderer", "------dashLength = BASE_DASH_LENGTH:" + BASE_DASH_LENGTH + " + offset:" + offset + " = " + dashLength);
// Log.v("ScatterTextRenderer", "drawDataSet " + i + ", index_" + index + ":" + text + ", textRectHeight:" + textRectHeight + ", offset:" + offset + ", dashLength:" + dashLength);
float xPosition = startX + step * (i + dataSet.getStartIndexOffset());
float yHighPosition = (max - value.getHigh()) / (max - min) * mContentRect.height();
float yLowPosition = (max - value.getLow()) / (max - min) * mContentRect.height();
float anchor = yHighPosition;
final float candlestickCenterX = (float) (xPosition + candleWidth * 0.5);
value.setCoordinate(candlestickCenterX, anchor);
float bottom = anchor - dashLength;
float top = bottom - textRectHeight - textPadding * 2;
float pathEnd = bottom;
// 如果此时顶部位置小于最小边距 向下
if(top < margin) {
anchor = yLowPosition;
bottom = anchor + dashLength + textRectHeight;
top = bottom - textRectHeight - textPadding * 2;
pathEnd = top;
}
RectF roundRect = new RectF();
float right, left;
switch (axisDependency) {
case AxisY.DEPENDENCY_LEFT:
right = candlestickCenterX + textPadding;
left = candlestickCenterX - textRectWidth - textPadding;
break;
case AxisY.DEPENDENCY_RIGHT:
right = candlestickCenterX + textRectWidth + textPadding;
left = candlestickCenterX - textPadding;
break;
case AxisY.DEPENDENCY_BOTH:
default:
right = candlestickCenterX + textRectWidth * 0.5f + textPadding;
left = candlestickCenterX - textRectWidth * 0.5f - textPadding;
break;
}
if (axisDependency == AxisY.DEPENDENCY_BOTH) {
float maxRight = mContentRect.width() - mRenderPaint.getStrokeWidth();
if (right > maxRight && candlestickCenterX < mContentRect.width()) {
right = maxRight;
left = maxRight - textRectWidth - textPadding * 2;
}
float minLeft = mRenderPaint.getStrokeWidth();
if (left < minLeft && candlestickCenterX > 0) {
left = minLeft;
right = left + textRectWidth + textPadding * 2;
}
}
if (dataSet.getAlign() == ALIGN_BOTTOM) {
anchor = yLowPosition;
bottom = anchor + dashLength + textRectHeight;
top = bottom - textRectHeight - textPadding * 2;
pathEnd = top;
// 如果此时底部位置大于(mContentRect.height() - margin) 向上
if(bottom > mContentRect.height() - margin) {
anchor = yHighPosition;
bottom = anchor - dashLength;
top = bottom - textRectHeight - textPadding * 2;
pathEnd = bottom;
}
// Log.w("ScatterTextRenderer", "drawDataSet " + i + ", index_" + index + ":" + text + ", h:" + (Math.abs(top - bottom)) + "(top:" + top + ", bottom:" + bottom + "), left:" + left + ", right:" + right);
}
if(candlestickCenterX < (textRectWidth * 0.5f + textPadding) && candlestickCenterX > 0 && dataSet.isBgCircle()) {
// 左边界
top = anchor + textRectHeight * 0.5f + textPadding;
bottom = anchor - textRectHeight * 0.5f - textPadding;
right = candlestickCenterX + dashLength + textRectWidth + textPadding * 2;
left = right - textRectWidth - textPadding * 2;
if(bottom < margin) {
bottom = margin;
top = bottom + textRectHeight + textPadding * 2;
}
if(top > mContentRect.height() - margin) {
top = mContentRect.height() - margin;
bottom = top - textRectHeight - textPadding * 2;
}
}
if(candlestickCenterX > mContentRect.width() - (textRectWidth * 0.5f + textPadding) && candlestickCenterX < mContentRect.width() && dataSet.isBgCircle()) {
// 右边界
top = anchor + textRectHeight * 0.5f + textPadding;
bottom = anchor - textRectHeight * 0.5f - textPadding;
left = candlestickCenterX - dashLength - textRectWidth - textPadding * 2;
right = left + textRectWidth + textPadding * 2;
if(bottom < margin) {
bottom = margin;
top = bottom + textRectHeight + textPadding * 2;
}
if(top > mContentRect.height() - margin) {
top = mContentRect.height() - margin;
bottom = top - textRectHeight - textPadding * 2;
}
}
roundRect.set(left, top, right, bottom);
mRenderPaint.setPathEffect(null);
mRenderPaint.setStyle(Paint.Style.FILL);
mRenderPaint.setAntiAlias(true);
mRenderPaint.setColor(bgColor);
float radius = dataSet.isBgCircle() ? textRectWidth : 2f;
canvas.drawRoundRect(roundRect, radius, radius, mRenderPaint);
mRenderPaint.setStyle(Paint.Style.STROKE);
mRenderPaint.setAntiAlias(true);
mRenderPaint.setColor(frameColor);
canvas.drawRoundRect(roundRect, radius, radius, mRenderPaint);
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
float distance = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;
float baseline = roundRect.centerY() + distance;
canvas.drawText(text, roundRect.centerX(), baseline, mTextPaint);
Path path = new Path();
if(candlestickCenterX < (textRectWidth * 0.5f + textPadding) && candlestickCenterX > 0 && dataSet.isBgCircle()) {
// 左边界
if(bottom == margin) {
// 上边界
path.moveTo(candlestickCenterX, anchor);
path.lineTo(candlestickCenterX + dashLength, anchor + (textRectHeight + textPadding * 2) * 0.5f);
} else if (top == mContentRect.height() - margin) {
// 下边界
path.moveTo(candlestickCenterX, anchor);
path.lineTo(candlestickCenterX + dashLength, anchor - (textRectHeight + textPadding * 2) * 0.5f);
} else {
path.moveTo(candlestickCenterX, anchor);
path.lineTo(candlestickCenterX + dashLength, anchor);
}
} else if(candlestickCenterX > mContentRect.width() - (textRectWidth * 0.5f + textPadding) && candlestickCenterX < mContentRect.width() && dataSet.isBgCircle()) {
// 右边界
if(bottom == margin) {
path.moveTo(candlestickCenterX, anchor);
path.lineTo(candlestickCenterX - dashLength, anchor + (textRectHeight + textPadding * 2) * 0.5f);
} else if (top == mContentRect.height() - margin) {
// 下边界
path.moveTo(candlestickCenterX, anchor);
path.lineTo(candlestickCenterX - dashLength, anchor - (textRectHeight + textPadding * 2) * 0.5f);
} else {
path.moveTo(candlestickCenterX, anchor);
path.lineTo(candlestickCenterX - dashLength, anchor);
}
} else {
path.moveTo(candlestickCenterX, anchor);
path.lineTo(candlestickCenterX, pathEnd);
}
mRenderPaint.setColor(lineColor);
mRenderPaint.setPathEffect(new DashPathEffect(new float[]{5, 5, 5, 5}, 0));
canvas.drawPath(path, mRenderPaint);
}
}
private int calOffset(int index, int i, int textPadding) {
List<ScatterTextDataSet> dataSets = getDataSet();
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(2);
Rect textBound = new Rect();
int offset = 0;
int invalidTime = 0;
for (int j = 0; j < index; j++) {
ScatterTextDataSet dataSet = dataSets.get(j);
if (dataSet.getAlign() != dataSets.get(index).getAlign()) {
invalidTime++;
continue;
}
ScatterTextValue value = dataSet.getEntryForIndex(i);
if (!value.isVisible() || Float.isNaN(value.getHigh()) || Float.isNaN(value.getLow())) {
invalidTime++;
continue;
}
String text = dataSet.getText();
paint.setTextSize(dataSet.getTextSize());
paint.getTextBounds(text, 0, text.length(), textBound);
int textHeight = textBound.height();
int oldOffset = offset;
offset += textHeight + textPadding * 2;
// Log.d("ScatterTextRenderer", "------calOffset " + i + " index_" + j + ":" + text + ", textHeight:" + textHeight + ", offset = " + oldOffset + " + textHeight:" + textHeight + " + PADDING * 2 = " + offset);
}
int oldOffset = offset;
if (offset > 0) {
offset += textPadding * (index - invalidTime);
// Log.d("ScatterTextRenderer", "------------offset = " + oldOffset + " + PADDING * " + (index - invalidTime) + " = " + offset);
} else {
// Log.d("ScatterTextRenderer", "------------offset = 0");
}
return offset;
}
@Override
public void renderHighlighted(Canvas canvas, @NonNull Highlight[] highlights) {
}
@Override
public void removeDataSet(ScatterTextDataSet dataSet) {
scatterTextData.remove(dataSet);
calcDataSetMinMax();
}
@Override
public void clearDataSet() {
scatterTextData.clear();
calcDataSetMinMax();
}
@Override
protected List<ScatterTextDataSet> getDataSet() {
return scatterTextData.getDataSets();
}
@Override
public ChartData<ScatterTextDataSet> getChartData() {
if (scatterTextData == null) scatterTextData = new ScatterTextData();
return scatterTextData;
}
}
@@ -0,0 +1,8 @@
package cn.jingzhuan.lib.chart2.renderer;
public enum TouchDirection {
left,
right,
both,
none
}
@@ -0,0 +1,150 @@
package cn.jingzhuan.lib.chart2.renderer;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.graphics.ColorUtils;
import java.util.List;
import cn.jingzhuan.lib.chart.Viewport;
import cn.jingzhuan.lib.chart.component.AxisY;
import cn.jingzhuan.lib.chart.component.Highlight;
import cn.jingzhuan.lib.chart.data.ChartData;
import cn.jingzhuan.lib.chart.data.Leaf;
import cn.jingzhuan.lib.chart.data.TreeData;
import cn.jingzhuan.lib.chart.data.TreeDataSet;
import cn.jingzhuan.lib.chart.data.TreeValue;
import cn.jingzhuan.lib.chart.event.OnViewportChangeListener;
import cn.jingzhuan.lib.chart2.base.Chart;
/**
* Created by guobosheng on 22/6/24.
*/
public class TreeChartRenderer extends AbstractDataRenderer<TreeDataSet> {
private TreeData mTreeDataSets;
private Chart mChart;
public TreeChartRenderer(final Chart chart) {
super(chart);
mChart = chart;
chart.setInternalViewportChangeListener(new OnViewportChangeListener() {
@Override
public void onViewportChange(Viewport viewport) {
mViewport.set(viewport);
calcDataSetMinMax();
}
});
}
@Override
protected void renderDataSet(Canvas canvas, ChartData<TreeDataSet> chartData) {
for (TreeDataSet dataSet : chartData.getDataSets()) {
renderDataSet(canvas, chartData, dataSet);
}
}
@Override
protected void renderDataSet(Canvas canvas, ChartData<TreeDataSet> chartData, TreeDataSet dataSet) {
if (mChart.getFocusIndex() == -1) mChart.setFocusIndex(dataSet.getValues().size() - 1);
if (dataSet.isVisible() && dataSet.getValues().size() > mChart.getFocusIndex()) {
drawTreeDataSet(canvas, dataSet, mChart.getFocusIndex(),
chartData.getLeftMax(), chartData.getLeftMin(),
chartData.getRightMax(), chartData.getRightMin());
}
}
private void drawTreeDataSet(Canvas canvas, TreeDataSet dataSet, int index,
float lMax, float lMin, float rMax, float rMin) {
mRenderPaint.setStrokeWidth(dataSet.getStrokeThickness());
mRenderPaint.setStyle(Paint.Style.FILL);
float min, max;
switch (dataSet.getAxisDependency()) {
case AxisY.DEPENDENCY_RIGHT:
min = rMin;
max = rMax;
break;
case AxisY.DEPENDENCY_BOTH:
case AxisY.DEPENDENCY_LEFT:
default:
min = lMin;
max = lMax;
break;
}
// 只在图中间绘制指定的 TreeValue
TreeValue treeValue = dataSet.getEntryForIndex(index);
if (!treeValue.isEnable()) return;
int leafCount = treeValue.getLeafCount();
if (leafCount < 1) return;
float zeroX = mContentRect.width() / 2f;
float maxLeafSpace = mContentRect.width() / 4f;
float maxLeafValue = treeValue.getMaxLeafValue();
for (int i = 0; i < leafCount; i++) {
Leaf leaf = treeValue.getLeafs().get(i);
if (Float.isNaN(leaf.getHigh())) continue;
float leftValue = leaf.getLeftValue();
float rightValue = leaf.getRightValue();
float high = leaf.getHigh() * mChartAnimator.getPhaseY();
float y = calcHeight(high, max, min);
treeValue.setCoordinate(mContentRect.width() / 2f, y);
float left = zeroX - (leftValue / maxLeafValue) * maxLeafSpace;
int positiveColor = ColorUtils.setAlphaComponent(dataSet.getPositiveColor(), dataSet.getColorAlpha());
mRenderPaint.setColor(positiveColor);
canvas.drawLine(left, y, zeroX, y, mRenderPaint);
float right = zeroX + (rightValue / maxLeafValue) * maxLeafSpace;
int negativeColor = ColorUtils.setAlphaComponent(dataSet.getNegativeColor(), dataSet.getColorAlpha());
mRenderPaint.setColor(negativeColor);
canvas.drawLine(zeroX, y, right, y, mRenderPaint);
}
}
private float calcHeight(float value, float max, float min) {
if (Float.compare(max, min) == 0) return 0;
return (max - value) / (max - min) * mContentRect.height();
}
@Override
public void renderHighlighted(Canvas canvas, @NonNull Highlight[] highlights) {
}
@Override
public void removeDataSet(TreeDataSet dataSet) {
if (dataSet == null) return;
mTreeDataSets.remove(dataSet);
calcDataSetMinMax();
}
@Override
public void clearDataSet() {
mTreeDataSets.clear();
calcDataSetMinMax();
}
@Override
protected List<TreeDataSet> getDataSet() {
return mTreeDataSets.getDataSets();
}
@Override
public TreeData getChartData() {
if (mTreeDataSets == null) mTreeDataSets = new TreeData();
return mTreeDataSets;
}
}
@@ -0,0 +1,56 @@
package cn.jingzhuan.lib.chart2.widget;
import android.content.Context;
import android.os.Build;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.animation.Animation;
import cn.jingzhuan.lib.chart.data.BarDataSet;
import cn.jingzhuan.lib.chart2.base.BaseChart;
import cn.jingzhuan.lib.chart2.renderer.BarChartRenderer;
/**
* Created by Donglua on 17/8/2.
*/
public class BarChart extends BaseChart {
public BarChart(Context context) {
super(context);
}
public BarChart(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public BarChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public BarChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public void initChart() {
super.initChart();
mRenderer = new BarChartRenderer(this);
}
@SuppressWarnings("unchecked")
public void addDataSet(BarDataSet barDataSet) {
mRenderer.addDataSet(barDataSet);
}
public void setDataSet(BarDataSet barDataSet) {
mRenderer.clearDataSet();
addDataSet(barDataSet);
}
@Override public void startAnimation(Animation animation) {
super.startAnimation(animation);
}
}

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