From 2dcd8f9377816d593aa50247bd6a0286f252bddb Mon Sep 17 00:00:00 2001 From: Gnanendra18 Date: Thu, 28 Apr 2022 19:39:52 +0530 Subject: [PATCH 1/5] Data Visualizations Setup Pie Chart Experiments Statistics Chart Implementation Charts Library Implementation --- app/build.gradle | 3 + app/src/main/AndroidManifest.xml | 501 +++++++++--------- .../java/gov/dol/childlabor/MainActivity.java | 7 +- .../dol/childlabor/StatisticsActivity.java | 15 + .../charts/DataVisualizationActivity.java | 16 + .../charts/HalfPieChartActivity.java | 165 ++++++ .../childlabor/charts/PieChartActivity.java | 207 ++++++++ app/src/main/res/drawable/circle.xml | 5 + .../layout/activity_data_visualization.xml | 24 + .../res/layout/activity_piechart_half.xml | 20 + .../main/res/layout/content_statistics.xml | 12 + settings.gradle | 1 + 12 files changed, 736 insertions(+), 240 deletions(-) create mode 100644 app/src/main/java/gov/dol/childlabor/charts/DataVisualizationActivity.java create mode 100644 app/src/main/java/gov/dol/childlabor/charts/HalfPieChartActivity.java create mode 100644 app/src/main/java/gov/dol/childlabor/charts/PieChartActivity.java create mode 100644 app/src/main/res/drawable/circle.xml create mode 100644 app/src/main/res/layout/activity_data_visualization.xml create mode 100644 app/src/main/res/layout/activity_piechart_half.xml diff --git a/app/build.gradle b/app/build.gradle index a079dc4..da872c6 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,6 +28,7 @@ android { } dependencies { + implementation 'androidx.constraintlayout:constraintlayout:2.1.3' compile fileTree(include: ['*.jar'], dir: 'libs') testCompile 'junit:junit:4.12' // compile 'com.android.support:appcompat-v7:26.1.0' @@ -38,4 +39,6 @@ dependencies { compile 'se.emilsjolander:stickylistheaders:2.7.0' compile 'com.google.android.gms:play-services-analytics:8.1.0' compile 'io.karim:materialtabs:2.0.2' + compile project(':MPChartLib') + implementation 'com.google.maps.android:android-maps-utils:2.2.3' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8d5306f..97d2058 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,240 +1,263 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/gov/dol/childlabor/MainActivity.java b/app/src/main/java/gov/dol/childlabor/MainActivity.java index 8a1411f..cdcd927 100755 --- a/app/src/main/java/gov/dol/childlabor/MainActivity.java +++ b/app/src/main/java/gov/dol/childlabor/MainActivity.java @@ -21,6 +21,8 @@ import androidx.core.view.ViewCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; +import gov.dol.childlabor.charts.DataVisualizationActivity; + public class MainActivity extends AppCompatActivity { TextView toolbarTextView; @@ -45,7 +47,7 @@ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCo setSupportActionBar(toolbar); - String[] items = {"Countries/Areas", "Goods", "Exploitation Types"}; + String[] items = {"Countries/Areas", "Goods", "Exploitation Types","Data Visualizations"}; ArrayAdapter itemsAdapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, items) { @Override public View getView(int position, View convertView, ViewGroup parent) { @@ -85,6 +87,9 @@ public void onItemClick(AdapterView parent, View view, int position, long id) case "Exploitation Types": intent = new Intent(getApplicationContext(), ExploitationTypeListSpinnerActivity.class); break; + case "Data Visualizations": + intent = new Intent(getApplicationContext(), DataVisualizationActivity.class); + break; } startActivity(intent); diff --git a/app/src/main/java/gov/dol/childlabor/StatisticsActivity.java b/app/src/main/java/gov/dol/childlabor/StatisticsActivity.java index e721b20..e0409f4 100755 --- a/app/src/main/java/gov/dol/childlabor/StatisticsActivity.java +++ b/app/src/main/java/gov/dol/childlabor/StatisticsActivity.java @@ -1,5 +1,6 @@ package gov.dol.childlabor; +import android.content.Intent; import android.graphics.Color; import android.os.Bundle; import android.view.LayoutInflater; @@ -15,6 +16,9 @@ import java.util.Arrays; import java.util.Locale; +import gov.dol.childlabor.charts.HalfPieChartActivity; +import gov.dol.childlabor.charts.PieChartActivity; + public class StatisticsActivity extends AppCompatActivity { String cname; @Override @@ -27,6 +31,8 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_statistics); + + if (Arrays.asList(MultiText).contains(country.getName())) { setContentView(R.layout.activity_statistics_multi); } @@ -37,6 +43,15 @@ protected void onCreate(Bundle savedInstanceState) { Country.Statistics statistics = country.statistics; + findViewById(R.id.open_analysis).setOnClickListener(view -> { + Intent intent = new Intent(this, PieChartActivity.class); + intent.putExtra("Country",country.getName()); + intent.putExtra("Agriculture",statistics.agriculturePercent); + intent.putExtra("Services",statistics.servicesPercent); + intent.putExtra("Industry",statistics.industryPercent); + startActivity(intent); + }); + if (Arrays.asList(MultiText).contains(country.getName())) { cname = country.getName(); displayTerritoriesworking((LinearLayout) findViewById(R.id.workinglinearlayout)); diff --git a/app/src/main/java/gov/dol/childlabor/charts/DataVisualizationActivity.java b/app/src/main/java/gov/dol/childlabor/charts/DataVisualizationActivity.java new file mode 100644 index 0000000..31e9725 --- /dev/null +++ b/app/src/main/java/gov/dol/childlabor/charts/DataVisualizationActivity.java @@ -0,0 +1,16 @@ +package gov.dol.childlabor.charts; + +import androidx.appcompat.app.AppCompatActivity; + +import android.os.Bundle; + +import gov.dol.childlabor.R; + +public class DataVisualizationActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_data_visualization); + } +} \ No newline at end of file diff --git a/app/src/main/java/gov/dol/childlabor/charts/HalfPieChartActivity.java b/app/src/main/java/gov/dol/childlabor/charts/HalfPieChartActivity.java new file mode 100644 index 0000000..0d80a34 --- /dev/null +++ b/app/src/main/java/gov/dol/childlabor/charts/HalfPieChartActivity.java @@ -0,0 +1,165 @@ +package gov.dol.childlabor.charts; + +import android.content.Intent; +import android.graphics.Color; +import android.graphics.Typeface; +import android.net.Uri; +import android.os.Bundle; +import android.text.SpannableString; +import android.text.style.ForegroundColorSpan; +import android.text.style.RelativeSizeSpan; +import android.text.style.StyleSpan; +import android.util.DisplayMetrics; +import android.view.Menu; +import android.view.MenuItem; +import android.view.WindowManager; +import android.widget.RelativeLayout; + +import androidx.appcompat.app.AppCompatActivity; + +import com.github.mikephil.charting.animation.Easing; +import com.github.mikephil.charting.charts.PieChart; +import com.github.mikephil.charting.components.Legend; +import com.github.mikephil.charting.data.PieData; +import com.github.mikephil.charting.data.PieDataSet; +import com.github.mikephil.charting.data.PieEntry; +import com.github.mikephil.charting.formatter.PercentFormatter; +import com.github.mikephil.charting.utils.ColorTemplate; + +import java.util.ArrayList; + +import gov.dol.childlabor.R; + +@SuppressWarnings("SameParameterValue") +public class HalfPieChartActivity extends AppCompatActivity { + + private PieChart chart; + String country = "Country"; + Float ag,se,in; + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_piechart_half); + + setTitle("HalfPieChartActivity"); + country = getIntent().getStringExtra("Country"); + String agriculture = getIntent().getStringExtra("Agriculture"); + String services = getIntent().getStringExtra("Services"); + String industry = getIntent().getStringExtra("Industry"); + try { + ag = Float.parseFloat(agriculture); + se = Float.parseFloat(services); + in = Float.parseFloat(industry); + }catch (Exception e){ + e.printStackTrace(); + ag = .333f; + se = .333f; + in = .333f; + } + + + + + chart = findViewById(R.id.chart1); + chart.setBackgroundColor(Color.WHITE); + + moveOffScreen(); + + chart.setUsePercentValues(true); + chart.getDescription().setEnabled(false); + + //chart.setCenterTextTypeface(tfLight); + chart.setCenterText(generateCenterSpannableText()); + + chart.setDrawHoleEnabled(true); + chart.setHoleColor(Color.WHITE); + + chart.setTransparentCircleColor(Color.WHITE); + chart.setTransparentCircleAlpha(110); + + chart.setHoleRadius(58f); + chart.setTransparentCircleRadius(61f); + + chart.setDrawCenterText(true); + + chart.setRotationEnabled(false); + chart.setHighlightPerTapEnabled(true); + + chart.setMaxAngle(360f); // HALF CHART + chart.setRotationAngle(360f); + chart.setCenterTextOffset(0, 0); + + setData(); + + chart.animateY(1400, Easing.EaseInOutQuad); + + Legend l = chart.getLegend(); + l.setVerticalAlignment(Legend.LegendVerticalAlignment.TOP); + l.setHorizontalAlignment(Legend.LegendHorizontalAlignment.CENTER); + l.setOrientation(Legend.LegendOrientation.HORIZONTAL); + l.setDrawInside(false); + l.setXEntrySpace(7f); + l.setYEntrySpace(0f); + l.setYOffset(0f); + + // entry label styling + chart.setEntryLabelColor(Color.WHITE); + //chart.setEntryLabelTypeface(tfRegular); + chart.setEntryLabelTextSize(12f); + } + + private void setData() { + + ArrayList values = new ArrayList<>(); + values.add(new PieEntry(ag*100, "Agriculture")); + values.add(new PieEntry(se*100, "Services")); + values.add(new PieEntry(in*100, "Industry")); + + PieDataSet dataSet = new PieDataSet(values, "Working Statistics"); + dataSet.setSliceSpace(3f); + dataSet.setSelectionShift(5f); + + dataSet.setColors(ColorTemplate.MATERIAL_COLORS); + //dataSet.setSelectionShift(0f); + + PieData data = new PieData(dataSet); + data.setValueFormatter(new PercentFormatter()); + data.setValueTextSize(11f); + data.setValueTextColor(Color.WHITE); + //data.setValueTypeface(tfLight); + chart.setData(data); + + chart.invalidate(); + } + + private SpannableString generateCenterSpannableText() { + + SpannableString s = new SpannableString(country+"\nWorking Statistics"); + s.setSpan(new RelativeSizeSpan(1.7f), 0, country.length(), 0); + s.setSpan(new StyleSpan(Typeface.NORMAL), country.length(), s.length() - country.length()-1, 0); + //s.setSpan(new ForegroundColorSpan(Color.GRAY), 14, s.length() - 15, 0); + //s.setSpan(new RelativeSizeSpan(.8f), 14, s.length() - 15, 0); + //s.setSpan(new StyleSpan(Typeface.ITALIC), s.length() - 14, s.length(), 0); + //s.setSpan(new ForegroundColorSpan(ColorTemplate.getHoloBlue()), s.length() - 14, s.length(), 0); + return s; + } + + private void moveOffScreen() { + + DisplayMetrics displayMetrics = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); + + int height = displayMetrics.heightPixels; + + int offset = (int)(height * 0.65); /* percent to move */ + + RelativeLayout.LayoutParams rlParams = + (RelativeLayout.LayoutParams) chart.getLayoutParams(); + rlParams.setMargins(0, 0, 0, -offset); + chart.setLayoutParams(rlParams); + } + + + +} diff --git a/app/src/main/java/gov/dol/childlabor/charts/PieChartActivity.java b/app/src/main/java/gov/dol/childlabor/charts/PieChartActivity.java new file mode 100644 index 0000000..1f2a202 --- /dev/null +++ b/app/src/main/java/gov/dol/childlabor/charts/PieChartActivity.java @@ -0,0 +1,207 @@ +package gov.dol.childlabor.charts; + +import android.Manifest; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.Color; +import android.graphics.Typeface; +import android.net.Uri; +import android.os.Bundle; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; +import android.text.SpannableString; +import android.text.style.ForegroundColorSpan; +import android.text.style.RelativeSizeSpan; +import android.text.style.StyleSpan; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.WindowManager; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; +import android.widget.TextView; + +import com.github.mikephil.charting.animation.Easing; +import com.github.mikephil.charting.charts.PieChart; +import com.github.mikephil.charting.components.Legend; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.PieData; +import com.github.mikephil.charting.data.PieDataSet; +import com.github.mikephil.charting.data.PieEntry; +import com.github.mikephil.charting.formatter.PercentFormatter; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.listener.OnChartValueSelectedListener; +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.MPPointF; + +import java.util.ArrayList; + +import gov.dol.childlabor.R; + +public class PieChartActivity extends AppCompatActivity implements + OnChartValueSelectedListener { + + private PieChart chart; + Float ag,se,in; + String country = "Country"; + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN); + setContentView(R.layout.activity_piechart_half); + + setTitle("PieChartActivity"); + + country = getIntent().getStringExtra("Country"); + String agriculture = getIntent().getStringExtra("Agriculture"); + String services = getIntent().getStringExtra("Services"); + String industry = getIntent().getStringExtra("Industry"); + try { + ag = Float.parseFloat(agriculture); + se = Float.parseFloat(services); + in = Float.parseFloat(industry); + }catch (Exception e){ + e.printStackTrace(); + ag = .333f; + se = .333f; + in = .333f; + } + + chart = findViewById(R.id.chart1); + chart.setUsePercentValues(true); + chart.getDescription().setEnabled(false); + chart.setExtraOffsets(5, 10, 5, 5); + + chart.setDragDecelerationFrictionCoef(0.95f); + + chart.setCenterText(generateCenterSpannableText()); + + chart.setDrawHoleEnabled(true); + chart.setHoleColor(Color.WHITE); + + chart.setTransparentCircleColor(Color.WHITE); + chart.setTransparentCircleAlpha(110); + + chart.setHoleRadius(58f); + chart.setTransparentCircleRadius(61f); + + chart.setDrawCenterText(true); + + chart.setRotationAngle(0); + // enable rotation of the chart by touch + chart.setRotationEnabled(true); + chart.setHighlightPerTapEnabled(true); + + // chart.setUnit(" €"); + // chart.setDrawUnitsInChart(true); + + // add a selection listener + chart.setOnChartValueSelectedListener(this); + + + chart.animateY(1400, Easing.EaseInOutQuad); + // chart.spin(2000, 0, 360); + + Legend l = chart.getLegend(); + l.setVerticalAlignment(Legend.LegendVerticalAlignment.TOP); + l.setHorizontalAlignment(Legend.LegendHorizontalAlignment.RIGHT); + l.setOrientation(Legend.LegendOrientation.VERTICAL); + l.setDrawInside(false); + l.setXEntrySpace(7f); + l.setYEntrySpace(0f); + l.setYOffset(0f); + + // entry label styling + chart.setEntryLabelColor(Color.WHITE); + chart.setEntryLabelTextSize(12f); + setData(); + } + + private void setData() { + //ArrayList entries = new ArrayList<>(); + + // NOTE: The order of the entries when being added to the entries array determines their position around the center of + // the chart. + ArrayList values = new ArrayList<>(); + values.add(new PieEntry(ag*100, "Agriculture")); + values.add(new PieEntry(se*100, "Services")); + values.add(new PieEntry(in*100, "Industry")); + + PieDataSet dataSet = new PieDataSet(values, "Election Results"); + + dataSet.setDrawIcons(false); + + dataSet.setSliceSpace(3f); + dataSet.setIconsOffset(new MPPointF(0, 40)); + dataSet.setSelectionShift(5f); + + // add a lot of colors + + ArrayList colors = new ArrayList<>(); + + for (int c : ColorTemplate.VORDIPLOM_COLORS) + colors.add(c); + + for (int c : ColorTemplate.JOYFUL_COLORS) + colors.add(c); + + for (int c : ColorTemplate.COLORFUL_COLORS) + colors.add(c); + + for (int c : ColorTemplate.LIBERTY_COLORS) + colors.add(c); + + for (int c : ColorTemplate.PASTEL_COLORS) + colors.add(c); + + colors.add(ColorTemplate.getHoloBlue()); + + dataSet.setColors(colors); + //dataSet.setSelectionShift(0f); + + PieData data = new PieData(dataSet); + data.setValueFormatter(new PercentFormatter()); + data.setValueTextSize(11f); + data.setValueTextColor(Color.WHITE); + chart.setData(data); + + // undo all highlights + chart.highlightValues(null); + + chart.invalidate(); + } + + + + + private SpannableString generateCenterSpannableText() { + + SpannableString s = new SpannableString(country+"\nWorking Statistics"); + s.setSpan(new RelativeSizeSpan(1.7f), 0, country.length(), 0); + s.setSpan(new StyleSpan(Typeface.NORMAL), country.length(), s.length() - country.length()-1, 0); + //s.setSpan(new ForegroundColorSpan(Color.GRAY), 14, s.length() - 15, 0); + //s.setSpan(new RelativeSizeSpan(.8f), 14, s.length() - 15, 0); + //s.setSpan(new StyleSpan(Typeface.ITALIC), s.length() - 14, s.length(), 0); + //s.setSpan(new ForegroundColorSpan(ColorTemplate.getHoloBlue()), s.length() - 14, s.length(), 0); + return s; + } + + @Override + public void onValueSelected(Entry e, Highlight h) { + + if (e == null) + return; + Log.i("VAL SELECTED", + "Value: " + e.getY() + ", index: " + h.getX() + + ", DataSet index: " + h.getDataSetIndex()); + } + + @Override + public void onNothingSelected() { + Log.i("PieChart", "nothing selected"); + } + +} diff --git a/app/src/main/res/drawable/circle.xml b/app/src/main/res/drawable/circle.xml new file mode 100644 index 0000000..2dba464 --- /dev/null +++ b/app/src/main/res/drawable/circle.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_data_visualization.xml b/app/src/main/res/layout/activity_data_visualization.xml new file mode 100644 index 0000000..d33c208 --- /dev/null +++ b/app/src/main/res/layout/activity_data_visualization.xml @@ -0,0 +1,24 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_piechart_half.xml b/app/src/main/res/layout/activity_piechart_half.xml new file mode 100644 index 0000000..9d58c85 --- /dev/null +++ b/app/src/main/res/layout/activity_piechart_half.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/app/src/main/res/layout/content_statistics.xml b/app/src/main/res/layout/content_statistics.xml index bb0aee9..e1d79b4 100755 --- a/app/src/main/res/layout/content_statistics.xml +++ b/app/src/main/res/layout/content_statistics.xml @@ -244,6 +244,18 @@ android:text="Data are from most recent year available. For more information, see Table 1 of the Report PDF." android:background="#dadada" /> + + diff --git a/settings.gradle b/settings.gradle index d3db109..a8da478 100755 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,2 @@ include ':app' +include ':MPChartLib' From e1f392f363ee28c28ef4d4575fff5e4cf6fb2a0d Mon Sep 17 00:00:00 2001 From: Gnanendra18 Date: Thu, 5 May 2022 21:39:08 +0530 Subject: [PATCH 2/5] proportional_area_chart --- .../dol/childlabor/charts/BubbleLayout.java | 529 ++++++++++++++++++ .../gov/dol/childlabor/charts/BubbleView.java | 156 ++++++ .../charts/DataVisualizationActivity.java | 49 +- .../layout/activity_data_visualization.xml | 179 +++++- .../res/layout/proportional_area_chart.xml | 20 + .../main/res/values/attr_bubble_layout.xml | 8 + app/src/main/res/values/colors.xml | 22 + 7 files changed, 956 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/gov/dol/childlabor/charts/BubbleLayout.java create mode 100644 app/src/main/java/gov/dol/childlabor/charts/BubbleView.java create mode 100644 app/src/main/res/layout/proportional_area_chart.xml create mode 100644 app/src/main/res/values/attr_bubble_layout.xml diff --git a/app/src/main/java/gov/dol/childlabor/charts/BubbleLayout.java b/app/src/main/java/gov/dol/childlabor/charts/BubbleLayout.java new file mode 100644 index 0000000..c4d15da --- /dev/null +++ b/app/src/main/java/gov/dol/childlabor/charts/BubbleLayout.java @@ -0,0 +1,529 @@ +package gov.dol.childlabor.charts; + +import android.graphics.Rect; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; + +import gov.dol.childlabor.R; + +public class BubbleLayout extends ViewGroup implements BubbleView.MoveListener { + + public static final int DEFAULT_PADDING = 10; + public static final int DEFAULT_MIN_SPEED = 200; + public static final int DEFAULT_MAX_SPEED = 500; + + private int padding = DEFAULT_PADDING; + private int minPxPerTenMilliseconds = DEFAULT_MIN_SPEED; + private int maxPxPerTenMilliseconds = DEFAULT_MAX_SPEED; + + private double mRadiansPiece = 2 * Math.PI / 6; + private int mRandomRadians = 0; + private List mBubbleInfos = new ArrayList<>(); + private Timer mTimer; + + private MyHandler mHandler; + + public BubbleLayout(Context context) { + this(context, null); + } + + public BubbleLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public BubbleLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr); + } + + private void init(Context context, AttributeSet attrs, int defStyleAttr) { + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.bubbleview_BubbleLayout, defStyleAttr, 0); + for (int i = 0; i < typedArray.getIndexCount(); i++) { + int id = typedArray.getIndex(i); + if (id == R.styleable.bubbleview_BubbleLayout_bubbleview_minSpeed) { + minPxPerTenMilliseconds = typedArray.getDimensionPixelSize(id, DEFAULT_MIN_SPEED); + } else if (id == R.styleable.bubbleview_BubbleLayout_bubbleview_maxSpeed) { + maxPxPerTenMilliseconds = typedArray.getDimensionPixelSize(id, DEFAULT_MAX_SPEED); + } else if (id == R.styleable.bubbleview_BubbleLayout_bubbleview_padding) { + padding = typedArray.getDimensionPixelSize(id, DEFAULT_PADDING); + } + } + typedArray.recycle(); + + mRandomRadians = getRandomBetween(0, (int) (2 * Math.PI)); + mHandler = new MyHandler(this); + mHandler.sendEmptyMessage(0); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + Rect baseRect = null; + int currentRadians = mRandomRadians; + List sortResult = sort(); + for (int i = 0; i < sortResult.size(); i++) { + View child = sortResult.get(i); + + BubbleInfo bubbleInfo = getBubbleInfoByView(child); + + if (bubbleInfo != null) { + BubbleView bubbleView = (BubbleView) child; + bubbleView.setMoveListener(this); + bubbleView.setBubbleInfo(bubbleInfo); + int radius = bubbleView.getMeasuredWidth() / 2; + if (i == 0) { + baseRect = getBounds(getMeasuredWidth() / 2 - radius, getMeasuredHeight() / 2 - radius, child.getMeasuredWidth(), child.getMeasuredHeight()); + child.layout(baseRect.left, baseRect.top, baseRect.right, baseRect.bottom); + bubbleInfo.setRect(baseRect); + } else { + int baseCenterX = baseRect.left + baseRect.width() / 2; + int baseCenterY = baseRect.top + baseRect.width() / 2; + + currentRadians += mRadiansPiece; + int[] center = getRadianPoint(baseRect.width() / 2 + padding + radius, baseCenterX, baseCenterY, currentRadians); + + Rect rect = getBounds(center[0] - radius, center[1] - radius, child.getMeasuredWidth(), child.getMeasuredHeight()); + child.layout(rect.left, rect.top, rect.right, rect.bottom); + bubbleInfo.setRect(rect); + } + } + } + } + + private BubbleInfo getBubbleInfoByView(View child) { + for (BubbleInfo info : mBubbleInfos) { + BubbleView bubbleView = (BubbleView) getChildAt(info.getIndex()); + if (bubbleView == child) { + return info; + } + } + return null; + } + + private int[] getRadianPoint(int amount, int x, int y, double radian) { + int resultX = x + (int) (amount * Math.cos(radian)); + int resultY = y + (int) (amount * Math.sin(radian)); + + return new int[]{resultX, resultY}; + } + + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + measureChildren(widthMeasureSpec, heightMeasureSpec); + + setupBubbleInfoList(); + + switch (widthMode) { + case MeasureSpec.EXACTLY: + case MeasureSpec.AT_MOST: + break; + } + + switch (heightMode) { + case MeasureSpec.EXACTLY: + break; + case MeasureSpec.AT_MOST: + case MeasureSpec.UNSPECIFIED: + break; + } + + setMeasuredDimension(widthSize, heightSize); + } + + public void addViewSortByWidth(BubbleView newChild) { + + LayoutParams param = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + newChild.setLayoutParams(param); + if (getChildCount() > 0) { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child instanceof BubbleView) { + BubbleView bubbleView = (BubbleView) child; + float textWidth = bubbleView.getTextMeasureWidth(); + if (newChild.getTextMeasureWidth() > textWidth) { + super.addView(newChild, i); + return; + } + } + } + } + super.addView(newChild); + } + + private void setupBubbleInfoList() { + int count = getChildCount(); + for (int i = 0; i < count; i++) { + BubbleInfo info = new BubbleInfo(); + info.setRadians(getRandomRadians()); + info.setSpeed(getRandomBetween(minPxPerTenMilliseconds, maxPxPerTenMilliseconds)); + info.setOldSpeed(info.getSpeed()); + info.setIndex(i); + mBubbleInfos.add(info); + } + } + + + private int getRandomBetween(int min, int max) { + return min + (int) (Math.random() * ((max - min) + 1)); + } + + private void setChildFrame(View child, int left, int top, int width, int height) { + child.layout(left, top, left + width, top + height); + } + + private Rect getBounds(int left, int top, int width, int height) { + return new Rect(left, top, left + width, top + height); + } + + + private boolean doRectOverlap(Rect rect0, Rect rect1) { + return !(rect0.left > rect1.right || rect1.left > rect0.right) && !(rect0.top > rect1.bottom || rect1.top > rect0.bottom); + } + + private boolean doCircleOverlap(Rect rect0, Rect rect1) { + int x0 = rect0.centerX(); + int y0 = rect0.centerY(); + int r0 = rect0.width() / 2; + int x1 = rect1.centerX(); + int y1 = rect1.centerY(); + int r1 = rect1.width() / 2; + + return Math.pow(x0 - x1, 2) + Math.pow(y0 - y1, 2) <= Math.pow(r0 + r1, 2); + } + + private boolean chooseFromTwo() { + return Math.random() > 0.5; + } + + private List sort() { + List allBubbleChild = new ArrayList<>(); + for (int i = 0; i < getChildCount(); i++) { + View view = getChildAt(i); + if (view != null && view instanceof BubbleView) { + allBubbleChild.add((BubbleView) view); + } + } + + List sortResult = new ArrayList<>(); + + if (allBubbleChild.size() > 2) { + sortResult.add(allBubbleChild.get(0)); + sortResult.add(allBubbleChild.get(1)); + + List halfList = allBubbleChild.subList(2, allBubbleChild.size()); + List quarter1List = halfList.subList(0, halfList.size() / 2); + List quarter2List = halfList.subList(halfList.size() / 2, halfList.size()); + int count = Math.max(quarter1List.size(), quarter2List.size()); + for (int i = 0; i < count; i++) { + if (i < quarter2List.size()) { + sortResult.add(quarter2List.get(i)); + } + if (i < quarter1List.size()) { + sortResult.add(quarter1List.get(i)); + } + } + + } else { + sortResult = allBubbleChild; + } + + return sortResult; + } + + private void startAnimate() { + if (mTimer == null) { + mTimer = new Timer(); + } + mTimer.schedule(new TimerTask() { + @Override + public void run() { + mHandler.sendEmptyMessage(0); + this.cancel(); + } + + }, 10); + + } + + private static class MyHandler extends Handler { + private WeakReference mBubbleLayout; + + public MyHandler(BubbleLayout layout) { + this.mBubbleLayout = new WeakReference<>(layout); + } + + @Override + public void handleMessage(Message msg) { + BubbleLayout layout = mBubbleLayout.get(); + int count = layout.getChildCount(); + for (int i = 0; i < count && layout.mBubbleInfos.size() > 0; i++) { + + BubbleInfo bubbleInfo = layout.mBubbleInfos.get(i); + + List overlapList = layout.hasOverlap(bubbleInfo); + + Point overlapPoint = layout.ifOverlapBounds(bubbleInfo); + if (overlapPoint != null) { + layout.reverseIfOverlapBounds(bubbleInfo); + } else if (overlapList.size() > 0) { + layout.dealWithOverlap(); + } + if(overlapList.size()!=0){ + layout.moveBubble(bubbleInfo); + } + + } + layout.startAnimate(); + } + } + + ; + + private void moveBubble(BubbleInfo info) { + View child = getChildAt(info.getIndex()); + int[] center = getRadianPoint(info.getSpeed(), child.getLeft() + child.getWidth() / 2, child.getTop() + child.getWidth() / 2, info.getRadians()); + Rect rect = getBounds(center[0] - child.getWidth() / 2, center[1] - child.getWidth() / 2, child.getMeasuredWidth(), child.getMeasuredHeight()); + info.setRect(rect); + child.layout(rect.left, rect.top, rect.right, rect.bottom); + } + + private void slowerBubbleIfNeeded(BubbleInfo info) { + if (info.getOldSpeed() > 0 && info.getSpeed() > info.getOldSpeed()) { + info.setSpeed(info.getSpeed() - 1); + } + } + + private BubbleInfo getNewMoveInfo(BubbleInfo bubbleInfo, View child, List overlapRect) { + Rect oldRect = bubbleInfo.getRect(); + + Point cooperate = getCooperatePoint(overlapRect); + Point overlapBoundPoint = ifOverlapBounds(bubbleInfo); + if (overlapBoundPoint != null) { + cooperate = new Point((cooperate.x + overlapBoundPoint.x) / 2, (cooperate.y + overlapBoundPoint.y) / 2); + } + + float overlapRadians = (float) getRadians(new float[]{oldRect.exactCenterX(), oldRect.exactCenterY()}, new float[]{cooperate.x, cooperate.y}); + double reverseRadians = getReverseRadians(overlapRadians); + + int[] centerNew = getRadianPoint(bubbleInfo.getSpeed(), child.getLeft() + child.getWidth() / 2, child.getTop() + child.getWidth() / 2, reverseRadians); + Rect rectNew = getBounds(centerNew[0] - child.getWidth() / 2, centerNew[1] - child.getWidth() / 2, child.getMeasuredWidth(), child.getMeasuredHeight()); + + BubbleInfo bubbleInfoNew = new BubbleInfo(); + bubbleInfoNew.setIndex(bubbleInfo.getIndex()); + bubbleInfoNew.setSpeed(bubbleInfo.getSpeed()); + bubbleInfoNew.setOldSpeed(bubbleInfo.getOldSpeed()); + bubbleInfoNew.setRadians(reverseRadians); + bubbleInfoNew.setRect(rectNew); + + return bubbleInfoNew; + + } + + private void dealWithOverlap() { + List tempBubbleInfoList = new ArrayList<>(); + for (BubbleInfo info : mBubbleInfos) { + List overlapList = hasOverlap(info); + if (overlapList.size() > 0) { + BubbleInfo bubbleInfoNew = getNewMoveInfo(info, getChildAt(info.getIndex()), overlapList); + slowerBubbleIfNeeded(bubbleInfoNew); + tempBubbleInfoList.add(bubbleInfoNew); + } + } + + for (int i = 0; i < tempBubbleInfoList.size(); i++) { + BubbleInfo tempBubbleInfo = tempBubbleInfoList.get(i); + BubbleInfo oldBubbleInfo = mBubbleInfos.get(tempBubbleInfo.getIndex()); + oldBubbleInfo.setRadians(tempBubbleInfo.getRadians()); + oldBubbleInfo.setOldSpeed(tempBubbleInfo.getOldSpeed()); + oldBubbleInfo.setIndex(tempBubbleInfo.getIndex()); + oldBubbleInfo.setSpeed(tempBubbleInfo.getSpeed()); + oldBubbleInfo.setRect(tempBubbleInfo.getRect()); + } + + } + + private Point getCooperatePoint(List overlapRect) { + int totalX = 0; + int totalY = 0; + for (BubbleInfo info : overlapRect) { + totalX += info.getRect().exactCenterX(); + totalY += info.getRect().exactCenterY(); + } + + return new Point(totalX / overlapRect.size(), totalY / overlapRect.size()); + } + + private double getReverseRadians(double radians) { + double reverseRadians; + if (radians > Math.PI) { + reverseRadians = radians - Math.PI; + } else { + reverseRadians = radians + Math.PI; + } + + return reverseRadians; + } + + private List hasOverlap(BubbleInfo bubbleInfo) { + int count = mBubbleInfos.size(); + List overlapList = new ArrayList<>(); + if (bubbleInfo.getRect() != null) { + for (int i = 0; i < count; i++) { + BubbleInfo otherInfo = mBubbleInfos.get(i); + if (i != bubbleInfo.getIndex()) { + if (otherInfo.getRect() != null) { + if (doCircleOverlap(otherInfo.getRect(), bubbleInfo.getRect())) { + overlapList.add(otherInfo); + } + } + } + } + } + return overlapList; + } + + private void reverseIfOverlapBounds(BubbleInfo bubbleInfo) { + Point overlapPoint = ifOverlapBounds(bubbleInfo); + Rect totalRect = new Rect(this.getLeft(), this.getTop(), this.getRight(), this.getBottom()); + if (overlapPoint != null) { + float overlapRadians = (float) getRadians(new float[]{bubbleInfo.getRect().exactCenterX(), bubbleInfo.getRect().exactCenterY()}, new float[]{overlapPoint.x, overlapPoint.y}); + if (!totalRect.contains(bubbleInfo.getRect().centerX(), bubbleInfo.getRect().centerY())) { + bubbleInfo.setRadians(overlapRadians); + } else { + double reverseRadians = getReverseRadians(overlapRadians); + bubbleInfo.setRadians(reverseRadians); + } + slowerBubbleIfNeeded(bubbleInfo); + } + } + + private Point ifOverlapBounds(BubbleInfo bubbleInfo) { + Rect rect = new Rect(this.getLeft(), this.getTop(), this.getRight(), this.getBottom()); + if (bubbleInfo.getRect() != null) { + Rect bubbleRect = bubbleInfo.getRect(); + List overlapPoints = new ArrayList<>(); + if (rect.left >= bubbleRect.left) { + Point overlapPoint = new Point(rect.left, bubbleRect.centerY()); + overlapPoints.add(overlapPoint); + } + if (rect.top >= bubbleRect.top) { + Point overlapPoint = new Point(bubbleRect.centerX(), rect.top); + overlapPoints.add(overlapPoint); + } + if (rect.right <= bubbleRect.right) { + Point overlapPoint = new Point(rect.right, bubbleRect.centerY()); + overlapPoints.add(overlapPoint); + } + if (rect.bottom <= bubbleRect.bottom) { + Point overlapPoint = new Point(bubbleRect.centerX(), rect.bottom); + overlapPoints.add(overlapPoint); + } + + if (overlapPoints.size() > 0) { + int totalX = 0; + int totalY = 0; + for (Point point : overlapPoints) { + totalX += point.x; + totalY += point.y; + } + return new Point(totalX / overlapPoints.size(), totalY / overlapPoints.size()); + } + } + + return null; + } + + private double getRandomRadians() { + return Math.random() * 2 * Math.PI; + } + + private double getRadians(float[] fromPoint, float[] toPoint) { + return Math.atan2(toPoint[1] - fromPoint[1], toPoint[0] - fromPoint[0]); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mTimer != null) { + mTimer.cancel(); + mTimer = null; + } + } + + @Override + public void onMove(BubbleInfo bubbleInfo, int centerX, int centerY, int deltaX, int deltaY, double velocity) { + velocity /= 6; + if (velocity > bubbleInfo.getSpeed()) { + float radians = (float) getRadians(new float[]{centerX, centerY}, new float[]{centerX + deltaX, centerY + deltaY}); + bubbleInfo.setRadians(radians); + bubbleInfo.setSpeed((int) velocity); + } + } +} +class BubbleInfo { + private int index; + private Rect rect; + private double radians; + private int speed; + private int oldSpeed; + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + public Rect getRect() { + return rect; + } + + public void setRect(Rect rect) { + this.rect = rect; + } + + public double getRadians() { + return radians; + } + + public void setRadians(double radians) { + this.radians = radians; + } + + public int getSpeed() { + return speed; + } + + public void setSpeed(int speed) { + this.speed = speed; + } + + public int getOldSpeed() { + return oldSpeed; + } + + public void setOldSpeed(int oldSpeed) { + this.oldSpeed = oldSpeed; + } +} \ No newline at end of file diff --git a/app/src/main/java/gov/dol/childlabor/charts/BubbleView.java b/app/src/main/java/gov/dol/childlabor/charts/BubbleView.java new file mode 100644 index 0000000..0243fdd --- /dev/null +++ b/app/src/main/java/gov/dol/childlabor/charts/BubbleView.java @@ -0,0 +1,156 @@ +package gov.dol.childlabor.charts; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.os.Handler; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.ViewConfiguration; +import android.widget.TextView; + +import androidx.appcompat.widget.AppCompatTextView; + +public class BubbleView extends AppCompatTextView { + + private Paint mPaint; + + private ViewConfiguration mViewConfiguration; + private VelocityTracker mVelocityTracker; + private int mLastX; + private int mLastY; + + private boolean mIsMoving; + private MoveListener mMoveListener; + private BubbleInfo mBubbleInfo; + + public BubbleView(Context context) { + super(context); + init(context); + } + + public BubbleView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public BubbleView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + private void init(Context context) { + mPaint = new Paint(); + mPaint.setStyle(Paint.Style.FILL); + mPaint.setAntiAlias(true); + mPaint.setColor(Color.RED); + + mViewConfiguration = ViewConfiguration.get(context); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth()); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + } + + @Override + protected void onDraw(Canvas canvas) { + int width = getWidth(); + int height = getHeight(); + canvas.drawCircle(width / 2, height / 2, width / 2, mPaint); + super.onDraw(canvas); + } + + public float getTextMeasureWidth() { + Rect bounds = new Rect(); + getPaint().getTextBounds(getText().toString(), 0, getText().toString().length(), bounds); + return bounds.width(); + } + + public void setCircleColor(int colorRes) { + int color = getResources().getColor(colorRes); + mPaint.setColor(color); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + int action = event.getActionMasked(); + int index = event.getActionIndex(); + int pointerId = event.getPointerId(index); + + int rawX = (int) event.getRawX(); + int rawY = (int) event.getRawY(); + + int[] location = new int[2]; + getLocationOnScreen(location); + Rect rect = new Rect(location[0], location[1], location[0] + getWidth(), location[1] + getHeight()); + + switch (action) { + case MotionEvent.ACTION_DOWN: + mLastX = rawX; + mLastY = rawY; + if (rect.contains(mLastX, mLastY)) { + mIsMoving = true; + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + mVelocityTracker.addMovement(event); + + break; + case MotionEvent.ACTION_MOVE: + mVelocityTracker.addMovement(event); + mVelocityTracker.computeCurrentVelocity(10); + if (mIsMoving) { + int deltaX = rawX - mLastX; + int deltaY = rawY - mLastY; + if (Math.abs(deltaX) > mViewConfiguration.getScaledTouchSlop() + || Math.abs(deltaY) > mViewConfiguration.getScaledTouchSlop()) { + if (mMoveListener != null) { + int centerX = getLeft() + getWidth() / 2; + int centerY = getTop() + getHeight() / 2; + float velocityX = mVelocityTracker.getXVelocity(pointerId); + float velocityY = mVelocityTracker.getYVelocity(pointerId); + double velocity = Math.sqrt(Math.pow(velocityX, 2) + Math.pow(velocityY, 2)); + mMoveListener.onMove(mBubbleInfo, centerX, centerY, deltaX, deltaY, velocity); + } + mLastX = rawX; + mLastY = rawY; + } + } + break; + case MotionEvent.ACTION_UP: + mIsMoving = false; + break; + case MotionEvent.ACTION_CANCEL: + mIsMoving = false; + mVelocityTracker.recycle(); + break; + } + return true; + } + + public void setMoveListener(MoveListener moveListener) { + this.mMoveListener = moveListener; + } + + public void setBubbleInfo(BubbleInfo bubbleInfo) { + this.mBubbleInfo = bubbleInfo; + } + + public interface MoveListener { + void onMove(BubbleInfo bubbleInfo, int centerX, int centerY, int deltaX, int deltaY, double velocity); + } +} \ No newline at end of file diff --git a/app/src/main/java/gov/dol/childlabor/charts/DataVisualizationActivity.java b/app/src/main/java/gov/dol/childlabor/charts/DataVisualizationActivity.java index 31e9725..167fd2f 100644 --- a/app/src/main/java/gov/dol/childlabor/charts/DataVisualizationActivity.java +++ b/app/src/main/java/gov/dol/childlabor/charts/DataVisualizationActivity.java @@ -2,7 +2,20 @@ import androidx.appcompat.app.AppCompatActivity; +import android.graphics.Color; import android.os.Bundle; +import android.os.Handler; +import android.util.DisplayMetrics; +import android.util.Pair; +import android.view.Gravity; + + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; import gov.dol.childlabor.R; @@ -11,6 +24,40 @@ public class DataVisualizationActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_data_visualization); + setContentView(R.layout.proportional_area_chart); + BubbleLayout layout = findViewById(R.id.bubble_layout); + Map> labels = new HashMap<>(); + labels.put("GOLD",new Pair(2.4f,R.color.yellow)); + labels.put("BRICKS",new Pair(2.0f,R.color.fuchsia)); + labels.put("SUGARCANE",new Pair(1.9f,R.color.silver)); + labels.put("COFFEE",new Pair(1.7f,R.color.purple)); + labels.put("COTTON",new Pair(1.7f,R.color.light_white)); + labels.put("TOBACCO",new Pair(1.7f,R.color.teal)); + labels.put("CATTLE",new Pair(1.4f,R.color.green)); + labels.put("FISH",new Pair(1.4f,R.color.aqua)); + labels.put("GARMENTS",new Pair(1.1f,R.color.red)); + labels.put("RICE",new Pair(0.9f,R.color.light_green)); + labels.put("COAL",new Pair(0.7f,R.color.light_yellow)); + + + + Iterator iterator = labels.keySet().iterator(); + + DisplayMetrics displayMetrics = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); + int height = displayMetrics.heightPixels; + int width = displayMetrics.widthPixels; + while (iterator.hasNext()){ + String label = iterator.next(); + Float value = labels.get(label).first*width*0.2f; + BubbleView bubbleView = new BubbleView(this); + bubbleView.setCircleColor(labels.get(label).second); + bubbleView.setText(label); + bubbleView.setGravity(Gravity.CENTER); + bubbleView.setPadding(10, 10, 10, 10); + bubbleView.setTextColor(Color.parseColor("#000000")); + layout.addView(bubbleView,value.intValue(),value.intValue()); + } + } } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_data_visualization.xml b/app/src/main/res/layout/activity_data_visualization.xml index d33c208..35ee1db 100644 --- a/app/src/main/res/layout/activity_data_visualization.xml +++ b/app/src/main/res/layout/activity_data_visualization.xml @@ -7,18 +7,185 @@ tools:context=".charts.DataVisualizationActivity"> + + + + + + + + + + + + + + + + + + + app:layout_constraintVertical_bias="0.499" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/proportional_area_chart.xml b/app/src/main/res/layout/proportional_area_chart.xml new file mode 100644 index 0000000..f650075 --- /dev/null +++ b/app/src/main/res/layout/proportional_area_chart.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/attr_bubble_layout.xml b/app/src/main/res/values/attr_bubble_layout.xml new file mode 100644 index 0000000..9d24cfd --- /dev/null +++ b/app/src/main/res/values/attr_bubble_layout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 7a66b05..fcd0971 100755 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -3,4 +3,26 @@ #003354 #002740 #FFF + + + #FFFF00 + #FF00FF + #FF0000 + #C0C0C0 + #808080 + #808000 + #800080 + #800000 + #00FFFF + #00FF00 + #008080 + #008000 + #0000FF + #000080 + #CAF1DE + #FFE7C7 + #F7F7F7 + + + From 2737b3f223d78707666657d86c616367323cb934 Mon Sep 17 00:00:00 2001 From: Gnanendra18 Date: Thu, 5 May 2022 21:42:16 +0530 Subject: [PATCH 3/5] MP Chart Library Added --- MPChartLib/.gitignore | 1 + MPChartLib/build.gradle | 29 + MPChartLib/src/main/AndroidManifest.xml | 13 + .../charting/animation/ChartAnimator.java | 207 ++ .../mikephil/charting/animation/Easing.java | 309 +++ .../charting/buffer/AbstractBuffer.java | 91 + .../mikephil/charting/buffer/BarBuffer.java | 130 ++ .../charting/buffer/HorizontalBarBuffer.java | 94 + .../mikephil/charting/charts/BarChart.java | 258 +++ .../charting/charts/BarLineChartBase.java | 1674 +++++++++++++++ .../mikephil/charting/charts/BubbleChart.java | 43 + .../charting/charts/CandleStickChart.java | 44 + .../mikephil/charting/charts/Chart.java | 1822 +++++++++++++++++ .../charting/charts/CombinedChart.java | 272 +++ .../charting/charts/HorizontalBarChart.java | 346 ++++ .../mikephil/charting/charts/LineChart.java | 50 + .../mikephil/charting/charts/PieChart.java | 804 ++++++++ .../charting/charts/PieRadarChartBase.java | 499 +++++ .../mikephil/charting/charts/RadarChart.java | 362 ++++ .../charting/charts/ScatterChart.java | 77 + .../charting/components/AxisBase.java | 816 ++++++++ .../charting/components/ComponentBase.java | 173 ++ .../charting/components/Description.java | 95 + .../mikephil/charting/components/IMarker.java | 47 + .../mikephil/charting/components/Legend.java | 825 ++++++++ .../charting/components/LegendEntry.java | 78 + .../charting/components/LimitLine.java | 215 ++ .../charting/components/MarkerImage.java | 167 ++ .../charting/components/MarkerView.java | 129 ++ .../mikephil/charting/components/XAxis.java | 118 ++ .../mikephil/charting/components/YAxis.java | 467 +++++ .../mikephil/charting/data/BarData.java | 119 ++ .../mikephil/charting/data/BarDataSet.java | 299 +++ .../mikephil/charting/data/BarEntry.java | 310 +++ .../data/BarLineScatterCandleBubbleData.java | 27 + .../BarLineScatterCandleBubbleDataSet.java | 48 + .../mikephil/charting/data/BaseDataSet.java | 506 +++++ .../mikephil/charting/data/BaseEntry.java | 97 + .../mikephil/charting/data/BubbleData.java | 34 + .../mikephil/charting/data/BubbleDataSet.java | 71 + .../mikephil/charting/data/BubbleEntry.java | 91 + .../mikephil/charting/data/CandleData.java | 21 + .../mikephil/charting/data/CandleDataSet.java | 295 +++ .../mikephil/charting/data/CandleEntry.java | 193 ++ .../mikephil/charting/data/ChartData.java | 821 ++++++++ .../mikephil/charting/data/CombinedData.java | 272 +++ .../mikephil/charting/data/DataSet.java | 456 +++++ .../github/mikephil/charting/data/Entry.java | 173 ++ .../mikephil/charting/data/LineData.java | 27 + .../mikephil/charting/data/LineDataSet.java | 417 ++++ .../charting/data/LineRadarDataSet.java | 135 ++ .../data/LineScatterCandleRadarDataSet.java | 120 ++ .../mikephil/charting/data/PieData.java | 100 + .../mikephil/charting/data/PieDataSet.java | 261 +++ .../mikephil/charting/data/PieEntry.java | 86 + .../mikephil/charting/data/RadarData.java | 58 + .../mikephil/charting/data/RadarDataSet.java | 122 ++ .../mikephil/charting/data/RadarEntry.java | 44 + .../mikephil/charting/data/ScatterData.java | 40 + .../charting/data/ScatterDataSet.java | 157 ++ .../charting/data/filter/Approximator.java | 102 + .../charting/data/filter/ApproximatorN.java | 146 ++ .../DrawingDataSetNotCreatedException.java | 14 + .../charting/formatter/ColorFormatter.java | 24 + .../formatter/DefaultAxisValueFormatter.java | 56 + .../formatter/DefaultFillFormatter.java | 45 + .../formatter/DefaultValueFormatter.java | 71 + .../formatter/IAxisValueFormatter.java | 23 + .../charting/formatter/IFillFormatter.java | 24 + .../charting/formatter/IValueFormatter.java | 29 + .../formatter/IndexAxisValueFormatter.java | 69 + .../formatter/LargeValueFormatter.java | 103 + .../charting/formatter/PercentFormatter.java | 49 + .../formatter/StackedValueFormatter.java | 75 + .../charting/highlight/BarHighlighter.java | 163 ++ .../charting/highlight/ChartHighlighter.java | 246 +++ .../highlight/CombinedHighlighter.java | 93 + .../charting/highlight/Highlight.java | 243 +++ .../highlight/HorizontalBarHighlighter.java | 85 + .../charting/highlight/IHighlighter.java | 17 + .../charting/highlight/PieHighlighter.java | 25 + .../highlight/PieRadarHighlighter.java | 66 + .../charting/highlight/RadarHighlighter.java | 79 + .../mikephil/charting/highlight/Range.java | 38 + .../dataprovider/BarDataProvider.java | 11 + ...arLineScatterCandleBubbleDataProvider.java | 16 + .../dataprovider/BubbleDataProvider.java | 8 + .../dataprovider/CandleDataProvider.java | 8 + .../dataprovider/ChartInterface.java | 69 + .../dataprovider/CombinedDataProvider.java | 11 + .../dataprovider/LineDataProvider.java | 11 + .../dataprovider/ScatterDataProvider.java | 8 + .../interfaces/datasets/IBarDataSet.java | 71 + .../IBarLineScatterCandleBubbleDataSet.java | 16 + .../interfaces/datasets/IBubbleDataSet.java | 27 + .../interfaces/datasets/ICandleDataSet.java | 85 + .../interfaces/datasets/IDataSet.java | 486 +++++ .../interfaces/datasets/ILineDataSet.java | 103 + .../datasets/ILineRadarDataSet.java | 58 + .../ILineScatterCandleRadarDataSet.java | 35 + .../interfaces/datasets/IPieDataSet.java | 82 + .../interfaces/datasets/IRadarDataSet.java | 30 + .../interfaces/datasets/IScatterDataSet.java | 38 + .../charting/jobs/AnimatedMoveViewJob.java | 65 + .../charting/jobs/AnimatedViewPortJob.java | 99 + .../charting/jobs/AnimatedZoomJob.java | 119 ++ .../mikephil/charting/jobs/MoveViewJob.java | 56 + .../mikephil/charting/jobs/ViewPortJob.java | 47 + .../mikephil/charting/jobs/ZoomJob.java | 87 + .../listener/BarLineChartTouchListener.java | 698 +++++++ .../charting/listener/ChartTouchListener.java | 143 ++ .../listener/OnChartGestureListener.java | 76 + .../OnChartValueSelectedListener.java | 27 + .../OnDrawLineChartTouchListener.java | 15 + .../charting/listener/OnDrawListener.java | 38 + .../listener/PieRadarChartTouchListener.java | 288 +++ .../mikephil/charting/matrix/Vector3.java | 136 ++ .../charting/model/GradientColor.java | 69 + .../charting/renderer/AxisRenderer.java | 293 +++ .../charting/renderer/BarChartRenderer.java | 498 +++++ .../BarLineScatterCandleBubbleRenderer.java | 96 + .../renderer/BubbleChartRenderer.java | 274 +++ .../renderer/CandleStickChartRenderer.java | 363 ++++ .../renderer/CombinedChartRenderer.java | 167 ++ .../charting/renderer/DataRenderer.java | 169 ++ .../renderer/HorizontalBarChartRenderer.java | 443 ++++ .../charting/renderer/LegendRenderer.java | 570 ++++++ .../charting/renderer/LineChartRenderer.java | 863 ++++++++ .../charting/renderer/LineRadarRenderer.java | 95 + .../LineScatterCandleRadarRenderer.java | 63 + .../charting/renderer/PieChartRenderer.java | 1070 ++++++++++ .../charting/renderer/RadarChartRenderer.java | 398 ++++ .../mikephil/charting/renderer/Renderer.java | 21 + .../renderer/ScatterChartRenderer.java | 197 ++ .../charting/renderer/XAxisRenderer.java | 401 ++++ .../XAxisRendererHorizontalBarChart.java | 310 +++ .../renderer/XAxisRendererRadarChart.java | 71 + .../charting/renderer/YAxisRenderer.java | 352 ++++ .../YAxisRendererHorizontalBarChart.java | 315 +++ .../renderer/YAxisRendererRadarChart.java | 232 +++ .../scatter/ChevronDownShapeRenderer.java | 41 + .../scatter/ChevronUpShapeRenderer.java | 42 + .../renderer/scatter/CircleShapeRenderer.java | 63 + .../renderer/scatter/CrossShapeRenderer.java | 41 + .../renderer/scatter/IShapeRenderer.java | 28 + .../renderer/scatter/SquareShapeRenderer.java | 63 + .../scatter/TriangleShapeRenderer.java | 80 + .../renderer/scatter/XShapeRenderer.java | 42 + .../charting/utils/ColorTemplate.java | 128 ++ .../charting/utils/EntryXComparator.java | 22 + .../github/mikephil/charting/utils/FSize.java | 79 + .../mikephil/charting/utils/FileUtils.java | 301 +++ .../github/mikephil/charting/utils/Fill.java | 342 ++++ .../utils/HorizontalViewPortHandler.java | 29 + .../mikephil/charting/utils/MPPointD.java | 53 + .../mikephil/charting/utils/MPPointF.java | 99 + .../mikephil/charting/utils/ObjectPool.java | 218 ++ .../mikephil/charting/utils/Transformer.java | 459 +++++ .../utils/TransformerHorizontalBarChart.java | 44 + .../github/mikephil/charting/utils/Utils.java | 779 +++++++ .../charting/utils/ViewPortHandler.java | 759 +++++++ .../charting/test/ApproximatorTest.java | 43 + .../charting/test/AxisRendererTest.java | 105 + .../mikephil/charting/test/BarDataTest.java | 72 + .../mikephil/charting/test/ChartDataTest.java | 211 ++ .../mikephil/charting/test/DataSetTest.java | 238 +++ .../test/LargeValueFormatterTest.java | 95 + .../charting/test/ObjectPoolTest.java | 240 +++ 168 files changed, 32653 insertions(+) create mode 100644 MPChartLib/.gitignore create mode 100644 MPChartLib/build.gradle create mode 100644 MPChartLib/src/main/AndroidManifest.xml create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/animation/ChartAnimator.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/animation/Easing.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/buffer/AbstractBuffer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/buffer/BarBuffer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/buffer/HorizontalBarBuffer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/charts/BarChart.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/charts/BarLineChartBase.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/charts/BubbleChart.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/charts/CandleStickChart.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/charts/Chart.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/charts/CombinedChart.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/charts/HorizontalBarChart.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/charts/LineChart.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/charts/PieChart.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/charts/PieRadarChartBase.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/charts/RadarChart.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/charts/ScatterChart.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/components/AxisBase.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/components/ComponentBase.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/components/Description.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/components/IMarker.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/components/Legend.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/components/LegendEntry.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/components/LimitLine.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/components/MarkerImage.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/components/MarkerView.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/components/XAxis.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/components/YAxis.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/BarData.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/BarDataSet.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/BarEntry.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/BarLineScatterCandleBubbleData.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/BarLineScatterCandleBubbleDataSet.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/BaseDataSet.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/BaseEntry.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/BubbleData.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/BubbleDataSet.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/BubbleEntry.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/CandleData.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/CandleDataSet.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/CandleEntry.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/ChartData.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/CombinedData.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/DataSet.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/Entry.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/LineData.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/LineDataSet.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/LineRadarDataSet.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/LineScatterCandleRadarDataSet.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/PieData.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/PieDataSet.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/PieEntry.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/RadarData.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/RadarDataSet.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/RadarEntry.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/ScatterData.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/ScatterDataSet.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/filter/Approximator.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/data/filter/ApproximatorN.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/exception/DrawingDataSetNotCreatedException.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/formatter/ColorFormatter.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/formatter/DefaultAxisValueFormatter.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/formatter/DefaultFillFormatter.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/formatter/DefaultValueFormatter.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IAxisValueFormatter.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IFillFormatter.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IValueFormatter.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IndexAxisValueFormatter.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/formatter/LargeValueFormatter.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/formatter/PercentFormatter.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/formatter/StackedValueFormatter.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/highlight/BarHighlighter.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/highlight/ChartHighlighter.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/highlight/CombinedHighlighter.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/highlight/Highlight.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/highlight/HorizontalBarHighlighter.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/highlight/IHighlighter.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/highlight/PieHighlighter.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/highlight/PieRadarHighlighter.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/highlight/RadarHighlighter.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/highlight/Range.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/BarDataProvider.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/BarLineScatterCandleBubbleDataProvider.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/BubbleDataProvider.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/CandleDataProvider.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/ChartInterface.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/CombinedDataProvider.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/LineDataProvider.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/ScatterDataProvider.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IBarDataSet.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IBarLineScatterCandleBubbleDataSet.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IBubbleDataSet.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ICandleDataSet.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IDataSet.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ILineDataSet.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ILineRadarDataSet.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ILineScatterCandleRadarDataSet.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IPieDataSet.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IRadarDataSet.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IScatterDataSet.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/jobs/AnimatedMoveViewJob.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/jobs/AnimatedViewPortJob.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/jobs/AnimatedZoomJob.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/jobs/MoveViewJob.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/jobs/ViewPortJob.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/jobs/ZoomJob.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/listener/BarLineChartTouchListener.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/listener/ChartTouchListener.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnChartGestureListener.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnChartValueSelectedListener.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnDrawLineChartTouchListener.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnDrawListener.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/listener/PieRadarChartTouchListener.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/matrix/Vector3.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/model/GradientColor.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/AxisRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BarChartRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BarLineScatterCandleBubbleRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BubbleChartRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/CandleStickChartRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/CombinedChartRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/DataRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/HorizontalBarChartRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LegendRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineChartRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineRadarRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineScatterCandleRadarRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/PieChartRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/RadarChartRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/Renderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/ScatterChartRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/XAxisRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/XAxisRendererHorizontalBarChart.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/XAxisRendererRadarChart.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/YAxisRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/YAxisRendererHorizontalBarChart.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/YAxisRendererRadarChart.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/ChevronDownShapeRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/ChevronUpShapeRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/CircleShapeRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/CrossShapeRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/IShapeRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/SquareShapeRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/TriangleShapeRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/XShapeRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/utils/ColorTemplate.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/utils/EntryXComparator.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/utils/FSize.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/utils/FileUtils.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/utils/Fill.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/utils/HorizontalViewPortHandler.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/utils/MPPointD.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/utils/MPPointF.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/utils/ObjectPool.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/utils/Transformer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/utils/TransformerHorizontalBarChart.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/utils/Utils.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/utils/ViewPortHandler.java create mode 100644 MPChartLib/src/test/java/com/github/mikephil/charting/test/ApproximatorTest.java create mode 100644 MPChartLib/src/test/java/com/github/mikephil/charting/test/AxisRendererTest.java create mode 100644 MPChartLib/src/test/java/com/github/mikephil/charting/test/BarDataTest.java create mode 100644 MPChartLib/src/test/java/com/github/mikephil/charting/test/ChartDataTest.java create mode 100644 MPChartLib/src/test/java/com/github/mikephil/charting/test/DataSetTest.java create mode 100644 MPChartLib/src/test/java/com/github/mikephil/charting/test/LargeValueFormatterTest.java create mode 100644 MPChartLib/src/test/java/com/github/mikephil/charting/test/ObjectPoolTest.java diff --git a/MPChartLib/.gitignore b/MPChartLib/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/MPChartLib/.gitignore @@ -0,0 +1 @@ +/build diff --git a/MPChartLib/build.gradle b/MPChartLib/build.gradle new file mode 100644 index 0000000..13283db --- /dev/null +++ b/MPChartLib/build.gradle @@ -0,0 +1,29 @@ +apply plugin: 'com.android.library' + +group='com.github.philjay' + +android { + compileSdkVersion 28 + buildToolsVersion '28.0.3' + defaultConfig { + minSdkVersion 14 + targetSdkVersion 28 + versionCode 3 + versionName '3.1.0' + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + testOptions { + unitTests.returnDefaultValues = true // this prevents "not mocked" error + } +} + +dependencies { + implementation 'androidx.annotation:annotation:1.0.0' + testImplementation 'junit:junit:4.12' +} + diff --git a/MPChartLib/src/main/AndroidManifest.xml b/MPChartLib/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d75f87c --- /dev/null +++ b/MPChartLib/src/main/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/animation/ChartAnimator.java b/MPChartLib/src/main/java/com/github/mikephil/charting/animation/ChartAnimator.java new file mode 100644 index 0000000..e5b82db --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/animation/ChartAnimator.java @@ -0,0 +1,207 @@ +package com.github.mikephil.charting.animation; + +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import androidx.annotation.RequiresApi; + +import com.github.mikephil.charting.animation.Easing.EasingFunction; + +/** + * 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() { } + + @RequiresApi(11) + public ChartAnimator(AnimatorUpdateListener listener) { + mListener = listener; + } + + @RequiresApi(11) + private ObjectAnimator xAnimator(int duration, EasingFunction easing) { + + ObjectAnimator animatorX = ObjectAnimator.ofFloat(this, "phaseX", 0f, 1f); + animatorX.setInterpolator(easing); + animatorX.setDuration(duration); + + return animatorX; + } + + @RequiresApi(11) + 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 + */ + @RequiresApi(11) + public void animateX(int durationMillis) { + animateX(durationMillis, Easing.Linear); + } + + /** + * Animates values along the X axis. + * + * @param durationMillis animation duration + * @param easing EasingFunction + */ + @RequiresApi(11) + 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 + */ + @RequiresApi(11) + public void animateXY(int durationMillisX, int durationMillisY) { + animateXY(durationMillisX, durationMillisY, Easing.Linear, Easing.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 + */ + @RequiresApi(11) + 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 + */ + @RequiresApi(11) + 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 + */ + @RequiresApi(11) + public void animateY(int durationMillis) { + animateY(durationMillis, Easing.Linear); + } + + /** + * Animates values along the Y axis. + * + * @param durationMillis animation duration + * @param easing EasingFunction + */ + @RequiresApi(11) + 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; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/animation/Easing.java b/MPChartLib/src/main/java/com/github/mikephil/charting/animation/Easing.java new file mode 100644 index 0000000..acb7dcc --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/animation/Easing.java @@ -0,0 +1,309 @@ +package com.github.mikephil.charting.animation; + +import android.animation.TimeInterpolator; +import androidx.annotation.RequiresApi; + +/** + * Easing options. + * + * @author Daniel Cohen Gindi + * @author Mick Ashton + */ +@SuppressWarnings("WeakerAccess") +@RequiresApi(11) +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; + } + }; + +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/buffer/AbstractBuffer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/buffer/AbstractBuffer.java new file mode 100644 index 0000000..958d12a --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/buffer/AbstractBuffer.java @@ -0,0 +1,91 @@ + +package com.github.mikephil.charting.buffer; + +import java.util.List; + +/** + * Buffer class to boost performance while drawing. Concept: Replace instead of + * recreate. + * + * @author Philipp Jahoda + * @param The data the buffer accepts to be fed with. + */ +public abstract class AbstractBuffer { + + /** index in the buffer */ + protected int index = 0; + + /** float-buffer that holds the data points to draw, order: x,y,x,y,... */ + public final float[] buffer; + + /** animation phase x-axis */ + protected float phaseX = 1f; + + /** animation phase y-axis */ + protected float phaseY = 1f; + + /** indicates from which x-index the visible data begins */ + protected int mFrom = 0; + + /** indicates to which x-index the visible data ranges */ + protected int mTo = 0; + + /** + * Initialization with buffer-size. + * + * @param size + */ + public AbstractBuffer(int size) { + index = 0; + buffer = new float[size]; + } + + /** limits the drawing on the x-axis */ + public void limitFrom(int from) { + if (from < 0) + from = 0; + mFrom = from; + } + + /** limits the drawing on the x-axis */ + public void limitTo(int to) { + if (to < 0) + to = 0; + mTo = to; + } + + /** + * Resets the buffer index to 0 and makes the buffer reusable. + */ + public void reset() { + index = 0; + } + + /** + * Returns the size (length) of the buffer array. + * + * @return + */ + public int size() { + return buffer.length; + } + + /** + * Set the phases used for animations. + * + * @param phaseX + * @param phaseY + */ + public void setPhases(float phaseX, float phaseY) { + this.phaseX = phaseX; + this.phaseY = phaseY; + } + + /** + * Builds up the buffer with the provided data and resets the buffer-index + * after feed-completion. This needs to run FAST. + * + * @param data + */ + public abstract void feed(T data); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/buffer/BarBuffer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/buffer/BarBuffer.java new file mode 100644 index 0000000..136a231 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/buffer/BarBuffer.java @@ -0,0 +1,130 @@ + +package com.github.mikephil.charting.buffer; + +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; + +public class BarBuffer extends AbstractBuffer { + + protected int mDataSetIndex = 0; + protected int mDataSetCount = 1; + protected boolean mContainsStacks = false; + protected boolean mInverted = false; + + /** width of the bar on the x-axis, in values (not pixels) */ + protected float mBarWidth = 1f; + + public BarBuffer(int size, int dataSetCount, boolean containsStacks) { + super(size); + this.mDataSetCount = dataSetCount; + this.mContainsStacks = containsStacks; + } + + public void setBarWidth(float barWidth) { + this.mBarWidth = barWidth; + } + + public void setDataSet(int index) { + this.mDataSetIndex = index; + } + + public void setInverted(boolean inverted) { + this.mInverted = inverted; + } + + protected void addBar(float left, float top, float right, float bottom) { + + buffer[index++] = left; + buffer[index++] = top; + buffer[index++] = right; + buffer[index++] = bottom; + } + + @Override + public void feed(IBarDataSet data) { + + float size = data.getEntryCount() * phaseX; + float barWidthHalf = mBarWidth / 2f; + + for (int i = 0; i < size; i++) { + + BarEntry e = data.getEntryForIndex(i); + + if(e == null) + continue; + + float x = e.getX(); + float y = e.getY(); + float[] vals = e.getYVals(); + + if (!mContainsStacks || vals == null) { + + float left = x - barWidthHalf; + float right = x + barWidthHalf; + float bottom, top; + + if (mInverted) { + bottom = y >= 0 ? y : 0; + top = y <= 0 ? y : 0; + } else { + top = y >= 0 ? y : 0; + bottom = y <= 0 ? y : 0; + } + + // multiply the height of the rect with the phase + if (top > 0) + top *= phaseY; + else + bottom *= phaseY; + + addBar(left, top, right, bottom); + + } else { + + float posY = 0f; + float negY = -e.getNegativeSum(); + float yStart = 0f; + + // fill the stack + for (int k = 0; k < vals.length; k++) { + + float value = vals[k]; + + if (value == 0.0f && (posY == 0.0f || negY == 0.0f)) { + // Take care of the situation of a 0.0 value, which overlaps a non-zero bar + y = value; + yStart = y; + } else if (value >= 0.0f) { + y = posY; + yStart = posY + value; + posY = yStart; + } else { + y = negY; + yStart = negY + Math.abs(value); + negY += Math.abs(value); + } + + float left = x - barWidthHalf; + float right = x + barWidthHalf; + float bottom, top; + + if (mInverted) { + bottom = y >= yStart ? y : yStart; + top = y <= yStart ? y : yStart; + } else { + top = y >= yStart ? y : yStart; + bottom = y <= yStart ? y : yStart; + } + + // multiply the height of the rect with the phase + top *= phaseY; + bottom *= phaseY; + + addBar(left, top, right, bottom); + } + } + } + + reset(); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/buffer/HorizontalBarBuffer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/buffer/HorizontalBarBuffer.java new file mode 100644 index 0000000..6b63bc3 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/buffer/HorizontalBarBuffer.java @@ -0,0 +1,94 @@ + +package com.github.mikephil.charting.buffer; + +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; + +public class HorizontalBarBuffer extends BarBuffer { + + public HorizontalBarBuffer(int size, int dataSetCount, boolean containsStacks) { + super(size, dataSetCount, containsStacks); + } + + @Override + public void feed(IBarDataSet data) { + + float size = data.getEntryCount() * phaseX; + float barWidthHalf = mBarWidth / 2f; + + for (int i = 0; i < size; i++) { + + BarEntry e = data.getEntryForIndex(i); + + if(e == null) + continue; + + float x = e.getX(); + float y = e.getY(); + float[] vals = e.getYVals(); + + if (!mContainsStacks || vals == null) { + + float bottom = x - barWidthHalf; + float top = x + barWidthHalf; + float left, right; + if (mInverted) { + left = y >= 0 ? y : 0; + right = y <= 0 ? y : 0; + } else { + right = y >= 0 ? y : 0; + left = y <= 0 ? y : 0; + } + + // multiply the height of the rect with the phase + if (right > 0) + right *= phaseY; + else + left *= phaseY; + + addBar(left, top, right, bottom); + + } else { + + float posY = 0f; + float negY = -e.getNegativeSum(); + float yStart = 0f; + + // fill the stack + for (int k = 0; k < vals.length; k++) { + + float value = vals[k]; + + if (value >= 0f) { + y = posY; + yStart = posY + value; + posY = yStart; + } else { + y = negY; + yStart = negY + Math.abs(value); + negY += Math.abs(value); + } + + float bottom = x - barWidthHalf; + float top = x + barWidthHalf; + float left, right; + if (mInverted) { + left = y >= yStart ? y : yStart; + right = y <= yStart ? y : yStart; + } else { + right = y >= yStart ? y : yStart; + left = y <= yStart ? y : yStart; + } + + // multiply the height of the rect with the phase + right *= phaseY; + left *= phaseY; + + addBar(left, top, right, bottom); + } + } + } + + reset(); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/charts/BarChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/BarChart.java new file mode 100644 index 0000000..2ba15c9 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/BarChart.java @@ -0,0 +1,258 @@ +package com.github.mikephil.charting.charts; + +import android.content.Context; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.util.Log; + +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.highlight.BarHighlighter; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.dataprovider.BarDataProvider; +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; +import com.github.mikephil.charting.renderer.BarChartRenderer; + +/** + * Chart that draws bars. + * + * @author Philipp Jahoda + */ +public class BarChart extends BarLineChartBase implements BarDataProvider { + + /** + * flag that indicates whether the highlight should be full-bar oriented, or single-value? + */ + protected boolean mHighlightFullBarEnabled = false; + + /** + * if set to true, all values are drawn above their bars, instead of below their top + */ + private boolean mDrawValueAboveBar = true; + + /** + * if set to true, a grey area is drawn behind each bar that indicates the maximum value + */ + private boolean mDrawBarShadow = false; + + private boolean mFitBars = false; + + public BarChart(Context context) { + super(context); + } + + public BarChart(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public BarChart(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void init() { + super.init(); + + mRenderer = new BarChartRenderer(this, mAnimator, mViewPortHandler); + + setHighlighter(new BarHighlighter(this)); + + getXAxis().setSpaceMin(0.5f); + getXAxis().setSpaceMax(0.5f); + } + + @Override + protected void calcMinMax() { + + if (mFitBars) { + mXAxis.calculate(mData.getXMin() - mData.getBarWidth() / 2f, mData.getXMax() + mData.getBarWidth() / 2f); + } else { + mXAxis.calculate(mData.getXMin(), mData.getXMax()); + } + + // calculate axis range (min / max) according to provided data + mAxisLeft.calculate(mData.getYMin(YAxis.AxisDependency.LEFT), mData.getYMax(YAxis.AxisDependency.LEFT)); + mAxisRight.calculate(mData.getYMin(YAxis.AxisDependency.RIGHT), mData.getYMax(YAxis.AxisDependency + .RIGHT)); + } + + /** + * Returns the Highlight object (contains x-index and DataSet index) of the selected value at the given touch + * point + * inside the BarChart. + * + * @param x + * @param y + * @return + */ + @Override + public Highlight getHighlightByTouchPoint(float x, float y) { + + if (mData == null) { + Log.e(LOG_TAG, "Can't select by touch. No data set."); + return null; + } else { + Highlight h = getHighlighter().getHighlight(x, y); + if (h == null || !isHighlightFullBarEnabled()) return h; + + // For isHighlightFullBarEnabled, remove stackIndex + return new Highlight(h.getX(), h.getY(), + h.getXPx(), h.getYPx(), + h.getDataSetIndex(), -1, h.getAxis()); + } + } + + /** + * Returns the bounding box of the specified Entry in the specified DataSet. Returns null if the Entry could not be + * found in the charts data. Performance-intensive code should use void getBarBounds(BarEntry, RectF) instead. + * + * @param e + * @return + */ + public RectF getBarBounds(BarEntry e) { + + RectF bounds = new RectF(); + getBarBounds(e, bounds); + + return bounds; + } + + /** + * The passed outputRect will be assigned the values of the bounding box of the specified Entry in the specified DataSet. + * The rect will be assigned Float.MIN_VALUE in all locations if the Entry could not be found in the charts data. + * + * @param e + * @return + */ + public void getBarBounds(BarEntry e, RectF outputRect) { + + RectF bounds = outputRect; + + IBarDataSet set = mData.getDataSetForEntry(e); + + if (set == null) { + bounds.set(Float.MIN_VALUE, Float.MIN_VALUE, Float.MIN_VALUE, Float.MIN_VALUE); + return; + } + + float y = e.getY(); + float x = e.getX(); + + float barWidth = mData.getBarWidth(); + + float left = x - barWidth / 2f; + float right = x + barWidth / 2f; + float top = y >= 0 ? y : 0; + float bottom = y <= 0 ? y : 0; + + bounds.set(left, top, right, bottom); + + getTransformer(set.getAxisDependency()).rectValueToPixel(outputRect); + } + + /** + * If set to true, all values are drawn above their bars, instead of below their top. + * + * @param enabled + */ + public void setDrawValueAboveBar(boolean enabled) { + mDrawValueAboveBar = enabled; + } + + /** + * returns true if drawing values above bars is enabled, false if not + * + * @return + */ + public boolean isDrawValueAboveBarEnabled() { + return mDrawValueAboveBar; + } + + /** + * If set to true, a grey area is drawn behind each bar that indicates the maximum value. Enabling his will reduce + * performance by about 50%. + * + * @param enabled + */ + public void setDrawBarShadow(boolean enabled) { + mDrawBarShadow = enabled; + } + + /** + * returns true if drawing shadows (maxvalue) for each bar is enabled, false if not + * + * @return + */ + public boolean isDrawBarShadowEnabled() { + return mDrawBarShadow; + } + + /** + * Set this to true to make the highlight operation full-bar oriented, false to make it highlight single values (relevant + * only for stacked). If enabled, highlighting operations will highlight the whole bar, even if only a single stack entry + * was tapped. + * Default: false + * + * @param enabled + */ + public void setHighlightFullBarEnabled(boolean enabled) { + mHighlightFullBarEnabled = enabled; + } + + /** + * @return true the highlight operation is be full-bar oriented, false if single-value + */ + @Override + public boolean isHighlightFullBarEnabled() { + return mHighlightFullBarEnabled; + } + + /** + * Highlights the value at the given x-value in the given DataSet. Provide + * -1 as the dataSetIndex to undo all highlighting. + * + * @param x + * @param dataSetIndex + * @param stackIndex the index inside the stack - only relevant for stacked entries + */ + public void highlightValue(float x, int dataSetIndex, int stackIndex) { + highlightValue(new Highlight(x, dataSetIndex, stackIndex), false); + } + + @Override + public BarData getBarData() { + return mData; + } + + /** + * Adds half of the bar width to each side of the x-axis range in order to allow the bars of the barchart to be + * fully displayed. + * Default: false + * + * @param enabled + */ + public void setFitBars(boolean enabled) { + mFitBars = enabled; + } + + /** + * Groups all BarDataSet objects this data object holds together by modifying the x-value of their entries. + * Previously set x-values of entries will be overwritten. Leaves space between bars and groups as specified + * by the parameters. + * Calls notifyDataSetChanged() afterwards. + * + * @param fromX the starting point on the x-axis where the grouping should begin + * @param groupSpace the space between groups of bars in values (not pixels) e.g. 0.8f for bar width 1f + * @param barSpace the space between individual bars in values (not pixels) e.g. 0.1f for bar width 1f + */ + public void groupBars(float fromX, float groupSpace, float barSpace) { + + if (getBarData() == null) { + throw new RuntimeException("You need to set data for the chart before grouping bars."); + } else { + getBarData().groupBars(fromX, groupSpace, barSpace); + notifyDataSetChanged(); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/charts/BarLineChartBase.java b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/BarLineChartBase.java new file mode 100644 index 0000000..0926dff --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/BarLineChartBase.java @@ -0,0 +1,1674 @@ + +package com.github.mikephil.charting.charts; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; + +import com.github.mikephil.charting.components.XAxis.XAxisPosition; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.components.YAxis.AxisDependency; +import com.github.mikephil.charting.data.BarLineScatterCandleBubbleData; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.highlight.ChartHighlighter; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.dataprovider.BarLineScatterCandleBubbleDataProvider; +import com.github.mikephil.charting.interfaces.datasets.IBarLineScatterCandleBubbleDataSet; +import com.github.mikephil.charting.jobs.AnimatedMoveViewJob; +import com.github.mikephil.charting.jobs.AnimatedZoomJob; +import com.github.mikephil.charting.jobs.MoveViewJob; +import com.github.mikephil.charting.jobs.ZoomJob; +import com.github.mikephil.charting.listener.BarLineChartTouchListener; +import com.github.mikephil.charting.listener.OnDrawListener; +import com.github.mikephil.charting.renderer.XAxisRenderer; +import com.github.mikephil.charting.renderer.YAxisRenderer; +import com.github.mikephil.charting.utils.MPPointD; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.Utils; + +/** + * Base-class of LineChart, BarChart, ScatterChart and CandleStickChart. + * + * @author Philipp Jahoda + */ +@SuppressLint("RtlHardcoded") +public abstract class BarLineChartBase>> + extends Chart implements BarLineScatterCandleBubbleDataProvider { + + /** + * the maximum number of entries to which values will be drawn + * (entry numbers greater than this value will cause value-labels to disappear) + */ + protected int mMaxVisibleCount = 100; + + /** + * flag that indicates if auto scaling on the y axis is enabled + */ + protected boolean mAutoScaleMinMaxEnabled = false; + + /** + * flag that indicates if pinch-zoom is enabled. if true, both x and y axis + * can be scaled with 2 fingers, if false, x and y axis can be scaled + * separately + */ + protected boolean mPinchZoomEnabled = false; + + /** + * flag that indicates if double tap zoom is enabled or not + */ + protected boolean mDoubleTapToZoomEnabled = true; + + /** + * flag that indicates if highlighting per dragging over a fully zoomed out + * chart is enabled + */ + protected boolean mHighlightPerDragEnabled = true; + + /** + * if true, dragging is enabled for the chart + */ + private boolean mDragXEnabled = true; + private boolean mDragYEnabled = true; + + private boolean mScaleXEnabled = true; + private boolean mScaleYEnabled = true; + + /** + * paint object for the (by default) lightgrey background of the grid + */ + protected Paint mGridBackgroundPaint; + + protected Paint mBorderPaint; + + /** + * flag indicating if the grid background should be drawn or not + */ + protected boolean mDrawGridBackground = false; + + protected boolean mDrawBorders = false; + + protected boolean mClipValuesToContent = false; + + protected boolean mClipDataToContent = true; + + /** + * Sets the minimum offset (padding) around the chart, defaults to 15 + */ + protected float mMinOffset = 15.f; + + /** + * flag indicating if the chart should stay at the same position after a rotation. Default is false. + */ + protected boolean mKeepPositionOnRotation = false; + + /** + * the listener for user drawing on the chart + */ + protected OnDrawListener mDrawListener; + + /** + * the object representing the labels on the left y-axis + */ + protected YAxis mAxisLeft; + + /** + * the object representing the labels on the right y-axis + */ + protected YAxis mAxisRight; + + protected YAxisRenderer mAxisRendererLeft; + protected YAxisRenderer mAxisRendererRight; + + protected Transformer mLeftAxisTransformer; + protected Transformer mRightAxisTransformer; + + protected XAxisRenderer mXAxisRenderer; + + // /** the approximator object used for data filtering */ + // private Approximator mApproximator; + + public BarLineChartBase(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public BarLineChartBase(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public BarLineChartBase(Context context) { + super(context); + } + + @Override + protected void init() { + super.init(); + + mAxisLeft = new YAxis(AxisDependency.LEFT); + mAxisRight = new YAxis(AxisDependency.RIGHT); + + mLeftAxisTransformer = new Transformer(mViewPortHandler); + mRightAxisTransformer = new Transformer(mViewPortHandler); + + mAxisRendererLeft = new YAxisRenderer(mViewPortHandler, mAxisLeft, mLeftAxisTransformer); + mAxisRendererRight = new YAxisRenderer(mViewPortHandler, mAxisRight, mRightAxisTransformer); + + mXAxisRenderer = new XAxisRenderer(mViewPortHandler, mXAxis, mLeftAxisTransformer); + + setHighlighter(new ChartHighlighter(this)); + + mChartTouchListener = new BarLineChartTouchListener(this, mViewPortHandler.getMatrixTouch(), 3f); + + mGridBackgroundPaint = new Paint(); + mGridBackgroundPaint.setStyle(Style.FILL); + // mGridBackgroundPaint.setColor(Color.WHITE); + mGridBackgroundPaint.setColor(Color.rgb(240, 240, 240)); // light + // grey + + mBorderPaint = new Paint(); + mBorderPaint.setStyle(Style.STROKE); + mBorderPaint.setColor(Color.BLACK); + mBorderPaint.setStrokeWidth(Utils.convertDpToPixel(1f)); + } + + // for performance tracking + private long totalTime = 0; + private long drawCycles = 0; + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (mData == null) + return; + + long starttime = System.currentTimeMillis(); + + // execute all drawing commands + drawGridBackground(canvas); + + if (mAutoScaleMinMaxEnabled) { + autoScale(); + } + + if (mAxisLeft.isEnabled()) + mAxisRendererLeft.computeAxis(mAxisLeft.mAxisMinimum, mAxisLeft.mAxisMaximum, mAxisLeft.isInverted()); + + if (mAxisRight.isEnabled()) + mAxisRendererRight.computeAxis(mAxisRight.mAxisMinimum, mAxisRight.mAxisMaximum, mAxisRight.isInverted()); + + if (mXAxis.isEnabled()) + mXAxisRenderer.computeAxis(mXAxis.mAxisMinimum, mXAxis.mAxisMaximum, false); + + mXAxisRenderer.renderAxisLine(canvas); + mAxisRendererLeft.renderAxisLine(canvas); + mAxisRendererRight.renderAxisLine(canvas); + + if (mXAxis.isDrawGridLinesBehindDataEnabled()) + mXAxisRenderer.renderGridLines(canvas); + + if (mAxisLeft.isDrawGridLinesBehindDataEnabled()) + mAxisRendererLeft.renderGridLines(canvas); + + if (mAxisRight.isDrawGridLinesBehindDataEnabled()) + mAxisRendererRight.renderGridLines(canvas); + + if (mXAxis.isEnabled() && mXAxis.isDrawLimitLinesBehindDataEnabled()) + mXAxisRenderer.renderLimitLines(canvas); + + if (mAxisLeft.isEnabled() && mAxisLeft.isDrawLimitLinesBehindDataEnabled()) + mAxisRendererLeft.renderLimitLines(canvas); + + if (mAxisRight.isEnabled() && mAxisRight.isDrawLimitLinesBehindDataEnabled()) + mAxisRendererRight.renderLimitLines(canvas); + + int clipRestoreCount = canvas.save(); + + if (isClipDataToContentEnabled()) { + // make sure the data cannot be drawn outside the content-rect + canvas.clipRect(mViewPortHandler.getContentRect()); + } + + mRenderer.drawData(canvas); + + if (!mXAxis.isDrawGridLinesBehindDataEnabled()) + mXAxisRenderer.renderGridLines(canvas); + + if (!mAxisLeft.isDrawGridLinesBehindDataEnabled()) + mAxisRendererLeft.renderGridLines(canvas); + + if (!mAxisRight.isDrawGridLinesBehindDataEnabled()) + mAxisRendererRight.renderGridLines(canvas); + + // if highlighting is enabled + if (valuesToHighlight()) + mRenderer.drawHighlighted(canvas, mIndicesToHighlight); + + // Removes clipping rectangle + canvas.restoreToCount(clipRestoreCount); + + mRenderer.drawExtras(canvas); + + if (mXAxis.isEnabled() && !mXAxis.isDrawLimitLinesBehindDataEnabled()) + mXAxisRenderer.renderLimitLines(canvas); + + if (mAxisLeft.isEnabled() && !mAxisLeft.isDrawLimitLinesBehindDataEnabled()) + mAxisRendererLeft.renderLimitLines(canvas); + + if (mAxisRight.isEnabled() && !mAxisRight.isDrawLimitLinesBehindDataEnabled()) + mAxisRendererRight.renderLimitLines(canvas); + + mXAxisRenderer.renderAxisLabels(canvas); + mAxisRendererLeft.renderAxisLabels(canvas); + mAxisRendererRight.renderAxisLabels(canvas); + + if (isClipValuesToContentEnabled()) { + clipRestoreCount = canvas.save(); + canvas.clipRect(mViewPortHandler.getContentRect()); + + mRenderer.drawValues(canvas); + + canvas.restoreToCount(clipRestoreCount); + } else { + mRenderer.drawValues(canvas); + } + + mLegendRenderer.renderLegend(canvas); + + drawDescription(canvas); + + drawMarkers(canvas); + + if (mLogEnabled) { + long drawtime = (System.currentTimeMillis() - starttime); + totalTime += drawtime; + drawCycles += 1; + long average = totalTime / drawCycles; + Log.i(LOG_TAG, "Drawtime: " + drawtime + " ms, average: " + average + " ms, cycles: " + + drawCycles); + } + } + + /** + * RESET PERFORMANCE TRACKING FIELDS + */ + public void resetTracking() { + totalTime = 0; + drawCycles = 0; + } + + protected void prepareValuePxMatrix() { + + if (mLogEnabled) + Log.i(LOG_TAG, "Preparing Value-Px Matrix, xmin: " + mXAxis.mAxisMinimum + ", xmax: " + + mXAxis.mAxisMaximum + ", xdelta: " + mXAxis.mAxisRange); + + mRightAxisTransformer.prepareMatrixValuePx(mXAxis.mAxisMinimum, + mXAxis.mAxisRange, + mAxisRight.mAxisRange, + mAxisRight.mAxisMinimum); + mLeftAxisTransformer.prepareMatrixValuePx(mXAxis.mAxisMinimum, + mXAxis.mAxisRange, + mAxisLeft.mAxisRange, + mAxisLeft.mAxisMinimum); + } + + protected void prepareOffsetMatrix() { + + mRightAxisTransformer.prepareMatrixOffset(mAxisRight.isInverted()); + mLeftAxisTransformer.prepareMatrixOffset(mAxisLeft.isInverted()); + } + + @Override + public void notifyDataSetChanged() { + + if (mData == null) { + if (mLogEnabled) + Log.i(LOG_TAG, "Preparing... DATA NOT SET."); + return; + } else { + if (mLogEnabled) + Log.i(LOG_TAG, "Preparing..."); + } + + if (mRenderer != null) + mRenderer.initBuffers(); + + calcMinMax(); + + mAxisRendererLeft.computeAxis(mAxisLeft.mAxisMinimum, mAxisLeft.mAxisMaximum, mAxisLeft.isInverted()); + mAxisRendererRight.computeAxis(mAxisRight.mAxisMinimum, mAxisRight.mAxisMaximum, mAxisRight.isInverted()); + mXAxisRenderer.computeAxis(mXAxis.mAxisMinimum, mXAxis.mAxisMaximum, false); + + if (mLegend != null) + mLegendRenderer.computeLegend(mData); + + calculateOffsets(); + } + + /** + * Performs auto scaling of the axis by recalculating the minimum and maximum y-values based on the entries currently in view. + */ + protected void autoScale() { + + final float fromX = getLowestVisibleX(); + final float toX = getHighestVisibleX(); + + mData.calcMinMaxY(fromX, toX); + + mXAxis.calculate(mData.getXMin(), mData.getXMax()); + + // calculate axis range (min / max) according to provided data + + if (mAxisLeft.isEnabled()) + mAxisLeft.calculate(mData.getYMin(AxisDependency.LEFT), + mData.getYMax(AxisDependency.LEFT)); + + if (mAxisRight.isEnabled()) + mAxisRight.calculate(mData.getYMin(AxisDependency.RIGHT), + mData.getYMax(AxisDependency.RIGHT)); + + calculateOffsets(); + } + + @Override + protected void calcMinMax() { + + mXAxis.calculate(mData.getXMin(), mData.getXMax()); + + // calculate axis range (min / max) according to provided data + mAxisLeft.calculate(mData.getYMin(AxisDependency.LEFT), mData.getYMax(AxisDependency.LEFT)); + mAxisRight.calculate(mData.getYMin(AxisDependency.RIGHT), mData.getYMax(AxisDependency + .RIGHT)); + } + + protected void calculateLegendOffsets(RectF offsets) { + + offsets.left = 0.f; + offsets.right = 0.f; + offsets.top = 0.f; + offsets.bottom = 0.f; + + if (mLegend == null || !mLegend.isEnabled() || mLegend.isDrawInsideEnabled()) + return; + + switch (mLegend.getOrientation()) { + case VERTICAL: + + switch (mLegend.getHorizontalAlignment()) { + case LEFT: + offsets.left += Math.min(mLegend.mNeededWidth, + mViewPortHandler.getChartWidth() * mLegend.getMaxSizePercent()) + + mLegend.getXOffset(); + break; + + case RIGHT: + offsets.right += Math.min(mLegend.mNeededWidth, + mViewPortHandler.getChartWidth() * mLegend.getMaxSizePercent()) + + mLegend.getXOffset(); + break; + + case CENTER: + + switch (mLegend.getVerticalAlignment()) { + case TOP: + offsets.top += Math.min(mLegend.mNeededHeight, + mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()) + + mLegend.getYOffset(); + break; + + case BOTTOM: + offsets.bottom += Math.min(mLegend.mNeededHeight, + mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()) + + mLegend.getYOffset(); + break; + + default: + break; + } + } + + break; + + case HORIZONTAL: + + switch (mLegend.getVerticalAlignment()) { + case TOP: + offsets.top += Math.min(mLegend.mNeededHeight, + mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()) + + mLegend.getYOffset(); + + + break; + + case BOTTOM: + offsets.bottom += Math.min(mLegend.mNeededHeight, + mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()) + + mLegend.getYOffset(); + + + break; + + default: + break; + } + break; + } + } + + private RectF mOffsetsBuffer = new RectF(); + + @Override + public void calculateOffsets() { + + if (!mCustomViewPortEnabled) { + + float offsetLeft = 0f, offsetRight = 0f, offsetTop = 0f, offsetBottom = 0f; + + calculateLegendOffsets(mOffsetsBuffer); + + offsetLeft += mOffsetsBuffer.left; + offsetTop += mOffsetsBuffer.top; + offsetRight += mOffsetsBuffer.right; + offsetBottom += mOffsetsBuffer.bottom; + + // offsets for y-labels + if (mAxisLeft.needsOffset()) { + offsetLeft += mAxisLeft.getRequiredWidthSpace(mAxisRendererLeft + .getPaintAxisLabels()); + } + + if (mAxisRight.needsOffset()) { + offsetRight += mAxisRight.getRequiredWidthSpace(mAxisRendererRight + .getPaintAxisLabels()); + } + + if (mXAxis.isEnabled() && mXAxis.isDrawLabelsEnabled()) { + + float xLabelHeight = mXAxis.mLabelRotatedHeight + mXAxis.getYOffset(); + + // offsets for x-labels + if (mXAxis.getPosition() == XAxisPosition.BOTTOM) { + + offsetBottom += xLabelHeight; + + } else if (mXAxis.getPosition() == XAxisPosition.TOP) { + + offsetTop += xLabelHeight; + + } else if (mXAxis.getPosition() == XAxisPosition.BOTH_SIDED) { + + offsetBottom += xLabelHeight; + offsetTop += xLabelHeight; + } + } + + offsetTop += getExtraTopOffset(); + offsetRight += getExtraRightOffset(); + offsetBottom += getExtraBottomOffset(); + offsetLeft += getExtraLeftOffset(); + + float minOffset = Utils.convertDpToPixel(mMinOffset); + + mViewPortHandler.restrainViewPort( + Math.max(minOffset, offsetLeft), + Math.max(minOffset, offsetTop), + Math.max(minOffset, offsetRight), + Math.max(minOffset, offsetBottom)); + + if (mLogEnabled) { + Log.i(LOG_TAG, "offsetLeft: " + offsetLeft + ", offsetTop: " + offsetTop + + ", offsetRight: " + offsetRight + ", offsetBottom: " + offsetBottom); + Log.i(LOG_TAG, "Content: " + mViewPortHandler.getContentRect().toString()); + } + } + + prepareOffsetMatrix(); + prepareValuePxMatrix(); + } + + /** + * draws the grid background + */ + protected void drawGridBackground(Canvas c) { + + if (mDrawGridBackground) { + + // draw the grid background + c.drawRect(mViewPortHandler.getContentRect(), mGridBackgroundPaint); + } + + if (mDrawBorders) { + c.drawRect(mViewPortHandler.getContentRect(), mBorderPaint); + } + } + + /** + * Returns the Transformer class that contains all matrices and is + * responsible for transforming values into pixels on the screen and + * backwards. + * + * @return + */ + public Transformer getTransformer(AxisDependency which) { + if (which == AxisDependency.LEFT) + return mLeftAxisTransformer; + else + return mRightAxisTransformer; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + super.onTouchEvent(event); + + if (mChartTouchListener == null || mData == null) + return false; + + // check if touch gestures are enabled + if (!mTouchEnabled) + return false; + else + return mChartTouchListener.onTouch(this, event); + } + + @Override + public void computeScroll() { + + if (mChartTouchListener instanceof BarLineChartTouchListener) + ((BarLineChartTouchListener) mChartTouchListener).computeScroll(); + } + + /** + * ################ ################ ################ ################ + */ + /** + * CODE BELOW THIS RELATED TO SCALING AND GESTURES AND MODIFICATION OF THE + * VIEWPORT + */ + + protected Matrix mZoomMatrixBuffer = new Matrix(); + + /** + * Zooms in by 1.4f, into the charts center. + */ + public void zoomIn() { + + MPPointF center = mViewPortHandler.getContentCenter(); + + mViewPortHandler.zoomIn(center.x, -center.y, mZoomMatrixBuffer); + mViewPortHandler.refresh(mZoomMatrixBuffer, this, false); + + MPPointF.recycleInstance(center); + + // Range might have changed, which means that Y-axis labels + // could have changed in size, affecting Y-axis size. + // So we need to recalculate offsets. + calculateOffsets(); + postInvalidate(); + } + + /** + * Zooms out by 0.7f, from the charts center. + */ + public void zoomOut() { + + MPPointF center = mViewPortHandler.getContentCenter(); + + mViewPortHandler.zoomOut(center.x, -center.y, mZoomMatrixBuffer); + mViewPortHandler.refresh(mZoomMatrixBuffer, this, false); + + MPPointF.recycleInstance(center); + + // Range might have changed, which means that Y-axis labels + // could have changed in size, affecting Y-axis size. + // So we need to recalculate offsets. + calculateOffsets(); + postInvalidate(); + } + + /** + * Zooms out to original size. + */ + public void resetZoom() { + + mViewPortHandler.resetZoom(mZoomMatrixBuffer); + mViewPortHandler.refresh(mZoomMatrixBuffer, this, false); + + // Range might have changed, which means that Y-axis labels + // could have changed in size, affecting Y-axis size. + // So we need to recalculate offsets. + calculateOffsets(); + postInvalidate(); + } + + /** + * Zooms in or out by the given scale factor. x and y are the coordinates + * (in pixels) of the zoom center. + * + * @param scaleX if < 1f --> zoom out, if > 1f --> zoom in + * @param scaleY if < 1f --> zoom out, if > 1f --> zoom in + * @param x + * @param y + */ + public void zoom(float scaleX, float scaleY, float x, float y) { + + mViewPortHandler.zoom(scaleX, scaleY, x, -y, mZoomMatrixBuffer); + mViewPortHandler.refresh(mZoomMatrixBuffer, this, false); + + // Range might have changed, which means that Y-axis labels + // could have changed in size, affecting Y-axis size. + // So we need to recalculate offsets. + calculateOffsets(); + postInvalidate(); + } + + /** + * Zooms in or out by the given scale factor. + * x and y are the values (NOT PIXELS) of the zoom center.. + * + * @param scaleX + * @param scaleY + * @param xValue + * @param yValue + * @param axis the axis relative to which the zoom should take place + */ + public void zoom(float scaleX, float scaleY, float xValue, float yValue, AxisDependency axis) { + + Runnable job = ZoomJob.getInstance(mViewPortHandler, scaleX, scaleY, xValue, yValue, getTransformer(axis), axis, this); + addViewportJob(job); + } + + /** + * Zooms to the center of the chart with the given scale factor. + * + * @param scaleX + * @param scaleY + */ + public void zoomToCenter(float scaleX, float scaleY) { + + MPPointF center = getCenterOffsets(); + + Matrix save = mZoomMatrixBuffer; + mViewPortHandler.zoom(scaleX, scaleY, center.x, -center.y, save); + mViewPortHandler.refresh(save, this, false); + } + + /** + * Zooms by the specified scale factor to the specified values on the specified axis. + * + * @param scaleX + * @param scaleY + * @param xValue + * @param yValue + * @param axis + * @param duration + */ + @TargetApi(11) + public void zoomAndCenterAnimated(float scaleX, float scaleY, float xValue, float yValue, AxisDependency axis, + long duration) { + + MPPointD origin = getValuesByTouchPoint(mViewPortHandler.contentLeft(), mViewPortHandler.contentTop(), axis); + + Runnable job = AnimatedZoomJob.getInstance(mViewPortHandler, this, getTransformer(axis), getAxis(axis), mXAxis + .mAxisRange, scaleX, scaleY, mViewPortHandler.getScaleX(), mViewPortHandler.getScaleY(), + xValue, yValue, (float) origin.x, (float) origin.y, duration); + addViewportJob(job); + + MPPointD.recycleInstance(origin); + } + + protected Matrix mFitScreenMatrixBuffer = new Matrix(); + + /** + * Resets all zooming and dragging and makes the chart fit exactly it's + * bounds. + */ + public void fitScreen() { + Matrix save = mFitScreenMatrixBuffer; + mViewPortHandler.fitScreen(save); + mViewPortHandler.refresh(save, this, false); + + calculateOffsets(); + postInvalidate(); + } + + /** + * Sets the minimum scale factor value to which can be zoomed out. 1f = + * fitScreen + * + * @param scaleX + * @param scaleY + */ + public void setScaleMinima(float scaleX, float scaleY) { + mViewPortHandler.setMinimumScaleX(scaleX); + mViewPortHandler.setMinimumScaleY(scaleY); + } + + /** + * Sets the size of the area (range on the x-axis) that should be maximum + * visible at once (no further zooming out allowed). If this is e.g. set to + * 10, no more than a range of 10 on the x-axis can be viewed at once without + * scrolling. + * + * @param maxXRange The maximum visible range of x-values. + */ + public void setVisibleXRangeMaximum(float maxXRange) { + float xScale = mXAxis.mAxisRange / (maxXRange); + mViewPortHandler.setMinimumScaleX(xScale); + } + + /** + * Sets the size of the area (range on the x-axis) that should be minimum + * visible at once (no further zooming in allowed). If this is e.g. set to + * 10, no less than a range of 10 on the x-axis can be viewed at once without + * scrolling. + * + * @param minXRange The minimum visible range of x-values. + */ + public void setVisibleXRangeMinimum(float minXRange) { + float xScale = mXAxis.mAxisRange / (minXRange); + mViewPortHandler.setMaximumScaleX(xScale); + } + + /** + * Limits the maximum and minimum x range that can be visible by pinching and zooming. e.g. minRange=10, maxRange=100 the + * smallest range to be displayed at once is 10, and no more than a range of 100 values can be viewed at once without + * scrolling + * + * @param minXRange + * @param maxXRange + */ + public void setVisibleXRange(float minXRange, float maxXRange) { + float minScale = mXAxis.mAxisRange / minXRange; + float maxScale = mXAxis.mAxisRange / maxXRange; + mViewPortHandler.setMinMaxScaleX(minScale, maxScale); + } + + /** + * Sets the size of the area (range on the y-axis) that should be maximum + * visible at once. + * + * @param maxYRange the maximum visible range on the y-axis + * @param axis the axis for which this limit should apply + */ + public void setVisibleYRangeMaximum(float maxYRange, AxisDependency axis) { + float yScale = getAxisRange(axis) / maxYRange; + mViewPortHandler.setMinimumScaleY(yScale); + } + + /** + * Sets the size of the area (range on the y-axis) that should be minimum visible at once, no further zooming in possible. + * + * @param minYRange + * @param axis the axis for which this limit should apply + */ + public void setVisibleYRangeMinimum(float minYRange, AxisDependency axis) { + float yScale = getAxisRange(axis) / minYRange; + mViewPortHandler.setMaximumScaleY(yScale); + } + + /** + * Limits the maximum and minimum y range that can be visible by pinching and zooming. + * + * @param minYRange + * @param maxYRange + * @param axis + */ + public void setVisibleYRange(float minYRange, float maxYRange, AxisDependency axis) { + float minScale = getAxisRange(axis) / minYRange; + float maxScale = getAxisRange(axis) / maxYRange; + mViewPortHandler.setMinMaxScaleY(minScale, maxScale); + } + + + /** + * Moves the left side of the current viewport to the specified x-position. + * This also refreshes the chart by calling invalidate(). + * + * @param xValue + */ + public void moveViewToX(float xValue) { + + Runnable job = MoveViewJob.getInstance(mViewPortHandler, xValue, 0f, + getTransformer(AxisDependency.LEFT), this); + + addViewportJob(job); + } + + /** + * This will move the left side of the current viewport to the specified + * x-value on the x-axis, and center the viewport to the specified y value on the y-axis. + * This also refreshes the chart by calling invalidate(). + * + * @param xValue + * @param yValue + * @param axis - which axis should be used as a reference for the y-axis + */ + public void moveViewTo(float xValue, float yValue, AxisDependency axis) { + + float yInView = getAxisRange(axis) / mViewPortHandler.getScaleY(); + + Runnable job = MoveViewJob.getInstance(mViewPortHandler, xValue, yValue + yInView / 2f, + getTransformer(axis), this); + + addViewportJob(job); + } + + /** + * This will move the left side of the current viewport to the specified x-value + * and center the viewport to the y value animated. + * This also refreshes the chart by calling invalidate(). + * + * @param xValue + * @param yValue + * @param axis + * @param duration the duration of the animation in milliseconds + */ + @TargetApi(11) + public void moveViewToAnimated(float xValue, float yValue, AxisDependency axis, long duration) { + + MPPointD bounds = getValuesByTouchPoint(mViewPortHandler.contentLeft(), mViewPortHandler.contentTop(), axis); + + float yInView = getAxisRange(axis) / mViewPortHandler.getScaleY(); + + Runnable job = AnimatedMoveViewJob.getInstance(mViewPortHandler, xValue, yValue + yInView / 2f, + getTransformer(axis), this, (float) bounds.x, (float) bounds.y, duration); + + addViewportJob(job); + + MPPointD.recycleInstance(bounds); + } + + /** + * Centers the viewport to the specified y value on the y-axis. + * This also refreshes the chart by calling invalidate(). + * + * @param yValue + * @param axis - which axis should be used as a reference for the y-axis + */ + public void centerViewToY(float yValue, AxisDependency axis) { + + float valsInView = getAxisRange(axis) / mViewPortHandler.getScaleY(); + + Runnable job = MoveViewJob.getInstance(mViewPortHandler, 0f, yValue + valsInView / 2f, + getTransformer(axis), this); + + addViewportJob(job); + } + + /** + * This will move the center of the current viewport to the specified + * x and y value. + * This also refreshes the chart by calling invalidate(). + * + * @param xValue + * @param yValue + * @param axis - which axis should be used as a reference for the y axis + */ + public void centerViewTo(float xValue, float yValue, AxisDependency axis) { + + float yInView = getAxisRange(axis) / mViewPortHandler.getScaleY(); + float xInView = getXAxis().mAxisRange / mViewPortHandler.getScaleX(); + + Runnable job = MoveViewJob.getInstance(mViewPortHandler, + xValue - xInView / 2f, yValue + yInView / 2f, + getTransformer(axis), this); + + addViewportJob(job); + } + + /** + * This will move the center of the current viewport to the specified + * x and y value animated. + * + * @param xValue + * @param yValue + * @param axis + * @param duration the duration of the animation in milliseconds + */ + @TargetApi(11) + public void centerViewToAnimated(float xValue, float yValue, AxisDependency axis, long duration) { + + MPPointD bounds = getValuesByTouchPoint(mViewPortHandler.contentLeft(), mViewPortHandler.contentTop(), axis); + + float yInView = getAxisRange(axis) / mViewPortHandler.getScaleY(); + float xInView = getXAxis().mAxisRange / mViewPortHandler.getScaleX(); + + Runnable job = AnimatedMoveViewJob.getInstance(mViewPortHandler, + xValue - xInView / 2f, yValue + yInView / 2f, + getTransformer(axis), this, (float) bounds.x, (float) bounds.y, duration); + + addViewportJob(job); + + MPPointD.recycleInstance(bounds); + } + + /** + * flag that indicates if a custom viewport offset has been set + */ + private boolean mCustomViewPortEnabled = false; + + /** + * Sets custom offsets for the current ViewPort (the offsets on the sides of + * the actual chart window). Setting this will prevent the chart from + * automatically calculating it's offsets. Use resetViewPortOffsets() to + * undo this. ONLY USE THIS WHEN YOU KNOW WHAT YOU ARE DOING, else use + * setExtraOffsets(...). + * + * @param left + * @param top + * @param right + * @param bottom + */ + public void setViewPortOffsets(final float left, final float top, + final float right, final float bottom) { + + mCustomViewPortEnabled = true; + post(new Runnable() { + + @Override + public void run() { + + mViewPortHandler.restrainViewPort(left, top, right, bottom); + prepareOffsetMatrix(); + prepareValuePxMatrix(); + } + }); + } + + /** + * Resets all custom offsets set via setViewPortOffsets(...) method. Allows + * the chart to again calculate all offsets automatically. + */ + public void resetViewPortOffsets() { + mCustomViewPortEnabled = false; + calculateOffsets(); + } + + /** + * ################ ################ ################ ################ + */ + /** CODE BELOW IS GETTERS AND SETTERS */ + + /** + * Returns the range of the specified axis. + * + * @param axis + * @return + */ + protected float getAxisRange(AxisDependency axis) { + if (axis == AxisDependency.LEFT) + return mAxisLeft.mAxisRange; + else + return mAxisRight.mAxisRange; + } + + /** + * Sets the OnDrawListener + * + * @param drawListener + */ + public void setOnDrawListener(OnDrawListener drawListener) { + this.mDrawListener = drawListener; + } + + /** + * Gets the OnDrawListener. May be null. + * + * @return + */ + public OnDrawListener getDrawListener() { + return mDrawListener; + } + + protected float[] mGetPositionBuffer = new float[2]; + + /** + * Returns a recyclable MPPointF instance. + * Returns the position (in pixels) the provided Entry has inside the chart + * view or null, if the provided Entry is null. + * + * @param e + * @return + */ + public MPPointF getPosition(Entry e, AxisDependency axis) { + + if (e == null) + return null; + + mGetPositionBuffer[0] = e.getX(); + mGetPositionBuffer[1] = e.getY(); + + getTransformer(axis).pointValuesToPixel(mGetPositionBuffer); + + return MPPointF.getInstance(mGetPositionBuffer[0], mGetPositionBuffer[1]); + } + + /** + * sets the number of maximum visible drawn values on the chart only active + * when setDrawValues() is enabled + * + * @param count + */ + public void setMaxVisibleValueCount(int count) { + this.mMaxVisibleCount = count; + } + + public int getMaxVisibleCount() { + return mMaxVisibleCount; + } + + /** + * Set this to true to allow highlighting per dragging over the chart + * surface when it is fully zoomed out. Default: true + * + * @param enabled + */ + public void setHighlightPerDragEnabled(boolean enabled) { + mHighlightPerDragEnabled = enabled; + } + + public boolean isHighlightPerDragEnabled() { + return mHighlightPerDragEnabled; + } + + /** + * Sets the color for the background of the chart-drawing area (everything + * behind the grid lines). + * + * @param color + */ + public void setGridBackgroundColor(int color) { + mGridBackgroundPaint.setColor(color); + } + + /** + * Set this to true to enable dragging (moving the chart with the finger) + * for the chart (this does not effect scaling). + * + * @param enabled + */ + public void setDragEnabled(boolean enabled) { + this.mDragXEnabled = enabled; + this.mDragYEnabled = enabled; + } + + /** + * Returns true if dragging is enabled for the chart, false if not. + * + * @return + */ + public boolean isDragEnabled() { + return mDragXEnabled || mDragYEnabled; + } + + /** + * Set this to true to enable dragging on the X axis + * + * @param enabled + */ + public void setDragXEnabled(boolean enabled) { + this.mDragXEnabled = enabled; + } + + /** + * Returns true if dragging on the X axis is enabled for the chart, false if not. + * + * @return + */ + public boolean isDragXEnabled() { + return mDragXEnabled; + } + + /** + * Set this to true to enable dragging on the Y axis + * + * @param enabled + */ + public void setDragYEnabled(boolean enabled) { + this.mDragYEnabled = enabled; + } + + /** + * Returns true if dragging on the Y axis is enabled for the chart, false if not. + * + * @return + */ + public boolean isDragYEnabled() { + return mDragYEnabled; + } + + /** + * Set this to true to enable scaling (zooming in and out by gesture) for + * the chart (this does not effect dragging) on both X- and Y-Axis. + * + * @param enabled + */ + public void setScaleEnabled(boolean enabled) { + this.mScaleXEnabled = enabled; + this.mScaleYEnabled = enabled; + } + + public void setScaleXEnabled(boolean enabled) { + mScaleXEnabled = enabled; + } + + public void setScaleYEnabled(boolean enabled) { + mScaleYEnabled = enabled; + } + + public boolean isScaleXEnabled() { + return mScaleXEnabled; + } + + public boolean isScaleYEnabled() { + return mScaleYEnabled; + } + + /** + * Set this to true to enable zooming in by double-tap on the chart. + * Default: enabled + * + * @param enabled + */ + public void setDoubleTapToZoomEnabled(boolean enabled) { + mDoubleTapToZoomEnabled = enabled; + } + + /** + * Returns true if zooming via double-tap is enabled false if not. + * + * @return + */ + public boolean isDoubleTapToZoomEnabled() { + return mDoubleTapToZoomEnabled; + } + + /** + * set this to true to draw the grid background, false if not + * + * @param enabled + */ + public void setDrawGridBackground(boolean enabled) { + mDrawGridBackground = enabled; + } + + /** + * When enabled, the borders rectangle will be rendered. + * If this is enabled, there is no point drawing the axis-lines of x- and y-axis. + * + * @param enabled + */ + public void setDrawBorders(boolean enabled) { + mDrawBorders = enabled; + } + + /** + * When enabled, the borders rectangle will be rendered. + * If this is enabled, there is no point drawing the axis-lines of x- and y-axis. + * + * @return + */ + public boolean isDrawBordersEnabled() { + return mDrawBorders; + } + + /** + * When enabled, the values will be clipped to contentRect, + * otherwise they can bleed outside the content rect. + * + * @param enabled + */ + public void setClipValuesToContent(boolean enabled) { + mClipValuesToContent = enabled; + } + + /** + * When disabled, the data and/or highlights will not be clipped to contentRect. Disabling this option can + * be useful, when the data lies fully within the content rect, but is drawn in such a way (such as thick lines) + * that there is unwanted clipping. + * + * @param enabled + */ + public void setClipDataToContent(boolean enabled) { + mClipDataToContent = enabled; + } + + /** + * When enabled, the values will be clipped to contentRect, + * otherwise they can bleed outside the content rect. + * + * @return + */ + public boolean isClipValuesToContentEnabled() { + return mClipValuesToContent; + } + + /** + * When disabled, the data and/or highlights will not be clipped to contentRect. Disabling this option can + * be useful, when the data lies fully within the content rect, but is drawn in such a way (such as thick lines) + * that there is unwanted clipping. + * + * @return + */ + public boolean isClipDataToContentEnabled() { + return mClipDataToContent; + } + + /** + * Sets the width of the border lines in dp. + * + * @param width + */ + public void setBorderWidth(float width) { + mBorderPaint.setStrokeWidth(Utils.convertDpToPixel(width)); + } + + /** + * Sets the color of the chart border lines. + * + * @param color + */ + public void setBorderColor(int color) { + mBorderPaint.setColor(color); + } + + /** + * Gets the minimum offset (padding) around the chart, defaults to 15.f + */ + public float getMinOffset() { + return mMinOffset; + } + + /** + * Sets the minimum offset (padding) around the chart, defaults to 15.f + */ + public void setMinOffset(float minOffset) { + mMinOffset = minOffset; + } + + /** + * Returns true if keeping the position on rotation is enabled and false if not. + */ + public boolean isKeepPositionOnRotation() { + return mKeepPositionOnRotation; + } + + /** + * Sets whether the chart should keep its position (zoom / scroll) after a rotation (orientation change) + */ + public void setKeepPositionOnRotation(boolean keepPositionOnRotation) { + mKeepPositionOnRotation = keepPositionOnRotation; + } + + /** + * Returns a recyclable MPPointD instance + * Returns the x and y values in the chart at the given touch point + * (encapsulated in a MPPointD). This method transforms pixel coordinates to + * coordinates / values in the chart. This is the opposite method to + * getPixelForValues(...). + * + * @param x + * @param y + * @return + */ + public MPPointD getValuesByTouchPoint(float x, float y, AxisDependency axis) { + MPPointD result = MPPointD.getInstance(0, 0); + getValuesByTouchPoint(x, y, axis, result); + return result; + } + + public void getValuesByTouchPoint(float x, float y, AxisDependency axis, MPPointD outputPoint) { + getTransformer(axis).getValuesByTouchPoint(x, y, outputPoint); + } + + /** + * Returns a recyclable MPPointD instance + * Transforms the given chart values into pixels. This is the opposite + * method to getValuesByTouchPoint(...). + * + * @param x + * @param y + * @return + */ + public MPPointD getPixelForValues(float x, float y, AxisDependency axis) { + return getTransformer(axis).getPixelForValues(x, y); + } + + /** + * returns the Entry object displayed at the touched position of the chart + * + * @param x + * @param y + * @return + */ + public Entry getEntryByTouchPoint(float x, float y) { + Highlight h = getHighlightByTouchPoint(x, y); + if (h != null) { + return mData.getEntryForHighlight(h); + } + return null; + } + + /** + * returns the DataSet object displayed at the touched position of the chart + * + * @param x + * @param y + * @return + */ + public IBarLineScatterCandleBubbleDataSet getDataSetByTouchPoint(float x, float y) { + Highlight h = getHighlightByTouchPoint(x, y); + if (h != null) { + return mData.getDataSetByIndex(h.getDataSetIndex()); + } + return null; + } + + /** + * buffer for storing lowest visible x point + */ + protected MPPointD posForGetLowestVisibleX = MPPointD.getInstance(0, 0); + + /** + * Returns the lowest x-index (value on the x-axis) that is still visible on + * the chart. + * + * @return + */ + @Override + public float getLowestVisibleX() { + getTransformer(AxisDependency.LEFT).getValuesByTouchPoint(mViewPortHandler.contentLeft(), + mViewPortHandler.contentBottom(), posForGetLowestVisibleX); + float result = (float) Math.max(mXAxis.mAxisMinimum, posForGetLowestVisibleX.x); + return result; + } + + /** + * buffer for storing highest visible x point + */ + protected MPPointD posForGetHighestVisibleX = MPPointD.getInstance(0, 0); + + /** + * Returns the highest x-index (value on the x-axis) that is still visible + * on the chart. + * + * @return + */ + @Override + public float getHighestVisibleX() { + getTransformer(AxisDependency.LEFT).getValuesByTouchPoint(mViewPortHandler.contentRight(), + mViewPortHandler.contentBottom(), posForGetHighestVisibleX); + float result = (float) Math.min(mXAxis.mAxisMaximum, posForGetHighestVisibleX.x); + return result; + } + + /** + * Returns the range visible on the x-axis. + * + * @return + */ + public float getVisibleXRange() { + return Math.abs(getHighestVisibleX() - getLowestVisibleX()); + } + + /** + * returns the current x-scale factor + */ + public float getScaleX() { + if (mViewPortHandler == null) + return 1f; + else + return mViewPortHandler.getScaleX(); + } + + /** + * returns the current y-scale factor + */ + public float getScaleY() { + if (mViewPortHandler == null) + return 1f; + else + return mViewPortHandler.getScaleY(); + } + + /** + * if the chart is fully zoomed out, return true + * + * @return + */ + public boolean isFullyZoomedOut() { + return mViewPortHandler.isFullyZoomedOut(); + } + + /** + * Returns the left y-axis object. In the horizontal bar-chart, this is the + * top axis. + * + * @return + */ + public YAxis getAxisLeft() { + return mAxisLeft; + } + + /** + * Returns the right y-axis object. In the horizontal bar-chart, this is the + * bottom axis. + * + * @return + */ + public YAxis getAxisRight() { + return mAxisRight; + } + + /** + * Returns the y-axis object to the corresponding AxisDependency. In the + * horizontal bar-chart, LEFT == top, RIGHT == BOTTOM + * + * @param axis + * @return + */ + public YAxis getAxis(AxisDependency axis) { + if (axis == AxisDependency.LEFT) + return mAxisLeft; + else + return mAxisRight; + } + + @Override + public boolean isInverted(AxisDependency axis) { + return getAxis(axis).isInverted(); + } + + /** + * If set to true, both x and y axis can be scaled simultaneously with 2 fingers, if false, + * x and y axis can be scaled separately. default: false + * + * @param enabled + */ + public void setPinchZoom(boolean enabled) { + mPinchZoomEnabled = enabled; + } + + /** + * returns true if pinch-zoom is enabled, false if not + * + * @return + */ + public boolean isPinchZoomEnabled() { + return mPinchZoomEnabled; + } + + /** + * Set an offset in dp that allows the user to drag the chart over it's + * bounds on the x-axis. + * + * @param offset + */ + public void setDragOffsetX(float offset) { + mViewPortHandler.setDragOffsetX(offset); + } + + /** + * Set an offset in dp that allows the user to drag the chart over it's + * bounds on the y-axis. + * + * @param offset + */ + public void setDragOffsetY(float offset) { + mViewPortHandler.setDragOffsetY(offset); + } + + /** + * Returns true if both drag offsets (x and y) are zero or smaller. + * + * @return + */ + public boolean hasNoDragOffset() { + return mViewPortHandler.hasNoDragOffset(); + } + + public XAxisRenderer getRendererXAxis() { + return mXAxisRenderer; + } + + /** + * Sets a custom XAxisRenderer and overrides the existing (default) one. + * + * @param xAxisRenderer + */ + public void setXAxisRenderer(XAxisRenderer xAxisRenderer) { + mXAxisRenderer = xAxisRenderer; + } + + public YAxisRenderer getRendererLeftYAxis() { + return mAxisRendererLeft; + } + + /** + * Sets a custom axis renderer for the left axis and overwrites the existing one. + * + * @param rendererLeftYAxis + */ + public void setRendererLeftYAxis(YAxisRenderer rendererLeftYAxis) { + mAxisRendererLeft = rendererLeftYAxis; + } + + public YAxisRenderer getRendererRightYAxis() { + return mAxisRendererRight; + } + + /** + * Sets a custom axis renderer for the right acis and overwrites the existing one. + * + * @param rendererRightYAxis + */ + public void setRendererRightYAxis(YAxisRenderer rendererRightYAxis) { + mAxisRendererRight = rendererRightYAxis; + } + + @Override + public float getYChartMax() { + return Math.max(mAxisLeft.mAxisMaximum, mAxisRight.mAxisMaximum); + } + + @Override + public float getYChartMin() { + return Math.min(mAxisLeft.mAxisMinimum, mAxisRight.mAxisMinimum); + } + + /** + * Returns true if either the left or the right or both axes are inverted. + * + * @return + */ + public boolean isAnyAxisInverted() { + if (mAxisLeft.isInverted()) + return true; + if (mAxisRight.isInverted()) + return true; + return false; + } + + /** + * Flag that indicates if auto scaling on the y axis is enabled. This is + * especially interesting for charts displaying financial data. + * + * @param enabled the y axis automatically adjusts to the min and max y + * values of the current x axis range whenever the viewport + * changes + */ + public void setAutoScaleMinMaxEnabled(boolean enabled) { + mAutoScaleMinMaxEnabled = enabled; + } + + /** + * @return true if auto scaling on the y axis is enabled. + * @default false + */ + public boolean isAutoScaleMinMaxEnabled() { + return mAutoScaleMinMaxEnabled; + } + + @Override + public void setPaint(Paint p, int which) { + super.setPaint(p, which); + + switch (which) { + case PAINT_GRID_BACKGROUND: + mGridBackgroundPaint = p; + break; + } + } + + @Override + public Paint getPaint(int which) { + Paint p = super.getPaint(which); + if (p != null) + return p; + + switch (which) { + case PAINT_GRID_BACKGROUND: + return mGridBackgroundPaint; + } + + return null; + } + + protected float[] mOnSizeChangedBuffer = new float[2]; + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + + // Saving current position of chart. + mOnSizeChangedBuffer[0] = mOnSizeChangedBuffer[1] = 0; + + if (mKeepPositionOnRotation) { + mOnSizeChangedBuffer[0] = mViewPortHandler.contentLeft(); + mOnSizeChangedBuffer[1] = mViewPortHandler.contentTop(); + getTransformer(AxisDependency.LEFT).pixelsToValue(mOnSizeChangedBuffer); + } + + //Superclass transforms chart. + super.onSizeChanged(w, h, oldw, oldh); + + if (mKeepPositionOnRotation) { + + //Restoring old position of chart. + getTransformer(AxisDependency.LEFT).pointValuesToPixel(mOnSizeChangedBuffer); + mViewPortHandler.centerViewPort(mOnSizeChangedBuffer, this); + } else { + mViewPortHandler.refresh(mViewPortHandler.getMatrixTouch(), this, true); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/charts/BubbleChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/BubbleChart.java new file mode 100644 index 0000000..23dac57 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/BubbleChart.java @@ -0,0 +1,43 @@ + +package com.github.mikephil.charting.charts; + +import android.content.Context; +import android.util.AttributeSet; + +import com.github.mikephil.charting.data.BubbleData; +import com.github.mikephil.charting.interfaces.dataprovider.BubbleDataProvider; +import com.github.mikephil.charting.renderer.BubbleChartRenderer; + +/** + * The BubbleChart. Draws bubbles. Bubble chart implementation: Copyright 2015 + * Pierre-Marc Airoldi Licensed under Apache License 2.0. In the BubbleChart, it + * is the area of the bubble, not the radius or diameter of the bubble that + * conveys the data. + * + * @author Philipp Jahoda + */ +public class BubbleChart extends BarLineChartBase implements BubbleDataProvider { + + public BubbleChart(Context context) { + super(context); + } + + public BubbleChart(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public BubbleChart(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void init() { + super.init(); + + mRenderer = new BubbleChartRenderer(this, mAnimator, mViewPortHandler); + } + + public BubbleData getBubbleData() { + return mData; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/charts/CandleStickChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/CandleStickChart.java new file mode 100644 index 0000000..fa36e35 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/CandleStickChart.java @@ -0,0 +1,44 @@ + +package com.github.mikephil.charting.charts; + +import android.content.Context; +import android.util.AttributeSet; + +import com.github.mikephil.charting.data.CandleData; +import com.github.mikephil.charting.interfaces.dataprovider.CandleDataProvider; +import com.github.mikephil.charting.renderer.CandleStickChartRenderer; + +/** + * Financial chart type that draws candle-sticks (OHCL chart). + * + * @author Philipp Jahoda + */ +public class CandleStickChart extends BarLineChartBase implements CandleDataProvider { + + public CandleStickChart(Context context) { + super(context); + } + + public CandleStickChart(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CandleStickChart(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void init() { + super.init(); + + mRenderer = new CandleStickChartRenderer(this, mAnimator, mViewPortHandler); + + getXAxis().setSpaceMin(0.5f); + getXAxis().setSpaceMax(0.5f); + } + + @Override + public CandleData getCandleData() { + return mData; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/charts/Chart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/Chart.java new file mode 100644 index 0000000..5cf49ea --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/Chart.java @@ -0,0 +1,1822 @@ + +package com.github.mikephil.charting.charts; + +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.ContentValues; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Environment; +import android.provider.MediaStore.Images; +import androidx.annotation.RequiresApi; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.animation.Easing; +import com.github.mikephil.charting.animation.Easing.EasingFunction; +import com.github.mikephil.charting.components.Description; +import com.github.mikephil.charting.components.IMarker; +import com.github.mikephil.charting.components.Legend; +import com.github.mikephil.charting.components.XAxis; +import com.github.mikephil.charting.data.ChartData; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.formatter.DefaultValueFormatter; +import com.github.mikephil.charting.formatter.IValueFormatter; +import com.github.mikephil.charting.highlight.ChartHighlighter; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.highlight.IHighlighter; +import com.github.mikephil.charting.interfaces.dataprovider.ChartInterface; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.listener.ChartTouchListener; +import com.github.mikephil.charting.listener.OnChartGestureListener; +import com.github.mikephil.charting.listener.OnChartValueSelectedListener; +import com.github.mikephil.charting.renderer.DataRenderer; +import com.github.mikephil.charting.renderer.LegendRenderer; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; + +/** + * Baseclass of all Chart-Views. + * + * @author Philipp Jahoda + */ +public abstract class Chart>> extends + ViewGroup + implements ChartInterface { + + public static final String LOG_TAG = "MPAndroidChart"; + + /** + * flag that indicates if logging is enabled or not + */ + protected boolean mLogEnabled = false; + + /** + * object that holds all data that was originally set for the chart, before + * it was modified or any filtering algorithms had been applied + */ + protected T mData = null; + + /** + * Flag that indicates if highlighting per tap (touch) is enabled + */ + protected boolean mHighLightPerTapEnabled = true; + + /** + * If set to true, chart continues to scroll after touch up + */ + private boolean mDragDecelerationEnabled = true; + + /** + * Deceleration friction coefficient in [0 ; 1] interval, higher values + * indicate that speed will decrease slowly, for example if it set to 0, it + * will stop immediately. 1 is an invalid value, and will be converted to + * 0.999f automatically. + */ + private float mDragDecelerationFrictionCoef = 0.9f; + + /** + * default value-formatter, number of digits depends on provided chart-data + */ + protected DefaultValueFormatter mDefaultValueFormatter = new DefaultValueFormatter(0); + + /** + * paint object used for drawing the description text in the bottom right + * corner of the chart + */ + protected Paint mDescPaint; + + /** + * paint object for drawing the information text when there are no values in + * the chart + */ + protected Paint mInfoPaint; + + /** + * the object representing the labels on the x-axis + */ + protected XAxis mXAxis; + + /** + * if true, touch gestures are enabled on the chart + */ + protected boolean mTouchEnabled = true; + + /** + * the object responsible for representing the description text + */ + protected Description mDescription; + + /** + * the legend object containing all data associated with the legend + */ + protected Legend mLegend; + + /** + * listener that is called when a value on the chart is selected + */ + protected OnChartValueSelectedListener mSelectionListener; + + protected ChartTouchListener mChartTouchListener; + + /** + * text that is displayed when the chart is empty + */ + private String mNoDataText = "No chart data available."; + + /** + * Gesture listener for custom callbacks when making gestures on the chart. + */ + private OnChartGestureListener mGestureListener; + + protected LegendRenderer mLegendRenderer; + + /** + * object responsible for rendering the data + */ + protected DataRenderer mRenderer; + + protected IHighlighter mHighlighter; + + /** + * object that manages the bounds and drawing constraints of the chart + */ + protected ViewPortHandler mViewPortHandler = new ViewPortHandler(); + + /** + * object responsible for animations + */ + protected ChartAnimator mAnimator; + + /** + * Extra offsets to be appended to the viewport + */ + private float mExtraTopOffset = 0.f, + mExtraRightOffset = 0.f, + mExtraBottomOffset = 0.f, + mExtraLeftOffset = 0.f; + + /** + * default constructor for initialization in code + */ + public Chart(Context context) { + super(context); + init(); + } + + /** + * constructor for initialization in xml + */ + public Chart(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + /** + * even more awesome constructor + */ + public Chart(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + /** + * initialize all paints and stuff + */ + protected void init() { + + setWillNotDraw(false); + // setLayerType(View.LAYER_TYPE_HARDWARE, null); + + mAnimator = new ChartAnimator(new AnimatorUpdateListener() { + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + // ViewCompat.postInvalidateOnAnimation(Chart.this); + postInvalidate(); + } + }); + + // initialize the utils + Utils.init(getContext()); + mMaxHighlightDistance = Utils.convertDpToPixel(500f); + + mDescription = new Description(); + mLegend = new Legend(); + + mLegendRenderer = new LegendRenderer(mViewPortHandler, mLegend); + + mXAxis = new XAxis(); + + mDescPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + mInfoPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mInfoPaint.setColor(Color.rgb(247, 189, 51)); // orange + mInfoPaint.setTextAlign(Align.CENTER); + mInfoPaint.setTextSize(Utils.convertDpToPixel(12f)); + + if (mLogEnabled) + Log.i("", "Chart.init()"); + } + + // public void initWithDummyData() { + // ColorTemplate template = new ColorTemplate(); + // template.addColorsForDataSets(ColorTemplate.COLORFUL_COLORS, + // getContext()); + // + // setColorTemplate(template); + // setDrawYValues(false); + // + // ArrayList xVals = new ArrayList(); + // Calendar calendar = Calendar.getInstance(); + // for (int i = 0; i < 12; i++) { + // xVals.add(calendar.getDisplayName(Calendar.MONTH, Calendar.SHORT, + // Locale.getDefault())); + // } + // + // ArrayList dataSets = new ArrayList(); + // for (int i = 0; i < 3; i++) { + // + // ArrayList yVals = new ArrayList(); + // + // for (int j = 0; j < 12; j++) { + // float val = (float) (Math.random() * 100); + // yVals.add(new Entry(val, j)); + // } + // + // DataSet set = new DataSet(yVals, "DataSet " + i); + // dataSets.add(set); // add the datasets + // } + // // create a data object with the datasets + // ChartData data = new ChartData(xVals, dataSets); + // setData(data); + // invalidate(); + // } + + /** + * Sets a new data object for the chart. The data object contains all values + * and information needed for displaying. + * + * @param data + */ + public void setData(T data) { + + mData = data; + mOffsetsCalculated = false; + + if (data == null) { + return; + } + + // calculate how many digits are needed + setupDefaultFormatter(data.getYMin(), data.getYMax()); + + for (IDataSet set : mData.getDataSets()) { + if (set.needsFormatter() || set.getValueFormatter() == mDefaultValueFormatter) + set.setValueFormatter(mDefaultValueFormatter); + } + + // let the chart know there is new data + notifyDataSetChanged(); + + if (mLogEnabled) + Log.i(LOG_TAG, "Data is set."); + } + + /** + * Clears the chart from all data (sets it to null) and refreshes it (by + * calling invalidate()). + */ + public void clear() { + mData = null; + mOffsetsCalculated = false; + mIndicesToHighlight = null; + mChartTouchListener.setLastHighlighted(null); + invalidate(); + } + + /** + * Removes all DataSets (and thereby Entries) from the chart. Does not set the data object to null. Also refreshes the + * chart by calling invalidate(). + */ + public void clearValues() { + mData.clearValues(); + invalidate(); + } + + /** + * Returns true if the chart is empty (meaning it's data object is either + * null or contains no entries). + * + * @return + */ + public boolean isEmpty() { + + if (mData == null) + return true; + else { + + if (mData.getEntryCount() <= 0) + return true; + else + return false; + } + } + + /** + * Lets the chart know its underlying data has changed and performs all + * necessary recalculations. It is crucial that this method is called + * everytime data is changed dynamically. Not calling this method can lead + * to crashes or unexpected behaviour. + */ + public abstract void notifyDataSetChanged(); + + /** + * Calculates the offsets of the chart to the border depending on the + * position of an eventual legend or depending on the length of the y-axis + * and x-axis labels and their position + */ + protected abstract void calculateOffsets(); + + /** + * Calculates the y-min and y-max value and the y-delta and x-delta value + */ + protected abstract void calcMinMax(); + + /** + * Calculates the required number of digits for the values that might be + * drawn in the chart (if enabled), and creates the default-value-formatter + */ + protected void setupDefaultFormatter(float min, float max) { + + float reference = 0f; + + if (mData == null || mData.getEntryCount() < 2) { + + reference = Math.max(Math.abs(min), Math.abs(max)); + } else { + reference = Math.abs(max - min); + } + + int digits = Utils.getDecimals(reference); + + // setup the formatter with a new number of digits + mDefaultValueFormatter.setup(digits); + } + + /** + * flag that indicates if offsets calculation has already been done or not + */ + private boolean mOffsetsCalculated = false; + + @Override + protected void onDraw(Canvas canvas) { + // super.onDraw(canvas); + + if (mData == null) { + + boolean hasText = !TextUtils.isEmpty(mNoDataText); + + if (hasText) { + MPPointF pt = getCenter(); + + switch (mInfoPaint.getTextAlign()) { + case LEFT: + pt.x = 0; + canvas.drawText(mNoDataText, pt.x, pt.y, mInfoPaint); + break; + + case RIGHT: + pt.x *= 2.0; + canvas.drawText(mNoDataText, pt.x, pt.y, mInfoPaint); + break; + + default: + canvas.drawText(mNoDataText, pt.x, pt.y, mInfoPaint); + break; + } + } + + return; + } + + if (!mOffsetsCalculated) { + + calculateOffsets(); + mOffsetsCalculated = true; + } + } + + /** + * Draws the description text in the bottom right corner of the chart (per default) + */ + protected void drawDescription(Canvas c) { + + // check if description should be drawn + if (mDescription != null && mDescription.isEnabled()) { + + MPPointF position = mDescription.getPosition(); + + mDescPaint.setTypeface(mDescription.getTypeface()); + mDescPaint.setTextSize(mDescription.getTextSize()); + mDescPaint.setColor(mDescription.getTextColor()); + mDescPaint.setTextAlign(mDescription.getTextAlign()); + + float x, y; + + // if no position specified, draw on default position + if (position == null) { + x = getWidth() - mViewPortHandler.offsetRight() - mDescription.getXOffset(); + y = getHeight() - mViewPortHandler.offsetBottom() - mDescription.getYOffset(); + } else { + x = position.x; + y = position.y; + } + + c.drawText(mDescription.getText(), x, y, mDescPaint); + } + } + + /** + * ################ ################ ################ ################ + */ + /** BELOW THIS CODE FOR HIGHLIGHTING */ + + /** + * array of Highlight objects that reference the highlighted slices in the + * chart + */ + protected Highlight[] mIndicesToHighlight; + + /** + * The maximum distance in dp away from an entry causing it to highlight. + */ + protected float mMaxHighlightDistance = 0f; + + @Override + public float getMaxHighlightDistance() { + return mMaxHighlightDistance; + } + + /** + * Sets the maximum distance in screen dp a touch can be away from an entry to cause it to get highlighted. + * Default: 500dp + * + * @param distDp + */ + public void setMaxHighlightDistance(float distDp) { + mMaxHighlightDistance = Utils.convertDpToPixel(distDp); + } + + /** + * Returns the array of currently highlighted values. This might a null or + * empty array if nothing is highlighted. + * + * @return + */ + public Highlight[] getHighlighted() { + return mIndicesToHighlight; + } + + /** + * Returns true if values can be highlighted via tap gesture, false if not. + * + * @return + */ + public boolean isHighlightPerTapEnabled() { + return mHighLightPerTapEnabled; + } + + /** + * Set this to false to prevent values from being highlighted by tap gesture. + * Values can still be highlighted via drag or programmatically. Default: true + * + * @param enabled + */ + public void setHighlightPerTapEnabled(boolean enabled) { + mHighLightPerTapEnabled = enabled; + } + + /** + * Returns true if there are values to highlight, false if there are no + * values to highlight. Checks if the highlight array is null, has a length + * of zero or if the first object is null. + * + * @return + */ + public boolean valuesToHighlight() { + return mIndicesToHighlight == null || mIndicesToHighlight.length <= 0 + || mIndicesToHighlight[0] == null ? false + : true; + } + + /** + * Sets the last highlighted value for the touchlistener. + * + * @param highs + */ + protected void setLastHighlighted(Highlight[] highs) { + + if (highs == null || highs.length <= 0 || highs[0] == null) { + mChartTouchListener.setLastHighlighted(null); + } else { + mChartTouchListener.setLastHighlighted(highs[0]); + } + } + + /** + * Highlights the values at the given indices in the given DataSets. Provide + * null or an empty array to undo all highlighting. This should be used to + * programmatically highlight values. + * This method *will not* call the listener. + * + * @param highs + */ + public void highlightValues(Highlight[] highs) { + + // set the indices to highlight + mIndicesToHighlight = highs; + + setLastHighlighted(highs); + + // redraw the chart + invalidate(); + } + + /** + * Highlights any y-value at the given x-value in the given DataSet. + * Provide -1 as the dataSetIndex to undo all highlighting. + * This method will call the listener. + * @param x The x-value to highlight + * @param dataSetIndex The dataset index to search in + * @param dataIndex The data index to search in (only used in CombinedChartView currently) + */ + public void highlightValue(float x, int dataSetIndex, int dataIndex) { + highlightValue(x, dataSetIndex, dataIndex, true); + } + + /** + * Highlights any y-value at the given x-value in the given DataSet. + * Provide -1 as the dataSetIndex to undo all highlighting. + * This method will call the listener. + * @param x The x-value to highlight + * @param dataSetIndex The dataset index to search in + */ + public void highlightValue(float x, int dataSetIndex) { + highlightValue(x, dataSetIndex, -1, true); + } + + /** + * Highlights the value at the given x-value and y-value in the given DataSet. + * Provide -1 as the dataSetIndex to undo all highlighting. + * This method will call the listener. + * @param x The x-value to highlight + * @param y The y-value to highlight. Supply `NaN` for "any" + * @param dataSetIndex The dataset index to search in + * @param dataIndex The data index to search in (only used in CombinedChartView currently) + */ + public void highlightValue(float x, float y, int dataSetIndex, int dataIndex) { + highlightValue(x, y, dataSetIndex, dataIndex, true); + } + + /** + * Highlights the value at the given x-value and y-value in the given DataSet. + * Provide -1 as the dataSetIndex to undo all highlighting. + * This method will call the listener. + * @param x The x-value to highlight + * @param y The y-value to highlight. Supply `NaN` for "any" + * @param dataSetIndex The dataset index to search in + */ + public void highlightValue(float x, float y, int dataSetIndex) { + highlightValue(x, y, dataSetIndex, -1, true); + } + + /** + * Highlights any y-value at the given x-value in the given DataSet. + * Provide -1 as the dataSetIndex to undo all highlighting. + * @param x The x-value to highlight + * @param dataSetIndex The dataset index to search in + * @param dataIndex The data index to search in (only used in CombinedChartView currently) + * @param callListener Should the listener be called for this change + */ + public void highlightValue(float x, int dataSetIndex, int dataIndex, boolean callListener) { + highlightValue(x, Float.NaN, dataSetIndex, dataIndex, callListener); + } + + /** + * Highlights any y-value at the given x-value in the given DataSet. + * Provide -1 as the dataSetIndex to undo all highlighting. + * @param x The x-value to highlight + * @param dataSetIndex The dataset index to search in + * @param callListener Should the listener be called for this change + */ + public void highlightValue(float x, int dataSetIndex, boolean callListener) { + highlightValue(x, Float.NaN, dataSetIndex, -1, callListener); + } + + /** + * Highlights any y-value at the given x-value in the given DataSet. + * Provide -1 as the dataSetIndex to undo all highlighting. + * @param x The x-value to highlight + * @param y The y-value to highlight. Supply `NaN` for "any" + * @param dataSetIndex The dataset index to search in + * @param dataIndex The data index to search in (only used in CombinedChartView currently) + * @param callListener Should the listener be called for this change + */ + public void highlightValue(float x, float y, int dataSetIndex, int dataIndex, boolean callListener) { + + if (dataSetIndex < 0 || dataSetIndex >= mData.getDataSetCount()) { + highlightValue(null, callListener); + } else { + highlightValue(new Highlight(x, y, dataSetIndex, dataIndex), callListener); + } + } + + /** + * Highlights any y-value at the given x-value in the given DataSet. + * Provide -1 as the dataSetIndex to undo all highlighting. + * @param x The x-value to highlight + * @param y The y-value to highlight. Supply `NaN` for "any" + * @param dataSetIndex The dataset index to search in + * @param callListener Should the listener be called for this change + */ + public void highlightValue(float x, float y, int dataSetIndex, boolean callListener) { + highlightValue(x, y, dataSetIndex, -1, callListener); + } + + /** + * Highlights the values represented by the provided Highlight object + * This method *will not* call the listener. + * + * @param highlight contains information about which entry should be highlighted + */ + public void highlightValue(Highlight highlight) { + highlightValue(highlight, false); + } + + /** + * Highlights the value selected by touch gesture. Unlike + * highlightValues(...), this generates a callback to the + * OnChartValueSelectedListener. + * + * @param high - the highlight object + * @param callListener - call the listener + */ + public void highlightValue(Highlight high, boolean callListener) { + + Entry e = null; + + if (high == null) + mIndicesToHighlight = null; + else { + + if (mLogEnabled) + Log.i(LOG_TAG, "Highlighted: " + high.toString()); + + e = mData.getEntryForHighlight(high); + if (e == null) { + mIndicesToHighlight = null; + high = null; + } else { + + // set the indices to highlight + mIndicesToHighlight = new Highlight[]{ + high + }; + } + } + + setLastHighlighted(mIndicesToHighlight); + + if (callListener && mSelectionListener != null) { + + if (!valuesToHighlight()) + mSelectionListener.onNothingSelected(); + else { + // notify the listener + mSelectionListener.onValueSelected(e, high); + } + } + + // redraw the chart + invalidate(); + } + + /** + * Returns the Highlight object (contains x-index and DataSet index) of the + * selected value at the given touch point inside the Line-, Scatter-, or + * CandleStick-Chart. + * + * @param x + * @param y + * @return + */ + public Highlight getHighlightByTouchPoint(float x, float y) { + + if (mData == null) { + Log.e(LOG_TAG, "Can't select by touch. No data set."); + return null; + } else + return getHighlighter().getHighlight(x, y); + } + + /** + * Set a new (e.g. custom) ChartTouchListener NOTE: make sure to + * setTouchEnabled(true); if you need touch gestures on the chart + * + * @param l + */ + public void setOnTouchListener(ChartTouchListener l) { + this.mChartTouchListener = l; + } + + /** + * Returns an instance of the currently active touch listener. + * + * @return + */ + public ChartTouchListener getOnTouchListener() { + return mChartTouchListener; + } + + /** + * ################ ################ ################ ################ + */ + /** BELOW CODE IS FOR THE MARKER VIEW */ + + /** + * if set to true, the marker view is drawn when a value is clicked + */ + protected boolean mDrawMarkers = true; + + /** + * the view that represents the marker + */ + protected IMarker mMarker; + + /** + * draws all MarkerViews on the highlighted positions + */ + protected void drawMarkers(Canvas canvas) { + + // if there is no marker view or drawing marker is disabled + if (mMarker == null || !isDrawMarkersEnabled() || !valuesToHighlight()) + return; + + for (int i = 0; i < mIndicesToHighlight.length; i++) { + + Highlight highlight = mIndicesToHighlight[i]; + + IDataSet set = mData.getDataSetByIndex(highlight.getDataSetIndex()); + + Entry e = mData.getEntryForHighlight(mIndicesToHighlight[i]); + int entryIndex = set.getEntryIndex(e); + + // make sure entry not null + if (e == null || entryIndex > set.getEntryCount() * mAnimator.getPhaseX()) + continue; + + float[] pos = getMarkerPosition(highlight); + + // check bounds + if (!mViewPortHandler.isInBounds(pos[0], pos[1])) + continue; + + // callbacks to update the content + mMarker.refreshContent(e, highlight); + + // draw the marker + mMarker.draw(canvas, pos[0], pos[1]); + } + } + + /** + * Returns the actual position in pixels of the MarkerView for the given + * Highlight object. + * + * @param high + * @return + */ + protected float[] getMarkerPosition(Highlight high) { + return new float[]{high.getDrawX(), high.getDrawY()}; + } + + /** + * ################ ################ ################ ################ + * ANIMATIONS ONLY WORK FOR API LEVEL 11 (Android 3.0.x) AND HIGHER. + */ + /** CODE BELOW THIS RELATED TO ANIMATION */ + + /** + * Returns the animator responsible for animating chart values. + * + * @return + */ + public ChartAnimator getAnimator() { + return mAnimator; + } + + /** + * If set to true, chart continues to scroll after touch up default: true + */ + public boolean isDragDecelerationEnabled() { + return mDragDecelerationEnabled; + } + + /** + * If set to true, chart continues to scroll after touch up. Default: true. + * + * @param enabled + */ + public void setDragDecelerationEnabled(boolean enabled) { + mDragDecelerationEnabled = enabled; + } + + /** + * Returns drag deceleration friction coefficient + * + * @return + */ + public float getDragDecelerationFrictionCoef() { + return mDragDecelerationFrictionCoef; + } + + /** + * Deceleration friction coefficient in [0 ; 1] interval, higher values + * indicate that speed will decrease slowly, for example if it set to 0, it + * will stop immediately. 1 is an invalid value, and will be converted to + * 0.999f automatically. + * + * @param newValue + */ + public void setDragDecelerationFrictionCoef(float newValue) { + + if (newValue < 0.f) + newValue = 0.f; + + if (newValue >= 1f) + newValue = 0.999f; + + mDragDecelerationFrictionCoef = newValue; + } + + /** + * ################ ################ ################ ################ + * ANIMATIONS ONLY WORK FOR API LEVEL 11 (Android 3.0.x) AND HIGHER. + */ + /** CODE BELOW FOR PROVIDING EASING FUNCTIONS */ + + /** + * Animates the drawing / rendering of the chart on both x- and y-axis with + * the specified animation time. If animate(...) is called, no further + * calling of invalidate() is necessary to refresh the chart. ANIMATIONS + * ONLY WORK FOR API LEVEL 11 (Android 3.0.x) AND HIGHER. + * + * @param durationMillisX + * @param durationMillisY + * @param easingX a custom easing function to be used on the animation phase + * @param easingY a custom easing function to be used on the animation phase + */ + @RequiresApi(11) + public void animateXY(int durationMillisX, int durationMillisY, EasingFunction easingX, + EasingFunction easingY) { + mAnimator.animateXY(durationMillisX, durationMillisY, easingX, easingY); + } + + /** + * Animates the drawing / rendering of the chart on both x- and y-axis with + * the specified animation time. If animate(...) is called, no further + * calling of invalidate() is necessary to refresh the chart. ANIMATIONS + * ONLY WORK FOR API LEVEL 11 (Android 3.0.x) AND HIGHER. + * + * @param durationMillisX + * @param durationMillisY + * @param easing a custom easing function to be used on the animation phase + */ + @RequiresApi(11) + public void animateXY(int durationMillisX, int durationMillisY, EasingFunction easing) { + mAnimator.animateXY(durationMillisX, durationMillisY, easing); + } + + /** + * Animates the rendering of the chart on the x-axis with the specified + * animation time. If animate(...) is called, no further calling of + * invalidate() is necessary to refresh the chart. ANIMATIONS ONLY WORK FOR + * API LEVEL 11 (Android 3.0.x) AND HIGHER. + * + * @param durationMillis + * @param easing a custom easing function to be used on the animation phase + */ + @RequiresApi(11) + public void animateX(int durationMillis, EasingFunction easing) { + mAnimator.animateX(durationMillis, easing); + } + + /** + * Animates the rendering of the chart on the y-axis with the specified + * animation time. If animate(...) is called, no further calling of + * invalidate() is necessary to refresh the chart. ANIMATIONS ONLY WORK FOR + * API LEVEL 11 (Android 3.0.x) AND HIGHER. + * + * @param durationMillis + * @param easing a custom easing function to be used on the animation phase + */ + @RequiresApi(11) + public void animateY(int durationMillis, EasingFunction easing) { + mAnimator.animateY(durationMillis, easing); + } + + /** + * ################ ################ ################ ################ + * ANIMATIONS ONLY WORK FOR API LEVEL 11 (Android 3.0.x) AND HIGHER. + */ + /** CODE BELOW FOR PREDEFINED EASING OPTIONS */ + + /** + * ################ ################ ################ ################ + * ANIMATIONS ONLY WORK FOR API LEVEL 11 (Android 3.0.x) AND HIGHER. + */ + /** CODE BELOW FOR ANIMATIONS WITHOUT EASING */ + + /** + * Animates the rendering of the chart on the x-axis with the specified + * animation time. If animate(...) is called, no further calling of + * invalidate() is necessary to refresh the chart. ANIMATIONS ONLY WORK FOR + * API LEVEL 11 (Android 3.0.x) AND HIGHER. + * + * @param durationMillis + */ + @RequiresApi(11) + public void animateX(int durationMillis) { + mAnimator.animateX(durationMillis); + } + + /** + * Animates the rendering of the chart on the y-axis with the specified + * animation time. If animate(...) is called, no further calling of + * invalidate() is necessary to refresh the chart. ANIMATIONS ONLY WORK FOR + * API LEVEL 11 (Android 3.0.x) AND HIGHER. + * + * @param durationMillis + */ + @RequiresApi(11) + public void animateY(int durationMillis) { + mAnimator.animateY(durationMillis); + } + + /** + * Animates the drawing / rendering of the chart on both x- and y-axis with + * the specified animation time. If animate(...) is called, no further + * calling of invalidate() is necessary to refresh the chart. ANIMATIONS + * ONLY WORK FOR API LEVEL 11 (Android 3.0.x) AND HIGHER. + * + * @param durationMillisX + * @param durationMillisY + */ + @RequiresApi(11) + public void animateXY(int durationMillisX, int durationMillisY) { + mAnimator.animateXY(durationMillisX, durationMillisY); + } + + /** + * ################ ################ ################ ################ + */ + /** BELOW THIS ONLY GETTERS AND SETTERS */ + + + /** + * Returns the object representing all x-labels, this method can be used to + * acquire the XAxis object and modify it (e.g. change the position of the + * labels, styling, etc.) + * + * @return + */ + public XAxis getXAxis() { + return mXAxis; + } + + /** + * Returns the default IValueFormatter that has been determined by the chart + * considering the provided minimum and maximum values. + * + * @return + */ + public IValueFormatter getDefaultValueFormatter() { + return mDefaultValueFormatter; + } + + /** + * set a selection listener for the chart + * + * @param l + */ + public void setOnChartValueSelectedListener(OnChartValueSelectedListener l) { + this.mSelectionListener = l; + } + + /** + * Sets a gesture-listener for the chart for custom callbacks when executing + * gestures on the chart surface. + * + * @param l + */ + public void setOnChartGestureListener(OnChartGestureListener l) { + this.mGestureListener = l; + } + + /** + * Returns the custom gesture listener. + * + * @return + */ + public OnChartGestureListener getOnChartGestureListener() { + return mGestureListener; + } + + /** + * returns the current y-max value across all DataSets + * + * @return + */ + public float getYMax() { + return mData.getYMax(); + } + + /** + * returns the current y-min value across all DataSets + * + * @return + */ + public float getYMin() { + return mData.getYMin(); + } + + @Override + public float getXChartMax() { + return mXAxis.mAxisMaximum; + } + + @Override + public float getXChartMin() { + return mXAxis.mAxisMinimum; + } + + @Override + public float getXRange() { + return mXAxis.mAxisRange; + } + + /** + * Returns a recyclable MPPointF instance. + * Returns the center point of the chart (the whole View) in pixels. + * + * @return + */ + public MPPointF getCenter() { + return MPPointF.getInstance(getWidth() / 2f, getHeight() / 2f); + } + + /** + * Returns a recyclable MPPointF instance. + * Returns the center of the chart taking offsets under consideration. + * (returns the center of the content rectangle) + * + * @return + */ + @Override + public MPPointF getCenterOffsets() { + return mViewPortHandler.getContentCenter(); + } + + /** + * Sets extra offsets (around the chart view) to be appended to the + * auto-calculated offsets. + * + * @param left + * @param top + * @param right + * @param bottom + */ + public void setExtraOffsets(float left, float top, float right, float bottom) { + setExtraLeftOffset(left); + setExtraTopOffset(top); + setExtraRightOffset(right); + setExtraBottomOffset(bottom); + } + + /** + * Set an extra offset to be appended to the viewport's top + */ + public void setExtraTopOffset(float offset) { + mExtraTopOffset = Utils.convertDpToPixel(offset); + } + + /** + * @return the extra offset to be appended to the viewport's top + */ + public float getExtraTopOffset() { + return mExtraTopOffset; + } + + /** + * Set an extra offset to be appended to the viewport's right + */ + public void setExtraRightOffset(float offset) { + mExtraRightOffset = Utils.convertDpToPixel(offset); + } + + /** + * @return the extra offset to be appended to the viewport's right + */ + public float getExtraRightOffset() { + return mExtraRightOffset; + } + + /** + * Set an extra offset to be appended to the viewport's bottom + */ + public void setExtraBottomOffset(float offset) { + mExtraBottomOffset = Utils.convertDpToPixel(offset); + } + + /** + * @return the extra offset to be appended to the viewport's bottom + */ + public float getExtraBottomOffset() { + return mExtraBottomOffset; + } + + /** + * Set an extra offset to be appended to the viewport's left + */ + public void setExtraLeftOffset(float offset) { + mExtraLeftOffset = Utils.convertDpToPixel(offset); + } + + /** + * @return the extra offset to be appended to the viewport's left + */ + public float getExtraLeftOffset() { + return mExtraLeftOffset; + } + + /** + * Set this to true to enable logcat outputs for the chart. Beware that + * logcat output decreases rendering performance. Default: disabled. + * + * @param enabled + */ + public void setLogEnabled(boolean enabled) { + mLogEnabled = enabled; + } + + /** + * Returns true if log-output is enabled for the chart, fals if not. + * + * @return + */ + public boolean isLogEnabled() { + return mLogEnabled; + } + + /** + * Sets the text that informs the user that there is no data available with + * which to draw the chart. + * + * @param text + */ + public void setNoDataText(String text) { + mNoDataText = text; + } + + /** + * Sets the color of the no data text. + * + * @param color + */ + public void setNoDataTextColor(int color) { + mInfoPaint.setColor(color); + } + + /** + * Sets the typeface to be used for the no data text. + * + * @param tf + */ + public void setNoDataTextTypeface(Typeface tf) { + mInfoPaint.setTypeface(tf); + } + + /** + * alignment of the no data text + * + * @param align + */ + public void setNoDataTextAlignment(Align align) { + mInfoPaint.setTextAlign(align); + } + + /** + * Set this to false to disable all gestures and touches on the chart, + * default: true + * + * @param enabled + */ + public void setTouchEnabled(boolean enabled) { + this.mTouchEnabled = enabled; + } + + /** + * sets the marker that is displayed when a value is clicked on the chart + * + * @param marker + */ + public void setMarker(IMarker marker) { + mMarker = marker; + } + + /** + * returns the marker that is set as a marker view for the chart + * + * @return + */ + public IMarker getMarker() { + return mMarker; + } + + @Deprecated + public void setMarkerView(IMarker v) { + setMarker(v); + } + + @Deprecated + public IMarker getMarkerView() { + return getMarker(); + } + + /** + * Sets a new Description object for the chart. + * + * @param desc + */ + public void setDescription(Description desc) { + this.mDescription = desc; + } + + /** + * Returns the Description object of the chart that is responsible for holding all information related + * to the description text that is displayed in the bottom right corner of the chart (by default). + * + * @return + */ + public Description getDescription() { + return mDescription; + } + + /** + * Returns the Legend object of the chart. This method can be used to get an + * instance of the legend in order to customize the automatically generated + * Legend. + * + * @return + */ + public Legend getLegend() { + return mLegend; + } + + /** + * Returns the renderer object responsible for rendering / drawing the + * Legend. + * + * @return + */ + public LegendRenderer getLegendRenderer() { + return mLegendRenderer; + } + + /** + * Returns the rectangle that defines the borders of the chart-value surface + * (into which the actual values are drawn). + * + * @return + */ + @Override + public RectF getContentRect() { + return mViewPortHandler.getContentRect(); + } + + /** + * disables intercept touchevents + */ + public void disableScroll() { + ViewParent parent = getParent(); + if (parent != null) + parent.requestDisallowInterceptTouchEvent(true); + } + + /** + * enables intercept touchevents + */ + public void enableScroll() { + ViewParent parent = getParent(); + if (parent != null) + parent.requestDisallowInterceptTouchEvent(false); + } + + /** + * paint for the grid background (only line and barchart) + */ + public static final int PAINT_GRID_BACKGROUND = 4; + + /** + * paint for the info text that is displayed when there are no values in the + * chart + */ + public static final int PAINT_INFO = 7; + + /** + * paint for the description text in the bottom right corner + */ + public static final int PAINT_DESCRIPTION = 11; + + /** + * paint for the hole in the middle of the pie chart + */ + public static final int PAINT_HOLE = 13; + + /** + * paint for the text in the middle of the pie chart + */ + public static final int PAINT_CENTER_TEXT = 14; + + /** + * paint used for the legend + */ + public static final int PAINT_LEGEND_LABEL = 18; + + /** + * set a new paint object for the specified parameter in the chart e.g. + * Chart.PAINT_VALUES + * + * @param p the new paint object + * @param which Chart.PAINT_VALUES, Chart.PAINT_GRID, Chart.PAINT_VALUES, + * ... + */ + public void setPaint(Paint p, int which) { + + switch (which) { + case PAINT_INFO: + mInfoPaint = p; + break; + case PAINT_DESCRIPTION: + mDescPaint = p; + break; + } + } + + /** + * Returns the paint object associated with the provided constant. + * + * @param which e.g. Chart.PAINT_LEGEND_LABEL + * @return + */ + public Paint getPaint(int which) { + switch (which) { + case PAINT_INFO: + return mInfoPaint; + case PAINT_DESCRIPTION: + return mDescPaint; + } + + return null; + } + + @Deprecated + public boolean isDrawMarkerViewsEnabled() { + return isDrawMarkersEnabled(); + } + + @Deprecated + public void setDrawMarkerViews(boolean enabled) { + setDrawMarkers(enabled); + } + + /** + * returns true if drawing the marker is enabled when tapping on values + * (use the setMarker(IMarker marker) method to specify a marker) + * + * @return + */ + public boolean isDrawMarkersEnabled() { + return mDrawMarkers; + } + + /** + * Set this to true to draw a user specified marker when tapping on + * chart values (use the setMarker(IMarker marker) method to specify a + * marker). Default: true + * + * @param enabled + */ + public void setDrawMarkers(boolean enabled) { + mDrawMarkers = enabled; + } + + /** + * Returns the ChartData object that has been set for the chart. + * + * @return + */ + public T getData() { + return mData; + } + + /** + * Returns the ViewPortHandler of the chart that is responsible for the + * content area of the chart and its offsets and dimensions. + * + * @return + */ + public ViewPortHandler getViewPortHandler() { + return mViewPortHandler; + } + + /** + * Returns the Renderer object the chart uses for drawing data. + * + * @return + */ + public DataRenderer getRenderer() { + return mRenderer; + } + + /** + * Sets a new DataRenderer object for the chart. + * + * @param renderer + */ + public void setRenderer(DataRenderer renderer) { + + if (renderer != null) + mRenderer = renderer; + } + + public IHighlighter getHighlighter() { + return mHighlighter; + } + + /** + * Sets a custom highligher object for the chart that handles / processes + * all highlight touch events performed on the chart-view. + * + * @param highlighter + */ + public void setHighlighter(ChartHighlighter highlighter) { + mHighlighter = highlighter; + } + + /** + * Returns a recyclable MPPointF instance. + * + * @return + */ + @Override + public MPPointF getCenterOfView() { + return getCenter(); + } + + /** + * Returns the bitmap that represents the chart. + * + * @return + */ + public Bitmap getChartBitmap() { + // Define a bitmap with the same size as the view + Bitmap returnedBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.RGB_565); + // Bind a canvas to it + Canvas canvas = new Canvas(returnedBitmap); + // Get the view's background + Drawable bgDrawable = getBackground(); + if (bgDrawable != null) + // has background drawable, then draw it on the canvas + bgDrawable.draw(canvas); + else + // does not have background drawable, then draw white background on + // the canvas + canvas.drawColor(Color.WHITE); + // draw the view on the canvas + draw(canvas); + // return the bitmap + return returnedBitmap; + } + + /** + * Saves the current chart state with the given name to the given path on + * the sdcard leaving the path empty "" will put the saved file directly on + * the SD card chart is saved as a PNG image, example: + * saveToPath("myfilename", "foldername1/foldername2"); + * + * @param title + * @param pathOnSD e.g. "folder1/folder2/folder3" + * @return returns true on success, false on error + */ + public boolean saveToPath(String title, String pathOnSD) { + + + + Bitmap b = getChartBitmap(); + + OutputStream stream = null; + try { + stream = new FileOutputStream(Environment.getExternalStorageDirectory().getPath() + + pathOnSD + "/" + title + + ".png"); + + /* + * Write bitmap to file using JPEG or PNG and 40% quality hint for + * JPEG. + */ + b.compress(CompressFormat.PNG, 40, stream); + + stream.close(); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + + return true; + } + + /** + * Saves the current state of the chart to the gallery as an image type. The + * compression must be set for JPEG only. 0 == maximum compression, 100 = low + * compression (high quality). NOTE: Needs permission WRITE_EXTERNAL_STORAGE + * + * @param fileName e.g. "my_image" + * @param subFolderPath e.g. "ChartPics" + * @param fileDescription e.g. "Chart details" + * @param format e.g. Bitmap.CompressFormat.PNG + * @param quality e.g. 50, min = 0, max = 100 + * @return returns true if saving was successful, false if not + */ + public boolean saveToGallery(String fileName, String subFolderPath, String fileDescription, Bitmap.CompressFormat + format, int quality) { + // restrain quality + if (quality < 0 || quality > 100) + quality = 50; + + long currentTime = System.currentTimeMillis(); + + File extBaseDir = Environment.getExternalStorageDirectory(); + File file = new File(extBaseDir.getAbsolutePath() + "/DCIM/" + subFolderPath); + if (!file.exists()) { + if (!file.mkdirs()) { + return false; + } + } + + String mimeType = ""; + switch (format) { + case PNG: + mimeType = "image/png"; + if (!fileName.endsWith(".png")) + fileName += ".png"; + break; + case WEBP: + mimeType = "image/webp"; + if (!fileName.endsWith(".webp")) + fileName += ".webp"; + break; + case JPEG: + default: + mimeType = "image/jpeg"; + if (!(fileName.endsWith(".jpg") || fileName.endsWith(".jpeg"))) + fileName += ".jpg"; + break; + } + + String filePath = file.getAbsolutePath() + "/" + fileName; + FileOutputStream out = null; + try { + out = new FileOutputStream(filePath); + + Bitmap b = getChartBitmap(); + b.compress(format, quality, out); + + out.flush(); + out.close(); + + } catch (IOException e) { + e.printStackTrace(); + + return false; + } + + long size = new File(filePath).length(); + + ContentValues values = new ContentValues(8); + + // store the details + values.put(Images.Media.TITLE, fileName); + values.put(Images.Media.DISPLAY_NAME, fileName); + values.put(Images.Media.DATE_ADDED, currentTime); + values.put(Images.Media.MIME_TYPE, mimeType); + values.put(Images.Media.DESCRIPTION, fileDescription); + values.put(Images.Media.ORIENTATION, 0); + values.put(Images.Media.DATA, filePath); + values.put(Images.Media.SIZE, size); + + return getContext().getContentResolver().insert(Images.Media.EXTERNAL_CONTENT_URI, values) != null; + } + + /** + * Saves the current state of the chart to the gallery as a JPEG image. The + * filename and compression can be set. 0 == maximum compression, 100 = low + * compression (high quality). NOTE: Needs permission WRITE_EXTERNAL_STORAGE + * + * @param fileName e.g. "my_image" + * @param quality e.g. 50, min = 0, max = 100 + * @return returns true if saving was successful, false if not + */ + public boolean saveToGallery(String fileName, int quality) { + return saveToGallery(fileName, "", "MPAndroidChart-Library Save", Bitmap.CompressFormat.PNG, quality); + } + + /** + * Saves the current state of the chart to the gallery as a PNG image. + * NOTE: Needs permission WRITE_EXTERNAL_STORAGE + * + * @param fileName e.g. "my_image" + * @return returns true if saving was successful, false if not + */ + public boolean saveToGallery(String fileName) { + return saveToGallery(fileName, "", "MPAndroidChart-Library Save", Bitmap.CompressFormat.PNG, 40); + } + + /** + * tasks to be done after the view is setup + */ + protected ArrayList mJobs = new ArrayList(); + + public void removeViewportJob(Runnable job) { + mJobs.remove(job); + } + + public void clearAllViewportJobs() { + mJobs.clear(); + } + + /** + * Either posts a job immediately if the chart has already setup it's + * dimensions or adds the job to the execution queue. + * + * @param job + */ + public void addViewportJob(Runnable job) { + + if (mViewPortHandler.hasChartDimens()) { + post(job); + } else { + mJobs.add(job); + } + } + + /** + * Returns all jobs that are scheduled to be executed after + * onSizeChanged(...). + * + * @return + */ + public ArrayList getJobs() { + return mJobs; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + + for (int i = 0; i < getChildCount(); i++) { + getChildAt(i).layout(left, top, right, bottom); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int size = (int) Utils.convertDpToPixel(50f); + setMeasuredDimension( + Math.max(getSuggestedMinimumWidth(), + resolveSize(size, + widthMeasureSpec)), + Math.max(getSuggestedMinimumHeight(), + resolveSize(size, + heightMeasureSpec))); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + if (mLogEnabled) + Log.i(LOG_TAG, "OnSizeChanged()"); + + if (w > 0 && h > 0 && w < 10000 && h < 10000) { + if (mLogEnabled) + Log.i(LOG_TAG, "Setting chart dimens, width: " + w + ", height: " + h); + mViewPortHandler.setChartDimens(w, h); + } else { + if (mLogEnabled) + Log.w(LOG_TAG, "*Avoiding* setting chart dimens! width: " + w + ", height: " + h); + } + + // This may cause the chart view to mutate properties affecting the view port -- + // lets do this before we try to run any pending jobs on the view port itself + notifyDataSetChanged(); + + for (Runnable r : mJobs) { + post(r); + } + + mJobs.clear(); + + super.onSizeChanged(w, h, oldw, oldh); + } + + /** + * Setting this to true will set the layer-type HARDWARE for the view, false + * will set layer-type SOFTWARE. + * + * @param enabled + */ + public void setHardwareAccelerationEnabled(boolean enabled) { + + if (enabled) + setLayerType(View.LAYER_TYPE_HARDWARE, null); + else + setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + //Log.i(LOG_TAG, "Detaching..."); + + if (mUnbind) + unbindDrawables(this); + } + + /** + * unbind flag + */ + private boolean mUnbind = false; + + /** + * Unbind all drawables to avoid memory leaks. + * Link: http://stackoverflow.com/a/6779164/1590502 + * + * @param view + */ + private void unbindDrawables(View view) { + + if (view.getBackground() != null) { + view.getBackground().setCallback(null); + } + if (view instanceof ViewGroup) { + for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) { + unbindDrawables(((ViewGroup) view).getChildAt(i)); + } + ((ViewGroup) view).removeAllViews(); + } + } + + /** + * Set this to true to enable "unbinding" of drawables. When a View is detached + * from a window. This helps avoid memory leaks. + * Default: false + * Link: http://stackoverflow.com/a/6779164/1590502 + * + * @param enabled + */ + public void setUnbindEnabled(boolean enabled) { + this.mUnbind = enabled; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/charts/CombinedChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/CombinedChart.java new file mode 100644 index 0000000..cd01f0e --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/CombinedChart.java @@ -0,0 +1,272 @@ + +package com.github.mikephil.charting.charts; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.util.Log; + +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.BubbleData; +import com.github.mikephil.charting.data.CandleData; +import com.github.mikephil.charting.data.CombinedData; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.LineData; +import com.github.mikephil.charting.data.ScatterData; +import com.github.mikephil.charting.highlight.CombinedHighlighter; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.dataprovider.CombinedDataProvider; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.renderer.CombinedChartRenderer; + +/** + * This chart class allows the combination of lines, bars, scatter and candle + * data all displayed in one chart area. + * + * @author Philipp Jahoda + */ +public class CombinedChart extends BarLineChartBase implements CombinedDataProvider { + + /** + * if set to true, all values are drawn above their bars, instead of below + * their top + */ + private boolean mDrawValueAboveBar = true; + + + /** + * flag that indicates whether the highlight should be full-bar oriented, or single-value? + */ + protected boolean mHighlightFullBarEnabled = false; + + /** + * if set to true, a grey area is drawn behind each bar that indicates the + * maximum value + */ + private boolean mDrawBarShadow = false; + + protected DrawOrder[] mDrawOrder; + + /** + * enum that allows to specify the order in which the different data objects + * for the combined-chart are drawn + */ + public enum DrawOrder { + BAR, BUBBLE, LINE, CANDLE, SCATTER + } + + public CombinedChart(Context context) { + super(context); + } + + public CombinedChart(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CombinedChart(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void init() { + super.init(); + + // Default values are not ready here yet + mDrawOrder = new DrawOrder[]{ + DrawOrder.BAR, DrawOrder.BUBBLE, DrawOrder.LINE, DrawOrder.CANDLE, DrawOrder.SCATTER + }; + + setHighlighter(new CombinedHighlighter(this, this)); + + // Old default behaviour + setHighlightFullBarEnabled(true); + + mRenderer = new CombinedChartRenderer(this, mAnimator, mViewPortHandler); + } + + @Override + public CombinedData getCombinedData() { + return mData; + } + + @Override + public void setData(CombinedData data) { + super.setData(data); + setHighlighter(new CombinedHighlighter(this, this)); + ((CombinedChartRenderer)mRenderer).createRenderers(); + mRenderer.initBuffers(); + } + + /** + * Returns the Highlight object (contains x-index and DataSet index) of the selected value at the given touch + * point + * inside the CombinedChart. + * + * @param x + * @param y + * @return + */ + @Override + public Highlight getHighlightByTouchPoint(float x, float y) { + + if (mData == null) { + Log.e(LOG_TAG, "Can't select by touch. No data set."); + return null; + } else { + Highlight h = getHighlighter().getHighlight(x, y); + if (h == null || !isHighlightFullBarEnabled()) return h; + + // For isHighlightFullBarEnabled, remove stackIndex + return new Highlight(h.getX(), h.getY(), + h.getXPx(), h.getYPx(), + h.getDataSetIndex(), -1, h.getAxis()); + } + } + + @Override + public LineData getLineData() { + if (mData == null) + return null; + return mData.getLineData(); + } + + @Override + public BarData getBarData() { + if (mData == null) + return null; + return mData.getBarData(); + } + + @Override + public ScatterData getScatterData() { + if (mData == null) + return null; + return mData.getScatterData(); + } + + @Override + public CandleData getCandleData() { + if (mData == null) + return null; + return mData.getCandleData(); + } + + @Override + public BubbleData getBubbleData() { + if (mData == null) + return null; + return mData.getBubbleData(); + } + + @Override + public boolean isDrawBarShadowEnabled() { + return mDrawBarShadow; + } + + @Override + public boolean isDrawValueAboveBarEnabled() { + return mDrawValueAboveBar; + } + + /** + * If set to true, all values are drawn above their bars, instead of below + * their top. + * + * @param enabled + */ + public void setDrawValueAboveBar(boolean enabled) { + mDrawValueAboveBar = enabled; + } + + + /** + * If set to true, a grey area is drawn behind each bar that indicates the + * maximum value. Enabling his will reduce performance by about 50%. + * + * @param enabled + */ + public void setDrawBarShadow(boolean enabled) { + mDrawBarShadow = enabled; + } + + /** + * Set this to true to make the highlight operation full-bar oriented, + * false to make it highlight single values (relevant only for stacked). + * + * @param enabled + */ + public void setHighlightFullBarEnabled(boolean enabled) { + mHighlightFullBarEnabled = enabled; + } + + /** + * @return true the highlight operation is be full-bar oriented, false if single-value + */ + @Override + public boolean isHighlightFullBarEnabled() { + return mHighlightFullBarEnabled; + } + + /** + * Returns the currently set draw order. + * + * @return + */ + public DrawOrder[] getDrawOrder() { + return mDrawOrder; + } + + /** + * Sets the order in which the provided data objects should be drawn. The + * earlier you place them in the provided array, the further they will be in + * the background. e.g. if you provide new DrawOrer[] { DrawOrder.BAR, + * DrawOrder.LINE }, the bars will be drawn behind the lines. + * + * @param order + */ + public void setDrawOrder(DrawOrder[] order) { + if (order == null || order.length <= 0) + return; + mDrawOrder = order; + } + + /** + * draws all MarkerViews on the highlighted positions + */ + protected void drawMarkers(Canvas canvas) { + + // if there is no marker view or drawing marker is disabled + if (mMarker == null || !isDrawMarkersEnabled() || !valuesToHighlight()) + return; + + for (int i = 0; i < mIndicesToHighlight.length; i++) { + + Highlight highlight = mIndicesToHighlight[i]; + + IDataSet set = mData.getDataSetByHighlight(highlight); + + Entry e = mData.getEntryForHighlight(highlight); + if (e == null) + continue; + + int entryIndex = set.getEntryIndex(e); + + // make sure entry not null + if (entryIndex > set.getEntryCount() * mAnimator.getPhaseX()) + continue; + + float[] pos = getMarkerPosition(highlight); + + // check bounds + if (!mViewPortHandler.isInBounds(pos[0], pos[1])) + continue; + + // callbacks to update the content + mMarker.refreshContent(e, highlight); + + // draw the marker + mMarker.draw(canvas, pos[0], pos[1]); + } + } + +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/charts/HorizontalBarChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/HorizontalBarChart.java new file mode 100644 index 0000000..9aac1ce --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/HorizontalBarChart.java @@ -0,0 +1,346 @@ +package com.github.mikephil.charting.charts; + +import android.content.Context; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.util.Log; + +import com.github.mikephil.charting.components.XAxis.XAxisPosition; +import com.github.mikephil.charting.components.YAxis.AxisDependency; +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.highlight.HorizontalBarHighlighter; +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; +import com.github.mikephil.charting.renderer.HorizontalBarChartRenderer; +import com.github.mikephil.charting.renderer.XAxisRendererHorizontalBarChart; +import com.github.mikephil.charting.renderer.YAxisRendererHorizontalBarChart; +import com.github.mikephil.charting.utils.HorizontalViewPortHandler; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.TransformerHorizontalBarChart; +import com.github.mikephil.charting.utils.Utils; + +/** + * BarChart with horizontal bar orientation. In this implementation, x- and y-axis are switched, meaning the YAxis class + * represents the horizontal values and the XAxis class represents the vertical values. + * + * @author Philipp Jahoda + */ +public class HorizontalBarChart extends BarChart { + + public HorizontalBarChart(Context context) { + super(context); + } + + public HorizontalBarChart(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public HorizontalBarChart(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void init() { + + mViewPortHandler = new HorizontalViewPortHandler(); + + super.init(); + + mLeftAxisTransformer = new TransformerHorizontalBarChart(mViewPortHandler); + mRightAxisTransformer = new TransformerHorizontalBarChart(mViewPortHandler); + + mRenderer = new HorizontalBarChartRenderer(this, mAnimator, mViewPortHandler); + setHighlighter(new HorizontalBarHighlighter(this)); + + mAxisRendererLeft = new YAxisRendererHorizontalBarChart(mViewPortHandler, mAxisLeft, mLeftAxisTransformer); + mAxisRendererRight = new YAxisRendererHorizontalBarChart(mViewPortHandler, mAxisRight, mRightAxisTransformer); + mXAxisRenderer = new XAxisRendererHorizontalBarChart(mViewPortHandler, mXAxis, mLeftAxisTransformer, this); + } + + private RectF mOffsetsBuffer = new RectF(); + + protected void calculateLegendOffsets(RectF offsets) { + + offsets.left = 0.f; + offsets.right = 0.f; + offsets.top = 0.f; + offsets.bottom = 0.f; + + if (mLegend == null || !mLegend.isEnabled() || mLegend.isDrawInsideEnabled()) + return; + + switch (mLegend.getOrientation()) { + case VERTICAL: + + switch (mLegend.getHorizontalAlignment()) { + case LEFT: + offsets.left += Math.min(mLegend.mNeededWidth, + mViewPortHandler.getChartWidth() * mLegend.getMaxSizePercent()) + + mLegend.getXOffset(); + break; + + case RIGHT: + offsets.right += Math.min(mLegend.mNeededWidth, + mViewPortHandler.getChartWidth() * mLegend.getMaxSizePercent()) + + mLegend.getXOffset(); + break; + + case CENTER: + + switch (mLegend.getVerticalAlignment()) { + case TOP: + offsets.top += Math.min(mLegend.mNeededHeight, + mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()) + + mLegend.getYOffset(); + break; + + case BOTTOM: + offsets.bottom += Math.min(mLegend.mNeededHeight, + mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()) + + mLegend.getYOffset(); + break; + + default: + break; + } + } + + break; + + case HORIZONTAL: + + switch (mLegend.getVerticalAlignment()) { + case TOP: + offsets.top += Math.min(mLegend.mNeededHeight, + mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()) + + mLegend.getYOffset(); + + if (mAxisLeft.isEnabled() && mAxisLeft.isDrawLabelsEnabled()) + offsets.top += mAxisLeft.getRequiredHeightSpace( + mAxisRendererLeft.getPaintAxisLabels()); + break; + + case BOTTOM: + offsets.bottom += Math.min(mLegend.mNeededHeight, + mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()) + + mLegend.getYOffset(); + + if (mAxisRight.isEnabled() && mAxisRight.isDrawLabelsEnabled()) + offsets.bottom += mAxisRight.getRequiredHeightSpace( + mAxisRendererRight.getPaintAxisLabels()); + break; + + default: + break; + } + break; + } + } + + @Override + public void calculateOffsets() { + + float offsetLeft = 0f, offsetRight = 0f, offsetTop = 0f, offsetBottom = 0f; + + calculateLegendOffsets(mOffsetsBuffer); + + offsetLeft += mOffsetsBuffer.left; + offsetTop += mOffsetsBuffer.top; + offsetRight += mOffsetsBuffer.right; + offsetBottom += mOffsetsBuffer.bottom; + + // offsets for y-labels + if (mAxisLeft.needsOffset()) { + offsetTop += mAxisLeft.getRequiredHeightSpace(mAxisRendererLeft.getPaintAxisLabels()); + } + + if (mAxisRight.needsOffset()) { + offsetBottom += mAxisRight.getRequiredHeightSpace(mAxisRendererRight.getPaintAxisLabels()); + } + + float xlabelwidth = mXAxis.mLabelRotatedWidth; + + if (mXAxis.isEnabled()) { + + // offsets for x-labels + if (mXAxis.getPosition() == XAxisPosition.BOTTOM) { + + offsetLeft += xlabelwidth; + + } else if (mXAxis.getPosition() == XAxisPosition.TOP) { + + offsetRight += xlabelwidth; + + } else if (mXAxis.getPosition() == XAxisPosition.BOTH_SIDED) { + + offsetLeft += xlabelwidth; + offsetRight += xlabelwidth; + } + } + + offsetTop += getExtraTopOffset(); + offsetRight += getExtraRightOffset(); + offsetBottom += getExtraBottomOffset(); + offsetLeft += getExtraLeftOffset(); + + float minOffset = Utils.convertDpToPixel(mMinOffset); + + mViewPortHandler.restrainViewPort( + Math.max(minOffset, offsetLeft), + Math.max(minOffset, offsetTop), + Math.max(minOffset, offsetRight), + Math.max(minOffset, offsetBottom)); + + if (mLogEnabled) { + Log.i(LOG_TAG, "offsetLeft: " + offsetLeft + ", offsetTop: " + offsetTop + ", offsetRight: " + + offsetRight + ", offsetBottom: " + + offsetBottom); + Log.i(LOG_TAG, "Content: " + mViewPortHandler.getContentRect().toString()); + } + + prepareOffsetMatrix(); + prepareValuePxMatrix(); + } + + @Override + protected void prepareValuePxMatrix() { + mRightAxisTransformer.prepareMatrixValuePx(mAxisRight.mAxisMinimum, mAxisRight.mAxisRange, mXAxis.mAxisRange, + mXAxis.mAxisMinimum); + mLeftAxisTransformer.prepareMatrixValuePx(mAxisLeft.mAxisMinimum, mAxisLeft.mAxisRange, mXAxis.mAxisRange, + mXAxis.mAxisMinimum); + } + + @Override + protected float[] getMarkerPosition(Highlight high) { + return new float[]{high.getDrawY(), high.getDrawX()}; + } + + @Override + public void getBarBounds(BarEntry e, RectF outputRect) { + + RectF bounds = outputRect; + IBarDataSet set = mData.getDataSetForEntry(e); + + if (set == null) { + outputRect.set(Float.MIN_VALUE, Float.MIN_VALUE, Float.MIN_VALUE, Float.MIN_VALUE); + return; + } + + float y = e.getY(); + float x = e.getX(); + + float barWidth = mData.getBarWidth(); + + float top = x - barWidth / 2f; + float bottom = x + barWidth / 2f; + float left = y >= 0 ? y : 0; + float right = y <= 0 ? y : 0; + + bounds.set(left, top, right, bottom); + + getTransformer(set.getAxisDependency()).rectValueToPixel(bounds); + + } + + protected float[] mGetPositionBuffer = new float[2]; + + /** + * Returns a recyclable MPPointF instance. + * + * @param e + * @param axis + * @return + */ + @Override + public MPPointF getPosition(Entry e, AxisDependency axis) { + + if (e == null) + return null; + + float[] vals = mGetPositionBuffer; + vals[0] = e.getY(); + vals[1] = e.getX(); + + getTransformer(axis).pointValuesToPixel(vals); + + return MPPointF.getInstance(vals[0], vals[1]); + } + + /** + * Returns the Highlight object (contains x-index and DataSet index) of the selected value at the given touch point + * inside the BarChart. + * + * @param x + * @param y + * @return + */ + @Override + public Highlight getHighlightByTouchPoint(float x, float y) { + + if (mData == null) { + if (mLogEnabled) + Log.e(LOG_TAG, "Can't select by touch. No data set."); + return null; + } else + return getHighlighter().getHighlight(y, x); // switch x and y + } + + @Override + public float getLowestVisibleX() { + getTransformer(AxisDependency.LEFT).getValuesByTouchPoint(mViewPortHandler.contentLeft(), + mViewPortHandler.contentBottom(), posForGetLowestVisibleX); + float result = (float) Math.max(mXAxis.mAxisMinimum, posForGetLowestVisibleX.y); + return result; + } + + @Override + public float getHighestVisibleX() { + getTransformer(AxisDependency.LEFT).getValuesByTouchPoint(mViewPortHandler.contentLeft(), + mViewPortHandler.contentTop(), posForGetHighestVisibleX); + float result = (float) Math.min(mXAxis.mAxisMaximum, posForGetHighestVisibleX.y); + return result; + } + + /** + * ###### VIEWPORT METHODS BELOW THIS ###### + */ + + @Override + public void setVisibleXRangeMaximum(float maxXRange) { + float xScale = mXAxis.mAxisRange / (maxXRange); + mViewPortHandler.setMinimumScaleY(xScale); + } + + @Override + public void setVisibleXRangeMinimum(float minXRange) { + float xScale = mXAxis.mAxisRange / (minXRange); + mViewPortHandler.setMaximumScaleY(xScale); + } + + @Override + public void setVisibleXRange(float minXRange, float maxXRange) { + float minScale = mXAxis.mAxisRange / minXRange; + float maxScale = mXAxis.mAxisRange / maxXRange; + mViewPortHandler.setMinMaxScaleY(minScale, maxScale); + } + + @Override + public void setVisibleYRangeMaximum(float maxYRange, AxisDependency axis) { + float yScale = getAxisRange(axis) / maxYRange; + mViewPortHandler.setMinimumScaleX(yScale); + } + + @Override + public void setVisibleYRangeMinimum(float minYRange, AxisDependency axis) { + float yScale = getAxisRange(axis) / minYRange; + mViewPortHandler.setMaximumScaleX(yScale); + } + + @Override + public void setVisibleYRange(float minYRange, float maxYRange, AxisDependency axis) { + float minScale = getAxisRange(axis) / minYRange; + float maxScale = getAxisRange(axis) / maxYRange; + mViewPortHandler.setMinMaxScaleX(minScale, maxScale); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/charts/LineChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/LineChart.java new file mode 100644 index 0000000..aa7afc4 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/LineChart.java @@ -0,0 +1,50 @@ + +package com.github.mikephil.charting.charts; + +import android.content.Context; +import android.util.AttributeSet; + +import com.github.mikephil.charting.data.LineData; +import com.github.mikephil.charting.interfaces.dataprovider.LineDataProvider; +import com.github.mikephil.charting.renderer.LineChartRenderer; + +/** + * Chart that draws lines, surfaces, circles, ... + * + * @author Philipp Jahoda + */ +public class LineChart extends BarLineChartBase implements LineDataProvider { + + public LineChart(Context context) { + super(context); + } + + public LineChart(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LineChart(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void init() { + super.init(); + + mRenderer = new LineChartRenderer(this, mAnimator, mViewPortHandler); + } + + @Override + public LineData getLineData() { + return mData; + } + + @Override + protected void onDetachedFromWindow() { + // releases the bitmap in the renderer to avoid oom error + if (mRenderer != null && mRenderer instanceof LineChartRenderer) { + ((LineChartRenderer) mRenderer).releaseBitmap(); + } + super.onDetachedFromWindow(); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/charts/PieChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/PieChart.java new file mode 100644 index 0000000..de11b3a --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/PieChart.java @@ -0,0 +1,804 @@ + +package com.github.mikephil.charting.charts; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.util.AttributeSet; + +import com.github.mikephil.charting.components.XAxis; +import com.github.mikephil.charting.data.PieData; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.highlight.PieHighlighter; +import com.github.mikephil.charting.interfaces.datasets.IPieDataSet; +import com.github.mikephil.charting.renderer.PieChartRenderer; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; + +import java.util.List; + +/** + * View that represents a pie chart. Draws cake like slices. + * + * @author Philipp Jahoda + */ +public class PieChart extends PieRadarChartBase { + + /** + * rect object that represents the bounds of the piechart, needed for + * drawing the circle + */ + private RectF mCircleBox = new RectF(); + + /** + * flag indicating if entry labels should be drawn or not + */ + private boolean mDrawEntryLabels = true; + + /** + * array that holds the width of each pie-slice in degrees + */ + private float[] mDrawAngles = new float[1]; + + /** + * array that holds the absolute angle in degrees of each slice + */ + private float[] mAbsoluteAngles = new float[1]; + + /** + * if true, the white hole inside the chart will be drawn + */ + private boolean mDrawHole = true; + + /** + * if true, the hole will see-through to the inner tips of the slices + */ + private boolean mDrawSlicesUnderHole = false; + + /** + * if true, the values inside the piechart are drawn as percent values + */ + private boolean mUsePercentValues = false; + + /** + * if true, the slices of the piechart are rounded + */ + private boolean mDrawRoundedSlices = false; + + /** + * variable for the text that is drawn in the center of the pie-chart + */ + private CharSequence mCenterText = ""; + + private MPPointF mCenterTextOffset = MPPointF.getInstance(0, 0); + + /** + * indicates the size of the hole in the center of the piechart, default: + * radius / 2 + */ + private float mHoleRadiusPercent = 50f; + + /** + * the radius of the transparent circle next to the chart-hole in the center + */ + protected float mTransparentCircleRadiusPercent = 55f; + + /** + * if enabled, centertext is drawn + */ + private boolean mDrawCenterText = true; + + private float mCenterTextRadiusPercent = 100.f; + + protected float mMaxAngle = 360f; + + /** + * Minimum angle to draw slices, this only works if there is enough room for all slices to have + * the minimum angle, default 0f. + */ + private float mMinAngleForSlices = 0f; + + public PieChart(Context context) { + super(context); + } + + public PieChart(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public PieChart(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void init() { + super.init(); + + mRenderer = new PieChartRenderer(this, mAnimator, mViewPortHandler); + mXAxis = null; + + mHighlighter = new PieHighlighter(this); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (mData == null) + return; + + mRenderer.drawData(canvas); + + if (valuesToHighlight()) + mRenderer.drawHighlighted(canvas, mIndicesToHighlight); + + mRenderer.drawExtras(canvas); + + mRenderer.drawValues(canvas); + + mLegendRenderer.renderLegend(canvas); + + drawDescription(canvas); + + drawMarkers(canvas); + } + + @Override + public void calculateOffsets() { + super.calculateOffsets(); + + // prevent nullpointer when no data set + if (mData == null) + return; + + float diameter = getDiameter(); + float radius = diameter / 2f; + + MPPointF c = getCenterOffsets(); + + float shift = mData.getDataSet().getSelectionShift(); + + // create the circle box that will contain the pie-chart (the bounds of + // the pie-chart) + mCircleBox.set(c.x - radius + shift, + c.y - radius + shift, + c.x + radius - shift, + c.y + radius - shift); + + MPPointF.recycleInstance(c); + } + + @Override + protected void calcMinMax() { + calcAngles(); + } + + @Override + protected float[] getMarkerPosition(Highlight highlight) { + + MPPointF center = getCenterCircleBox(); + float r = getRadius(); + + float off = r / 10f * 3.6f; + + if (isDrawHoleEnabled()) { + off = (r - (r / 100f * getHoleRadius())) / 2f; + } + + r -= off; // offset to keep things inside the chart + + float rotationAngle = getRotationAngle(); + + int entryIndex = (int) highlight.getX(); + + // offset needed to center the drawn text in the slice + float offset = mDrawAngles[entryIndex] / 2; + + // calculate the text position + float x = (float) (r + * Math.cos(Math.toRadians((rotationAngle + mAbsoluteAngles[entryIndex] - offset) + * mAnimator.getPhaseY())) + center.x); + float y = (float) (r + * Math.sin(Math.toRadians((rotationAngle + mAbsoluteAngles[entryIndex] - offset) + * mAnimator.getPhaseY())) + center.y); + + MPPointF.recycleInstance(center); + return new float[]{x, y}; + } + + /** + * calculates the needed angles for the chart slices + */ + private void calcAngles() { + + int entryCount = mData.getEntryCount(); + + if (mDrawAngles.length != entryCount) { + mDrawAngles = new float[entryCount]; + } else { + for (int i = 0; i < entryCount; i++) { + mDrawAngles[i] = 0; + } + } + if (mAbsoluteAngles.length != entryCount) { + mAbsoluteAngles = new float[entryCount]; + } else { + for (int i = 0; i < entryCount; i++) { + mAbsoluteAngles[i] = 0; + } + } + + float yValueSum = mData.getYValueSum(); + + List dataSets = mData.getDataSets(); + + boolean hasMinAngle = mMinAngleForSlices != 0f && entryCount * mMinAngleForSlices <= mMaxAngle; + float[] minAngles = new float[entryCount]; + + int cnt = 0; + float offset = 0f; + float diff = 0f; + + for (int i = 0; i < mData.getDataSetCount(); i++) { + + IPieDataSet set = dataSets.get(i); + + for (int j = 0; j < set.getEntryCount(); j++) { + + float drawAngle = calcAngle(Math.abs(set.getEntryForIndex(j).getY()), yValueSum); + + if (hasMinAngle) { + float temp = drawAngle - mMinAngleForSlices; + if (temp <= 0) { + minAngles[cnt] = mMinAngleForSlices; + offset += -temp; + } else { + minAngles[cnt] = drawAngle; + diff += temp; + } + } + + mDrawAngles[cnt] = drawAngle; + + if (cnt == 0) { + mAbsoluteAngles[cnt] = mDrawAngles[cnt]; + } else { + mAbsoluteAngles[cnt] = mAbsoluteAngles[cnt - 1] + mDrawAngles[cnt]; + } + + cnt++; + } + } + + if (hasMinAngle) { + // Correct bigger slices by relatively reducing their angles based on the total angle needed to subtract + // This requires that `entryCount * mMinAngleForSlices <= mMaxAngle` be true to properly work! + for (int i = 0; i < entryCount; i++) { + minAngles[i] -= (minAngles[i] - mMinAngleForSlices) / diff * offset; + if (i == 0) { + mAbsoluteAngles[0] = minAngles[0]; + } else { + mAbsoluteAngles[i] = mAbsoluteAngles[i - 1] + minAngles[i]; + } + } + + mDrawAngles = minAngles; + } + } + + /** + * Checks if the given index is set to be highlighted. + * + * @param index + * @return + */ + public boolean needsHighlight(int index) { + + // no highlight + if (!valuesToHighlight()) + return false; + + for (int i = 0; i < mIndicesToHighlight.length; i++) + + // check if the xvalue for the given dataset needs highlight + if ((int) mIndicesToHighlight[i].getX() == index) + return true; + + return false; + } + + /** + * calculates the needed angle for a given value + * + * @param value + * @return + */ + private float calcAngle(float value) { + return calcAngle(value, mData.getYValueSum()); + } + + /** + * calculates the needed angle for a given value + * + * @param value + * @param yValueSum + * @return + */ + private float calcAngle(float value, float yValueSum) { + return value / yValueSum * mMaxAngle; + } + + /** + * This will throw an exception, PieChart has no XAxis object. + * + * @return + */ + @Deprecated + @Override + public XAxis getXAxis() { + throw new RuntimeException("PieChart has no XAxis"); + } + + @Override + public int getIndexForAngle(float angle) { + + // take the current angle of the chart into consideration + float a = Utils.getNormalizedAngle(angle - getRotationAngle()); + + for (int i = 0; i < mAbsoluteAngles.length; i++) { + if (mAbsoluteAngles[i] > a) + return i; + } + + return -1; // return -1 if no index found + } + + /** + * Returns the index of the DataSet this x-index belongs to. + * + * @param xIndex + * @return + */ + public int getDataSetIndexForIndex(int xIndex) { + + List dataSets = mData.getDataSets(); + + for (int i = 0; i < dataSets.size(); i++) { + if (dataSets.get(i).getEntryForXValue(xIndex, Float.NaN) != null) + return i; + } + + return -1; + } + + /** + * returns an integer array of all the different angles the chart slices + * have the angles in the returned array determine how much space (of 360°) + * each slice takes + * + * @return + */ + public float[] getDrawAngles() { + return mDrawAngles; + } + + /** + * returns the absolute angles of the different chart slices (where the + * slices end) + * + * @return + */ + public float[] getAbsoluteAngles() { + return mAbsoluteAngles; + } + + /** + * Sets the color for the hole that is drawn in the center of the PieChart + * (if enabled). + * + * @param color + */ + public void setHoleColor(int color) { + ((PieChartRenderer) mRenderer).getPaintHole().setColor(color); + } + + /** + * Enable or disable the visibility of the inner tips of the slices behind the hole + */ + public void setDrawSlicesUnderHole(boolean enable) { + mDrawSlicesUnderHole = enable; + } + + /** + * Returns true if the inner tips of the slices are visible behind the hole, + * false if not. + * + * @return true if slices are visible behind the hole. + */ + public boolean isDrawSlicesUnderHoleEnabled() { + return mDrawSlicesUnderHole; + } + + /** + * set this to true to draw the pie center empty + * + * @param enabled + */ + public void setDrawHoleEnabled(boolean enabled) { + this.mDrawHole = enabled; + } + + /** + * returns true if the hole in the center of the pie-chart is set to be + * visible, false if not + * + * @return + */ + public boolean isDrawHoleEnabled() { + return mDrawHole; + } + + /** + * Sets the text String that is displayed in the center of the PieChart. + * + * @param text + */ + public void setCenterText(CharSequence text) { + if (text == null) + mCenterText = ""; + else + mCenterText = text; + } + + /** + * returns the text that is drawn in the center of the pie-chart + * + * @return + */ + public CharSequence getCenterText() { + return mCenterText; + } + + /** + * set this to true to draw the text that is displayed in the center of the + * pie chart + * + * @param enabled + */ + public void setDrawCenterText(boolean enabled) { + this.mDrawCenterText = enabled; + } + + /** + * returns true if drawing the center text is enabled + * + * @return + */ + public boolean isDrawCenterTextEnabled() { + return mDrawCenterText; + } + + @Override + protected float getRequiredLegendOffset() { + return mLegendRenderer.getLabelPaint().getTextSize() * 2.f; + } + + @Override + protected float getRequiredBaseOffset() { + return 0; + } + + @Override + public float getRadius() { + if (mCircleBox == null) + return 0; + else + return Math.min(mCircleBox.width() / 2f, mCircleBox.height() / 2f); + } + + /** + * returns the circlebox, the boundingbox of the pie-chart slices + * + * @return + */ + public RectF getCircleBox() { + return mCircleBox; + } + + /** + * returns the center of the circlebox + * + * @return + */ + public MPPointF getCenterCircleBox() { + return MPPointF.getInstance(mCircleBox.centerX(), mCircleBox.centerY()); + } + + /** + * sets the typeface for the center-text paint + * + * @param t + */ + public void setCenterTextTypeface(Typeface t) { + ((PieChartRenderer) mRenderer).getPaintCenterText().setTypeface(t); + } + + /** + * Sets the size of the center text of the PieChart in dp. + * + * @param sizeDp + */ + public void setCenterTextSize(float sizeDp) { + ((PieChartRenderer) mRenderer).getPaintCenterText().setTextSize( + Utils.convertDpToPixel(sizeDp)); + } + + /** + * Sets the size of the center text of the PieChart in pixels. + * + * @param sizePixels + */ + public void setCenterTextSizePixels(float sizePixels) { + ((PieChartRenderer) mRenderer).getPaintCenterText().setTextSize(sizePixels); + } + + /** + * Sets the offset the center text should have from it's original position in dp. Default x = 0, y = 0 + * + * @param x + * @param y + */ + public void setCenterTextOffset(float x, float y) { + mCenterTextOffset.x = Utils.convertDpToPixel(x); + mCenterTextOffset.y = Utils.convertDpToPixel(y); + } + + /** + * Returns the offset on the x- and y-axis the center text has in dp. + * + * @return + */ + public MPPointF getCenterTextOffset() { + return MPPointF.getInstance(mCenterTextOffset.x, mCenterTextOffset.y); + } + + /** + * Sets the color of the center text of the PieChart. + * + * @param color + */ + public void setCenterTextColor(int color) { + ((PieChartRenderer) mRenderer).getPaintCenterText().setColor(color); + } + + /** + * sets the radius of the hole in the center of the piechart in percent of + * the maximum radius (max = the radius of the whole chart), default 50% + * + * @param percent + */ + public void setHoleRadius(final float percent) { + mHoleRadiusPercent = percent; + } + + /** + * Returns the size of the hole radius in percent of the total radius. + * + * @return + */ + public float getHoleRadius() { + return mHoleRadiusPercent; + } + + /** + * Sets the color the transparent-circle should have. + * + * @param color + */ + public void setTransparentCircleColor(int color) { + + Paint p = ((PieChartRenderer) mRenderer).getPaintTransparentCircle(); + int alpha = p.getAlpha(); + p.setColor(color); + p.setAlpha(alpha); + } + + /** + * sets the radius of the transparent circle that is drawn next to the hole + * in the piechart in percent of the maximum radius (max = the radius of the + * whole chart), default 55% -> means 5% larger than the center-hole by + * default + * + * @param percent + */ + public void setTransparentCircleRadius(final float percent) { + mTransparentCircleRadiusPercent = percent; + } + + public float getTransparentCircleRadius() { + return mTransparentCircleRadiusPercent; + } + + /** + * Sets the amount of transparency the transparent circle should have 0 = fully transparent, + * 255 = fully opaque. + * Default value is 100. + * + * @param alpha 0-255 + */ + public void setTransparentCircleAlpha(int alpha) { + ((PieChartRenderer) mRenderer).getPaintTransparentCircle().setAlpha(alpha); + } + + /** + * Set this to true to draw the entry labels into the pie slices (Provided by the getLabel() method of the PieEntry class). + * Deprecated -> use setDrawEntryLabels(...) instead. + * + * @param enabled + */ + @Deprecated + public void setDrawSliceText(boolean enabled) { + mDrawEntryLabels = enabled; + } + + /** + * Set this to true to draw the entry labels into the pie slices (Provided by the getLabel() method of the PieEntry class). + * + * @param enabled + */ + public void setDrawEntryLabels(boolean enabled) { + mDrawEntryLabels = enabled; + } + + /** + * Returns true if drawing the entry labels is enabled, false if not. + * + * @return + */ + public boolean isDrawEntryLabelsEnabled() { + return mDrawEntryLabels; + } + + /** + * Sets the color the entry labels are drawn with. + * + * @param color + */ + public void setEntryLabelColor(int color) { + ((PieChartRenderer) mRenderer).getPaintEntryLabels().setColor(color); + } + + /** + * Sets a custom Typeface for the drawing of the entry labels. + * + * @param tf + */ + public void setEntryLabelTypeface(Typeface tf) { + ((PieChartRenderer) mRenderer).getPaintEntryLabels().setTypeface(tf); + } + + /** + * Sets the size of the entry labels in dp. Default: 13dp + * + * @param size + */ + public void setEntryLabelTextSize(float size) { + ((PieChartRenderer) mRenderer).getPaintEntryLabels().setTextSize(Utils.convertDpToPixel(size)); + } + + /** + * Sets whether to draw slices in a curved fashion, only works if drawing the hole is enabled + * and if the slices are not drawn under the hole. + * + * @param enabled draw curved ends of slices + */ + public void setDrawRoundedSlices(boolean enabled) { + mDrawRoundedSlices = enabled; + } + + /** + * Returns true if the chart is set to draw each end of a pie-slice + * "rounded". + * + * @return + */ + public boolean isDrawRoundedSlicesEnabled() { + return mDrawRoundedSlices; + } + + /** + * If this is enabled, values inside the PieChart are drawn in percent and + * not with their original value. Values provided for the IValueFormatter to + * format are then provided in percent. + * + * @param enabled + */ + public void setUsePercentValues(boolean enabled) { + mUsePercentValues = enabled; + } + + /** + * Returns true if using percentage values is enabled for the chart. + * + * @return + */ + public boolean isUsePercentValuesEnabled() { + return mUsePercentValues; + } + + /** + * the rectangular radius of the bounding box for the center text, as a percentage of the pie + * hole + * default 1.f (100%) + */ + public void setCenterTextRadiusPercent(float percent) { + mCenterTextRadiusPercent = percent; + } + + /** + * the rectangular radius of the bounding box for the center text, as a percentage of the pie + * hole + * default 1.f (100%) + */ + public float getCenterTextRadiusPercent() { + return mCenterTextRadiusPercent; + } + + public float getMaxAngle() { + return mMaxAngle; + } + + /** + * Sets the max angle that is used for calculating the pie-circle. 360f means + * it's a full PieChart, 180f results in a half-pie-chart. Default: 360f + * + * @param maxangle min 90, max 360 + */ + public void setMaxAngle(float maxangle) { + + if (maxangle > 360) + maxangle = 360f; + + if (maxangle < 90) + maxangle = 90f; + + this.mMaxAngle = maxangle; + } + + /** + * The minimum angle slices on the chart are rendered with, default is 0f. + * + * @return minimum angle for slices + */ + public float getMinAngleForSlices() { + return mMinAngleForSlices; + } + + /** + * Set the angle to set minimum size for slices, you must call {@link #notifyDataSetChanged()} + * and {@link #invalidate()} when changing this, only works if there is enough room for all + * slices to have the minimum angle. + * + * @param minAngle minimum 0, maximum is half of {@link #setMaxAngle(float)} + */ + public void setMinAngleForSlices(float minAngle) { + + if (minAngle > (mMaxAngle / 2f)) + minAngle = mMaxAngle / 2f; + else if (minAngle < 0) + minAngle = 0f; + + this.mMinAngleForSlices = minAngle; + } + + @Override + protected void onDetachedFromWindow() { + // releases the bitmap in the renderer to avoid oom error + if (mRenderer != null && mRenderer instanceof PieChartRenderer) { + ((PieChartRenderer) mRenderer).releaseBitmap(); + } + super.onDetachedFromWindow(); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/charts/PieRadarChartBase.java b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/PieRadarChartBase.java new file mode 100644 index 0000000..e6d38d3 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/PieRadarChartBase.java @@ -0,0 +1,499 @@ + +package com.github.mikephil.charting.charts; + +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; + +import com.github.mikephil.charting.animation.Easing; +import com.github.mikephil.charting.animation.Easing.EasingFunction; +import com.github.mikephil.charting.components.Legend; +import com.github.mikephil.charting.components.XAxis; +import com.github.mikephil.charting.data.ChartData; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.listener.PieRadarChartTouchListener; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; + +/** + * Baseclass of PieChart and RadarChart. + * + * @author Philipp Jahoda + */ +public abstract class PieRadarChartBase>> + extends Chart { + + /** + * holds the normalized version of the current rotation angle of the chart + */ + private float mRotationAngle = 270f; + + /** + * holds the raw version of the current rotation angle of the chart + */ + private float mRawRotationAngle = 270f; + + /** + * flag that indicates if rotation is enabled or not + */ + protected boolean mRotateEnabled = true; + + /** + * Sets the minimum offset (padding) around the chart, defaults to 0.f + */ + protected float mMinOffset = 0.f; + + public PieRadarChartBase(Context context) { + super(context); + } + + public PieRadarChartBase(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public PieRadarChartBase(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void init() { + super.init(); + + mChartTouchListener = new PieRadarChartTouchListener(this); + } + + @Override + protected void calcMinMax() { + //mXAxis.mAxisRange = mData.getXVals().size() - 1; + } + + @Override + public int getMaxVisibleCount() { + return mData.getEntryCount(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + // use the pie- and radarchart listener own listener + if (mTouchEnabled && mChartTouchListener != null) + return mChartTouchListener.onTouch(this, event); + else + return super.onTouchEvent(event); + } + + @Override + public void computeScroll() { + + if (mChartTouchListener instanceof PieRadarChartTouchListener) + ((PieRadarChartTouchListener) mChartTouchListener).computeScroll(); + } + + @Override + public void notifyDataSetChanged() { + if (mData == null) + return; + + calcMinMax(); + + if (mLegend != null) + mLegendRenderer.computeLegend(mData); + + calculateOffsets(); + } + + @Override + public void calculateOffsets() { + + float legendLeft = 0f, legendRight = 0f, legendBottom = 0f, legendTop = 0f; + + if (mLegend != null && mLegend.isEnabled() && !mLegend.isDrawInsideEnabled()) { + + float fullLegendWidth = Math.min(mLegend.mNeededWidth, + mViewPortHandler.getChartWidth() * mLegend.getMaxSizePercent()); + + switch (mLegend.getOrientation()) { + case VERTICAL: { + float xLegendOffset = 0.f; + + if (mLegend.getHorizontalAlignment() == Legend.LegendHorizontalAlignment.LEFT + || mLegend.getHorizontalAlignment() == Legend.LegendHorizontalAlignment.RIGHT) { + if (mLegend.getVerticalAlignment() == Legend.LegendVerticalAlignment.CENTER) { + // this is the space between the legend and the chart + final float spacing = Utils.convertDpToPixel(13f); + + xLegendOffset = fullLegendWidth + spacing; + + } else { + // this is the space between the legend and the chart + float spacing = Utils.convertDpToPixel(8f); + + float legendWidth = fullLegendWidth + spacing; + float legendHeight = mLegend.mNeededHeight + mLegend.mTextHeightMax; + + MPPointF center = getCenter(); + + float bottomX = mLegend.getHorizontalAlignment() == + Legend.LegendHorizontalAlignment.RIGHT + ? getWidth() - legendWidth + 15.f + : legendWidth - 15.f; + float bottomY = legendHeight + 15.f; + float distLegend = distanceToCenter(bottomX, bottomY); + + MPPointF reference = getPosition(center, getRadius(), + getAngleForPoint(bottomX, bottomY)); + + float distReference = distanceToCenter(reference.x, reference.y); + float minOffset = Utils.convertDpToPixel(5f); + + if (bottomY >= center.y && getHeight() - legendWidth > getWidth()) { + xLegendOffset = legendWidth; + } else if (distLegend < distReference) { + + float diff = distReference - distLegend; + xLegendOffset = minOffset + diff; + } + + MPPointF.recycleInstance(center); + MPPointF.recycleInstance(reference); + } + } + + switch (mLegend.getHorizontalAlignment()) { + case LEFT: + legendLeft = xLegendOffset; + break; + + case RIGHT: + legendRight = xLegendOffset; + break; + + case CENTER: + switch (mLegend.getVerticalAlignment()) { + case TOP: + legendTop = Math.min(mLegend.mNeededHeight, + mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()); + break; + case BOTTOM: + legendBottom = Math.min(mLegend.mNeededHeight, + mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()); + break; + } + break; + } + } + break; + + case HORIZONTAL: + float yLegendOffset = 0.f; + + if (mLegend.getVerticalAlignment() == Legend.LegendVerticalAlignment.TOP || + mLegend.getVerticalAlignment() == Legend.LegendVerticalAlignment.BOTTOM) { + + // It's possible that we do not need this offset anymore as it + // is available through the extraOffsets, but changing it can mean + // changing default visibility for existing apps. + float yOffset = getRequiredLegendOffset(); + + yLegendOffset = Math.min(mLegend.mNeededHeight + yOffset, + mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent()); + + switch (mLegend.getVerticalAlignment()) { + case TOP: + legendTop = yLegendOffset; + break; + case BOTTOM: + legendBottom = yLegendOffset; + break; + } + } + break; + } + + legendLeft += getRequiredBaseOffset(); + legendRight += getRequiredBaseOffset(); + legendTop += getRequiredBaseOffset(); + legendBottom += getRequiredBaseOffset(); + } + + float minOffset = Utils.convertDpToPixel(mMinOffset); + + if (this instanceof RadarChart) { + XAxis x = this.getXAxis(); + + if (x.isEnabled() && x.isDrawLabelsEnabled()) { + minOffset = Math.max(minOffset, x.mLabelRotatedWidth); + } + } + + legendTop += getExtraTopOffset(); + legendRight += getExtraRightOffset(); + legendBottom += getExtraBottomOffset(); + legendLeft += getExtraLeftOffset(); + + float offsetLeft = Math.max(minOffset, legendLeft); + float offsetTop = Math.max(minOffset, legendTop); + float offsetRight = Math.max(minOffset, legendRight); + float offsetBottom = Math.max(minOffset, Math.max(getRequiredBaseOffset(), legendBottom)); + + mViewPortHandler.restrainViewPort(offsetLeft, offsetTop, offsetRight, offsetBottom); + + if (mLogEnabled) + Log.i(LOG_TAG, "offsetLeft: " + offsetLeft + ", offsetTop: " + offsetTop + + ", offsetRight: " + offsetRight + ", offsetBottom: " + offsetBottom); + } + + /** + * returns the angle relative to the chart center for the given point on the + * chart in degrees. The angle is always between 0 and 360°, 0° is NORTH, + * 90° is EAST, ... + * + * @param x + * @param y + * @return + */ + public float getAngleForPoint(float x, float y) { + + MPPointF c = getCenterOffsets(); + + double tx = x - c.x, ty = y - c.y; + double length = Math.sqrt(tx * tx + ty * ty); + double r = Math.acos(ty / length); + + float angle = (float) Math.toDegrees(r); + + if (x > c.x) + angle = 360f - angle; + + // add 90° because chart starts EAST + angle = angle + 90f; + + // neutralize overflow + if (angle > 360f) + angle = angle - 360f; + + MPPointF.recycleInstance(c); + + return angle; + } + + /** + * Returns a recyclable MPPointF instance. + * Calculates the position around a center point, depending on the distance + * from the center, and the angle of the position around the center. + * + * @param center + * @param dist + * @param angle in degrees, converted to radians internally + * @return + */ + public MPPointF getPosition(MPPointF center, float dist, float angle) { + + MPPointF p = MPPointF.getInstance(0, 0); + getPosition(center, dist, angle, p); + return p; + } + + public void getPosition(MPPointF center, float dist, float angle, MPPointF outputPoint) { + outputPoint.x = (float) (center.x + dist * Math.cos(Math.toRadians(angle))); + outputPoint.y = (float) (center.y + dist * Math.sin(Math.toRadians(angle))); + } + + /** + * Returns the distance of a certain point on the chart to the center of the + * chart. + * + * @param x + * @param y + * @return + */ + public float distanceToCenter(float x, float y) { + + MPPointF c = getCenterOffsets(); + + float dist = 0f; + + float xDist = 0f; + float yDist = 0f; + + if (x > c.x) { + xDist = x - c.x; + } else { + xDist = c.x - x; + } + + if (y > c.y) { + yDist = y - c.y; + } else { + yDist = c.y - y; + } + + // pythagoras + dist = (float) Math.sqrt(Math.pow(xDist, 2.0) + Math.pow(yDist, 2.0)); + + MPPointF.recycleInstance(c); + + return dist; + } + + /** + * Returns the xIndex for the given angle around the center of the chart. + * Returns -1 if not found / outofbounds. + * + * @param angle + * @return + */ + public abstract int getIndexForAngle(float angle); + + /** + * Set an offset for the rotation of the RadarChart in degrees. Default 270f + * --> top (NORTH) + * + * @param angle + */ + public void setRotationAngle(float angle) { + mRawRotationAngle = angle; + mRotationAngle = Utils.getNormalizedAngle(mRawRotationAngle); + } + + /** + * gets the raw version of the current rotation angle of the pie chart the + * returned value could be any value, negative or positive, outside of the + * 360 degrees. this is used when working with rotation direction, mainly by + * gestures and animations. + * + * @return + */ + public float getRawRotationAngle() { + return mRawRotationAngle; + } + + /** + * gets a normalized version of the current rotation angle of the pie chart, + * which will always be between 0.0 < 360.0 + * + * @return + */ + public float getRotationAngle() { + return mRotationAngle; + } + + /** + * Set this to true to enable the rotation / spinning of the chart by touch. + * Set it to false to disable it. Default: true + * + * @param enabled + */ + public void setRotationEnabled(boolean enabled) { + mRotateEnabled = enabled; + } + + /** + * Returns true if rotation of the chart by touch is enabled, false if not. + * + * @return + */ + public boolean isRotationEnabled() { + return mRotateEnabled; + } + + /** + * Gets the minimum offset (padding) around the chart, defaults to 0.f + */ + public float getMinOffset() { + return mMinOffset; + } + + /** + * Sets the minimum offset (padding) around the chart, defaults to 0.f + */ + public void setMinOffset(float minOffset) { + mMinOffset = minOffset; + } + + /** + * returns the diameter of the pie- or radar-chart + * + * @return + */ + public float getDiameter() { + RectF content = mViewPortHandler.getContentRect(); + content.left += getExtraLeftOffset(); + content.top += getExtraTopOffset(); + content.right -= getExtraRightOffset(); + content.bottom -= getExtraBottomOffset(); + return Math.min(content.width(), content.height()); + } + + /** + * Returns the radius of the chart in pixels. + * + * @return + */ + public abstract float getRadius(); + + /** + * Returns the required offset for the chart legend. + * + * @return + */ + protected abstract float getRequiredLegendOffset(); + + /** + * Returns the base offset needed for the chart without calculating the + * legend size. + * + * @return + */ + protected abstract float getRequiredBaseOffset(); + + @Override + public float getYChartMax() { + // TODO Auto-generated method stub + return 0; + } + + @Override + public float getYChartMin() { + // TODO Auto-generated method stub + return 0; + } + + /** + * ################ ################ ################ ################ + */ + /** CODE BELOW THIS RELATED TO ANIMATION */ + + /** + * Applys a spin animation to the Chart. + * + * @param durationmillis + * @param fromangle + * @param toangle + */ + @SuppressLint("NewApi") + public void spin(int durationmillis, float fromangle, float toangle, EasingFunction easing) { + + setRotationAngle(fromangle); + + ObjectAnimator spinAnimator = ObjectAnimator.ofFloat(this, "rotationAngle", fromangle, + toangle); + spinAnimator.setDuration(durationmillis); + spinAnimator.setInterpolator(easing); + + spinAnimator.addUpdateListener(new AnimatorUpdateListener() { + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + postInvalidate(); + } + }); + spinAnimator.start(); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/charts/RadarChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/RadarChart.java new file mode 100644 index 0000000..8c08853 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/RadarChart.java @@ -0,0 +1,362 @@ + +package com.github.mikephil.charting.charts; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.RectF; +import android.util.AttributeSet; + +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.components.YAxis.AxisDependency; +import com.github.mikephil.charting.data.RadarData; +import com.github.mikephil.charting.highlight.RadarHighlighter; +import com.github.mikephil.charting.renderer.RadarChartRenderer; +import com.github.mikephil.charting.renderer.XAxisRendererRadarChart; +import com.github.mikephil.charting.renderer.YAxisRendererRadarChart; +import com.github.mikephil.charting.utils.Utils; + +/** + * Implementation of the RadarChart, a "spidernet"-like chart. It works best + * when displaying 5-10 entries per DataSet. + * + * @author Philipp Jahoda + */ +public class RadarChart extends PieRadarChartBase { + + /** + * width of the main web lines + */ + private float mWebLineWidth = 2.5f; + + /** + * width of the inner web lines + */ + private float mInnerWebLineWidth = 1.5f; + + /** + * color for the main web lines + */ + private int mWebColor = Color.rgb(122, 122, 122); + + /** + * color for the inner web + */ + private int mWebColorInner = Color.rgb(122, 122, 122); + + /** + * transparency the grid is drawn with (0-255) + */ + private int mWebAlpha = 150; + + /** + * flag indicating if the web lines should be drawn or not + */ + private boolean mDrawWeb = true; + + /** + * modulus that determines how many labels and web-lines are skipped before the next is drawn + */ + private int mSkipWebLineCount = 0; + + /** + * the object reprsenting the y-axis labels + */ + private YAxis mYAxis; + + protected YAxisRendererRadarChart mYAxisRenderer; + protected XAxisRendererRadarChart mXAxisRenderer; + + public RadarChart(Context context) { + super(context); + } + + public RadarChart(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public RadarChart(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void init() { + super.init(); + + mYAxis = new YAxis(AxisDependency.LEFT); + mYAxis.setLabelXOffset(10f); + + mWebLineWidth = Utils.convertDpToPixel(1.5f); + mInnerWebLineWidth = Utils.convertDpToPixel(0.75f); + + mRenderer = new RadarChartRenderer(this, mAnimator, mViewPortHandler); + mYAxisRenderer = new YAxisRendererRadarChart(mViewPortHandler, mYAxis, this); + mXAxisRenderer = new XAxisRendererRadarChart(mViewPortHandler, mXAxis, this); + + mHighlighter = new RadarHighlighter(this); + } + + @Override + protected void calcMinMax() { + super.calcMinMax(); + + mYAxis.calculate(mData.getYMin(AxisDependency.LEFT), mData.getYMax(AxisDependency.LEFT)); + mXAxis.calculate(0, mData.getMaxEntryCountSet().getEntryCount()); + } + + @Override + public void notifyDataSetChanged() { + if (mData == null) + return; + + calcMinMax(); + + mYAxisRenderer.computeAxis(mYAxis.mAxisMinimum, mYAxis.mAxisMaximum, mYAxis.isInverted()); + mXAxisRenderer.computeAxis(mXAxis.mAxisMinimum, mXAxis.mAxisMaximum, false); + + if (mLegend != null && !mLegend.isLegendCustom()) + mLegendRenderer.computeLegend(mData); + + calculateOffsets(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (mData == null) + return; + +// if (mYAxis.isEnabled()) +// mYAxisRenderer.computeAxis(mYAxis.mAxisMinimum, mYAxis.mAxisMaximum, mYAxis.isInverted()); + + if (mXAxis.isEnabled()) + mXAxisRenderer.computeAxis(mXAxis.mAxisMinimum, mXAxis.mAxisMaximum, false); + + mXAxisRenderer.renderAxisLabels(canvas); + + if (mDrawWeb) + mRenderer.drawExtras(canvas); + + if (mYAxis.isEnabled() && mYAxis.isDrawLimitLinesBehindDataEnabled()) + mYAxisRenderer.renderLimitLines(canvas); + + mRenderer.drawData(canvas); + + if (valuesToHighlight()) + mRenderer.drawHighlighted(canvas, mIndicesToHighlight); + + if (mYAxis.isEnabled() && !mYAxis.isDrawLimitLinesBehindDataEnabled()) + mYAxisRenderer.renderLimitLines(canvas); + + mYAxisRenderer.renderAxisLabels(canvas); + + mRenderer.drawValues(canvas); + + mLegendRenderer.renderLegend(canvas); + + drawDescription(canvas); + + drawMarkers(canvas); + } + + /** + * Returns the factor that is needed to transform values into pixels. + * + * @return + */ + public float getFactor() { + RectF content = mViewPortHandler.getContentRect(); + return Math.min(content.width() / 2f, content.height() / 2f) / mYAxis.mAxisRange; + } + + /** + * Returns the angle that each slice in the radar chart occupies. + * + * @return + */ + public float getSliceAngle() { + return 360f / (float) mData.getMaxEntryCountSet().getEntryCount(); + } + + @Override + public int getIndexForAngle(float angle) { + + // take the current angle of the chart into consideration + float a = Utils.getNormalizedAngle(angle - getRotationAngle()); + + float sliceangle = getSliceAngle(); + + int max = mData.getMaxEntryCountSet().getEntryCount(); + + int index = 0; + + for (int i = 0; i < max; i++) { + + float referenceAngle = sliceangle * (i + 1) - sliceangle / 2f; + + if (referenceAngle > a) { + index = i; + break; + } + } + + return index; + } + + /** + * Returns the object that represents all y-labels of the RadarChart. + * + * @return + */ + public YAxis getYAxis() { + return mYAxis; + } + + /** + * Sets the width of the web lines that come from the center. + * + * @param width + */ + public void setWebLineWidth(float width) { + mWebLineWidth = Utils.convertDpToPixel(width); + } + + public float getWebLineWidth() { + return mWebLineWidth; + } + + /** + * Sets the width of the web lines that are in between the lines coming from + * the center. + * + * @param width + */ + public void setWebLineWidthInner(float width) { + mInnerWebLineWidth = Utils.convertDpToPixel(width); + } + + public float getWebLineWidthInner() { + return mInnerWebLineWidth; + } + + /** + * Sets the transparency (alpha) value for all web lines, default: 150, 255 + * = 100% opaque, 0 = 100% transparent + * + * @param alpha + */ + public void setWebAlpha(int alpha) { + mWebAlpha = alpha; + } + + /** + * Returns the alpha value for all web lines. + * + * @return + */ + public int getWebAlpha() { + return mWebAlpha; + } + + /** + * Sets the color for the web lines that come from the center. Don't forget + * to use getResources().getColor(...) when loading a color from the + * resources. Default: Color.rgb(122, 122, 122) + * + * @param color + */ + public void setWebColor(int color) { + mWebColor = color; + } + + public int getWebColor() { + return mWebColor; + } + + /** + * Sets the color for the web lines in between the lines that come from the + * center. Don't forget to use getResources().getColor(...) when loading a + * color from the resources. Default: Color.rgb(122, 122, 122) + * + * @param color + */ + public void setWebColorInner(int color) { + mWebColorInner = color; + } + + public int getWebColorInner() { + return mWebColorInner; + } + + /** + * If set to true, drawing the web is enabled, if set to false, drawing the + * whole web is disabled. Default: true + * + * @param enabled + */ + public void setDrawWeb(boolean enabled) { + mDrawWeb = enabled; + } + + /** + * Sets the number of web-lines that should be skipped on chart web before the + * next one is drawn. This targets the lines that come from the center of the RadarChart. + * + * @param count if count = 1 -> 1 line is skipped in between + */ + public void setSkipWebLineCount(int count) { + + mSkipWebLineCount = Math.max(0, count); + } + + /** + * Returns the modulus that is used for skipping web-lines. + * + * @return + */ + public int getSkipWebLineCount() { + return mSkipWebLineCount; + } + + @Override + protected float getRequiredLegendOffset() { + return mLegendRenderer.getLabelPaint().getTextSize() * 4.f; + } + + @Override + protected float getRequiredBaseOffset() { + return mXAxis.isEnabled() && mXAxis.isDrawLabelsEnabled() ? + mXAxis.mLabelRotatedWidth : + Utils.convertDpToPixel(10f); + } + + @Override + public float getRadius() { + RectF content = mViewPortHandler.getContentRect(); + return Math.min(content.width() / 2f, content.height() / 2f); + } + + /** + * Returns the maximum value this chart can display on it's y-axis. + */ + public float getYChartMax() { + return mYAxis.mAxisMaximum; + } + + /** + * Returns the minimum value this chart can display on it's y-axis. + */ + public float getYChartMin() { + return mYAxis.mAxisMinimum; + } + + /** + * Returns the range of y-values this chart can display. + * + * @return + */ + public float getYRange() { + return mYAxis.mAxisRange; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/charts/ScatterChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/ScatterChart.java new file mode 100644 index 0000000..37e8395 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/charts/ScatterChart.java @@ -0,0 +1,77 @@ + +package com.github.mikephil.charting.charts; + +import android.content.Context; +import android.util.AttributeSet; + +import com.github.mikephil.charting.data.ScatterData; +import com.github.mikephil.charting.interfaces.dataprovider.ScatterDataProvider; +import com.github.mikephil.charting.renderer.ScatterChartRenderer; + +/** + * The ScatterChart. Draws dots, triangles, squares and custom shapes into the + * Chart-View. CIRCLE and SCQUARE offer the best performance, TRIANGLE has the + * worst performance. + * + * @author Philipp Jahoda + */ +public class ScatterChart extends BarLineChartBase implements ScatterDataProvider { + + public ScatterChart(Context context) { + super(context); + } + + public ScatterChart(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ScatterChart(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + + @Override + protected void init() { + super.init(); + + mRenderer = new ScatterChartRenderer(this, mAnimator, mViewPortHandler); + + getXAxis().setSpaceMin(0.5f); + getXAxis().setSpaceMax(0.5f); + } + + @Override + public ScatterData getScatterData() { + return mData; + } + + /** + * Predefined ScatterShapes that allow the specification of a shape a ScatterDataSet should be drawn with. + * If a ScatterShape is specified for a ScatterDataSet, the required renderer is set. + */ + public enum ScatterShape { + + SQUARE("SQUARE"), + CIRCLE("CIRCLE"), + TRIANGLE("TRIANGLE"), + CROSS("CROSS"), + X("X"), + CHEVRON_UP("CHEVRON_UP"), + CHEVRON_DOWN("CHEVRON_DOWN"); + + private final String shapeIdentifier; + + ScatterShape(final String shapeIdentifier) { + this.shapeIdentifier = shapeIdentifier; + } + + @Override + public String toString() { + return shapeIdentifier; + } + + public static ScatterShape[] getAllDefaultShapes() { + return new ScatterShape[]{SQUARE, CIRCLE, TRIANGLE, CROSS, X, CHEVRON_UP, CHEVRON_DOWN}; + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/components/AxisBase.java b/MPChartLib/src/main/java/com/github/mikephil/charting/components/AxisBase.java new file mode 100644 index 0000000..c90b4fc --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/components/AxisBase.java @@ -0,0 +1,816 @@ + +package com.github.mikephil.charting.components; + +import android.graphics.Color; +import android.graphics.DashPathEffect; +import android.util.Log; + +import com.github.mikephil.charting.formatter.DefaultAxisValueFormatter; +import com.github.mikephil.charting.formatter.IAxisValueFormatter; +import com.github.mikephil.charting.utils.Utils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Base-class of all axes (previously called labels). + * + * @author Philipp Jahoda + */ +public abstract class AxisBase extends ComponentBase { + + /** + * custom formatter that is used instead of the auto-formatter if set + */ + protected IAxisValueFormatter mAxisValueFormatter; + + private int mGridColor = Color.GRAY; + + private float mGridLineWidth = 1f; + + private int mAxisLineColor = Color.GRAY; + + private float mAxisLineWidth = 1f; + + /** + * the actual array of entries + */ + public float[] mEntries = new float[]{}; + + /** + * axis label entries only used for centered labels + */ + public float[] mCenteredEntries = new float[]{}; + + /** + * the number of entries the legend contains + */ + public int mEntryCount; + + /** + * the number of decimal digits to use + */ + public int mDecimals; + + /** + * the number of label entries the axis should have, default 6 + */ + private int mLabelCount = 6; + + /** + * the minimum interval between axis values + */ + protected float mGranularity = 1.0f; + + /** + * When true, axis labels are controlled by the `granularity` property. + * When false, axis values could possibly be repeated. + * This could happen if two adjacent axis values are rounded to same value. + * If using granularity this could be avoided by having fewer axis values visible. + */ + protected boolean mGranularityEnabled = false; + + /** + * if true, the set number of y-labels will be forced + */ + protected boolean mForceLabels = false; + + /** + * flag indicating if the grid lines for this axis should be drawn + */ + protected boolean mDrawGridLines = true; + + /** + * flag that indicates if the line alongside the axis is drawn or not + */ + protected boolean mDrawAxisLine = true; + + /** + * flag that indicates of the labels of this axis should be drawn or not + */ + protected boolean mDrawLabels = true; + + protected boolean mCenterAxisLabels = false; + + /** + * the path effect of the axis line that makes dashed lines possible + */ + private DashPathEffect mAxisLineDashPathEffect = null; + + /** + * the path effect of the grid lines that makes dashed lines possible + */ + private DashPathEffect mGridDashPathEffect = null; + + /** + * array of limit lines that can be set for the axis + */ + protected List mLimitLines; + + /** + * flag indicating the limit lines layer depth + */ + protected boolean mDrawLimitLineBehindData = false; + + /** + * flag indicating the grid lines layer depth + */ + protected boolean mDrawGridLinesBehindData = true; + + /** + * Extra spacing for `axisMinimum` to be added to automatically calculated `axisMinimum` + */ + protected float mSpaceMin = 0.f; + + /** + * Extra spacing for `axisMaximum` to be added to automatically calculated `axisMaximum` + */ + protected float mSpaceMax = 0.f; + + /** + * flag indicating that the axis-min value has been customized + */ + protected boolean mCustomAxisMin = false; + + /** + * flag indicating that the axis-max value has been customized + */ + protected boolean mCustomAxisMax = false; + + /** + * don't touch this direclty, use setter + */ + public float mAxisMaximum = 0f; + + /** + * don't touch this directly, use setter + */ + public float mAxisMinimum = 0f; + + /** + * the total range of values this axis covers + */ + public float mAxisRange = 0f; + + private int mAxisMinLabels = 2; + private int mAxisMaxLabels = 25; + + /** + * The minumum number of labels on the axis + */ + public int getAxisMinLabels() { + return mAxisMinLabels; + } + + /** + * The minumum number of labels on the axis + */ + public void setAxisMinLabels(int labels) { + if (labels > 0) + mAxisMinLabels = labels; + } + + /** + * The maximum number of labels on the axis + */ + public int getAxisMaxLabels() { + return mAxisMaxLabels; + } + + /** + * The maximum number of labels on the axis + */ + public void setAxisMaxLabels(int labels) { + if (labels > 0) + mAxisMaxLabels = labels; + } + + /** + * default constructor + */ + public AxisBase() { + this.mTextSize = Utils.convertDpToPixel(10f); + this.mXOffset = Utils.convertDpToPixel(5f); + this.mYOffset = Utils.convertDpToPixel(5f); + this.mLimitLines = new ArrayList(); + } + + /** + * Set this to true to enable drawing the grid lines for this axis. + * + * @param enabled + */ + public void setDrawGridLines(boolean enabled) { + mDrawGridLines = enabled; + } + + /** + * Returns true if drawing grid lines is enabled for this axis. + * + * @return + */ + public boolean isDrawGridLinesEnabled() { + return mDrawGridLines; + } + + /** + * Set this to true if the line alongside the axis should be drawn or not. + * + * @param enabled + */ + public void setDrawAxisLine(boolean enabled) { + mDrawAxisLine = enabled; + } + + /** + * Returns true if the line alongside the axis should be drawn. + * + * @return + */ + public boolean isDrawAxisLineEnabled() { + return mDrawAxisLine; + } + + /** + * Centers the axis labels instead of drawing them at their original position. + * This is useful especially for grouped BarChart. + * + * @param enabled + */ + public void setCenterAxisLabels(boolean enabled) { + mCenterAxisLabels = enabled; + } + + public boolean isCenterAxisLabelsEnabled() { + return mCenterAxisLabels && mEntryCount > 0; + } + + /** + * Sets the color of the grid lines for this axis (the horizontal lines + * coming from each label). + * + * @param color + */ + public void setGridColor(int color) { + mGridColor = color; + } + + /** + * Returns the color of the grid lines for this axis (the horizontal lines + * coming from each label). + * + * @return + */ + public int getGridColor() { + return mGridColor; + } + + /** + * Sets the width of the border surrounding the chart in dp. + * + * @param width + */ + public void setAxisLineWidth(float width) { + mAxisLineWidth = Utils.convertDpToPixel(width); + } + + /** + * Returns the width of the axis line (line alongside the axis). + * + * @return + */ + public float getAxisLineWidth() { + return mAxisLineWidth; + } + + /** + * Sets the width of the grid lines that are drawn away from each axis + * label. + * + * @param width + */ + public void setGridLineWidth(float width) { + mGridLineWidth = Utils.convertDpToPixel(width); + } + + /** + * Returns the width of the grid lines that are drawn away from each axis + * label. + * + * @return + */ + public float getGridLineWidth() { + return mGridLineWidth; + } + + /** + * Sets the color of the border surrounding the chart. + * + * @param color + */ + public void setAxisLineColor(int color) { + mAxisLineColor = color; + } + + /** + * Returns the color of the axis line (line alongside the axis). + * + * @return + */ + public int getAxisLineColor() { + return mAxisLineColor; + } + + /** + * Set this to true to enable drawing the labels of this axis (this will not + * affect drawing the grid lines or axis lines). + * + * @param enabled + */ + public void setDrawLabels(boolean enabled) { + mDrawLabels = enabled; + } + + /** + * Returns true if drawing the labels is enabled for this axis. + * + * @return + */ + public boolean isDrawLabelsEnabled() { + return mDrawLabels; + } + + /** + * Sets the number of label entries for the y-axis max = 25, min = 2, default: 6, be aware + * that this number is not fixed. + * + * @param count the number of y-axis labels that should be displayed + */ + public void setLabelCount(int count) { + + if (count > getAxisMaxLabels()) + count = getAxisMaxLabels(); + if (count < getAxisMinLabels()) + count = getAxisMinLabels(); + + mLabelCount = count; + mForceLabels = false; + } + + /** + * sets the number of label entries for the y-axis max = 25, min = 2, default: 6, be aware + * that this number is not + * fixed (if force == false) and can only be approximated. + * + * @param count the number of y-axis labels that should be displayed + * @param force if enabled, the set label count will be forced, meaning that the exact + * specified count of labels will + * be drawn and evenly distributed alongside the axis - this might cause labels + * to have uneven values + */ + public void setLabelCount(int count, boolean force) { + + setLabelCount(count); + mForceLabels = force; + } + + /** + * Returns true if focing the y-label count is enabled. Default: false + * + * @return + */ + public boolean isForceLabelsEnabled() { + return mForceLabels; + } + + /** + * Returns the number of label entries the y-axis should have + * + * @return + */ + public int getLabelCount() { + return mLabelCount; + } + + /** + * @return true if granularity is enabled + */ + public boolean isGranularityEnabled() { + return mGranularityEnabled; + } + + /** + * Enabled/disable granularity control on axis value intervals. If enabled, the axis + * interval is not allowed to go below a certain granularity. Default: false + * + * @param enabled + */ + public void setGranularityEnabled(boolean enabled) { + mGranularityEnabled = enabled; + } + + /** + * @return the minimum interval between axis values + */ + public float getGranularity() { + return mGranularity; + } + + /** + * Set a minimum interval for the axis when zooming in. The axis is not allowed to go below + * that limit. This can be used to avoid label duplicating when zooming in. + * + * @param granularity + */ + public void setGranularity(float granularity) { + mGranularity = granularity; + // set this to true if it was disabled, as it makes no sense to call this method with granularity disabled + mGranularityEnabled = true; + } + + /** + * Adds a new LimitLine to this axis. + * + * @param l + */ + public void addLimitLine(LimitLine l) { + mLimitLines.add(l); + + if (mLimitLines.size() > 6) { + Log.e("MPAndroiChart", + "Warning! You have more than 6 LimitLines on your axis, do you really want " + + "that?"); + } + } + + /** + * Removes the specified LimitLine from the axis. + * + * @param l + */ + public void removeLimitLine(LimitLine l) { + mLimitLines.remove(l); + } + + /** + * Removes all LimitLines from the axis. + */ + public void removeAllLimitLines() { + mLimitLines.clear(); + } + + /** + * Returns the LimitLines of this axis. + * + * @return + */ + public List getLimitLines() { + return mLimitLines; + } + + /** + * If this is set to true, the LimitLines are drawn behind the actual data, + * otherwise on top. Default: false + * + * @param enabled + */ + public void setDrawLimitLinesBehindData(boolean enabled) { + mDrawLimitLineBehindData = enabled; + } + + public boolean isDrawLimitLinesBehindDataEnabled() { + return mDrawLimitLineBehindData; + } + + /** + * If this is set to false, the grid lines are draw on top of the actual data, + * otherwise behind. Default: true + * + * @param enabled + */ + public void setDrawGridLinesBehindData(boolean enabled) { mDrawGridLinesBehindData = enabled; } + + public boolean isDrawGridLinesBehindDataEnabled() { + return mDrawGridLinesBehindData; + } + + /** + * Returns the longest formatted label (in terms of characters), this axis + * contains. + * + * @return + */ + public String getLongestLabel() { + + String longest = ""; + + for (int i = 0; i < mEntries.length; i++) { + String text = getFormattedLabel(i); + + if (text != null && longest.length() < text.length()) + longest = text; + } + + return longest; + } + + public String getFormattedLabel(int index) { + + if (index < 0 || index >= mEntries.length) + return ""; + else + return getValueFormatter().getFormattedValue(mEntries[index], this); + } + + /** + * Sets the formatter to be used for formatting the axis labels. If no formatter is set, the + * chart will + * automatically determine a reasonable formatting (concerning decimals) for all the values + * that are drawn inside + * the chart. Use chart.getDefaultValueFormatter() to use the formatter calculated by the chart. + * + * @param f + */ + public void setValueFormatter(IAxisValueFormatter f) { + + if (f == null) + mAxisValueFormatter = new DefaultAxisValueFormatter(mDecimals); + else + mAxisValueFormatter = f; + } + + /** + * Returns the formatter used for formatting the axis labels. + * + * @return + */ + public IAxisValueFormatter getValueFormatter() { + + if (mAxisValueFormatter == null || + (mAxisValueFormatter instanceof DefaultAxisValueFormatter && + ((DefaultAxisValueFormatter)mAxisValueFormatter).getDecimalDigits() != mDecimals)) + mAxisValueFormatter = new DefaultAxisValueFormatter(mDecimals); + + return mAxisValueFormatter; + } + + /** + * Enables the grid line to be drawn in dashed mode, e.g. like this + * "- - - - - -". THIS ONLY WORKS IF HARDWARE-ACCELERATION IS TURNED OFF. + * Keep in mind that hardware acceleration boosts performance. + * + * @param lineLength the length of the line pieces + * @param spaceLength the length of space in between the pieces + * @param phase offset, in degrees (normally, use 0) + */ + public void enableGridDashedLine(float lineLength, float spaceLength, float phase) { + mGridDashPathEffect = new DashPathEffect(new float[]{ + lineLength, spaceLength + }, phase); + } + + /** + * Enables the grid line to be drawn in dashed mode, e.g. like this + * "- - - - - -". THIS ONLY WORKS IF HARDWARE-ACCELERATION IS TURNED OFF. + * Keep in mind that hardware acceleration boosts performance. + * + * @param effect the DashPathEffect + */ + public void setGridDashedLine(DashPathEffect effect) { + mGridDashPathEffect = effect; + } + + /** + * Disables the grid line to be drawn in dashed mode. + */ + public void disableGridDashedLine() { + mGridDashPathEffect = null; + } + + /** + * Returns true if the grid dashed-line effect is enabled, false if not. + * + * @return + */ + public boolean isGridDashedLineEnabled() { + return mGridDashPathEffect == null ? false : true; + } + + /** + * returns the DashPathEffect that is set for grid line + * + * @return + */ + public DashPathEffect getGridDashPathEffect() { + return mGridDashPathEffect; + } + + + /** + * Enables the axis line to be drawn in dashed mode, e.g. like this + * "- - - - - -". THIS ONLY WORKS IF HARDWARE-ACCELERATION IS TURNED OFF. + * Keep in mind that hardware acceleration boosts performance. + * + * @param lineLength the length of the line pieces + * @param spaceLength the length of space in between the pieces + * @param phase offset, in degrees (normally, use 0) + */ + public void enableAxisLineDashedLine(float lineLength, float spaceLength, float phase) { + mAxisLineDashPathEffect = new DashPathEffect(new float[]{ + lineLength, spaceLength + }, phase); + } + + /** + * Enables the axis line to be drawn in dashed mode, e.g. like this + * "- - - - - -". THIS ONLY WORKS IF HARDWARE-ACCELERATION IS TURNED OFF. + * Keep in mind that hardware acceleration boosts performance. + * + * @param effect the DashPathEffect + */ + public void setAxisLineDashedLine(DashPathEffect effect) { + mAxisLineDashPathEffect = effect; + } + + /** + * Disables the axis line to be drawn in dashed mode. + */ + public void disableAxisLineDashedLine() { + mAxisLineDashPathEffect = null; + } + + /** + * Returns true if the axis dashed-line effect is enabled, false if not. + * + * @return + */ + public boolean isAxisLineDashedLineEnabled() { + return mAxisLineDashPathEffect == null ? false : true; + } + + /** + * returns the DashPathEffect that is set for axis line + * + * @return + */ + public DashPathEffect getAxisLineDashPathEffect() { + return mAxisLineDashPathEffect; + } + + /** + * ###### BELOW CODE RELATED TO CUSTOM AXIS VALUES ###### + */ + + public float getAxisMaximum() { + return mAxisMaximum; + } + + public float getAxisMinimum() { + return mAxisMinimum; + } + + /** + * By calling this method, any custom maximum value that has been previously set is reseted, + * and the calculation is + * done automatically. + */ + public void resetAxisMaximum() { + mCustomAxisMax = false; + } + + /** + * Returns true if the axis max value has been customized (and is not calculated automatically) + * + * @return + */ + public boolean isAxisMaxCustom() { + return mCustomAxisMax; + } + + /** + * By calling this method, any custom minimum value that has been previously set is reseted, + * and the calculation is + * done automatically. + */ + public void resetAxisMinimum() { + mCustomAxisMin = false; + } + + /** + * Returns true if the axis min value has been customized (and is not calculated automatically) + * + * @return + */ + public boolean isAxisMinCustom() { + return mCustomAxisMin; + } + + /** + * Set a custom minimum value for this axis. If set, this value will not be calculated + * automatically depending on + * the provided data. Use resetAxisMinValue() to undo this. Do not forget to call + * setStartAtZero(false) if you use + * this method. Otherwise, the axis-minimum value will still be forced to 0. + * + * @param min + */ + public void setAxisMinimum(float min) { + mCustomAxisMin = true; + mAxisMinimum = min; + this.mAxisRange = Math.abs(mAxisMaximum - min); + } + + /** + * Use setAxisMinimum(...) instead. + * + * @param min + */ + @Deprecated + public void setAxisMinValue(float min) { + setAxisMinimum(min); + } + + /** + * Set a custom maximum value for this axis. If set, this value will not be calculated + * automatically depending on + * the provided data. Use resetAxisMaxValue() to undo this. + * + * @param max + */ + public void setAxisMaximum(float max) { + mCustomAxisMax = true; + mAxisMaximum = max; + this.mAxisRange = Math.abs(max - mAxisMinimum); + } + + /** + * Use setAxisMaximum(...) instead. + * + * @param max + */ + @Deprecated + public void setAxisMaxValue(float max) { + setAxisMaximum(max); + } + + /** + * Calculates the minimum / maximum and range values of the axis with the given + * minimum and maximum values from the chart data. + * + * @param dataMin the min value according to chart data + * @param dataMax the max value according to chart data + */ + public void calculate(float dataMin, float dataMax) { + + // if custom, use value as is, else use data value + float min = mCustomAxisMin ? mAxisMinimum : (dataMin - mSpaceMin); + float max = mCustomAxisMax ? mAxisMaximum : (dataMax + mSpaceMax); + + // temporary range (before calculations) + float range = Math.abs(max - min); + + // in case all values are equal + if (range == 0f) { + max = max + 1f; + min = min - 1f; + } + + this.mAxisMinimum = min; + this.mAxisMaximum = max; + + // actual range + this.mAxisRange = Math.abs(max - min); + } + + /** + * Gets extra spacing for `axisMinimum` to be added to automatically calculated `axisMinimum` + */ + public float getSpaceMin() + { + return mSpaceMin; + } + + /** + * Sets extra spacing for `axisMinimum` to be added to automatically calculated `axisMinimum` + */ + public void setSpaceMin(float mSpaceMin) + { + this.mSpaceMin = mSpaceMin; + } + + /** + * Gets extra spacing for `axisMaximum` to be added to automatically calculated `axisMaximum` + */ + public float getSpaceMax() + { + return mSpaceMax; + } + + /** + * Sets extra spacing for `axisMaximum` to be added to automatically calculated `axisMaximum` + */ + public void setSpaceMax(float mSpaceMax) + { + this.mSpaceMax = mSpaceMax; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/components/ComponentBase.java b/MPChartLib/src/main/java/com/github/mikephil/charting/components/ComponentBase.java new file mode 100644 index 0000000..d3a1d4d --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/components/ComponentBase.java @@ -0,0 +1,173 @@ + +package com.github.mikephil.charting.components; + +import android.graphics.Color; +import android.graphics.Typeface; + +import com.github.mikephil.charting.utils.Utils; + +/** + * This class encapsulates everything both Axis, Legend and LimitLines have in common. + * + * @author Philipp Jahoda + */ +public abstract class ComponentBase { + + /** + * flag that indicates if this axis / legend is enabled or not + */ + protected boolean mEnabled = true; + + /** + * the offset in pixels this component has on the x-axis + */ + protected float mXOffset = 5f; + + /** + * the offset in pixels this component has on the Y-axis + */ + protected float mYOffset = 5f; + + /** + * the typeface used for the labels + */ + protected Typeface mTypeface = null; + + /** + * the text size of the labels + */ + protected float mTextSize = Utils.convertDpToPixel(10f); + + /** + * the text color to use for the labels + */ + protected int mTextColor = Color.BLACK; + + + public ComponentBase() { + + } + + /** + * Returns the used offset on the x-axis for drawing the axis or legend + * labels. This offset is applied before and after the label. + * + * @return + */ + public float getXOffset() { + return mXOffset; + } + + /** + * Sets the used x-axis offset for the labels on this axis. + * + * @param xOffset + */ + public void setXOffset(float xOffset) { + mXOffset = Utils.convertDpToPixel(xOffset); + } + + /** + * Returns the used offset on the x-axis for drawing the axis labels. This + * offset is applied before and after the label. + * + * @return + */ + public float getYOffset() { + return mYOffset; + } + + /** + * Sets the used y-axis offset for the labels on this axis. For the legend, + * higher offset means the legend as a whole will be placed further away + * from the top. + * + * @param yOffset + */ + public void setYOffset(float yOffset) { + mYOffset = Utils.convertDpToPixel(yOffset); + } + + /** + * returns the Typeface used for the labels, returns null if none is set + * + * @return + */ + public Typeface getTypeface() { + return mTypeface; + } + + /** + * sets a specific Typeface for the labels + * + * @param tf + */ + public void setTypeface(Typeface tf) { + mTypeface = tf; + } + + /** + * sets the size of the label text in density pixels min = 6f, max = 24f, default + * 10f + * + * @param size the text size, in DP + */ + public void setTextSize(float size) { + + if (size > 24f) + size = 24f; + if (size < 6f) + size = 6f; + + mTextSize = Utils.convertDpToPixel(size); + } + + /** + * returns the text size that is currently set for the labels, in pixels + * + * @return + */ + public float getTextSize() { + return mTextSize; + } + + + /** + * Sets the text color to use for the labels. Make sure to use + * getResources().getColor(...) when using a color from the resources. + * + * @param color + */ + public void setTextColor(int color) { + mTextColor = color; + } + + /** + * Returns the text color that is set for the labels. + * + * @return + */ + public int getTextColor() { + return mTextColor; + } + + /** + * Set this to true if this component should be enabled (should be drawn), + * false if not. If disabled, nothing of this component will be drawn. + * Default: true + * + * @param enabled + */ + public void setEnabled(boolean enabled) { + mEnabled = enabled; + } + + /** + * Returns true if this comonent is enabled (should be drawn), false if not. + * + * @return + */ + public boolean isEnabled() { + return mEnabled; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/components/Description.java b/MPChartLib/src/main/java/com/github/mikephil/charting/components/Description.java new file mode 100644 index 0000000..18294a3 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/components/Description.java @@ -0,0 +1,95 @@ +package com.github.mikephil.charting.components; + +import android.graphics.Paint; + +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; + +/** + * Created by Philipp Jahoda on 17/09/16. + */ +public class Description extends ComponentBase { + + /** + * the text used in the description + */ + private String text = "Description Label"; + + /** + * the custom position of the description text + */ + private MPPointF mPosition; + + /** + * the alignment of the description text + */ + private Paint.Align mTextAlign = Paint.Align.RIGHT; + + public Description() { + super(); + + // default size + mTextSize = Utils.convertDpToPixel(8f); + } + + /** + * Sets the text to be shown as the description. + * Never set this to null as this will cause nullpointer exception when drawing with Android Canvas. + * + * @param text + */ + public void setText(String text) { + this.text = text; + } + + /** + * Returns the description text. + * + * @return + */ + public String getText() { + return text; + } + + /** + * Sets a custom position for the description text in pixels on the screen. + * + * @param x - xcoordinate + * @param y - ycoordinate + */ + public void setPosition(float x, float y) { + if (mPosition == null) { + mPosition = MPPointF.getInstance(x, y); + } else { + mPosition.x = x; + mPosition.y = y; + } + } + + /** + * Returns the customized position of the description, or null if none set. + * + * @return + */ + public MPPointF getPosition() { + return mPosition; + } + + /** + * Sets the text alignment of the description text. Default RIGHT. + * + * @param align + */ + public void setTextAlign(Paint.Align align) { + this.mTextAlign = align; + } + + /** + * Returns the text alignment of the description. + * + * @return + */ + public Paint.Align getTextAlign() { + return mTextAlign; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/components/IMarker.java b/MPChartLib/src/main/java/com/github/mikephil/charting/components/IMarker.java new file mode 100644 index 0000000..3b8ca43 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/components/IMarker.java @@ -0,0 +1,47 @@ +package com.github.mikephil.charting.components; + +import android.graphics.Canvas; + +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.utils.MPPointF; + +public interface IMarker { + + /** + * @return The desired (general) offset you wish the IMarker to have on the x- and y-axis. + * By returning x: -(width / 2) you will center the IMarker horizontally. + * By returning y: -(height / 2) you will center the IMarker vertically. + */ + MPPointF getOffset(); + + /** + * @return The offset for drawing at the specific `point`. This allows conditional adjusting of the Marker position. + * If you have no adjustments to make, return getOffset(). + * + * @param posX This is the X position at which the marker wants to be drawn. + * You can adjust the offset conditionally based on this argument. + * @param posY This is the X position at which the marker wants to be drawn. + * You can adjust the offset conditionally based on this argument. + */ + MPPointF getOffsetForDrawingAtPoint(float posX, float posY); + + /** + * This method enables a specified custom IMarker to update it's content every time the IMarker is redrawn. + * + * @param e The Entry the IMarker belongs to. This can also be any subclass of Entry, like BarEntry or + * CandleEntry, simply cast it at runtime. + * @param highlight The highlight object contains information about the highlighted value such as it's dataset-index, the + * selected range or stack-index (only stacked bar entries). + */ + void refreshContent(Entry e, Highlight highlight); + + /** + * Draws the IMarker on the given position on the screen with the given Canvas object. + * + * @param canvas + * @param posX + * @param posY + */ + void draw(Canvas canvas, float posX, float posY); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/components/Legend.java b/MPChartLib/src/main/java/com/github/mikephil/charting/components/Legend.java new file mode 100644 index 0000000..7081292 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/components/Legend.java @@ -0,0 +1,825 @@ +package com.github.mikephil.charting.components; + +import android.graphics.DashPathEffect; +import android.graphics.Paint; + +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.FSize; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class representing the legend of the chart. The legend will contain one entry + * per color and DataSet. Multiple colors in one DataSet are grouped together. + * The legend object is NOT available before setting data to the chart. + * + * @author Philipp Jahoda + */ +public class Legend extends ComponentBase { + + public enum LegendForm { + /** + * Avoid drawing a form + */ + NONE, + + /** + * Do not draw the a form, but leave space for it + */ + EMPTY, + + /** + * Use default (default dataset's form to the legend's form) + */ + DEFAULT, + + /** + * Draw a square + */ + SQUARE, + + /** + * Draw a circle + */ + CIRCLE, + + /** + * Draw a horizontal line + */ + LINE + } + + public enum LegendHorizontalAlignment { + LEFT, CENTER, RIGHT + } + + public enum LegendVerticalAlignment { + TOP, CENTER, BOTTOM + } + + public enum LegendOrientation { + HORIZONTAL, VERTICAL + } + + public enum LegendDirection { + LEFT_TO_RIGHT, RIGHT_TO_LEFT + } + + /** + * The legend entries array + */ + private LegendEntry[] mEntries = new LegendEntry[]{}; + + /** + * Entries that will be appended to the end of the auto calculated entries after calculating the legend. + * (if the legend has already been calculated, you will need to call notifyDataSetChanged() to let the changes take effect) + */ + private LegendEntry[] mExtraEntries; + + /** + * Are the legend labels/colors a custom value or auto calculated? If false, + * then it's auto, if true, then custom. default false (automatic legend) + */ + private boolean mIsLegendCustom = false; + + private LegendHorizontalAlignment mHorizontalAlignment = LegendHorizontalAlignment.LEFT; + private LegendVerticalAlignment mVerticalAlignment = LegendVerticalAlignment.BOTTOM; + private LegendOrientation mOrientation = LegendOrientation.HORIZONTAL; + private boolean mDrawInside = false; + + /** + * the text direction for the legend + */ + private LegendDirection mDirection = LegendDirection.LEFT_TO_RIGHT; + + /** + * the shape/form the legend colors are drawn in + */ + private LegendForm mShape = LegendForm.SQUARE; + + /** + * the size of the legend forms/shapes + */ + private float mFormSize = 8f; + + /** + * the size of the legend forms/shapes + */ + private float mFormLineWidth = 3f; + + /** + * Line dash path effect used for shapes that consist of lines. + */ + private DashPathEffect mFormLineDashEffect = null; + + /** + * the space between the legend entries on a horizontal axis, default 6f + */ + private float mXEntrySpace = 6f; + + /** + * the space between the legend entries on a vertical axis, default 5f + */ + private float mYEntrySpace = 0f; + + /** + * the space between the legend entries on a vertical axis, default 2f + * private float mYEntrySpace = 2f; /** the space between the form and the + * actual label/text + */ + private float mFormToTextSpace = 5f; + + /** + * the space that should be left between stacked forms + */ + private float mStackSpace = 3f; + + /** + * the maximum relative size out of the whole chart view in percent + */ + private float mMaxSizePercent = 0.95f; + + /** + * default constructor + */ + public Legend() { + + this.mTextSize = Utils.convertDpToPixel(10f); + this.mXOffset = Utils.convertDpToPixel(5f); + this.mYOffset = Utils.convertDpToPixel(3f); // 2 + } + + /** + * Constructor. Provide entries for the legend. + * + * @param entries + */ + public Legend(LegendEntry[] entries) { + this(); + + if (entries == null) { + throw new IllegalArgumentException("entries array is NULL"); + } + + this.mEntries = entries; + } + + /** + * This method sets the automatically computed colors for the legend. Use setCustom(...) to set custom colors. + * + * @param entries + */ + public void setEntries(List entries) { + mEntries = entries.toArray(new LegendEntry[entries.size()]); + } + + public LegendEntry[] getEntries() { + return mEntries; + } + + /** + * returns the maximum length in pixels across all legend labels + formsize + * + formtotextspace + * + * @param p the paint object used for rendering the text + * @return + */ + public float getMaximumEntryWidth(Paint p) { + + float max = 0f; + float maxFormSize = 0f; + float formToTextSpace = Utils.convertDpToPixel(mFormToTextSpace); + + for (LegendEntry entry : mEntries) { + final float formSize = Utils.convertDpToPixel( + Float.isNaN(entry.formSize) + ? mFormSize : entry.formSize); + if (formSize > maxFormSize) + maxFormSize = formSize; + + String label = entry.label; + if (label == null) continue; + + float length = (float) Utils.calcTextWidth(p, label); + + if (length > max) + max = length; + } + + return max + maxFormSize + formToTextSpace; + } + + /** + * returns the maximum height in pixels across all legend labels + * + * @param p the paint object used for rendering the text + * @return + */ + public float getMaximumEntryHeight(Paint p) { + + float max = 0f; + + for (LegendEntry entry : mEntries) { + String label = entry.label; + if (label == null) continue; + + float length = (float) Utils.calcTextHeight(p, label); + + if (length > max) + max = length; + } + + return max; + } + + public LegendEntry[] getExtraEntries() { + + return mExtraEntries; + } + + public void setExtra(List entries) { + mExtraEntries = entries.toArray(new LegendEntry[entries.size()]); + } + + public void setExtra(LegendEntry[] entries) { + if (entries == null) + entries = new LegendEntry[]{}; + mExtraEntries = entries; + } + + /** + * Entries that will be appended to the end of the auto calculated + * entries after calculating the legend. + * (if the legend has already been calculated, you will need to call notifyDataSetChanged() + * to let the changes take effect) + */ + public void setExtra(int[] colors, String[] labels) { + + List entries = new ArrayList<>(); + + for (int i = 0; i < Math.min(colors.length, labels.length); i++) { + final LegendEntry entry = new LegendEntry(); + entry.formColor = colors[i]; + entry.label = labels[i]; + + if (entry.formColor == ColorTemplate.COLOR_SKIP || + entry.formColor == 0) + entry.form = LegendForm.NONE; + else if (entry.formColor == ColorTemplate.COLOR_NONE) + entry.form = LegendForm.EMPTY; + + entries.add(entry); + } + + mExtraEntries = entries.toArray(new LegendEntry[entries.size()]); + } + + /** + * Sets a custom legend's entries array. + * * A null label will start a group. + * This will disable the feature that automatically calculates the legend + * entries from the datasets. + * Call resetCustom() to re-enable automatic calculation (and then + * notifyDataSetChanged() is needed to auto-calculate the legend again) + */ + public void setCustom(LegendEntry[] entries) { + + mEntries = entries; + mIsLegendCustom = true; + } + + /** + * Sets a custom legend's entries array. + * * A null label will start a group. + * This will disable the feature that automatically calculates the legend + * entries from the datasets. + * Call resetCustom() to re-enable automatic calculation (and then + * notifyDataSetChanged() is needed to auto-calculate the legend again) + */ + public void setCustom(List entries) { + + mEntries = entries.toArray(new LegendEntry[entries.size()]); + mIsLegendCustom = true; + } + + /** + * Calling this will disable the custom legend entries (set by + * setCustom(...)). Instead, the entries will again be calculated + * automatically (after notifyDataSetChanged() is called). + */ + public void resetCustom() { + mIsLegendCustom = false; + } + + /** + * @return true if a custom legend entries has been set default + * false (automatic legend) + */ + public boolean isLegendCustom() { + return mIsLegendCustom; + } + + /** + * returns the horizontal alignment of the legend + * + * @return + */ + public LegendHorizontalAlignment getHorizontalAlignment() { + return mHorizontalAlignment; + } + + /** + * sets the horizontal alignment of the legend + * + * @param value + */ + public void setHorizontalAlignment(LegendHorizontalAlignment value) { + mHorizontalAlignment = value; + } + + /** + * returns the vertical alignment of the legend + * + * @return + */ + public LegendVerticalAlignment getVerticalAlignment() { + return mVerticalAlignment; + } + + /** + * sets the vertical alignment of the legend + * + * @param value + */ + public void setVerticalAlignment(LegendVerticalAlignment value) { + mVerticalAlignment = value; + } + + /** + * returns the orientation of the legend + * + * @return + */ + public LegendOrientation getOrientation() { + return mOrientation; + } + + /** + * sets the orientation of the legend + * + * @param value + */ + public void setOrientation(LegendOrientation value) { + mOrientation = value; + } + + /** + * returns whether the legend will draw inside the chart or outside + * + * @return + */ + public boolean isDrawInsideEnabled() { + return mDrawInside; + } + + /** + * sets whether the legend will draw inside the chart or outside + * + * @param value + */ + public void setDrawInside(boolean value) { + mDrawInside = value; + } + + /** + * returns the text direction of the legend + * + * @return + */ + public LegendDirection getDirection() { + return mDirection; + } + + /** + * sets the text direction of the legend + * + * @param pos + */ + public void setDirection(LegendDirection pos) { + mDirection = pos; + } + + /** + * returns the current form/shape that is set for the legend + * + * @return + */ + public LegendForm getForm() { + return mShape; + } + + /** + * sets the form/shape of the legend forms + * + * @param shape + */ + public void setForm(LegendForm shape) { + mShape = shape; + } + + /** + * sets the size in dp of the legend forms, default 8f + * + * @param size + */ + public void setFormSize(float size) { + mFormSize = size; + } + + /** + * returns the size in dp of the legend forms + * + * @return + */ + public float getFormSize() { + return mFormSize; + } + + /** + * sets the line width in dp for forms that consist of lines, default 3f + * + * @param size + */ + public void setFormLineWidth(float size) { + mFormLineWidth = size; + } + + /** + * returns the line width in dp for drawing forms that consist of lines + * + * @return + */ + public float getFormLineWidth() { + return mFormLineWidth; + } + + /** + * Sets the line dash path effect used for shapes that consist of lines. + * + * @param dashPathEffect + */ + public void setFormLineDashEffect(DashPathEffect dashPathEffect) { + mFormLineDashEffect = dashPathEffect; + } + + /** + * @return The line dash path effect used for shapes that consist of lines. + */ + public DashPathEffect getFormLineDashEffect() { + return mFormLineDashEffect; + } + + /** + * returns the space between the legend entries on a horizontal axis in + * pixels + * + * @return + */ + public float getXEntrySpace() { + return mXEntrySpace; + } + + /** + * sets the space between the legend entries on a horizontal axis in pixels, + * converts to dp internally + * + * @param space + */ + public void setXEntrySpace(float space) { + mXEntrySpace = space; + } + + /** + * returns the space between the legend entries on a vertical axis in pixels + * + * @return + */ + public float getYEntrySpace() { + return mYEntrySpace; + } + + /** + * sets the space between the legend entries on a vertical axis in pixels, + * converts to dp internally + * + * @param space + */ + public void setYEntrySpace(float space) { + mYEntrySpace = space; + } + + /** + * returns the space between the form and the actual label/text + * + * @return + */ + public float getFormToTextSpace() { + return mFormToTextSpace; + } + + /** + * sets the space between the form and the actual label/text, converts to dp + * internally + * + * @param space + */ + public void setFormToTextSpace(float space) { + this.mFormToTextSpace = space; + } + + /** + * returns the space that is left out between stacked forms (with no label) + * + * @return + */ + public float getStackSpace() { + return mStackSpace; + } + + /** + * sets the space that is left out between stacked forms (with no label) + * + * @param space + */ + public void setStackSpace(float space) { + mStackSpace = space; + } + + /** + * the total width of the legend (needed width space) + */ + public float mNeededWidth = 0f; + + /** + * the total height of the legend (needed height space) + */ + public float mNeededHeight = 0f; + + public float mTextHeightMax = 0f; + + public float mTextWidthMax = 0f; + + /** + * flag that indicates if word wrapping is enabled + */ + private boolean mWordWrapEnabled = false; + + /** + * Should the legend word wrap? / this is currently supported only for: + * BelowChartLeft, BelowChartRight, BelowChartCenter. / note that word + * wrapping a legend takes a toll on performance. / you may want to set + * maxSizePercent when word wrapping, to set the point where the text wraps. + * / default: false + * + * @param enabled + */ + public void setWordWrapEnabled(boolean enabled) { + mWordWrapEnabled = enabled; + } + + /** + * If this is set, then word wrapping the legend is enabled. This means the + * legend will not be cut off if too long. + * + * @return + */ + public boolean isWordWrapEnabled() { + return mWordWrapEnabled; + } + + /** + * The maximum relative size out of the whole chart view. / If the legend is + * to the right/left of the chart, then this affects the width of the + * legend. / If the legend is to the top/bottom of the chart, then this + * affects the height of the legend. / If the legend is the center of the + * piechart, then this defines the size of the rectangular bounds out of the + * size of the "hole". / default: 0.95f (95%) + * + * @return + */ + public float getMaxSizePercent() { + return mMaxSizePercent; + } + + /** + * The maximum relative size out of the whole chart view. / If + * the legend is to the right/left of the chart, then this affects the width + * of the legend. / If the legend is to the top/bottom of the chart, then + * this affects the height of the legend. / default: 0.95f (95%) + * + * @param maxSize + */ + public void setMaxSizePercent(float maxSize) { + mMaxSizePercent = maxSize; + } + + private List mCalculatedLabelSizes = new ArrayList<>(16); + private List mCalculatedLabelBreakPoints = new ArrayList<>(16); + private List mCalculatedLineSizes = new ArrayList<>(16); + + public List getCalculatedLabelSizes() { + return mCalculatedLabelSizes; + } + + public List getCalculatedLabelBreakPoints() { + return mCalculatedLabelBreakPoints; + } + + public List getCalculatedLineSizes() { + return mCalculatedLineSizes; + } + + /** + * Calculates the dimensions of the Legend. This includes the maximum width + * and height of a single entry, as well as the total width and height of + * the Legend. + * + * @param labelpaint + */ + public void calculateDimensions(Paint labelpaint, ViewPortHandler viewPortHandler) { + + float defaultFormSize = Utils.convertDpToPixel(mFormSize); + float stackSpace = Utils.convertDpToPixel(mStackSpace); + float formToTextSpace = Utils.convertDpToPixel(mFormToTextSpace); + float xEntrySpace = Utils.convertDpToPixel(mXEntrySpace); + float yEntrySpace = Utils.convertDpToPixel(mYEntrySpace); + boolean wordWrapEnabled = mWordWrapEnabled; + LegendEntry[] entries = mEntries; + int entryCount = entries.length; + + mTextWidthMax = getMaximumEntryWidth(labelpaint); + mTextHeightMax = getMaximumEntryHeight(labelpaint); + + switch (mOrientation) { + case VERTICAL: { + + float maxWidth = 0f, maxHeight = 0f, width = 0f; + float labelLineHeight = Utils.getLineHeight(labelpaint); + boolean wasStacked = false; + + for (int i = 0; i < entryCount; i++) { + + LegendEntry e = entries[i]; + boolean drawingForm = e.form != LegendForm.NONE; + float formSize = Float.isNaN(e.formSize) + ? defaultFormSize + : Utils.convertDpToPixel(e.formSize); + String label = e.label; + + if (!wasStacked) + width = 0.f; + + if (drawingForm) { + if (wasStacked) + width += stackSpace; + width += formSize; + } + + // grouped forms have null labels + if (label != null) { + + // make a step to the left + if (drawingForm && !wasStacked) + width += formToTextSpace; + else if (wasStacked) { + maxWidth = Math.max(maxWidth, width); + maxHeight += labelLineHeight + yEntrySpace; + width = 0.f; + wasStacked = false; + } + + width += Utils.calcTextWidth(labelpaint, label); + + maxHeight += labelLineHeight + yEntrySpace; + } else { + wasStacked = true; + width += formSize; + if (i < entryCount - 1) + width += stackSpace; + } + + maxWidth = Math.max(maxWidth, width); + } + + mNeededWidth = maxWidth; + mNeededHeight = maxHeight; + + break; + } + case HORIZONTAL: { + + float labelLineHeight = Utils.getLineHeight(labelpaint); + float labelLineSpacing = Utils.getLineSpacing(labelpaint) + yEntrySpace; + float contentWidth = viewPortHandler.contentWidth() * mMaxSizePercent; + + // Start calculating layout + float maxLineWidth = 0.f; + float currentLineWidth = 0.f; + float requiredWidth = 0.f; + int stackedStartIndex = -1; + + mCalculatedLabelBreakPoints.clear(); + mCalculatedLabelSizes.clear(); + mCalculatedLineSizes.clear(); + + for (int i = 0; i < entryCount; i++) { + + LegendEntry e = entries[i]; + boolean drawingForm = e.form != LegendForm.NONE; + float formSize = Float.isNaN(e.formSize) + ? defaultFormSize + : Utils.convertDpToPixel(e.formSize); + String label = e.label; + + mCalculatedLabelBreakPoints.add(false); + + if (stackedStartIndex == -1) { + // we are not stacking, so required width is for this label + // only + requiredWidth = 0.f; + } else { + // add the spacing appropriate for stacked labels/forms + requiredWidth += stackSpace; + } + + // grouped forms have null labels + if (label != null) { + + mCalculatedLabelSizes.add(Utils.calcTextSize(labelpaint, label)); + requiredWidth += drawingForm ? formToTextSpace + formSize : 0.f; + requiredWidth += mCalculatedLabelSizes.get(i).width; + } else { + + mCalculatedLabelSizes.add(FSize.getInstance(0.f, 0.f)); + requiredWidth += drawingForm ? formSize : 0.f; + + if (stackedStartIndex == -1) { + // mark this index as we might want to break here later + stackedStartIndex = i; + } + } + + if (label != null || i == entryCount - 1) { + + float requiredSpacing = currentLineWidth == 0.f ? 0.f : xEntrySpace; + + if (!wordWrapEnabled // No word wrapping, it must fit. + // The line is empty, it must fit + || currentLineWidth == 0.f + // It simply fits + || (contentWidth - currentLineWidth >= + requiredSpacing + requiredWidth)) { + // Expand current line + currentLineWidth += requiredSpacing + requiredWidth; + } else { // It doesn't fit, we need to wrap a line + + // Add current line size to array + mCalculatedLineSizes.add(FSize.getInstance(currentLineWidth, labelLineHeight)); + maxLineWidth = Math.max(maxLineWidth, currentLineWidth); + + // Start a new line + mCalculatedLabelBreakPoints.set( + stackedStartIndex > -1 ? stackedStartIndex + : i, true); + currentLineWidth = requiredWidth; + } + + if (i == entryCount - 1) { + // Add last line size to array + mCalculatedLineSizes.add(FSize.getInstance(currentLineWidth, labelLineHeight)); + maxLineWidth = Math.max(maxLineWidth, currentLineWidth); + } + } + + stackedStartIndex = label != null ? -1 : stackedStartIndex; + } + + mNeededWidth = maxLineWidth; + mNeededHeight = labelLineHeight + * (float) (mCalculatedLineSizes.size()) + + labelLineSpacing * + (float) (mCalculatedLineSizes.size() == 0 + ? 0 + : (mCalculatedLineSizes.size() - 1)); + + break; + } + } + + mNeededHeight += mYOffset; + mNeededWidth += mXOffset; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/components/LegendEntry.java b/MPChartLib/src/main/java/com/github/mikephil/charting/components/LegendEntry.java new file mode 100644 index 0000000..3acec0f --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/components/LegendEntry.java @@ -0,0 +1,78 @@ +package com.github.mikephil.charting.components; + + +import android.graphics.DashPathEffect; + +import com.github.mikephil.charting.utils.ColorTemplate; + +public class LegendEntry { + public LegendEntry() { + + } + + /** + * + * @param label The legend entry text. A `null` label will start a group. + * @param form The form to draw for this entry. + * @param formSize Set to NaN to use the legend's default. + * @param formLineWidth Set to NaN to use the legend's default. + * @param formLineDashEffect Set to nil to use the legend's default. + * @param formColor The color for drawing the form. + */ + public LegendEntry(String label, + Legend.LegendForm form, + float formSize, + float formLineWidth, + DashPathEffect formLineDashEffect, + int formColor) + { + this.label = label; + this.form = form; + this.formSize = formSize; + this.formLineWidth = formLineWidth; + this.formLineDashEffect = formLineDashEffect; + this.formColor = formColor; + } + + /** + * The legend entry text. + * A `null` label will start a group. + */ + public String label; + + /** + * The form to draw for this entry. + * + * `NONE` will avoid drawing a form, and any related space. + * `EMPTY` will avoid drawing a form, but keep its space. + * `DEFAULT` will use the Legend's default. + */ + public Legend.LegendForm form = Legend.LegendForm.DEFAULT; + + /** + * Form size will be considered except for when .None is used + * + * Set as NaN to use the legend's default + */ + public float formSize = Float.NaN; + + /** + * Line width used for shapes that consist of lines. + * + * Set as NaN to use the legend's default + */ + public float formLineWidth = Float.NaN; + + /** + * Line dash path effect used for shapes that consist of lines. + * + * Set to null to use the legend's default + */ + public DashPathEffect formLineDashEffect = null; + + /** + * The color for drawing the form + */ + public int formColor = ColorTemplate.COLOR_NONE; + +} \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/components/LimitLine.java b/MPChartLib/src/main/java/com/github/mikephil/charting/components/LimitLine.java new file mode 100644 index 0000000..8fcdee8 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/components/LimitLine.java @@ -0,0 +1,215 @@ + +package com.github.mikephil.charting.components; + +import android.graphics.Color; +import android.graphics.DashPathEffect; +import android.graphics.Paint; +import android.graphics.Typeface; + +import com.github.mikephil.charting.utils.Utils; + +/** + * The limit line is an additional feature for all Line-, Bar- and + * ScatterCharts. It allows the displaying of an additional line in the chart + * that marks a certain maximum / limit on the specified axis (x- or y-axis). + * + * @author Philipp Jahoda + */ +public class LimitLine extends ComponentBase { + + /** limit / maximum (the y-value or xIndex) */ + private float mLimit = 0f; + + /** the width of the limit line */ + private float mLineWidth = 2f; + + /** the color of the limit line */ + private int mLineColor = Color.rgb(237, 91, 91); + + /** the style of the label text */ + private Paint.Style mTextStyle = Paint.Style.FILL_AND_STROKE; + + /** label string that is drawn next to the limit line */ + private String mLabel = ""; + + /** the path effect of this LimitLine that makes dashed lines possible */ + private DashPathEffect mDashPathEffect = null; + + /** indicates the position of the LimitLine label */ + private LimitLabelPosition mLabelPosition = LimitLabelPosition.RIGHT_TOP; + + /** enum that indicates the position of the LimitLine label */ + public enum LimitLabelPosition { + LEFT_TOP, LEFT_BOTTOM, RIGHT_TOP, RIGHT_BOTTOM + } + + /** + * Constructor with limit. + * + * @param limit - the position (the value) on the y-axis (y-value) or x-axis + * (xIndex) where this line should appear + */ + public LimitLine(float limit) { + mLimit = limit; + } + + /** + * Constructor with limit and label. + * + * @param limit - the position (the value) on the y-axis (y-value) or x-axis + * (xIndex) where this line should appear + * @param label - provide "" if no label is required + */ + public LimitLine(float limit, String label) { + mLimit = limit; + mLabel = label; + } + + /** + * Returns the limit that is set for this line. + * + * @return + */ + public float getLimit() { + return mLimit; + } + + /** + * set the line width of the chart (min = 0.2f, max = 12f); default 2f NOTE: + * thinner line == better performance, thicker line == worse performance + * + * @param width + */ + public void setLineWidth(float width) { + + if (width < 0.2f) + width = 0.2f; + if (width > 12.0f) + width = 12.0f; + mLineWidth = Utils.convertDpToPixel(width); + } + + /** + * returns the width of limit line + * + * @return + */ + public float getLineWidth() { + return mLineWidth; + } + + /** + * Sets the linecolor for this LimitLine. Make sure to use + * getResources().getColor(...) + * + * @param color + */ + public void setLineColor(int color) { + mLineColor = color; + } + + /** + * Returns the color that is used for this LimitLine + * + * @return + */ + public int getLineColor() { + return mLineColor; + } + + /** + * Enables the line to be drawn in dashed mode, e.g. like this "- - - - - -" + * + * @param lineLength the length of the line pieces + * @param spaceLength the length of space inbetween the pieces + * @param phase offset, in degrees (normally, use 0) + */ + public void enableDashedLine(float lineLength, float spaceLength, float phase) { + mDashPathEffect = new DashPathEffect(new float[] { + lineLength, spaceLength + }, phase); + } + + /** + * Disables the line to be drawn in dashed mode. + */ + public void disableDashedLine() { + mDashPathEffect = null; + } + + /** + * Returns true if the dashed-line effect is enabled, false if not. Default: + * disabled + * + * @return + */ + public boolean isDashedLineEnabled() { + return mDashPathEffect == null ? false : true; + } + + /** + * returns the DashPathEffect that is set for this LimitLine + * + * @return + */ + public DashPathEffect getDashPathEffect() { + return mDashPathEffect; + } + + /** + * Sets the color of the value-text that is drawn next to the LimitLine. + * Default: Paint.Style.FILL_AND_STROKE + * + * @param style + */ + public void setTextStyle(Paint.Style style) { + this.mTextStyle = style; + } + + /** + * Returns the color of the value-text that is drawn next to the LimitLine. + * + * @return + */ + public Paint.Style getTextStyle() { + return mTextStyle; + } + + /** + * Sets the position of the LimitLine value label (either on the right or on + * the left edge of the chart). Not supported for RadarChart. + * + * @param pos + */ + public void setLabelPosition(LimitLabelPosition pos) { + mLabelPosition = pos; + } + + /** + * Returns the position of the LimitLine label (value). + * + * @return + */ + public LimitLabelPosition getLabelPosition() { + return mLabelPosition; + } + + /** + * Sets the label that is drawn next to the limit line. Provide "" if no + * label is required. + * + * @param label + */ + public void setLabel(String label) { + mLabel = label; + } + + /** + * Returns the label that is drawn next to the limit line. + * + * @return + */ + public String getLabel() { + return mLabel; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/components/MarkerImage.java b/MPChartLib/src/main/java/com/github/mikephil/charting/components/MarkerImage.java new file mode 100644 index 0000000..7bd7b8e --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/components/MarkerImage.java @@ -0,0 +1,167 @@ +package com.github.mikephil.charting.components; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.RelativeLayout; + +import com.github.mikephil.charting.charts.Chart; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.utils.FSize; +import com.github.mikephil.charting.utils.MPPointF; + +import java.lang.ref.WeakReference; + +/** + * View that can be displayed when selecting values in the chart. Extend this class to provide custom layouts for your + * markers. + * + * @author Philipp Jahoda + */ +public class MarkerImage implements IMarker { + + private Context mContext; + private Drawable mDrawable; + + private MPPointF mOffset = new MPPointF(); + private MPPointF mOffset2 = new MPPointF(); + private WeakReference mWeakChart; + + private FSize mSize = new FSize(); + private Rect mDrawableBoundsCache = new Rect(); + + /** + * Constructor. Sets up the MarkerView with a custom layout resource. + * + * @param context + * @param drawableResourceId the drawable resource to render + */ + public MarkerImage(Context context, int drawableResourceId) { + mContext = context; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + { + mDrawable = mContext.getResources().getDrawable(drawableResourceId, null); + } + else + { + mDrawable = mContext.getResources().getDrawable(drawableResourceId); + } + } + + public void setOffset(MPPointF offset) { + mOffset = offset; + + if (mOffset == null) { + mOffset = new MPPointF(); + } + } + + public void setOffset(float offsetX, float offsetY) { + mOffset.x = offsetX; + mOffset.y = offsetY; + } + + @Override + public MPPointF getOffset() { + return mOffset; + } + + public void setSize(FSize size) { + mSize = size; + + if (mSize == null) { + mSize = new FSize(); + } + } + + public FSize getSize() { + return mSize; + } + + public void setChartView(Chart chart) { + mWeakChart = new WeakReference<>(chart); + } + + public Chart getChartView() { + return mWeakChart == null ? null : mWeakChart.get(); + } + + @Override + public MPPointF getOffsetForDrawingAtPoint(float posX, float posY) { + + MPPointF offset = getOffset(); + mOffset2.x = offset.x; + mOffset2.y = offset.y; + + Chart chart = getChartView(); + + float width = mSize.width; + float height = mSize.height; + + if (width == 0.f && mDrawable != null) { + width = mDrawable.getIntrinsicWidth(); + } + if (height == 0.f && mDrawable != null) { + height = mDrawable.getIntrinsicHeight(); + } + + if (posX + mOffset2.x < 0) { + mOffset2.x = - posX; + } else if (chart != null && posX + width + mOffset2.x > chart.getWidth()) { + mOffset2.x = chart.getWidth() - posX - width; + } + + if (posY + mOffset2.y < 0) { + mOffset2.y = - posY; + } else if (chart != null && posY + height + mOffset2.y > chart.getHeight()) { + mOffset2.y = chart.getHeight() - posY - height; + } + + return mOffset2; + } + + @Override + public void refreshContent(Entry e, Highlight highlight) { + + } + + @Override + public void draw(Canvas canvas, float posX, float posY) { + + if (mDrawable == null) return; + + MPPointF offset = getOffsetForDrawingAtPoint(posX, posY); + + float width = mSize.width; + float height = mSize.height; + + if (width == 0.f) { + width = mDrawable.getIntrinsicWidth(); + } + if (height == 0.f) { + height = mDrawable.getIntrinsicHeight(); + } + + mDrawable.copyBounds(mDrawableBoundsCache); + mDrawable.setBounds( + mDrawableBoundsCache.left, + mDrawableBoundsCache.top, + mDrawableBoundsCache.left + (int)width, + mDrawableBoundsCache.top + (int)height); + + int saveId = canvas.save(); + // translate to the correct position and draw + canvas.translate(posX + offset.x, posY + offset.y); + mDrawable.draw(canvas); + canvas.restoreToCount(saveId); + + mDrawable.setBounds(mDrawableBoundsCache); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/components/MarkerView.java b/MPChartLib/src/main/java/com/github/mikephil/charting/components/MarkerView.java new file mode 100644 index 0000000..162e88e --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/components/MarkerView.java @@ -0,0 +1,129 @@ +package com.github.mikephil.charting.components; + +import android.content.Context; +import android.graphics.Canvas; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.RelativeLayout; + +import com.github.mikephil.charting.charts.Chart; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.utils.FSize; +import com.github.mikephil.charting.utils.MPPointF; + +import java.lang.ref.WeakReference; + +/** + * View that can be displayed when selecting values in the chart. Extend this class to provide custom layouts for your + * markers. + * + * @author Philipp Jahoda + */ +public class MarkerView extends RelativeLayout implements IMarker { + + private MPPointF mOffset = new MPPointF(); + private MPPointF mOffset2 = new MPPointF(); + private WeakReference mWeakChart; + + /** + * Constructor. Sets up the MarkerView with a custom layout resource. + * + * @param context + * @param layoutResource the layout resource to use for the MarkerView + */ + public MarkerView(Context context, int layoutResource) { + super(context); + setupLayoutResource(layoutResource); + } + + /** + * Sets the layout resource for a custom MarkerView. + * + * @param layoutResource + */ + private void setupLayoutResource(int layoutResource) { + + View inflated = LayoutInflater.from(getContext()).inflate(layoutResource, this); + + inflated.setLayoutParams(new LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT)); + inflated.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + + // measure(getWidth(), getHeight()); + inflated.layout(0, 0, inflated.getMeasuredWidth(), inflated.getMeasuredHeight()); + } + + public void setOffset(MPPointF offset) { + mOffset = offset; + + if (mOffset == null) { + mOffset = new MPPointF(); + } + } + + public void setOffset(float offsetX, float offsetY) { + mOffset.x = offsetX; + mOffset.y = offsetY; + } + + @Override + public MPPointF getOffset() { + return mOffset; + } + + public void setChartView(Chart chart) { + mWeakChart = new WeakReference<>(chart); + } + + public Chart getChartView() { + return mWeakChart == null ? null : mWeakChart.get(); + } + + @Override + public MPPointF getOffsetForDrawingAtPoint(float posX, float posY) { + + MPPointF offset = getOffset(); + mOffset2.x = offset.x; + mOffset2.y = offset.y; + + Chart chart = getChartView(); + + float width = getWidth(); + float height = getHeight(); + + if (posX + mOffset2.x < 0) { + mOffset2.x = - posX; + } else if (chart != null && posX + width + mOffset2.x > chart.getWidth()) { + mOffset2.x = chart.getWidth() - posX - width; + } + + if (posY + mOffset2.y < 0) { + mOffset2.y = - posY; + } else if (chart != null && posY + height + mOffset2.y > chart.getHeight()) { + mOffset2.y = chart.getHeight() - posY - height; + } + + return mOffset2; + } + + @Override + public void refreshContent(Entry e, Highlight highlight) { + + measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + layout(0, 0, getMeasuredWidth(), getMeasuredHeight()); + + } + + @Override + public void draw(Canvas canvas, float posX, float posY) { + + MPPointF offset = getOffsetForDrawingAtPoint(posX, posY); + + int saveId = canvas.save(); + // translate to the correct position and draw + canvas.translate(posX + offset.x, posY + offset.y); + draw(canvas); + canvas.restoreToCount(saveId); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/components/XAxis.java b/MPChartLib/src/main/java/com/github/mikephil/charting/components/XAxis.java new file mode 100644 index 0000000..77d4aaf --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/components/XAxis.java @@ -0,0 +1,118 @@ + +package com.github.mikephil.charting.components; + +import com.github.mikephil.charting.utils.Utils; + +/** + * Class representing the x-axis labels settings. Only use the setter methods to + * modify it. Do not access public variables directly. Be aware that not all + * features the XLabels class provides are suitable for the RadarChart. + * + * @author Philipp Jahoda + */ +public class XAxis extends AxisBase { + + /** + * width of the x-axis labels in pixels - this is automatically + * calculated by the computeSize() methods in the renderers + */ + public int mLabelWidth = 1; + + /** + * height of the x-axis labels in pixels - this is automatically + * calculated by the computeSize() methods in the renderers + */ + public int mLabelHeight = 1; + + /** + * width of the (rotated) x-axis labels in pixels - this is automatically + * calculated by the computeSize() methods in the renderers + */ + public int mLabelRotatedWidth = 1; + + /** + * height of the (rotated) x-axis labels in pixels - this is automatically + * calculated by the computeSize() methods in the renderers + */ + public int mLabelRotatedHeight = 1; + + /** + * This is the angle for drawing the X axis labels (in degrees) + */ + protected float mLabelRotationAngle = 0f; + + /** + * if set to true, the chart will avoid that the first and last label entry + * in the chart "clip" off the edge of the chart + */ + private boolean mAvoidFirstLastClipping = false; + + /** + * the position of the x-labels relative to the chart + */ + private XAxisPosition mPosition = XAxisPosition.TOP; + + /** + * enum for the position of the x-labels relative to the chart + */ + public enum XAxisPosition { + TOP, BOTTOM, BOTH_SIDED, TOP_INSIDE, BOTTOM_INSIDE + } + + public XAxis() { + super(); + + mYOffset = Utils.convertDpToPixel(4.f); // -3 + } + + /** + * returns the position of the x-labels + */ + public XAxisPosition getPosition() { + return mPosition; + } + + /** + * sets the position of the x-labels + * + * @param pos + */ + public void setPosition(XAxisPosition pos) { + mPosition = pos; + } + + /** + * returns the angle for drawing the X axis labels (in degrees) + */ + public float getLabelRotationAngle() { + return mLabelRotationAngle; + } + + /** + * sets the angle for drawing the X axis labels (in degrees) + * + * @param angle the angle in degrees + */ + public void setLabelRotationAngle(float angle) { + mLabelRotationAngle = angle; + } + + /** + * if set to true, the chart will avoid that the first and last label entry + * in the chart "clip" off the edge of the chart or the screen + * + * @param enabled + */ + public void setAvoidFirstLastClipping(boolean enabled) { + mAvoidFirstLastClipping = enabled; + } + + /** + * returns true if avoid-first-lastclipping is enabled, false if not + * + * @return + */ + public boolean isAvoidFirstLastClippingEnabled() { + return mAvoidFirstLastClipping; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/components/YAxis.java b/MPChartLib/src/main/java/com/github/mikephil/charting/components/YAxis.java new file mode 100644 index 0000000..d2071ec --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/components/YAxis.java @@ -0,0 +1,467 @@ +package com.github.mikephil.charting.components; + +import android.graphics.Color; +import android.graphics.Paint; + +import com.github.mikephil.charting.utils.Utils; + +/** + * Class representing the y-axis labels settings and its entries. Only use the setter methods to + * modify it. Do not + * access public variables directly. Be aware that not all features the YLabels class provides + * are suitable for the + * RadarChart. Customizations that affect the value range of the axis need to be applied before + * setting data for the + * chart. + * + * @author Philipp Jahoda + */ +public class YAxis extends AxisBase { + + /** + * indicates if the bottom y-label entry is drawn or not + */ + private boolean mDrawBottomYLabelEntry = true; + + /** + * indicates if the top y-label entry is drawn or not + */ + private boolean mDrawTopYLabelEntry = true; + + /** + * flag that indicates if the axis is inverted or not + */ + protected boolean mInverted = false; + + /** + * flag that indicates if the zero-line should be drawn regardless of other grid lines + */ + protected boolean mDrawZeroLine = false; + + /** + * flag indicating that auto scale min restriction should be used + */ + private boolean mUseAutoScaleRestrictionMin = false; + + /** + * flag indicating that auto scale max restriction should be used + */ + private boolean mUseAutoScaleRestrictionMax = false; + + /** + * Color of the zero line + */ + protected int mZeroLineColor = Color.GRAY; + + /** + * Width of the zero line in pixels + */ + protected float mZeroLineWidth = 1f; + + /** + * axis space from the largest value to the top in percent of the total axis range + */ + protected float mSpacePercentTop = 10f; + + /** + * axis space from the smallest value to the bottom in percent of the total axis range + */ + protected float mSpacePercentBottom = 10f; + + /** + * the position of the y-labels relative to the chart + */ + private YAxisLabelPosition mPosition = YAxisLabelPosition.OUTSIDE_CHART; + + /** + * the horizontal offset of the y-label + */ + private float mXLabelOffset = 0.0f; + + /** + * enum for the position of the y-labels relative to the chart + */ + public enum YAxisLabelPosition { + OUTSIDE_CHART, INSIDE_CHART + } + + /** + * the side this axis object represents + */ + private AxisDependency mAxisDependency; + + /** + * the minimum width that the axis should take (in dp). + *

+ * default: 0.0 + */ + protected float mMinWidth = 0.f; + + /** + * the maximum width that the axis can take (in dp). + * use Inifinity for disabling the maximum + * default: Float.POSITIVE_INFINITY (no maximum specified) + */ + protected float mMaxWidth = Float.POSITIVE_INFINITY; + + /** + * Enum that specifies the axis a DataSet should be plotted against, either LEFT or RIGHT. + * + * @author Philipp Jahoda + */ + public enum AxisDependency { + LEFT, RIGHT + } + + public YAxis() { + super(); + + // default left + this.mAxisDependency = AxisDependency.LEFT; + this.mYOffset = 0f; + } + + public YAxis(AxisDependency position) { + super(); + this.mAxisDependency = position; + this.mYOffset = 0f; + } + + public AxisDependency getAxisDependency() { + return mAxisDependency; + } + + /** + * @return the minimum width that the axis should take (in dp). + */ + public float getMinWidth() { + return mMinWidth; + } + + /** + * Sets the minimum width that the axis should take (in dp). + * + * @param minWidth + */ + public void setMinWidth(float minWidth) { + mMinWidth = minWidth; + } + + /** + * @return the maximum width that the axis can take (in dp). + */ + public float getMaxWidth() { + return mMaxWidth; + } + + /** + * Sets the maximum width that the axis can take (in dp). + * + * @param maxWidth + */ + public void setMaxWidth(float maxWidth) { + mMaxWidth = maxWidth; + } + + /** + * returns the position of the y-labels + */ + public YAxisLabelPosition getLabelPosition() { + return mPosition; + } + + /** + * sets the position of the y-labels + * + * @param pos + */ + public void setPosition(YAxisLabelPosition pos) { + mPosition = pos; + } + + /** + * returns the horizontal offset of the y-label + */ + public float getLabelXOffset() { + return mXLabelOffset; + } + + /** + * sets the horizontal offset of the y-label + * + * @param xOffset + */ + public void setLabelXOffset(float xOffset) { + mXLabelOffset = xOffset; + } + + /** + * returns true if drawing the top y-axis label entry is enabled + * + * @return + */ + public boolean isDrawTopYLabelEntryEnabled() { + return mDrawTopYLabelEntry; + } + + /** + * returns true if drawing the bottom y-axis label entry is enabled + * + * @return + */ + public boolean isDrawBottomYLabelEntryEnabled() { + return mDrawBottomYLabelEntry; + } + + /** + * set this to true to enable drawing the top y-label entry. Disabling this can be helpful + * when the top y-label and + * left x-label interfere with each other. default: true + * + * @param enabled + */ + public void setDrawTopYLabelEntry(boolean enabled) { + mDrawTopYLabelEntry = enabled; + } + + /** + * If this is set to true, the y-axis is inverted which means that low values are on top of + * the chart, high values + * on bottom. + * + * @param enabled + */ + public void setInverted(boolean enabled) { + mInverted = enabled; + } + + /** + * If this returns true, the y-axis is inverted. + * + * @return + */ + public boolean isInverted() { + return mInverted; + } + + /** + * This method is deprecated. + * Use setAxisMinimum(...) / setAxisMaximum(...) instead. + * + * @param startAtZero + */ + @Deprecated + public void setStartAtZero(boolean startAtZero) { + if (startAtZero) + setAxisMinimum(0f); + else + resetAxisMinimum(); + } + + /** + * Sets the top axis space in percent of the full range. Default 10f + * + * @param percent + */ + public void setSpaceTop(float percent) { + mSpacePercentTop = percent; + } + + /** + * Returns the top axis space in percent of the full range. Default 10f + * + * @return + */ + public float getSpaceTop() { + return mSpacePercentTop; + } + + /** + * Sets the bottom axis space in percent of the full range. Default 10f + * + * @param percent + */ + public void setSpaceBottom(float percent) { + mSpacePercentBottom = percent; + } + + /** + * Returns the bottom axis space in percent of the full range. Default 10f + * + * @return + */ + public float getSpaceBottom() { + return mSpacePercentBottom; + } + + public boolean isDrawZeroLineEnabled() { + return mDrawZeroLine; + } + + /** + * Set this to true to draw the zero-line regardless of weather other + * grid-lines are enabled or not. Default: false + * + * @param mDrawZeroLine + */ + public void setDrawZeroLine(boolean mDrawZeroLine) { + this.mDrawZeroLine = mDrawZeroLine; + } + + public int getZeroLineColor() { + return mZeroLineColor; + } + + /** + * Sets the color of the zero line + * + * @param color + */ + public void setZeroLineColor(int color) { + mZeroLineColor = color; + } + + public float getZeroLineWidth() { + return mZeroLineWidth; + } + + /** + * Sets the width of the zero line in dp + * + * @param width + */ + public void setZeroLineWidth(float width) { + this.mZeroLineWidth = Utils.convertDpToPixel(width); + } + + /** + * This is for normal (not horizontal) charts horizontal spacing. + * + * @param p + * @return + */ + public float getRequiredWidthSpace(Paint p) { + + p.setTextSize(mTextSize); + + String label = getLongestLabel(); + float width = (float) Utils.calcTextWidth(p, label) + getXOffset() * 2f; + + float minWidth = getMinWidth(); + float maxWidth = getMaxWidth(); + + if (minWidth > 0.f) + minWidth = Utils.convertDpToPixel(minWidth); + + if (maxWidth > 0.f && maxWidth != Float.POSITIVE_INFINITY) + maxWidth = Utils.convertDpToPixel(maxWidth); + + width = Math.max(minWidth, Math.min(width, maxWidth > 0.0 ? maxWidth : width)); + + return width; + } + + /** + * This is for HorizontalBarChart vertical spacing. + * + * @param p + * @return + */ + public float getRequiredHeightSpace(Paint p) { + + p.setTextSize(mTextSize); + + String label = getLongestLabel(); + return (float) Utils.calcTextHeight(p, label) + getYOffset() * 2f; + } + + /** + * Returns true if this axis needs horizontal offset, false if no offset is needed. + * + * @return + */ + public boolean needsOffset() { + if (isEnabled() && isDrawLabelsEnabled() && getLabelPosition() == YAxisLabelPosition + .OUTSIDE_CHART) + return true; + else + return false; + } + + /** + * Returns true if autoscale restriction for axis min value is enabled + */ + @Deprecated + public boolean isUseAutoScaleMinRestriction( ) { + return mUseAutoScaleRestrictionMin; + } + + /** + * Sets autoscale restriction for axis min value as enabled/disabled + */ + @Deprecated + public void setUseAutoScaleMinRestriction( boolean isEnabled ) { + mUseAutoScaleRestrictionMin = isEnabled; + } + + /** + * Returns true if autoscale restriction for axis max value is enabled + */ + @Deprecated + public boolean isUseAutoScaleMaxRestriction() { + return mUseAutoScaleRestrictionMax; + } + + /** + * Sets autoscale restriction for axis max value as enabled/disabled + */ + @Deprecated + public void setUseAutoScaleMaxRestriction( boolean isEnabled ) { + mUseAutoScaleRestrictionMax = isEnabled; + } + + + @Override + public void calculate(float dataMin, float dataMax) { + + float min = dataMin; + float max = dataMax; + + // Make sure max is greater than min + // Discussion: https://github.com/danielgindi/Charts/pull/3650#discussion_r221409991 + if (min > max) + { + if (mCustomAxisMax && mCustomAxisMin) + { + float t = min; + min = max; + max = t; + } + else if (mCustomAxisMax) + { + min = max < 0f ? max * 1.5f : max * 0.5f; + } + else if (mCustomAxisMin) + { + max = min < 0f ? min * 0.5f : min * 1.5f; + } + } + + float range = Math.abs(max - min); + + // in case all values are equal + if (range == 0f) { + max = max + 1f; + min = min - 1f; + } + + // recalculate + range = Math.abs(max - min); + + // calc extra spacing + this.mAxisMinimum = mCustomAxisMin ? this.mAxisMinimum : min - (range / 100f) * getSpaceBottom(); + this.mAxisMaximum = mCustomAxisMax ? this.mAxisMaximum : max + (range / 100f) * getSpaceTop(); + + this.mAxisRange = Math.abs(this.mAxisMinimum - this.mAxisMaximum); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarData.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarData.java new file mode 100644 index 0000000..16d60f6 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarData.java @@ -0,0 +1,119 @@ + +package com.github.mikephil.charting.data; + +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; + +import java.util.List; + +/** + * Data object that represents all data for the BarChart. + * + * @author Philipp Jahoda + */ +public class BarData extends BarLineScatterCandleBubbleData { + + /** + * the width of the bars on the x-axis, in values (not pixels) + */ + private float mBarWidth = 0.85f; + + public BarData() { + super(); + } + + public BarData(IBarDataSet... dataSets) { + super(dataSets); + } + + public BarData(List dataSets) { + super(dataSets); + } + + /** + * Sets the width each bar should have on the x-axis (in values, not pixels). + * Default 0.85f + * + * @param mBarWidth + */ + public void setBarWidth(float mBarWidth) { + this.mBarWidth = mBarWidth; + } + + public float getBarWidth() { + return mBarWidth; + } + + /** + * Groups all BarDataSet objects this data object holds together by modifying the x-value of their entries. + * Previously set x-values of entries will be overwritten. Leaves space between bars and groups as specified + * by the parameters. + * Do not forget to call notifyDataSetChanged() on your BarChart object after calling this method. + * + * @param fromX the starting point on the x-axis where the grouping should begin + * @param groupSpace the space between groups of bars in values (not pixels) e.g. 0.8f for bar width 1f + * @param barSpace the space between individual bars in values (not pixels) e.g. 0.1f for bar width 1f + */ + public void groupBars(float fromX, float groupSpace, float barSpace) { + + int setCount = mDataSets.size(); + if (setCount <= 1) { + throw new RuntimeException("BarData needs to hold at least 2 BarDataSets to allow grouping."); + } + + IBarDataSet max = getMaxEntryCountSet(); + int maxEntryCount = max.getEntryCount(); + + float groupSpaceWidthHalf = groupSpace / 2f; + float barSpaceHalf = barSpace / 2f; + float barWidthHalf = mBarWidth / 2f; + + float interval = getGroupWidth(groupSpace, barSpace); + + for (int i = 0; i < maxEntryCount; i++) { + + float start = fromX; + fromX += groupSpaceWidthHalf; + + for (IBarDataSet set : mDataSets) { + + fromX += barSpaceHalf; + fromX += barWidthHalf; + + if (i < set.getEntryCount()) { + + BarEntry entry = set.getEntryForIndex(i); + + if (entry != null) { + entry.setX(fromX); + } + } + + fromX += barWidthHalf; + fromX += barSpaceHalf; + } + + fromX += groupSpaceWidthHalf; + float end = fromX; + float innerInterval = end - start; + float diff = interval - innerInterval; + + // correct rounding errors + if (diff > 0 || diff < 0) { + fromX += diff; + } + } + + notifyDataChanged(); + } + + /** + * In case of grouped bars, this method returns the space an individual group of bar needs on the x-axis. + * + * @param groupSpace + * @param barSpace + * @return + */ + public float getGroupWidth(float groupSpace, float barSpace) { + return mDataSets.size() * (mBarWidth + barSpace) + groupSpace; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarDataSet.java new file mode 100644 index 0000000..e656388 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarDataSet.java @@ -0,0 +1,299 @@ + +package com.github.mikephil.charting.data; + +import android.graphics.Color; + +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; +import com.github.mikephil.charting.utils.Fill; + +import java.util.ArrayList; +import java.util.List; + +public class BarDataSet extends BarLineScatterCandleBubbleDataSet implements IBarDataSet { + + /** + * the maximum number of bars that are stacked upon each other, this value + * is calculated from the Entries that are added to the DataSet + */ + private int mStackSize = 1; + + /** + * the color used for drawing the bar shadows + */ + private int mBarShadowColor = Color.rgb(215, 215, 215); + + private float mBarBorderWidth = 0.0f; + + private int mBarBorderColor = Color.BLACK; + + /** + * the alpha value used to draw the highlight indicator bar + */ + private int mHighLightAlpha = 120; + + /** + * the overall entry count, including counting each stack-value individually + */ + private int mEntryCountStacks = 0; + + /** + * array of labels used to describe the different values of the stacked bars + */ + private String[] mStackLabels = new String[]{}; + + protected List mFills = null; + + public BarDataSet(List yVals, String label) { + super(yVals, label); + + mHighLightColor = Color.rgb(0, 0, 0); + + calcStackSize(yVals); + calcEntryCountIncludingStacks(yVals); + } + + @Override + public DataSet copy() { + List entries = new ArrayList(); + for (int i = 0; i < mEntries.size(); i++) { + entries.add(mEntries.get(i).copy()); + } + BarDataSet copied = new BarDataSet(entries, getLabel()); + copy(copied); + return copied; + } + + protected void copy(BarDataSet barDataSet) { + super.copy(barDataSet); + barDataSet.mStackSize = mStackSize; + barDataSet.mBarShadowColor = mBarShadowColor; + barDataSet.mBarBorderWidth = mBarBorderWidth; + barDataSet.mStackLabels = mStackLabels; + barDataSet.mHighLightAlpha = mHighLightAlpha; + } + + @Override + public List getFills() { + return mFills; + } + + @Override + public Fill getFill(int index) { + return mFills.get(index % mFills.size()); + } + + /** + * This method is deprecated. + * Use getFills() instead. + */ + @Deprecated + public List getGradients() { + return mFills; + } + + /** + * This method is deprecated. + * Use getFill(...) instead. + * + * @param index + */ + @Deprecated + public Fill getGradient(int index) { + return getFill(index); + } + + /** + * Sets the start and end color for gradient color, ONLY color that should be used for this DataSet. + * + * @param startColor + * @param endColor + */ + public void setGradientColor(int startColor, int endColor) { + mFills.clear(); + mFills.add(new Fill(startColor, endColor)); + } + + /** + * This method is deprecated. + * Use setFills(...) instead. + * + * @param gradientColors + */ + @Deprecated + public void setGradientColors(List gradientColors) { + this.mFills = gradientColors; + } + + /** + * Sets the fills for the bars in this dataset. + * + * @param fills + */ + public void setFills(List fills) { + this.mFills = fills; + } + + /** + * Calculates the total number of entries this DataSet represents, including + * stacks. All values belonging to a stack are calculated separately. + */ + private void calcEntryCountIncludingStacks(List yVals) { + + mEntryCountStacks = 0; + + for (int i = 0; i < yVals.size(); i++) { + + float[] vals = yVals.get(i).getYVals(); + + if (vals == null) + mEntryCountStacks++; + else + mEntryCountStacks += vals.length; + } + } + + /** + * calculates the maximum stacksize that occurs in the Entries array of this + * DataSet + */ + private void calcStackSize(List yVals) { + + for (int i = 0; i < yVals.size(); i++) { + + float[] vals = yVals.get(i).getYVals(); + + if (vals != null && vals.length > mStackSize) + mStackSize = vals.length; + } + } + + @Override + protected void calcMinMax(BarEntry e) { + + if (e != null && !Float.isNaN(e.getY())) { + + if (e.getYVals() == null) { + + if (e.getY() < mYMin) + mYMin = e.getY(); + + if (e.getY() > mYMax) + mYMax = e.getY(); + } else { + + if (-e.getNegativeSum() < mYMin) + mYMin = -e.getNegativeSum(); + + if (e.getPositiveSum() > mYMax) + mYMax = e.getPositiveSum(); + } + + calcMinMaxX(e); + } + } + + @Override + public int getStackSize() { + return mStackSize; + } + + @Override + public boolean isStacked() { + return mStackSize > 1 ? true : false; + } + + /** + * returns the overall entry count, including counting each stack-value + * individually + * + * @return + */ + public int getEntryCountStacks() { + return mEntryCountStacks; + } + + /** + * Sets the color used for drawing the bar-shadows. The bar shadows is a + * surface behind the bar that indicates the maximum value. Don't for get to + * use getResources().getColor(...) to set this. Or Color.rgb(...). + * + * @param color + */ + public void setBarShadowColor(int color) { + mBarShadowColor = color; + } + + @Override + public int getBarShadowColor() { + return mBarShadowColor; + } + + /** + * Sets the width used for drawing borders around the bars. + * If borderWidth == 0, no border will be drawn. + * + * @return + */ + public void setBarBorderWidth(float width) { + mBarBorderWidth = width; + } + + /** + * Returns the width used for drawing borders around the bars. + * If borderWidth == 0, no border will be drawn. + * + * @return + */ + @Override + public float getBarBorderWidth() { + return mBarBorderWidth; + } + + /** + * Sets the color drawing borders around the bars. + * + * @return + */ + public void setBarBorderColor(int color) { + mBarBorderColor = color; + } + + /** + * Returns the color drawing borders around the bars. + * + * @return + */ + @Override + public int getBarBorderColor() { + return mBarBorderColor; + } + + /** + * Set the alpha value (transparency) that is used for drawing the highlight + * indicator bar. min = 0 (fully transparent), max = 255 (fully opaque) + * + * @param alpha + */ + public void setHighLightAlpha(int alpha) { + mHighLightAlpha = alpha; + } + + @Override + public int getHighLightAlpha() { + return mHighLightAlpha; + } + + /** + * Sets labels for different values of bar-stacks, in case there are one. + * + * @param labels + */ + public void setStackLabels(String[] labels) { + mStackLabels = labels; + } + + @Override + public String[] getStackLabels() { + return mStackLabels; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarEntry.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarEntry.java new file mode 100644 index 0000000..365ef51 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarEntry.java @@ -0,0 +1,310 @@ +package com.github.mikephil.charting.data; + +import android.annotation.SuppressLint; +import android.graphics.drawable.Drawable; + +import com.github.mikephil.charting.highlight.Range; + +/** + * Entry class for the BarChart. (especially stacked bars) + * + * @author Philipp Jahoda + */ +@SuppressLint("ParcelCreator") +public class BarEntry extends Entry { + + /** + * the values the stacked barchart holds + */ + private float[] mYVals; + + /** + * the ranges for the individual stack values - automatically calculated + */ + private Range[] mRanges; + + /** + * the sum of all negative values this entry (if stacked) contains + */ + private float mNegativeSum; + + /** + * the sum of all positive values this entry (if stacked) contains + */ + private float mPositiveSum; + + /** + * Constructor for normal bars (not stacked). + * + * @param x + * @param y + */ + public BarEntry(float x, float y) { + super(x, y); + } + + /** + * Constructor for normal bars (not stacked). + * + * @param x + * @param y + * @param data - Spot for additional data this Entry represents. + */ + public BarEntry(float x, float y, Object data) { + super(x, y, data); + } + + /** + * Constructor for normal bars (not stacked). + * + * @param x + * @param y + * @param icon - icon image + */ + public BarEntry(float x, float y, Drawable icon) { + super(x, y, icon); + } + + /** + * Constructor for normal bars (not stacked). + * + * @param x + * @param y + * @param icon - icon image + * @param data - Spot for additional data this Entry represents. + */ + public BarEntry(float x, float y, Drawable icon, Object data) { + super(x, y, icon, data); + } + + /** + * Constructor for stacked bar entries. One data object for whole stack + * + * @param x + * @param vals - the stack values, use at least 2 + */ + public BarEntry(float x, float[] vals) { + super(x, calcSum(vals)); + + this.mYVals = vals; + calcPosNegSum(); + calcRanges(); + } + + /** + * Constructor for stacked bar entries. One data object for whole stack + * + * @param x + * @param vals - the stack values, use at least 2 + * @param data - Spot for additional data this Entry represents. + */ + public BarEntry(float x, float[] vals, Object data) { + super(x, calcSum(vals), data); + + this.mYVals = vals; + calcPosNegSum(); + calcRanges(); + } + + /** + * Constructor for stacked bar entries. One data object for whole stack + * + * @param x + * @param vals - the stack values, use at least 2 + * @param icon - icon image + */ + public BarEntry(float x, float[] vals, Drawable icon) { + super(x, calcSum(vals), icon); + + this.mYVals = vals; + calcPosNegSum(); + calcRanges(); + } + + /** + * Constructor for stacked bar entries. One data object for whole stack + * + * @param x + * @param vals - the stack values, use at least 2 + * @param icon - icon image + * @param data - Spot for additional data this Entry represents. + */ + public BarEntry(float x, float[] vals, Drawable icon, Object data) { + super(x, calcSum(vals), icon, data); + + this.mYVals = vals; + calcPosNegSum(); + calcRanges(); + } + + /** + * Returns an exact copy of the BarEntry. + */ + public BarEntry copy() { + + BarEntry copied = new BarEntry(getX(), getY(), getData()); + copied.setVals(mYVals); + return copied; + } + + /** + * Returns the stacked values this BarEntry represents, or null, if only a single value is represented (then, use + * getY()). + * + * @return + */ + public float[] getYVals() { + return mYVals; + } + + /** + * Set the array of values this BarEntry should represent. + * + * @param vals + */ + public void setVals(float[] vals) { + setY(calcSum(vals)); + mYVals = vals; + calcPosNegSum(); + calcRanges(); + } + + /** + * Returns the value of this BarEntry. If the entry is stacked, it returns the positive sum of all values. + * + * @return + */ + @Override + public float getY() { + return super.getY(); + } + + /** + * Returns the ranges of the individual stack-entries. Will return null if this entry is not stacked. + * + * @return + */ + public Range[] getRanges() { + return mRanges; + } + + /** + * Returns true if this BarEntry is stacked (has a values array), false if not. + * + * @return + */ + public boolean isStacked() { + return mYVals != null; + } + + /** + * Use `getSumBelow(stackIndex)` instead. + */ + @Deprecated + public float getBelowSum(int stackIndex) { + return getSumBelow(stackIndex); + } + + public float getSumBelow(int stackIndex) { + + if (mYVals == null) + return 0; + + float remainder = 0f; + int index = mYVals.length - 1; + + while (index > stackIndex && index >= 0) { + remainder += mYVals[index]; + index--; + } + + return remainder; + } + + /** + * Reuturns the sum of all positive values this entry (if stacked) contains. + * + * @return + */ + public float getPositiveSum() { + return mPositiveSum; + } + + /** + * Returns the sum of all negative values this entry (if stacked) contains. (this is a positive number) + * + * @return + */ + public float getNegativeSum() { + return mNegativeSum; + } + + private void calcPosNegSum() { + + if (mYVals == null) { + mNegativeSum = 0; + mPositiveSum = 0; + return; + } + + float sumNeg = 0f; + float sumPos = 0f; + + for (float f : mYVals) { + if (f <= 0f) + sumNeg += Math.abs(f); + else + sumPos += f; + } + + mNegativeSum = sumNeg; + mPositiveSum = sumPos; + } + + /** + * Calculates the sum across all values of the given stack. + * + * @param vals + * @return + */ + private static float calcSum(float[] vals) { + + if (vals == null) + return 0f; + + float sum = 0f; + + for (float f : vals) + sum += f; + + return sum; + } + + protected void calcRanges() { + + float[] values = getYVals(); + + if (values == null || values.length == 0) + return; + + mRanges = new Range[values.length]; + + float negRemain = -getNegativeSum(); + float posRemain = 0f; + + for (int i = 0; i < mRanges.length; i++) { + + float value = values[i]; + + if (value < 0) { + mRanges[i] = new Range(negRemain, negRemain - value); + negRemain -= value; + } else { + mRanges[i] = new Range(posRemain, posRemain + value); + posRemain += value; + } + } + } +} + + diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarLineScatterCandleBubbleData.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarLineScatterCandleBubbleData.java new file mode 100644 index 0000000..c09eade --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarLineScatterCandleBubbleData.java @@ -0,0 +1,27 @@ + +package com.github.mikephil.charting.data; + +import com.github.mikephil.charting.interfaces.datasets.IBarLineScatterCandleBubbleDataSet; + +import java.util.List; + +/** + * Baseclass for all Line, Bar, Scatter, Candle and Bubble data. + * + * @author Philipp Jahoda + */ +public abstract class BarLineScatterCandleBubbleData> + extends ChartData { + + public BarLineScatterCandleBubbleData() { + super(); + } + + public BarLineScatterCandleBubbleData(T... sets) { + super(sets); + } + + public BarLineScatterCandleBubbleData(List sets) { + super(sets); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarLineScatterCandleBubbleDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarLineScatterCandleBubbleDataSet.java new file mode 100644 index 0000000..eab6dcc --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BarLineScatterCandleBubbleDataSet.java @@ -0,0 +1,48 @@ + +package com.github.mikephil.charting.data; + +import android.graphics.Color; + +import com.github.mikephil.charting.interfaces.datasets.IBarLineScatterCandleBubbleDataSet; + +import java.util.List; + +/** + * Baseclass of all DataSets for Bar-, Line-, Scatter- and CandleStickChart. + * + * @author Philipp Jahoda + */ +public abstract class BarLineScatterCandleBubbleDataSet + extends DataSet + implements IBarLineScatterCandleBubbleDataSet { + + /** + * default highlight color + */ + protected int mHighLightColor = Color.rgb(255, 187, 115); + + public BarLineScatterCandleBubbleDataSet(List yVals, String label) { + super(yVals, label); + } + + /** + * Sets the color that is used for drawing the highlight indicators. Dont + * forget to resolve the color using getResources().getColor(...) or + * Color.rgb(...). + * + * @param color + */ + public void setHighLightColor(int color) { + mHighLightColor = color; + } + + @Override + public int getHighLightColor() { + return mHighLightColor; + } + + protected void copy(BarLineScatterCandleBubbleDataSet barLineScatterCandleBubbleDataSet) { + super.copy(barLineScatterCandleBubbleDataSet); + barLineScatterCandleBubbleDataSet.mHighLightColor = mHighLightColor; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/BaseDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BaseDataSet.java new file mode 100644 index 0000000..7e7445c --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BaseDataSet.java @@ -0,0 +1,506 @@ +package com.github.mikephil.charting.data; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.DashPathEffect; +import android.graphics.Typeface; + +import com.github.mikephil.charting.components.Legend; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.formatter.IValueFormatter; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Philipp Jahoda on 21/10/15. + * This is the base dataset of all DataSets. It's purpose is to implement critical methods + * provided by the IDataSet interface. + */ +public abstract class BaseDataSet implements IDataSet { + + /** + * List representing all colors that are used for this DataSet + */ + protected List mColors = null; + + /** + * List representing all colors that are used for drawing the actual values for this DataSet + */ + protected List mValueColors = null; + + /** + * label that describes the DataSet or the data the DataSet represents + */ + private String mLabel = "DataSet"; + + /** + * this specifies which axis this DataSet should be plotted against + */ + protected YAxis.AxisDependency mAxisDependency = YAxis.AxisDependency.LEFT; + + /** + * if true, value highlightning is enabled + */ + protected boolean mHighlightEnabled = true; + + /** + * custom formatter that is used instead of the auto-formatter if set + */ + protected transient IValueFormatter mValueFormatter; + + /** + * the typeface used for the value text + */ + protected Typeface mValueTypeface; + + private Legend.LegendForm mForm = Legend.LegendForm.DEFAULT; + private float mFormSize = Float.NaN; + private float mFormLineWidth = Float.NaN; + private DashPathEffect mFormLineDashEffect = null; + + /** + * if true, y-values are drawn on the chart + */ + protected boolean mDrawValues = true; + + /** + * if true, y-icons are drawn on the chart + */ + protected boolean mDrawIcons = true; + + /** + * the offset for drawing icons (in dp) + */ + protected MPPointF mIconsOffset = new MPPointF(); + + /** + * the size of the value-text labels + */ + protected float mValueTextSize = 17f; + + /** + * flag that indicates if the DataSet is visible or not + */ + protected boolean mVisible = true; + + /** + * Default constructor. + */ + public BaseDataSet() { + mColors = new ArrayList(); + mValueColors = new ArrayList(); + + // default color + mColors.add(Color.rgb(140, 234, 255)); + mValueColors.add(Color.BLACK); + } + + /** + * Constructor with label. + * + * @param label + */ + public BaseDataSet(String label) { + this(); + this.mLabel = label; + } + + /** + * Use this method to tell the data set that the underlying data has changed. + */ + public void notifyDataSetChanged() { + calcMinMax(); + } + + + /** + * ###### ###### COLOR GETTING RELATED METHODS ##### ###### + */ + + @Override + public List getColors() { + return mColors; + } + + public List getValueColors() { + return mValueColors; + } + + @Override + public int getColor() { + return mColors.get(0); + } + + @Override + public int getColor(int index) { + return mColors.get(index % mColors.size()); + } + + /** + * ###### ###### COLOR SETTING RELATED METHODS ##### ###### + */ + + /** + * Sets the colors that should be used fore this DataSet. Colors are reused + * as soon as the number of Entries the DataSet represents is higher than + * the size of the colors array. If you are using colors from the resources, + * make sure that the colors are already prepared (by calling + * getResources().getColor(...)) before adding them to the DataSet. + * + * @param colors + */ + public void setColors(List colors) { + this.mColors = colors; + } + + /** + * Sets the colors that should be used fore this DataSet. Colors are reused + * as soon as the number of Entries the DataSet represents is higher than + * the size of the colors array. If you are using colors from the resources, + * make sure that the colors are already prepared (by calling + * getResources().getColor(...)) before adding them to the DataSet. + * + * @param colors + */ + public void setColors(int... colors) { + this.mColors = ColorTemplate.createColors(colors); + } + + /** + * Sets the colors that should be used fore this DataSet. Colors are reused + * as soon as the number of Entries the DataSet represents is higher than + * the size of the colors array. You can use + * "new int[] { R.color.red, R.color.green, ... }" to provide colors for + * this method. Internally, the colors are resolved using + * getResources().getColor(...) + * + * @param colors + */ + public void setColors(int[] colors, Context c) { + + if (mColors == null) { + mColors = new ArrayList<>(); + } + + mColors.clear(); + + for (int color : colors) { + mColors.add(c.getResources().getColor(color)); + } + } + + /** + * Adds a new color to the colors array of the DataSet. + * + * @param color + */ + public void addColor(int color) { + if (mColors == null) + mColors = new ArrayList(); + mColors.add(color); + } + + /** + * Sets the one and ONLY color that should be used for this DataSet. + * Internally, this recreates the colors array and adds the specified color. + * + * @param color + */ + public void setColor(int color) { + resetColors(); + mColors.add(color); + } + + /** + * Sets a color with a specific alpha value. + * + * @param color + * @param alpha from 0-255 + */ + public void setColor(int color, int alpha) { + setColor(Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color))); + } + + /** + * Sets colors with a specific alpha value. + * + * @param colors + * @param alpha + */ + public void setColors(int[] colors, int alpha) { + resetColors(); + for (int color : colors) { + addColor(Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color))); + } + } + + /** + * Resets all colors of this DataSet and recreates the colors array. + */ + public void resetColors() { + if (mColors == null) { + mColors = new ArrayList(); + } + mColors.clear(); + } + + /** + * ###### ###### OTHER STYLING RELATED METHODS ##### ###### + */ + + @Override + public void setLabel(String label) { + mLabel = label; + } + + @Override + public String getLabel() { + return mLabel; + } + + @Override + public void setHighlightEnabled(boolean enabled) { + mHighlightEnabled = enabled; + } + + @Override + public boolean isHighlightEnabled() { + return mHighlightEnabled; + } + + @Override + public void setValueFormatter(IValueFormatter f) { + + if (f == null) + return; + else + mValueFormatter = f; + } + + @Override + public IValueFormatter getValueFormatter() { + if (needsFormatter()) + return Utils.getDefaultValueFormatter(); + return mValueFormatter; + } + + @Override + public boolean needsFormatter() { + return mValueFormatter == null; + } + + @Override + public void setValueTextColor(int color) { + mValueColors.clear(); + mValueColors.add(color); + } + + @Override + public void setValueTextColors(List colors) { + mValueColors = colors; + } + + @Override + public void setValueTypeface(Typeface tf) { + mValueTypeface = tf; + } + + @Override + public void setValueTextSize(float size) { + mValueTextSize = Utils.convertDpToPixel(size); + } + + @Override + public int getValueTextColor() { + return mValueColors.get(0); + } + + @Override + public int getValueTextColor(int index) { + return mValueColors.get(index % mValueColors.size()); + } + + @Override + public Typeface getValueTypeface() { + return mValueTypeface; + } + + @Override + public float getValueTextSize() { + return mValueTextSize; + } + + public void setForm(Legend.LegendForm form) { + mForm = form; + } + + @Override + public Legend.LegendForm getForm() { + return mForm; + } + + public void setFormSize(float formSize) { + mFormSize = formSize; + } + + @Override + public float getFormSize() { + return mFormSize; + } + + public void setFormLineWidth(float formLineWidth) { + mFormLineWidth = formLineWidth; + } + + @Override + public float getFormLineWidth() { + return mFormLineWidth; + } + + public void setFormLineDashEffect(DashPathEffect dashPathEffect) { + mFormLineDashEffect = dashPathEffect; + } + + @Override + public DashPathEffect getFormLineDashEffect() { + return mFormLineDashEffect; + } + + @Override + public void setDrawValues(boolean enabled) { + this.mDrawValues = enabled; + } + + @Override + public boolean isDrawValuesEnabled() { + return mDrawValues; + } + + @Override + public void setDrawIcons(boolean enabled) { + mDrawIcons = enabled; + } + + @Override + public boolean isDrawIconsEnabled() { + return mDrawIcons; + } + + @Override + public void setIconsOffset(MPPointF offsetDp) { + + mIconsOffset.x = offsetDp.x; + mIconsOffset.y = offsetDp.y; + } + + @Override + public MPPointF getIconsOffset() { + return mIconsOffset; + } + + @Override + public void setVisible(boolean visible) { + mVisible = visible; + } + + @Override + public boolean isVisible() { + return mVisible; + } + + @Override + public YAxis.AxisDependency getAxisDependency() { + return mAxisDependency; + } + + @Override + public void setAxisDependency(YAxis.AxisDependency dependency) { + mAxisDependency = dependency; + } + + + /** + * ###### ###### DATA RELATED METHODS ###### ###### + */ + + @Override + public int getIndexInEntries(int xIndex) { + + for (int i = 0; i < getEntryCount(); i++) { + if (xIndex == getEntryForIndex(i).getX()) + return i; + } + + return -1; + } + + @Override + public boolean removeFirst() { + + if (getEntryCount() > 0) { + + T entry = getEntryForIndex(0); + return removeEntry(entry); + } else + return false; + } + + @Override + public boolean removeLast() { + + if (getEntryCount() > 0) { + + T e = getEntryForIndex(getEntryCount() - 1); + return removeEntry(e); + } else + return false; + } + + @Override + public boolean removeEntryByXValue(float xValue) { + + T e = getEntryForXValue(xValue, Float.NaN); + return removeEntry(e); + } + + @Override + public boolean removeEntry(int index) { + + T e = getEntryForIndex(index); + return removeEntry(e); + } + + @Override + public boolean contains(T e) { + + for (int i = 0; i < getEntryCount(); i++) { + if (getEntryForIndex(i).equals(e)) + return true; + } + + return false; + } + + protected void copy(BaseDataSet baseDataSet) { + baseDataSet.mAxisDependency = mAxisDependency; + baseDataSet.mColors = mColors; + baseDataSet.mDrawIcons = mDrawIcons; + baseDataSet.mDrawValues = mDrawValues; + baseDataSet.mForm = mForm; + baseDataSet.mFormLineDashEffect = mFormLineDashEffect; + baseDataSet.mFormLineWidth = mFormLineWidth; + baseDataSet.mFormSize = mFormSize; + baseDataSet.mHighlightEnabled = mHighlightEnabled; + baseDataSet.mIconsOffset = mIconsOffset; + baseDataSet.mValueColors = mValueColors; + baseDataSet.mValueFormatter = mValueFormatter; + baseDataSet.mValueColors = mValueColors; + baseDataSet.mValueTextSize = mValueTextSize; + baseDataSet.mVisible = mVisible; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/BaseEntry.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BaseEntry.java new file mode 100644 index 0000000..eb1ada8 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BaseEntry.java @@ -0,0 +1,97 @@ +package com.github.mikephil.charting.data; + +import android.graphics.drawable.Drawable; + +/** + * Created by Philipp Jahoda on 02/06/16. + */ +public abstract class BaseEntry { + + /** the y value */ + private float y = 0f; + + /** optional spot for additional data this Entry represents */ + private Object mData = null; + + /** optional icon image */ + private Drawable mIcon = null; + + public BaseEntry() { + + } + + public BaseEntry(float y) { + this.y = y; + } + + public BaseEntry(float y, Object data) { + this(y); + this.mData = data; + } + + public BaseEntry(float y, Drawable icon) { + this(y); + this.mIcon = icon; + } + + public BaseEntry(float y, Drawable icon, Object data) { + this(y); + this.mIcon = icon; + this.mData = data; + } + + /** + * Returns the y value of this Entry. + * + * @return + */ + public float getY() { + return y; + } + + /** + * Sets the icon drawable + * + * @param icon + */ + public void setIcon(Drawable icon) { + this.mIcon = icon; + } + + /** + * Returns the icon of this Entry. + * + * @return + */ + public Drawable getIcon() { + return mIcon; + } + + /** + * Sets the y-value for the Entry. + * + * @param y + */ + public void setY(float y) { + this.y = y; + } + + /** + * Returns the data, additional information that this Entry represents, or + * null, if no data has been specified. + * + * @return + */ + public Object getData() { + return mData; + } + + /** + * Sets additional data this Entry should represent. + * + * @param data + */ + public void setData(Object data) { + this.mData = data; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/BubbleData.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BubbleData.java new file mode 100644 index 0000000..8fb72f1 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BubbleData.java @@ -0,0 +1,34 @@ + +package com.github.mikephil.charting.data; + +import com.github.mikephil.charting.interfaces.datasets.IBubbleDataSet; + +import java.util.List; + +public class BubbleData extends BarLineScatterCandleBubbleData { + + public BubbleData() { + super(); + } + + public BubbleData(IBubbleDataSet... dataSets) { + super(dataSets); + } + + public BubbleData(List dataSets) { + super(dataSets); + } + + + /** + * Sets the width of the circle that surrounds the bubble when highlighted + * for all DataSet objects this data object contains, in dp. + * + * @param width + */ + public void setHighlightCircleWidth(float width) { + for (IBubbleDataSet set : mDataSets) { + set.setHighlightCircleWidth(width); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/BubbleDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BubbleDataSet.java new file mode 100644 index 0000000..9ef87fb --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BubbleDataSet.java @@ -0,0 +1,71 @@ + +package com.github.mikephil.charting.data; + +import com.github.mikephil.charting.interfaces.datasets.IBubbleDataSet; +import com.github.mikephil.charting.utils.Utils; + +import java.util.ArrayList; +import java.util.List; + +public class BubbleDataSet extends BarLineScatterCandleBubbleDataSet implements IBubbleDataSet { + + protected float mMaxSize; + protected boolean mNormalizeSize = true; + + private float mHighlightCircleWidth = 2.5f; + + public BubbleDataSet(List yVals, String label) { + super(yVals, label); + } + + @Override + public void setHighlightCircleWidth(float width) { + mHighlightCircleWidth = Utils.convertDpToPixel(width); + } + + @Override + public float getHighlightCircleWidth() { + return mHighlightCircleWidth; + } + + @Override + protected void calcMinMax(BubbleEntry e) { + super.calcMinMax(e); + + final float size = e.getSize(); + + if (size > mMaxSize) { + mMaxSize = size; + } + } + + @Override + public DataSet copy() { + List entries = new ArrayList(); + for (int i = 0; i < mEntries.size(); i++) { + entries.add(mEntries.get(i).copy()); + } + BubbleDataSet copied = new BubbleDataSet(entries, getLabel()); + copy(copied); + return copied; + } + + protected void copy(BubbleDataSet bubbleDataSet) { + bubbleDataSet.mHighlightCircleWidth = mHighlightCircleWidth; + bubbleDataSet.mNormalizeSize = mNormalizeSize; + } + + @Override + public float getMaxSize() { + return mMaxSize; + } + + @Override + public boolean isNormalizeSizeEnabled() { + return mNormalizeSize; + } + + public void setNormalizeSizeEnabled(boolean normalizeSize) { + mNormalizeSize = normalizeSize; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/BubbleEntry.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BubbleEntry.java new file mode 100644 index 0000000..35d8330 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/BubbleEntry.java @@ -0,0 +1,91 @@ + +package com.github.mikephil.charting.data; + +import android.annotation.SuppressLint; +import android.graphics.drawable.Drawable; + +/** + * Subclass of Entry that holds a value for one entry in a BubbleChart. Bubble + * chart implementation: Copyright 2015 Pierre-Marc Airoldi Licensed under + * Apache License 2.0 + * + * @author Philipp Jahoda + */ +@SuppressLint("ParcelCreator") +public class BubbleEntry extends Entry { + + /** size value */ + private float mSize = 0f; + + /** + * Constructor. + * + * @param x The value on the x-axis. + * @param y The value on the y-axis. + * @param size The size of the bubble. + */ + public BubbleEntry(float x, float y, float size) { + super(x, y); + this.mSize = size; + } + + /** + * Constructor. + * + * @param x The value on the x-axis. + * @param y The value on the y-axis. + * @param size The size of the bubble. + * @param data Spot for additional data this Entry represents. + */ + public BubbleEntry(float x, float y, float size, Object data) { + super(x, y, data); + this.mSize = size; + } + + /** + * Constructor. + * + * @param x The value on the x-axis. + * @param y The value on the y-axis. + * @param size The size of the bubble. + * @param icon Icon image + */ + public BubbleEntry(float x, float y, float size, Drawable icon) { + super(x, y, icon); + this.mSize = size; + } + + /** + * Constructor. + * + * @param x The value on the x-axis. + * @param y The value on the y-axis. + * @param size The size of the bubble. + * @param icon Icon image + * @param data Spot for additional data this Entry represents. + */ + public BubbleEntry(float x, float y, float size, Drawable icon, Object data) { + super(x, y, icon, data); + this.mSize = size; + } + + public BubbleEntry copy() { + + BubbleEntry c = new BubbleEntry(getX(), getY(), mSize, getData()); + return c; + } + + /** + * Returns the size of this entry (the size of the bubble). + * + * @return + */ + public float getSize() { + return mSize; + } + + public void setSize(float size) { + this.mSize = size; + } + +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/CandleData.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/CandleData.java new file mode 100644 index 0000000..1e90db2 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/CandleData.java @@ -0,0 +1,21 @@ +package com.github.mikephil.charting.data; + +import com.github.mikephil.charting.interfaces.datasets.ICandleDataSet; + +import java.util.ArrayList; +import java.util.List; + +public class CandleData extends BarLineScatterCandleBubbleData { + + public CandleData() { + super(); + } + + public CandleData(List dataSets) { + super(dataSets); + } + + public CandleData(ICandleDataSet... dataSets) { + super(dataSets); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/CandleDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/CandleDataSet.java new file mode 100644 index 0000000..dcd5b76 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/CandleDataSet.java @@ -0,0 +1,295 @@ + +package com.github.mikephil.charting.data; + +import android.graphics.Paint; + +import com.github.mikephil.charting.interfaces.datasets.ICandleDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.Utils; + +import java.util.ArrayList; +import java.util.List; + +/** + * DataSet for the CandleStickChart. + * + * @author Philipp Jahoda + */ +public class CandleDataSet extends LineScatterCandleRadarDataSet implements ICandleDataSet { + + /** + * the width of the shadow of the candle + */ + private float mShadowWidth = 3f; + + /** + * should the candle bars show? + * when false, only "ticks" will show + *

+ * - default: true + */ + private boolean mShowCandleBar = true; + + /** + * the space between the candle entries, default 0.1f (10%) + */ + private float mBarSpace = 0.1f; + + /** + * use candle color for the shadow + */ + private boolean mShadowColorSameAsCandle = false; + + /** + * paint style when open < close + * increasing candlesticks are traditionally hollow + */ + protected Paint.Style mIncreasingPaintStyle = Paint.Style.STROKE; + + /** + * paint style when open > close + * descreasing candlesticks are traditionally filled + */ + protected Paint.Style mDecreasingPaintStyle = Paint.Style.FILL; + + /** + * color for open == close + */ + protected int mNeutralColor = ColorTemplate.COLOR_SKIP; + + /** + * color for open < close + */ + protected int mIncreasingColor = ColorTemplate.COLOR_SKIP; + + /** + * color for open > close + */ + protected int mDecreasingColor = ColorTemplate.COLOR_SKIP; + + /** + * shadow line color, set -1 for backward compatibility and uses default + * color + */ + protected int mShadowColor = ColorTemplate.COLOR_SKIP; + + public CandleDataSet(List yVals, String label) { + super(yVals, label); + } + + @Override + public DataSet copy() { + List entries = new ArrayList(); + for (int i = 0; i < mEntries.size(); i++) { + entries.add(mEntries.get(i).copy()); + } + CandleDataSet copied = new CandleDataSet(entries, getLabel()); + copy(copied); + return copied; + } + + protected void copy(CandleDataSet candleDataSet) { + super.copy(candleDataSet); + candleDataSet.mShadowWidth = mShadowWidth; + candleDataSet.mShowCandleBar = mShowCandleBar; + candleDataSet.mBarSpace = mBarSpace; + candleDataSet.mShadowColorSameAsCandle = mShadowColorSameAsCandle; + candleDataSet.mHighLightColor = mHighLightColor; + candleDataSet.mIncreasingPaintStyle = mIncreasingPaintStyle; + candleDataSet.mDecreasingPaintStyle = mDecreasingPaintStyle; + candleDataSet.mNeutralColor = mNeutralColor; + candleDataSet.mIncreasingColor = mIncreasingColor; + candleDataSet.mDecreasingColor = mDecreasingColor; + candleDataSet.mShadowColor = mShadowColor; + } + + @Override + protected void calcMinMax(CandleEntry e) { + + if (e.getLow() < mYMin) + mYMin = e.getLow(); + + if (e.getHigh() > mYMax) + mYMax = e.getHigh(); + + calcMinMaxX(e); + } + + @Override + protected void calcMinMaxY(CandleEntry e) { + + if (e.getHigh() < mYMin) + mYMin = e.getHigh(); + + if (e.getHigh() > mYMax) + mYMax = e.getHigh(); + + if (e.getLow() < mYMin) + mYMin = e.getLow(); + + if (e.getLow() > mYMax) + mYMax = e.getLow(); + } + + /** + * Sets the space that is left out on the left and right side of each + * candle, default 0.1f (10%), max 0.45f, min 0f + * + * @param space + */ + public void setBarSpace(float space) { + + if (space < 0f) + space = 0f; + if (space > 0.45f) + space = 0.45f; + + mBarSpace = space; + } + + @Override + public float getBarSpace() { + return mBarSpace; + } + + /** + * Sets the width of the candle-shadow-line in pixels. Default 3f. + * + * @param width + */ + public void setShadowWidth(float width) { + mShadowWidth = Utils.convertDpToPixel(width); + } + + @Override + public float getShadowWidth() { + return mShadowWidth; + } + + /** + * Sets whether the candle bars should show? + * + * @param showCandleBar + */ + public void setShowCandleBar(boolean showCandleBar) { + mShowCandleBar = showCandleBar; + } + + @Override + public boolean getShowCandleBar() { + return mShowCandleBar; + } + + // TODO + /** + * It is necessary to implement ColorsList class that will encapsulate + * colors list functionality, because It's wrong to copy paste setColor, + * addColor, ... resetColors for each time when we want to add a coloring + * options for one of objects + * + * @author Mesrop + */ + + /** BELOW THIS COLOR HANDLING */ + + /** + * Sets the one and ONLY color that should be used for this DataSet when + * open == close. + * + * @param color + */ + public void setNeutralColor(int color) { + mNeutralColor = color; + } + + @Override + public int getNeutralColor() { + return mNeutralColor; + } + + /** + * Sets the one and ONLY color that should be used for this DataSet when + * open <= close. + * + * @param color + */ + public void setIncreasingColor(int color) { + mIncreasingColor = color; + } + + @Override + public int getIncreasingColor() { + return mIncreasingColor; + } + + /** + * Sets the one and ONLY color that should be used for this DataSet when + * open > close. + * + * @param color + */ + public void setDecreasingColor(int color) { + mDecreasingColor = color; + } + + @Override + public int getDecreasingColor() { + return mDecreasingColor; + } + + @Override + public Paint.Style getIncreasingPaintStyle() { + return mIncreasingPaintStyle; + } + + /** + * Sets paint style when open < close + * + * @param paintStyle + */ + public void setIncreasingPaintStyle(Paint.Style paintStyle) { + this.mIncreasingPaintStyle = paintStyle; + } + + @Override + public Paint.Style getDecreasingPaintStyle() { + return mDecreasingPaintStyle; + } + + /** + * Sets paint style when open > close + * + * @param decreasingPaintStyle + */ + public void setDecreasingPaintStyle(Paint.Style decreasingPaintStyle) { + this.mDecreasingPaintStyle = decreasingPaintStyle; + } + + @Override + public int getShadowColor() { + return mShadowColor; + } + + /** + * Sets shadow color for all entries + * + * @param shadowColor + */ + public void setShadowColor(int shadowColor) { + this.mShadowColor = shadowColor; + } + + @Override + public boolean getShadowColorSameAsCandle() { + return mShadowColorSameAsCandle; + } + + /** + * Sets shadow color to be the same color as the candle color + * + * @param shadowColorSameAsCandle + */ + public void setShadowColorSameAsCandle(boolean shadowColorSameAsCandle) { + this.mShadowColorSameAsCandle = shadowColorSameAsCandle; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/CandleEntry.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/CandleEntry.java new file mode 100644 index 0000000..5a04995 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/CandleEntry.java @@ -0,0 +1,193 @@ + +package com.github.mikephil.charting.data; + +import android.annotation.SuppressLint; +import android.graphics.drawable.Drawable; + +/** + * Subclass of Entry that holds all values for one entry in a CandleStickChart. + * + * @author Philipp Jahoda + */ +@SuppressLint("ParcelCreator") +public class CandleEntry extends Entry { + + /** shadow-high value */ + private float mShadowHigh = 0f; + + /** shadow-low value */ + private float mShadowLow = 0f; + + /** close value */ + private float mClose = 0f; + + /** open value */ + private float mOpen = 0f; + + /** + * Constructor. + * + * @param x The value on the x-axis + * @param shadowH The (shadow) high value + * @param shadowL The (shadow) low value + * @param open The open value + * @param close The close value + */ + public CandleEntry(float x, float shadowH, float shadowL, float open, float close) { + super(x, (shadowH + shadowL) / 2f); + + this.mShadowHigh = shadowH; + this.mShadowLow = shadowL; + this.mOpen = open; + this.mClose = close; + } + + /** + * Constructor. + * + * @param x The value on the x-axis + * @param shadowH The (shadow) high value + * @param shadowL The (shadow) low value + * @param open + * @param close + * @param data Spot for additional data this Entry represents + */ + public CandleEntry(float x, float shadowH, float shadowL, float open, float close, + Object data) { + super(x, (shadowH + shadowL) / 2f, data); + + this.mShadowHigh = shadowH; + this.mShadowLow = shadowL; + this.mOpen = open; + this.mClose = close; + } + + /** + * Constructor. + * + * @param x The value on the x-axis + * @param shadowH The (shadow) high value + * @param shadowL The (shadow) low value + * @param open + * @param close + * @param icon Icon image + */ + public CandleEntry(float x, float shadowH, float shadowL, float open, float close, + Drawable icon) { + super(x, (shadowH + shadowL) / 2f, icon); + + this.mShadowHigh = shadowH; + this.mShadowLow = shadowL; + this.mOpen = open; + this.mClose = close; + } + + /** + * Constructor. + * + * @param x The value on the x-axis + * @param shadowH The (shadow) high value + * @param shadowL The (shadow) low value + * @param open + * @param close + * @param icon Icon image + * @param data Spot for additional data this Entry represents + */ + public CandleEntry(float x, float shadowH, float shadowL, float open, float close, + Drawable icon, Object data) { + super(x, (shadowH + shadowL) / 2f, icon, data); + + this.mShadowHigh = shadowH; + this.mShadowLow = shadowL; + this.mOpen = open; + this.mClose = close; + } + + /** + * Returns the overall range (difference) between shadow-high and + * shadow-low. + * + * @return + */ + public float getShadowRange() { + return Math.abs(mShadowHigh - mShadowLow); + } + + /** + * Returns the body size (difference between open and close). + * + * @return + */ + public float getBodyRange() { + return Math.abs(mOpen - mClose); + } + + /** + * Returns the center value of the candle. (Middle value between high and + * low) + */ + @Override + public float getY() { + return super.getY(); + } + + public CandleEntry copy() { + + CandleEntry c = new CandleEntry(getX(), mShadowHigh, mShadowLow, mOpen, + mClose, getData()); + + return c; + } + + /** + * Returns the upper shadows highest value. + * + * @return + */ + public float getHigh() { + return mShadowHigh; + } + + public void setHigh(float mShadowHigh) { + this.mShadowHigh = mShadowHigh; + } + + /** + * Returns the lower shadows lowest value. + * + * @return + */ + public float getLow() { + return mShadowLow; + } + + public void setLow(float mShadowLow) { + this.mShadowLow = mShadowLow; + } + + /** + * Returns the bodys close value. + * + * @return + */ + public float getClose() { + return mClose; + } + + public void setClose(float mClose) { + this.mClose = mClose; + } + + /** + * Returns the bodys open value. + * + * @return + */ + public float getOpen() { + return mOpen; + } + + public void setOpen(float mOpen) { + this.mOpen = mOpen; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/ChartData.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/ChartData.java new file mode 100644 index 0000000..bfc5ed7 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/ChartData.java @@ -0,0 +1,821 @@ + +package com.github.mikephil.charting.data; + +import android.graphics.Typeface; +import android.util.Log; + +import com.github.mikephil.charting.components.YAxis.AxisDependency; +import com.github.mikephil.charting.formatter.IValueFormatter; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class that holds all relevant data that represents the chart. That involves + * at least one (or more) DataSets, and an array of x-values. + * + * @author Philipp Jahoda + */ +public abstract class ChartData> { + + /** + * maximum y-value in the value array across all axes + */ + protected float mYMax = -Float.MAX_VALUE; + + /** + * the minimum y-value in the value array across all axes + */ + protected float mYMin = Float.MAX_VALUE; + + /** + * maximum x-value in the value array + */ + protected float mXMax = -Float.MAX_VALUE; + + /** + * minimum x-value in the value array + */ + protected float mXMin = Float.MAX_VALUE; + + + protected float mLeftAxisMax = -Float.MAX_VALUE; + + protected float mLeftAxisMin = Float.MAX_VALUE; + + protected float mRightAxisMax = -Float.MAX_VALUE; + + protected float mRightAxisMin = Float.MAX_VALUE; + + /** + * array that holds all DataSets the ChartData object represents + */ + protected List mDataSets; + + /** + * Default constructor. + */ + public ChartData() { + mDataSets = new ArrayList(); + } + + /** + * Constructor taking single or multiple DataSet objects. + * + * @param dataSets + */ + public ChartData(T... dataSets) { + mDataSets = arrayToList(dataSets); + notifyDataChanged(); + } + + /** + * Created because Arrays.asList(...) does not support modification. + * + * @param array + * @return + */ + private List arrayToList(T[] array) { + + List list = new ArrayList<>(); + + for (T set : array) { + list.add(set); + } + + return list; + } + + /** + * constructor for chart data + * + * @param sets the dataset array + */ + public ChartData(List sets) { + this.mDataSets = sets; + notifyDataChanged(); + } + + /** + * Call this method to let the ChartData know that the underlying data has + * changed. Calling this performs all necessary recalculations needed when + * the contained data has changed. + */ + public void notifyDataChanged() { + calcMinMax(); + } + + /** + * Calc minimum and maximum y-values over all DataSets. + * Tell DataSets to recalculate their min and max y-values, this is only needed for autoScaleMinMax. + * + * @param fromX the x-value to start the calculation from + * @param toX the x-value to which the calculation should be performed + */ + public void calcMinMaxY(float fromX, float toX) { + + for (T set : mDataSets) { + set.calcMinMaxY(fromX, toX); + } + + // apply the new data + calcMinMax(); + } + + /** + * Calc minimum and maximum values (both x and y) over all DataSets. + */ + protected void calcMinMax() { + + if (mDataSets == null) + return; + + mYMax = -Float.MAX_VALUE; + mYMin = Float.MAX_VALUE; + mXMax = -Float.MAX_VALUE; + mXMin = Float.MAX_VALUE; + + for (T set : mDataSets) { + calcMinMax(set); + } + + mLeftAxisMax = -Float.MAX_VALUE; + mLeftAxisMin = Float.MAX_VALUE; + mRightAxisMax = -Float.MAX_VALUE; + mRightAxisMin = Float.MAX_VALUE; + + // left axis + T firstLeft = getFirstLeft(mDataSets); + + if (firstLeft != null) { + + mLeftAxisMax = firstLeft.getYMax(); + mLeftAxisMin = firstLeft.getYMin(); + + for (T dataSet : mDataSets) { + if (dataSet.getAxisDependency() == AxisDependency.LEFT) { + if (dataSet.getYMin() < mLeftAxisMin) + mLeftAxisMin = dataSet.getYMin(); + + if (dataSet.getYMax() > mLeftAxisMax) + mLeftAxisMax = dataSet.getYMax(); + } + } + } + + // right axis + T firstRight = getFirstRight(mDataSets); + + if (firstRight != null) { + + mRightAxisMax = firstRight.getYMax(); + mRightAxisMin = firstRight.getYMin(); + + for (T dataSet : mDataSets) { + if (dataSet.getAxisDependency() == AxisDependency.RIGHT) { + if (dataSet.getYMin() < mRightAxisMin) + mRightAxisMin = dataSet.getYMin(); + + if (dataSet.getYMax() > mRightAxisMax) + mRightAxisMax = dataSet.getYMax(); + } + } + } + } + + /** ONLY GETTERS AND SETTERS BELOW THIS */ + + /** + * returns the number of LineDataSets this object contains + * + * @return + */ + public int getDataSetCount() { + if (mDataSets == null) + return 0; + return mDataSets.size(); + } + + /** + * Returns the smallest y-value the data object contains. + * + * @return + */ + public float getYMin() { + return mYMin; + } + + /** + * Returns the minimum y-value for the specified axis. + * + * @param axis + * @return + */ + public float getYMin(AxisDependency axis) { + if (axis == AxisDependency.LEFT) { + + if (mLeftAxisMin == Float.MAX_VALUE) { + return mRightAxisMin; + } else + return mLeftAxisMin; + } else { + if (mRightAxisMin == Float.MAX_VALUE) { + return mLeftAxisMin; + } else + return mRightAxisMin; + } + } + + /** + * Returns the greatest y-value the data object contains. + * + * @return + */ + public float getYMax() { + return mYMax; + } + + /** + * Returns the maximum y-value for the specified axis. + * + * @param axis + * @return + */ + public float getYMax(AxisDependency axis) { + if (axis == AxisDependency.LEFT) { + + if (mLeftAxisMax == -Float.MAX_VALUE) { + return mRightAxisMax; + } else + return mLeftAxisMax; + } else { + if (mRightAxisMax == -Float.MAX_VALUE) { + return mLeftAxisMax; + } else + return mRightAxisMax; + } + } + + /** + * Returns the minimum x-value this data object contains. + * + * @return + */ + public float getXMin() { + return mXMin; + } + + /** + * Returns the maximum x-value this data object contains. + * + * @return + */ + public float getXMax() { + return mXMax; + } + + /** + * Returns all DataSet objects this ChartData object holds. + * + * @return + */ + public List getDataSets() { + return mDataSets; + } + + /** + * Retrieve the index of a DataSet with a specific label from the ChartData. + * Search can be case sensitive or not. IMPORTANT: This method does + * calculations at runtime, do not over-use in performance critical + * situations. + * + * @param dataSets the DataSet array to search + * @param label + * @param ignorecase if true, the search is not case-sensitive + * @return + */ + protected int getDataSetIndexByLabel(List dataSets, String label, + boolean ignorecase) { + + if (ignorecase) { + for (int i = 0; i < dataSets.size(); i++) + if (label.equalsIgnoreCase(dataSets.get(i).getLabel())) + return i; + } else { + for (int i = 0; i < dataSets.size(); i++) + if (label.equals(dataSets.get(i).getLabel())) + return i; + } + + return -1; + } + + /** + * Returns the labels of all DataSets as a string array. + * + * @return + */ + public String[] getDataSetLabels() { + + String[] types = new String[mDataSets.size()]; + + for (int i = 0; i < mDataSets.size(); i++) { + types[i] = mDataSets.get(i).getLabel(); + } + + return types; + } + + /** + * Get the Entry for a corresponding highlight object + * + * @param highlight + * @return the entry that is highlighted + */ + public Entry getEntryForHighlight(Highlight highlight) { + if (highlight.getDataSetIndex() >= mDataSets.size()) + return null; + else { + return mDataSets.get(highlight.getDataSetIndex()).getEntryForXValue(highlight.getX(), highlight.getY()); + } + } + + /** + * Returns the DataSet object with the given label. Search can be case + * sensitive or not. IMPORTANT: This method does calculations at runtime. + * Use with care in performance critical situations. + * + * @param label + * @param ignorecase + * @return + */ + public T getDataSetByLabel(String label, boolean ignorecase) { + + int index = getDataSetIndexByLabel(mDataSets, label, ignorecase); + + if (index < 0 || index >= mDataSets.size()) + return null; + else + return mDataSets.get(index); + } + + public T getDataSetByIndex(int index) { + + if (mDataSets == null || index < 0 || index >= mDataSets.size()) + return null; + + return mDataSets.get(index); + } + + /** + * Adds a DataSet dynamically. + * + * @param d + */ + public void addDataSet(T d) { + + if (d == null) + return; + + calcMinMax(d); + + mDataSets.add(d); + } + + /** + * Removes the given DataSet from this data object. Also recalculates all + * minimum and maximum values. Returns true if a DataSet was removed, false + * if no DataSet could be removed. + * + * @param d + */ + public boolean removeDataSet(T d) { + + if (d == null) + return false; + + boolean removed = mDataSets.remove(d); + + // if a DataSet was removed + if (removed) { + notifyDataChanged(); + } + + return removed; + } + + /** + * Removes the DataSet at the given index in the DataSet array from the data + * object. Also recalculates all minimum and maximum values. Returns true if + * a DataSet was removed, false if no DataSet could be removed. + * + * @param index + */ + public boolean removeDataSet(int index) { + + if (index >= mDataSets.size() || index < 0) + return false; + + T set = mDataSets.get(index); + return removeDataSet(set); + } + + /** + * Adds an Entry to the DataSet at the specified index. + * Entries are added to the end of the list. + * + * @param e + * @param dataSetIndex + */ + public void addEntry(Entry e, int dataSetIndex) { + + if (mDataSets.size() > dataSetIndex && dataSetIndex >= 0) { + + IDataSet set = mDataSets.get(dataSetIndex); + // add the entry to the dataset + if (!set.addEntry(e)) + return; + + calcMinMax(e, set.getAxisDependency()); + + } else { + Log.e("addEntry", "Cannot add Entry because dataSetIndex too high or too low."); + } + } + + /** + * Adjusts the current minimum and maximum values based on the provided Entry object. + * + * @param e + * @param axis + */ + protected void calcMinMax(Entry e, AxisDependency axis) { + + if (mYMax < e.getY()) + mYMax = e.getY(); + if (mYMin > e.getY()) + mYMin = e.getY(); + + if (mXMax < e.getX()) + mXMax = e.getX(); + if (mXMin > e.getX()) + mXMin = e.getX(); + + if (axis == AxisDependency.LEFT) { + + if (mLeftAxisMax < e.getY()) + mLeftAxisMax = e.getY(); + if (mLeftAxisMin > e.getY()) + mLeftAxisMin = e.getY(); + } else { + if (mRightAxisMax < e.getY()) + mRightAxisMax = e.getY(); + if (mRightAxisMin > e.getY()) + mRightAxisMin = e.getY(); + } + } + + /** + * Adjusts the minimum and maximum values based on the given DataSet. + * + * @param d + */ + protected void calcMinMax(T d) { + + if (mYMax < d.getYMax()) + mYMax = d.getYMax(); + if (mYMin > d.getYMin()) + mYMin = d.getYMin(); + + if (mXMax < d.getXMax()) + mXMax = d.getXMax(); + if (mXMin > d.getXMin()) + mXMin = d.getXMin(); + + if (d.getAxisDependency() == AxisDependency.LEFT) { + + if (mLeftAxisMax < d.getYMax()) + mLeftAxisMax = d.getYMax(); + if (mLeftAxisMin > d.getYMin()) + mLeftAxisMin = d.getYMin(); + } else { + if (mRightAxisMax < d.getYMax()) + mRightAxisMax = d.getYMax(); + if (mRightAxisMin > d.getYMin()) + mRightAxisMin = d.getYMin(); + } + } + + /** + * Removes the given Entry object from the DataSet at the specified index. + * + * @param e + * @param dataSetIndex + */ + public boolean removeEntry(Entry e, int dataSetIndex) { + + // entry null, outofbounds + if (e == null || dataSetIndex >= mDataSets.size()) + return false; + + IDataSet set = mDataSets.get(dataSetIndex); + + if (set != null) { + // remove the entry from the dataset + boolean removed = set.removeEntry(e); + + if (removed) { + notifyDataChanged(); + } + + return removed; + } else + return false; + } + + /** + * Removes the Entry object closest to the given DataSet at the + * specified index. Returns true if an Entry was removed, false if no Entry + * was found that meets the specified requirements. + * + * @param xValue + * @param dataSetIndex + * @return + */ + public boolean removeEntry(float xValue, int dataSetIndex) { + + if (dataSetIndex >= mDataSets.size()) + return false; + + IDataSet dataSet = mDataSets.get(dataSetIndex); + Entry e = dataSet.getEntryForXValue(xValue, Float.NaN); + + if (e == null) + return false; + + return removeEntry(e, dataSetIndex); + } + + /** + * Returns the DataSet that contains the provided Entry, or null, if no + * DataSet contains this Entry. + * + * @param e + * @return + */ + public T getDataSetForEntry(Entry e) { + + if (e == null) + return null; + + for (int i = 0; i < mDataSets.size(); i++) { + + T set = mDataSets.get(i); + + for (int j = 0; j < set.getEntryCount(); j++) { + if (e.equalTo(set.getEntryForXValue(e.getX(), e.getY()))) + return set; + } + } + + return null; + } + + /** + * Returns all colors used across all DataSet objects this object + * represents. + * + * @return + */ + public int[] getColors() { + + if (mDataSets == null) + return null; + + int clrcnt = 0; + + for (int i = 0; i < mDataSets.size(); i++) { + clrcnt += mDataSets.get(i).getColors().size(); + } + + int[] colors = new int[clrcnt]; + int cnt = 0; + + for (int i = 0; i < mDataSets.size(); i++) { + + List clrs = mDataSets.get(i).getColors(); + + for (Integer clr : clrs) { + colors[cnt] = clr; + cnt++; + } + } + + return colors; + } + + /** + * Returns the index of the provided DataSet in the DataSet array of this data object, or -1 if it does not exist. + * + * @param dataSet + * @return + */ + public int getIndexOfDataSet(T dataSet) { + return mDataSets.indexOf(dataSet); + } + + /** + * Returns the first DataSet from the datasets-array that has it's dependency on the left axis. + * Returns null if no DataSet with left dependency could be found. + * + * @return + */ + protected T getFirstLeft(List sets) { + for (T dataSet : sets) { + if (dataSet.getAxisDependency() == AxisDependency.LEFT) + return dataSet; + } + return null; + } + + /** + * Returns the first DataSet from the datasets-array that has it's dependency on the right axis. + * Returns null if no DataSet with right dependency could be found. + * + * @return + */ + public T getFirstRight(List sets) { + for (T dataSet : sets) { + if (dataSet.getAxisDependency() == AxisDependency.RIGHT) + return dataSet; + } + return null; + } + + /** + * Sets a custom IValueFormatter for all DataSets this data object contains. + * + * @param f + */ + public void setValueFormatter(IValueFormatter f) { + if (f == null) + return; + else { + for (IDataSet set : mDataSets) { + set.setValueFormatter(f); + } + } + } + + /** + * Sets the color of the value-text (color in which the value-labels are + * drawn) for all DataSets this data object contains. + * + * @param color + */ + public void setValueTextColor(int color) { + for (IDataSet set : mDataSets) { + set.setValueTextColor(color); + } + } + + /** + * Sets the same list of value-colors for all DataSets this + * data object contains. + * + * @param colors + */ + public void setValueTextColors(List colors) { + for (IDataSet set : mDataSets) { + set.setValueTextColors(colors); + } + } + + /** + * Sets the Typeface for all value-labels for all DataSets this data object + * contains. + * + * @param tf + */ + public void setValueTypeface(Typeface tf) { + for (IDataSet set : mDataSets) { + set.setValueTypeface(tf); + } + } + + /** + * Sets the size (in dp) of the value-text for all DataSets this data object + * contains. + * + * @param size + */ + public void setValueTextSize(float size) { + for (IDataSet set : mDataSets) { + set.setValueTextSize(size); + } + } + + /** + * Enables / disables drawing values (value-text) for all DataSets this data + * object contains. + * + * @param enabled + */ + public void setDrawValues(boolean enabled) { + for (IDataSet set : mDataSets) { + set.setDrawValues(enabled); + } + } + + /** + * Enables / disables highlighting values for all DataSets this data object + * contains. If set to true, this means that values can + * be highlighted programmatically or by touch gesture. + */ + public void setHighlightEnabled(boolean enabled) { + for (IDataSet set : mDataSets) { + set.setHighlightEnabled(enabled); + } + } + + /** + * Returns true if highlighting of all underlying values is enabled, false + * if not. + * + * @return + */ + public boolean isHighlightEnabled() { + for (IDataSet set : mDataSets) { + if (!set.isHighlightEnabled()) + return false; + } + return true; + } + + /** + * Clears this data object from all DataSets and removes all Entries. Don't + * forget to invalidate the chart after this. + */ + public void clearValues() { + if (mDataSets != null) { + mDataSets.clear(); + } + notifyDataChanged(); + } + + /** + * Checks if this data object contains the specified DataSet. Returns true + * if so, false if not. + * + * @param dataSet + * @return + */ + public boolean contains(T dataSet) { + + for (T set : mDataSets) { + if (set.equals(dataSet)) + return true; + } + + return false; + } + + /** + * Returns the total entry count across all DataSet objects this data object contains. + * + * @return + */ + public int getEntryCount() { + + int count = 0; + + for (T set : mDataSets) { + count += set.getEntryCount(); + } + + return count; + } + + /** + * Returns the DataSet object with the maximum number of entries or null if there are no DataSets. + * + * @return + */ + public T getMaxEntryCountSet() { + + if (mDataSets == null || mDataSets.isEmpty()) + return null; + + T max = mDataSets.get(0); + + for (T set : mDataSets) { + + if (set.getEntryCount() > max.getEntryCount()) + max = set; + } + + return max; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/CombinedData.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/CombinedData.java new file mode 100644 index 0000000..0b36aa3 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/CombinedData.java @@ -0,0 +1,272 @@ + +package com.github.mikephil.charting.data; + +import android.util.Log; + +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.datasets.IBarLineScatterCandleBubbleDataSet; + +import java.util.ArrayList; +import java.util.List; + +/** + * Data object that allows the combination of Line-, Bar-, Scatter-, Bubble- and + * CandleData. Used in the CombinedChart class. + * + * @author Philipp Jahoda + */ +public class CombinedData extends BarLineScatterCandleBubbleData> { + + private LineData mLineData; + private BarData mBarData; + private ScatterData mScatterData; + private CandleData mCandleData; + private BubbleData mBubbleData; + + public CombinedData() { + super(); + } + + public void setData(LineData data) { + mLineData = data; + notifyDataChanged(); + } + + public void setData(BarData data) { + mBarData = data; + notifyDataChanged(); + } + + public void setData(ScatterData data) { + mScatterData = data; + notifyDataChanged(); + } + + public void setData(CandleData data) { + mCandleData = data; + notifyDataChanged(); + } + + public void setData(BubbleData data) { + mBubbleData = data; + notifyDataChanged(); + } + + @Override + public void calcMinMax() { + + if(mDataSets == null){ + mDataSets = new ArrayList<>(); + } + mDataSets.clear(); + + mYMax = -Float.MAX_VALUE; + mYMin = Float.MAX_VALUE; + mXMax = -Float.MAX_VALUE; + mXMin = Float.MAX_VALUE; + + mLeftAxisMax = -Float.MAX_VALUE; + mLeftAxisMin = Float.MAX_VALUE; + mRightAxisMax = -Float.MAX_VALUE; + mRightAxisMin = Float.MAX_VALUE; + + List allData = getAllData(); + + for (ChartData data : allData) { + + data.calcMinMax(); + + List> sets = data.getDataSets(); + mDataSets.addAll(sets); + + if (data.getYMax() > mYMax) + mYMax = data.getYMax(); + + if (data.getYMin() < mYMin) + mYMin = data.getYMin(); + + if (data.getXMax() > mXMax) + mXMax = data.getXMax(); + + if (data.getXMin() < mXMin) + mXMin = data.getXMin(); + + for (IBarLineScatterCandleBubbleDataSet dataset : sets) { + if (dataset.getAxisDependency() == YAxis.AxisDependency.LEFT) { + if (dataset.getYMax() > mLeftAxisMax) { + mLeftAxisMax = dataset.getYMax(); + } + + if (dataset.getYMin() < mLeftAxisMin) { + mLeftAxisMin = dataset.getYMin(); + } + } + else { + if (dataset.getYMax() > mRightAxisMax) { + mRightAxisMax = dataset.getYMax(); + } + + if (dataset.getYMin() < mRightAxisMin) { + mRightAxisMin = dataset.getYMin(); + } + } + } + } + } + + public BubbleData getBubbleData() { + return mBubbleData; + } + + public LineData getLineData() { + return mLineData; + } + + public BarData getBarData() { + return mBarData; + } + + public ScatterData getScatterData() { + return mScatterData; + } + + public CandleData getCandleData() { + return mCandleData; + } + + /** + * Returns all data objects in row: line-bar-scatter-candle-bubble if not null. + * + * @return + */ + public List getAllData() { + + List data = new ArrayList(); + if (mLineData != null) + data.add(mLineData); + if (mBarData != null) + data.add(mBarData); + if (mScatterData != null) + data.add(mScatterData); + if (mCandleData != null) + data.add(mCandleData); + if (mBubbleData != null) + data.add(mBubbleData); + + return data; + } + + public BarLineScatterCandleBubbleData getDataByIndex(int index) { + return getAllData().get(index); + } + + @Override + public void notifyDataChanged() { + if (mLineData != null) + mLineData.notifyDataChanged(); + if (mBarData != null) + mBarData.notifyDataChanged(); + if (mCandleData != null) + mCandleData.notifyDataChanged(); + if (mScatterData != null) + mScatterData.notifyDataChanged(); + if (mBubbleData != null) + mBubbleData.notifyDataChanged(); + + calcMinMax(); // recalculate everything + } + + /** + * Get the Entry for a corresponding highlight object + * + * @param highlight + * @return the entry that is highlighted + */ + @Override + public Entry getEntryForHighlight(Highlight highlight) { + + if (highlight.getDataIndex() >= getAllData().size()) + return null; + + ChartData data = getDataByIndex(highlight.getDataIndex()); + + if (highlight.getDataSetIndex() >= data.getDataSetCount()) + return null; + + // The value of the highlighted entry could be NaN - + // if we are not interested in highlighting a specific value. + + List entries = data.getDataSetByIndex(highlight.getDataSetIndex()) + .getEntriesForXValue(highlight.getX()); + for (Entry entry : entries) + if (entry.getY() == highlight.getY() || + Float.isNaN(highlight.getY())) + return entry; + + return null; + } + + /** + * Get dataset for highlight + * + * @param highlight current highlight + * @return dataset related to highlight + */ + public IBarLineScatterCandleBubbleDataSet getDataSetByHighlight(Highlight highlight) { + if (highlight.getDataIndex() >= getAllData().size()) + return null; + + BarLineScatterCandleBubbleData data = getDataByIndex(highlight.getDataIndex()); + + if (highlight.getDataSetIndex() >= data.getDataSetCount()) + return null; + + return (IBarLineScatterCandleBubbleDataSet) + data.getDataSets().get(highlight.getDataSetIndex()); + } + + public int getDataIndex(ChartData data) { + return getAllData().indexOf(data); + } + + @Override + public boolean removeDataSet(IBarLineScatterCandleBubbleDataSet d) { + + List datas = getAllData(); + + boolean success = false; + + for (ChartData data : datas) { + + success = data.removeDataSet(d); + + if (success) { + break; + } + } + + return success; + } + + @Deprecated + @Override + public boolean removeDataSet(int index) { + Log.e("MPAndroidChart", "removeDataSet(int index) not supported for CombinedData"); + return false; + } + + @Deprecated + @Override + public boolean removeEntry(Entry e, int dataSetIndex) { + Log.e("MPAndroidChart", "removeEntry(...) not supported for CombinedData"); + return false; + } + + @Deprecated + @Override + public boolean removeEntry(float xValue, int dataSetIndex) { + Log.e("MPAndroidChart", "removeEntry(...) not supported for CombinedData"); + return false; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/DataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/DataSet.java new file mode 100644 index 0000000..fda07ef --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/DataSet.java @@ -0,0 +1,456 @@ + +package com.github.mikephil.charting.data; + +import java.util.ArrayList; +import java.util.List; + +/** + * The DataSet class represents one group or type of entries (Entry) in the + * Chart that belong together. It is designed to logically separate different + * groups of values inside the Chart (e.g. the values for a specific line in the + * LineChart, or the values of a specific group of bars in the BarChart). + * + * @author Philipp Jahoda + */ +public abstract class DataSet extends BaseDataSet { + + /** + * the entries that this DataSet represents / holds together + */ + protected List mEntries; + + /** + * maximum y-value in the value array + */ + protected float mYMax = -Float.MAX_VALUE; + + /** + * minimum y-value in the value array + */ + protected float mYMin = Float.MAX_VALUE; + + /** + * maximum x-value in the value array + */ + protected float mXMax = -Float.MAX_VALUE; + + /** + * minimum x-value in the value array + */ + protected float mXMin = Float.MAX_VALUE; + + + /** + * Creates a new DataSet object with the given values (entries) it represents. Also, a + * label that describes the DataSet can be specified. The label can also be + * used to retrieve the DataSet from a ChartData object. + * + * @param entries + * @param label + */ + public DataSet(List entries, String label) { + super(label); + this.mEntries = entries; + + if (mEntries == null) + mEntries = new ArrayList(); + + calcMinMax(); + } + + @Override + public void calcMinMax() { + + mYMax = -Float.MAX_VALUE; + mYMin = Float.MAX_VALUE; + mXMax = -Float.MAX_VALUE; + mXMin = Float.MAX_VALUE; + + if (mEntries == null || mEntries.isEmpty()) + return; + + for (T e : mEntries) { + calcMinMax(e); + } + } + + @Override + public void calcMinMaxY(float fromX, float toX) { + mYMax = -Float.MAX_VALUE; + mYMin = Float.MAX_VALUE; + + if (mEntries == null || mEntries.isEmpty()) + return; + + int indexFrom = getEntryIndex(fromX, Float.NaN, Rounding.DOWN); + int indexTo = getEntryIndex(toX, Float.NaN, Rounding.UP); + + if (indexTo < indexFrom) return; + + for (int i = indexFrom; i <= indexTo; i++) { + + // only recalculate y + calcMinMaxY(mEntries.get(i)); + } + } + + /** + * Updates the min and max x and y value of this DataSet based on the given Entry. + * + * @param e + */ + protected void calcMinMax(T e) { + + if (e == null) + return; + + calcMinMaxX(e); + + calcMinMaxY(e); + } + + protected void calcMinMaxX(T e) { + + if (e.getX() < mXMin) + mXMin = e.getX(); + + if (e.getX() > mXMax) + mXMax = e.getX(); + } + + protected void calcMinMaxY(T e) { + + if (e.getY() < mYMin) + mYMin = e.getY(); + + if (e.getY() > mYMax) + mYMax = e.getY(); + } + + @Override + public int getEntryCount() { + return mEntries.size(); + } + + /** + * This method is deprecated. + * Use getEntries() instead. + * + * @return + */ + @Deprecated + public List getValues() { + return mEntries; + } + + /** + * Returns the array of entries that this DataSet represents. + * + * @return + */ + public List getEntries() { + return mEntries; + } + + /** + * This method is deprecated. + * Use setEntries(...) instead. + * + * @param values + */ + @Deprecated + public void setValues(List values) { + setEntries(values); + } + + /** + * Sets the array of entries that this DataSet represents, and calls notifyDataSetChanged() + * + * @return + */ + public void setEntries(List entries) { + mEntries = entries; + notifyDataSetChanged(); + } + + /** + * Provides an exact copy of the DataSet this method is used on. + * + * @return + */ + public abstract DataSet copy(); + + /** + * + * @param dataSet + */ + protected void copy(DataSet dataSet) { + super.copy(dataSet); + } + + @Override + public String toString() { + StringBuffer buffer = new StringBuffer(); + buffer.append(toSimpleString()); + for (int i = 0; i < mEntries.size(); i++) { + buffer.append(mEntries.get(i).toString() + " "); + } + return buffer.toString(); + } + + /** + * Returns a simple string representation of the DataSet with the type and + * the number of Entries. + * + * @return + */ + public String toSimpleString() { + StringBuffer buffer = new StringBuffer(); + buffer.append("DataSet, label: " + (getLabel() == null ? "" : getLabel()) + ", entries: " + mEntries.size() + + "\n"); + return buffer.toString(); + } + + @Override + public float getYMin() { + return mYMin; + } + + @Override + public float getYMax() { + return mYMax; + } + + @Override + public float getXMin() { + return mXMin; + } + + @Override + public float getXMax() { + return mXMax; + } + + @Override + public void addEntryOrdered(T e) { + + if (e == null) + return; + + if (mEntries == null) { + mEntries = new ArrayList(); + } + + calcMinMax(e); + + if (mEntries.size() > 0 && mEntries.get(mEntries.size() - 1).getX() > e.getX()) { + int closestIndex = getEntryIndex(e.getX(), e.getY(), Rounding.UP); + mEntries.add(closestIndex, e); + } else { + mEntries.add(e); + } + } + + @Override + public void clear() { + mEntries.clear(); + notifyDataSetChanged(); + } + + @Override + public boolean addEntry(T e) { + + if (e == null) + return false; + + List values = getEntries(); + if (values == null) { + values = new ArrayList<>(); + } + + calcMinMax(e); + + // add the entry + return values.add(e); + } + + @Override + public boolean removeEntry(T e) { + + if (e == null) + return false; + + if (mEntries == null) + return false; + + // remove the entry + boolean removed = mEntries.remove(e); + + if (removed) { + calcMinMax(); + } + + return removed; + } + + @Override + public int getEntryIndex(Entry e) { + return mEntries.indexOf(e); + } + + @Override + public T getEntryForXValue(float xValue, float closestToY, Rounding rounding) { + + int index = getEntryIndex(xValue, closestToY, rounding); + if (index > -1) + return mEntries.get(index); + return null; + } + + @Override + public T getEntryForXValue(float xValue, float closestToY) { + return getEntryForXValue(xValue, closestToY, Rounding.CLOSEST); + } + + @Override + public T getEntryForIndex(int index) { + return mEntries.get(index); + } + + @Override + public int getEntryIndex(float xValue, float closestToY, Rounding rounding) { + + if (mEntries == null || mEntries.isEmpty()) + return -1; + + int low = 0; + int high = mEntries.size() - 1; + int closest = high; + + while (low < high) { + int m = (low + high) / 2; + + final float d1 = mEntries.get(m).getX() - xValue, + d2 = mEntries.get(m + 1).getX() - xValue, + ad1 = Math.abs(d1), ad2 = Math.abs(d2); + + if (ad2 < ad1) { + // [m + 1] is closer to xValue + // Search in an higher place + low = m + 1; + } else if (ad1 < ad2) { + // [m] is closer to xValue + // Search in a lower place + high = m; + } else { + // We have multiple sequential x-value with same distance + + if (d1 >= 0.0) { + // Search in a lower place + high = m; + } else if (d1 < 0.0) { + // Search in an higher place + low = m + 1; + } + } + + closest = high; + } + + if (closest != -1) { + float closestXValue = mEntries.get(closest).getX(); + if (rounding == Rounding.UP) { + // If rounding up, and found x-value is lower than specified x, and we can go upper... + if (closestXValue < xValue && closest < mEntries.size() - 1) { + ++closest; + } + } else if (rounding == Rounding.DOWN) { + // If rounding down, and found x-value is upper than specified x, and we can go lower... + if (closestXValue > xValue && closest > 0) { + --closest; + } + } + + // Search by closest to y-value + if (!Float.isNaN(closestToY)) { + while (closest > 0 && mEntries.get(closest - 1).getX() == closestXValue) + closest -= 1; + + float closestYValue = mEntries.get(closest).getY(); + int closestYIndex = closest; + + while (true) { + closest += 1; + if (closest >= mEntries.size()) + break; + + final Entry value = mEntries.get(closest); + + if (value.getX() != closestXValue) + break; + + if (Math.abs(value.getY() - closestToY) <= Math.abs(closestYValue - closestToY)) { + closestYValue = closestToY; + closestYIndex = closest; + } + } + + closest = closestYIndex; + } + } + + return closest; + } + + @Override + public List getEntriesForXValue(float xValue) { + + List entries = new ArrayList(); + + int low = 0; + int high = mEntries.size() - 1; + + while (low <= high) { + int m = (high + low) / 2; + T entry = mEntries.get(m); + + // if we have a match + if (xValue == entry.getX()) { + while (m > 0 && mEntries.get(m - 1).getX() == xValue) + m--; + + high = mEntries.size(); + + // loop over all "equal" entries + for (; m < high; m++) { + entry = mEntries.get(m); + if (entry.getX() == xValue) { + entries.add(entry); + } else { + break; + } + } + + break; + } else { + if (xValue > entry.getX()) + low = m + 1; + else + high = m - 1; + } + } + + return entries; + } + + /** + * Determines how to round DataSet index values for + * {@link DataSet#getEntryIndex(float, float, Rounding)} DataSet.getEntryIndex()} + * when an exact x-index is not found. + */ + public enum Rounding { + UP, + DOWN, + CLOSEST, + } +} \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/Entry.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/Entry.java new file mode 100644 index 0000000..b7a8879 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/Entry.java @@ -0,0 +1,173 @@ + +package com.github.mikephil.charting.data; + +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.os.ParcelFormatException; +import android.os.Parcelable; + +import com.github.mikephil.charting.utils.Utils; + +/** + * Class representing one entry in the chart. Might contain multiple values. + * Might only contain a single value depending on the used constructor. + * + * @author Philipp Jahoda + */ +public class Entry extends BaseEntry implements Parcelable { + + /** the x value */ + private float x = 0f; + + public Entry() { + + } + + /** + * A Entry represents one single entry in the chart. + * + * @param x the x value + * @param y the y value (the actual value of the entry) + */ + public Entry(float x, float y) { + super(y); + this.x = x; + } + + /** + * A Entry represents one single entry in the chart. + * + * @param x the x value + * @param y the y value (the actual value of the entry) + * @param data Spot for additional data this Entry represents. + */ + public Entry(float x, float y, Object data) { + super(y, data); + this.x = x; + } + + /** + * A Entry represents one single entry in the chart. + * + * @param x the x value + * @param y the y value (the actual value of the entry) + * @param icon icon image + */ + public Entry(float x, float y, Drawable icon) { + super(y, icon); + this.x = x; + } + + /** + * A Entry represents one single entry in the chart. + * + * @param x the x value + * @param y the y value (the actual value of the entry) + * @param icon icon image + * @param data Spot for additional data this Entry represents. + */ + public Entry(float x, float y, Drawable icon, Object data) { + super(y, icon, data); + this.x = x; + } + + /** + * Returns the x-value of this Entry object. + * + * @return + */ + public float getX() { + return x; + } + + /** + * Sets the x-value of this Entry object. + * + * @param x + */ + public void setX(float x) { + this.x = x; + } + + /** + * returns an exact copy of the entry + * + * @return + */ + public Entry copy() { + Entry e = new Entry(x, getY(), getData()); + return e; + } + + /** + * Compares value, xIndex and data of the entries. Returns true if entries + * are equal in those points, false if not. Does not check by hash-code like + * it's done by the "equals" method. + * + * @param e + * @return + */ + public boolean equalTo(Entry e) { + + if (e == null) + return false; + + if (e.getData() != this.getData()) + return false; + + if (Math.abs(e.x - this.x) > Utils.FLOAT_EPSILON) + return false; + + if (Math.abs(e.getY() - this.getY()) > Utils.FLOAT_EPSILON) + return false; + + return true; + } + + /** + * returns a string representation of the entry containing x-index and value + */ + @Override + public String toString() { + return "Entry, x: " + x + " y: " + getY(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeFloat(this.x); + dest.writeFloat(this.getY()); + if (getData() != null) { + if (getData() instanceof Parcelable) { + dest.writeInt(1); + dest.writeParcelable((Parcelable) this.getData(), flags); + } else { + throw new ParcelFormatException("Cannot parcel an Entry with non-parcelable data"); + } + } else { + dest.writeInt(0); + } + } + + protected Entry(Parcel in) { + this.x = in.readFloat(); + this.setY(in.readFloat()); + if (in.readInt() == 1) { + this.setData(in.readParcelable(Object.class.getClassLoader())); + } + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public Entry createFromParcel(Parcel source) { + return new Entry(source); + } + + public Entry[] newArray(int size) { + return new Entry[size]; + } + }; +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/LineData.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/LineData.java new file mode 100644 index 0000000..4cf5448 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/LineData.java @@ -0,0 +1,27 @@ + +package com.github.mikephil.charting.data; + +import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; + +import java.util.ArrayList; +import java.util.List; + +/** + * Data object that encapsulates all data associated with a LineChart. + * + * @author Philipp Jahoda + */ +public class LineData extends BarLineScatterCandleBubbleData { + + public LineData() { + super(); + } + + public LineData(ILineDataSet... dataSets) { + super(dataSets); + } + + public LineData(List dataSets) { + super(dataSets); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/LineDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/LineDataSet.java new file mode 100644 index 0000000..10d1837 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/LineDataSet.java @@ -0,0 +1,417 @@ + +package com.github.mikephil.charting.data; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.DashPathEffect; +import android.util.Log; + +import com.github.mikephil.charting.formatter.DefaultFillFormatter; +import com.github.mikephil.charting.formatter.IFillFormatter; +import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.Utils; + +import java.util.ArrayList; +import java.util.List; + +public class LineDataSet extends LineRadarDataSet implements ILineDataSet { + + /** + * Drawing mode for this line dataset + **/ + private LineDataSet.Mode mMode = Mode.LINEAR; + + /** + * List representing all colors that are used for the circles + */ + private List mCircleColors = null; + + /** + * the color of the inner circles + */ + private int mCircleHoleColor = Color.WHITE; + + /** + * the radius of the circle-shaped value indicators + */ + private float mCircleRadius = 8f; + + /** + * the hole radius of the circle-shaped value indicators + */ + private float mCircleHoleRadius = 4f; + + /** + * sets the intensity of the cubic lines + */ + private float mCubicIntensity = 0.2f; + + /** + * the path effect of this DataSet that makes dashed lines possible + */ + private DashPathEffect mDashPathEffect = null; + + /** + * formatter for customizing the position of the fill-line + */ + private IFillFormatter mFillFormatter = new DefaultFillFormatter(); + + /** + * if true, drawing circles is enabled + */ + private boolean mDrawCircles = true; + + private boolean mDrawCircleHole = true; + + + public LineDataSet(List yVals, String label) { + super(yVals, label); + + // mCircleRadius = Utils.convertDpToPixel(4f); + // mLineWidth = Utils.convertDpToPixel(1f); + + if (mCircleColors == null) { + mCircleColors = new ArrayList(); + } + mCircleColors.clear(); + + // default colors + // mColors.add(Color.rgb(192, 255, 140)); + // mColors.add(Color.rgb(255, 247, 140)); + mCircleColors.add(Color.rgb(140, 234, 255)); + } + + @Override + public DataSet copy() { + List entries = new ArrayList(); + for (int i = 0; i < mEntries.size(); i++) { + entries.add(mEntries.get(i).copy()); + } + LineDataSet copied = new LineDataSet(entries, getLabel()); + copy(copied); + return copied; + } + + protected void copy(LineDataSet lineDataSet) { + super.copy(lineDataSet); + lineDataSet.mCircleColors = mCircleColors; + lineDataSet.mCircleHoleColor = mCircleHoleColor; + lineDataSet.mCircleHoleRadius = mCircleHoleRadius; + lineDataSet.mCircleRadius = mCircleRadius; + lineDataSet.mCubicIntensity = mCubicIntensity; + lineDataSet.mDashPathEffect = mDashPathEffect; + lineDataSet.mDrawCircleHole = mDrawCircleHole; + lineDataSet.mDrawCircles = mDrawCircleHole; + lineDataSet.mFillFormatter = mFillFormatter; + lineDataSet.mMode = mMode; + } + + /** + * Returns the drawing mode for this line dataset + * + * @return + */ + @Override + public LineDataSet.Mode getMode() { + return mMode; + } + + /** + * Returns the drawing mode for this LineDataSet + * + * @return + */ + public void setMode(LineDataSet.Mode mode) { + mMode = mode; + } + + /** + * Sets the intensity for cubic lines (if enabled). Max = 1f = very cubic, + * Min = 0.05f = low cubic effect, Default: 0.2f + * + * @param intensity + */ + public void setCubicIntensity(float intensity) { + + if (intensity > 1f) + intensity = 1f; + if (intensity < 0.05f) + intensity = 0.05f; + + mCubicIntensity = intensity; + } + + @Override + public float getCubicIntensity() { + return mCubicIntensity; + } + + + /** + * Sets the radius of the drawn circles. + * Default radius = 4f, Min = 1f + * + * @param radius + */ + public void setCircleRadius(float radius) { + + if (radius >= 1f) { + mCircleRadius = Utils.convertDpToPixel(radius); + } else { + Log.e("LineDataSet", "Circle radius cannot be < 1"); + } + } + + @Override + public float getCircleRadius() { + return mCircleRadius; + } + + /** + * Sets the hole radius of the drawn circles. + * Default radius = 2f, Min = 0.5f + * + * @param holeRadius + */ + public void setCircleHoleRadius(float holeRadius) { + + if (holeRadius >= 0.5f) { + mCircleHoleRadius = Utils.convertDpToPixel(holeRadius); + } else { + Log.e("LineDataSet", "Circle radius cannot be < 0.5"); + } + } + + @Override + public float getCircleHoleRadius() { + return mCircleHoleRadius; + } + + /** + * sets the size (radius) of the circle shpaed value indicators, + * default size = 4f + *

+ * This method is deprecated because of unclarity. Use setCircleRadius instead. + * + * @param size + */ + @Deprecated + public void setCircleSize(float size) { + setCircleRadius(size); + } + + /** + * This function is deprecated because of unclarity. Use getCircleRadius instead. + */ + @Deprecated + public float getCircleSize() { + return getCircleRadius(); + } + + /** + * Enables the line to be drawn in dashed mode, e.g. like this + * "- - - - - -". THIS ONLY WORKS IF HARDWARE-ACCELERATION IS TURNED OFF. + * Keep in mind that hardware acceleration boosts performance. + * + * @param lineLength the length of the line pieces + * @param spaceLength the length of space in between the pieces + * @param phase offset, in degrees (normally, use 0) + */ + public void enableDashedLine(float lineLength, float spaceLength, float phase) { + mDashPathEffect = new DashPathEffect(new float[]{ + lineLength, spaceLength + }, phase); + } + + /** + * Disables the line to be drawn in dashed mode. + */ + public void disableDashedLine() { + mDashPathEffect = null; + } + + @Override + public boolean isDashedLineEnabled() { + return mDashPathEffect == null ? false : true; + } + + @Override + public DashPathEffect getDashPathEffect() { + return mDashPathEffect; + } + + /** + * set this to true to enable the drawing of circle indicators for this + * DataSet, default true + * + * @param enabled + */ + public void setDrawCircles(boolean enabled) { + this.mDrawCircles = enabled; + } + + @Override + public boolean isDrawCirclesEnabled() { + return mDrawCircles; + } + + @Deprecated + @Override + public boolean isDrawCubicEnabled() { + return mMode == Mode.CUBIC_BEZIER; + } + + @Deprecated + @Override + public boolean isDrawSteppedEnabled() { + return mMode == Mode.STEPPED; + } + + /** ALL CODE BELOW RELATED TO CIRCLE-COLORS */ + + /** + * returns all colors specified for the circles + * + * @return + */ + public List getCircleColors() { + return mCircleColors; + } + + @Override + public int getCircleColor(int index) { + return mCircleColors.get(index); + } + + @Override + public int getCircleColorCount() { + return mCircleColors.size(); + } + + /** + * Sets the colors that should be used for the circles of this DataSet. + * Colors are reused as soon as the number of Entries the DataSet represents + * is higher than the size of the colors array. Make sure that the colors + * are already prepared (by calling getResources().getColor(...)) before + * adding them to the DataSet. + * + * @param colors + */ + public void setCircleColors(List colors) { + mCircleColors = colors; + } + + /** + * Sets the colors that should be used for the circles of this DataSet. + * Colors are reused as soon as the number of Entries the DataSet represents + * is higher than the size of the colors array. Make sure that the colors + * are already prepared (by calling getResources().getColor(...)) before + * adding them to the DataSet. + * + * @param colors + */ + public void setCircleColors(int... colors) { + this.mCircleColors = ColorTemplate.createColors(colors); + } + + /** + * ets the colors that should be used for the circles of this DataSet. + * Colors are reused as soon as the number of Entries the DataSet represents + * is higher than the size of the colors array. You can use + * "new String[] { R.color.red, R.color.green, ... }" to provide colors for + * this method. Internally, the colors are resolved using + * getResources().getColor(...) + * + * @param colors + */ + public void setCircleColors(int[] colors, Context c) { + + List clrs = mCircleColors; + if (clrs == null) { + clrs = new ArrayList<>(); + } + clrs.clear(); + + for (int color : colors) { + clrs.add(c.getResources().getColor(color)); + } + + mCircleColors = clrs; + } + + /** + * Sets the one and ONLY color that should be used for this DataSet. + * Internally, this recreates the colors array and adds the specified color. + * + * @param color + */ + public void setCircleColor(int color) { + resetCircleColors(); + mCircleColors.add(color); + } + + /** + * resets the circle-colors array and creates a new one + */ + public void resetCircleColors() { + if (mCircleColors == null) { + mCircleColors = new ArrayList(); + } + mCircleColors.clear(); + } + + /** + * Sets the color of the inner circle of the line-circles. + * + * @param color + */ + public void setCircleHoleColor(int color) { + mCircleHoleColor = color; + } + + @Override + public int getCircleHoleColor() { + return mCircleHoleColor; + } + + /** + * Set this to true to allow drawing a hole in each data circle. + * + * @param enabled + */ + public void setDrawCircleHole(boolean enabled) { + mDrawCircleHole = enabled; + } + + @Override + public boolean isDrawCircleHoleEnabled() { + return mDrawCircleHole; + } + + /** + * Sets a custom IFillFormatter to the chart that handles the position of the + * filled-line for each DataSet. Set this to null to use the default logic. + * + * @param formatter + */ + public void setFillFormatter(IFillFormatter formatter) { + + if (formatter == null) + mFillFormatter = new DefaultFillFormatter(); + else + mFillFormatter = formatter; + } + + @Override + public IFillFormatter getFillFormatter() { + return mFillFormatter; + } + + public enum Mode { + LINEAR, + STEPPED, + CUBIC_BEZIER, + HORIZONTAL_BEZIER + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/LineRadarDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/LineRadarDataSet.java new file mode 100644 index 0000000..b4347e4 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/LineRadarDataSet.java @@ -0,0 +1,135 @@ + +package com.github.mikephil.charting.data; + +import android.annotation.TargetApi; +import android.graphics.Color; +import android.graphics.drawable.Drawable; + +import com.github.mikephil.charting.interfaces.datasets.ILineRadarDataSet; +import com.github.mikephil.charting.utils.Utils; + +import java.util.List; + +/** + * Base dataset for line and radar DataSets. + * + * @author Philipp Jahoda + */ +public abstract class LineRadarDataSet extends LineScatterCandleRadarDataSet implements ILineRadarDataSet { + + // TODO: Move to using `Fill` class + /** + * the color that is used for filling the line surface + */ + private int mFillColor = Color.rgb(140, 234, 255); + + /** + * the drawable to be used for filling the line surface + */ + protected Drawable mFillDrawable; + + /** + * transparency used for filling line surface + */ + private int mFillAlpha = 85; + + /** + * the width of the drawn data lines + */ + private float mLineWidth = 2.5f; + + /** + * if true, the data will also be drawn filled + */ + private boolean mDrawFilled = false; + + + public LineRadarDataSet(List yVals, String label) { + super(yVals, label); + } + + @Override + public int getFillColor() { + return mFillColor; + } + + /** + * Sets the color that is used for filling the area below the line. + * Resets an eventually set "fillDrawable". + * + * @param color + */ + public void setFillColor(int color) { + mFillColor = color; + mFillDrawable = null; + } + + @Override + public Drawable getFillDrawable() { + return mFillDrawable; + } + + /** + * Sets the drawable to be used to fill the area below the line. + * + * @param drawable + */ + @TargetApi(18) + public void setFillDrawable(Drawable drawable) { + this.mFillDrawable = drawable; + } + + @Override + public int getFillAlpha() { + return mFillAlpha; + } + + /** + * sets the alpha value (transparency) that is used for filling the line + * surface (0-255), default: 85 + * + * @param alpha + */ + public void setFillAlpha(int alpha) { + mFillAlpha = alpha; + } + + /** + * set the line width of the chart (min = 0.2f, max = 10f); default 1f NOTE: + * thinner line == better performance, thicker line == worse performance + * + * @param width + */ + public void setLineWidth(float width) { + + if (width < 0.0f) + width = 0.0f; + if (width > 10.0f) + width = 10.0f; + mLineWidth = Utils.convertDpToPixel(width); + } + + @Override + public float getLineWidth() { + return mLineWidth; + } + + @Override + public void setDrawFilled(boolean filled) { + mDrawFilled = filled; + } + + @Override + public boolean isDrawFilledEnabled() { + return mDrawFilled; + } + + protected void copy(LineRadarDataSet lineRadarDataSet) { + super.copy(lineRadarDataSet); + lineRadarDataSet.mDrawFilled = mDrawFilled; + lineRadarDataSet.mFillAlpha = mFillAlpha; + lineRadarDataSet.mFillColor = mFillColor; + lineRadarDataSet.mFillDrawable = mFillDrawable; + lineRadarDataSet.mLineWidth = mLineWidth; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/LineScatterCandleRadarDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/LineScatterCandleRadarDataSet.java new file mode 100644 index 0000000..d4618d8 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/LineScatterCandleRadarDataSet.java @@ -0,0 +1,120 @@ +package com.github.mikephil.charting.data; + +import android.graphics.DashPathEffect; + +import com.github.mikephil.charting.interfaces.datasets.ILineScatterCandleRadarDataSet; +import com.github.mikephil.charting.utils.Utils; + +import java.util.List; + +/** + * Created by Philipp Jahoda on 11/07/15. + */ +public abstract class LineScatterCandleRadarDataSet extends BarLineScatterCandleBubbleDataSet implements ILineScatterCandleRadarDataSet { + + protected boolean mDrawVerticalHighlightIndicator = true; + protected boolean mDrawHorizontalHighlightIndicator = true; + + /** the width of the highlight indicator lines */ + protected float mHighlightLineWidth = 0.5f; + + /** the path effect for dashed highlight-lines */ + protected DashPathEffect mHighlightDashPathEffect = null; + + + public LineScatterCandleRadarDataSet(List yVals, String label) { + super(yVals, label); + mHighlightLineWidth = Utils.convertDpToPixel(0.5f); + } + + /** + * Enables / disables the horizontal highlight-indicator. If disabled, the indicator is not drawn. + * @param enabled + */ + public void setDrawHorizontalHighlightIndicator(boolean enabled) { + this.mDrawHorizontalHighlightIndicator = enabled; + } + + /** + * Enables / disables the vertical highlight-indicator. If disabled, the indicator is not drawn. + * @param enabled + */ + public void setDrawVerticalHighlightIndicator(boolean enabled) { + this.mDrawVerticalHighlightIndicator = enabled; + } + + /** + * Enables / disables both vertical and horizontal highlight-indicators. + * @param enabled + */ + public void setDrawHighlightIndicators(boolean enabled) { + setDrawVerticalHighlightIndicator(enabled); + setDrawHorizontalHighlightIndicator(enabled); + } + + @Override + public boolean isVerticalHighlightIndicatorEnabled() { + return mDrawVerticalHighlightIndicator; + } + + @Override + public boolean isHorizontalHighlightIndicatorEnabled() { + return mDrawHorizontalHighlightIndicator; + } + + /** + * Sets the width of the highlight line in dp. + * @param width + */ + public void setHighlightLineWidth(float width) { + mHighlightLineWidth = Utils.convertDpToPixel(width); + } + + @Override + public float getHighlightLineWidth() { + return mHighlightLineWidth; + } + + /** + * Enables the highlight-line to be drawn in dashed mode, e.g. like this "- - - - - -" + * + * @param lineLength the length of the line pieces + * @param spaceLength the length of space inbetween the line-pieces + * @param phase offset, in degrees (normally, use 0) + */ + public void enableDashedHighlightLine(float lineLength, float spaceLength, float phase) { + mHighlightDashPathEffect = new DashPathEffect(new float[] { + lineLength, spaceLength + }, phase); + } + + /** + * Disables the highlight-line to be drawn in dashed mode. + */ + public void disableDashedHighlightLine() { + mHighlightDashPathEffect = null; + } + + /** + * Returns true if the dashed-line effect is enabled for highlight lines, false if not. + * Default: disabled + * + * @return + */ + public boolean isDashedHighlightLineEnabled() { + return mHighlightDashPathEffect == null ? false : true; + } + + @Override + public DashPathEffect getDashPathEffectHighlight() { + return mHighlightDashPathEffect; + } + + protected void copy(LineScatterCandleRadarDataSet lineScatterCandleRadarDataSet) { + super.copy(lineScatterCandleRadarDataSet); + lineScatterCandleRadarDataSet.mDrawHorizontalHighlightIndicator = mDrawHorizontalHighlightIndicator; + lineScatterCandleRadarDataSet.mDrawVerticalHighlightIndicator = mDrawVerticalHighlightIndicator; + lineScatterCandleRadarDataSet.mHighlightLineWidth = mHighlightLineWidth; + lineScatterCandleRadarDataSet.mHighlightDashPathEffect = mHighlightDashPathEffect; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/PieData.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/PieData.java new file mode 100644 index 0000000..423ce19 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/PieData.java @@ -0,0 +1,100 @@ + +package com.github.mikephil.charting.data; + +import android.util.Log; + +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.datasets.IPieDataSet; + +import java.util.ArrayList; +import java.util.List; + +/** + * A PieData object can only represent one DataSet. Unlike all other charts, the + * legend labels of the PieChart are created from the x-values array, and not + * from the DataSet labels. Each PieData object can only represent one + * PieDataSet (multiple PieDataSets inside a single PieChart are not possible). + * + * @author Philipp Jahoda + */ +public class PieData extends ChartData { + + public PieData() { + super(); + } + + public PieData(IPieDataSet dataSet) { + super(dataSet); + } + + /** + * Sets the PieDataSet this data object should represent. + * + * @param dataSet + */ + public void setDataSet(IPieDataSet dataSet) { + mDataSets.clear(); + mDataSets.add(dataSet); + notifyDataChanged(); + } + + /** + * Returns the DataSet this PieData object represents. A PieData object can + * only contain one DataSet. + * + * @return + */ + public IPieDataSet getDataSet() { + return mDataSets.get(0); + } + + @Override + public List getDataSets() { + List dataSets = super.getDataSets(); + + if (dataSets.size() < 1) { + Log.e("MPAndroidChart", + "Found multiple data sets while pie chart only allows one"); + } + + return dataSets; + } + + /** + * The PieData object can only have one DataSet. Use getDataSet() method instead. + * + * @param index + * @return + */ + @Override + public IPieDataSet getDataSetByIndex(int index) { + return index == 0 ? getDataSet() : null; + } + + @Override + public IPieDataSet getDataSetByLabel(String label, boolean ignorecase) { + return ignorecase ? label.equalsIgnoreCase(mDataSets.get(0).getLabel()) ? mDataSets.get(0) + : null : label.equals(mDataSets.get(0).getLabel()) ? mDataSets.get(0) : null; + } + + @Override + public Entry getEntryForHighlight(Highlight highlight) { + return getDataSet().getEntryForIndex((int) highlight.getX()); + } + + /** + * Returns the sum of all values in this PieData object. + * + * @return + */ + public float getYValueSum() { + + float sum = 0; + + for (int i = 0; i < getDataSet().getEntryCount(); i++) + sum += getDataSet().getEntryForIndex(i).getY(); + + + return sum; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/PieDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/PieDataSet.java new file mode 100644 index 0000000..c83b245 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/PieDataSet.java @@ -0,0 +1,261 @@ + +package com.github.mikephil.charting.data; + +import com.github.mikephil.charting.interfaces.datasets.IPieDataSet; +import com.github.mikephil.charting.utils.Utils; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class PieDataSet extends DataSet implements IPieDataSet { + + /** + * the space in pixels between the chart-slices, default 0f + */ + private float mSliceSpace = 0f; + private boolean mAutomaticallyDisableSliceSpacing; + + /** + * indicates the selection distance of a pie slice + */ + private float mShift = 18f; + + private ValuePosition mXValuePosition = ValuePosition.INSIDE_SLICE; + private ValuePosition mYValuePosition = ValuePosition.INSIDE_SLICE; + private int mValueLineColor = 0xff000000; + private boolean mUseValueColorForLine = false; + private float mValueLineWidth = 1.0f; + private float mValueLinePart1OffsetPercentage = 75.f; + private float mValueLinePart1Length = 0.3f; + private float mValueLinePart2Length = 0.4f; + private boolean mValueLineVariableLength = true; + private Integer mHighlightColor = null; + + public PieDataSet(List yVals, String label) { + super(yVals, label); +// mShift = Utils.convertDpToPixel(12f); + } + + @Override + public DataSet copy() { + List entries = new ArrayList<>(); + for (int i = 0; i < mEntries.size(); i++) { + entries.add(mEntries.get(i).copy()); + } + PieDataSet copied = new PieDataSet(entries, getLabel()); + copy(copied); + return copied; + } + + protected void copy(PieDataSet pieDataSet) { + super.copy(pieDataSet); + } + + @Override + protected void calcMinMax(PieEntry e) { + + if (e == null) + return; + + calcMinMaxY(e); + } + + /** + * Sets the space that is left out between the piechart-slices in dp. + * Default: 0 --> no space, maximum 20f + * + * @param spaceDp + */ + public void setSliceSpace(float spaceDp) { + + if (spaceDp > 20) + spaceDp = 20f; + if (spaceDp < 0) + spaceDp = 0f; + + mSliceSpace = Utils.convertDpToPixel(spaceDp); + } + + @Override + public float getSliceSpace() { + return mSliceSpace; + } + + /** + * When enabled, slice spacing will be 0.0 when the smallest value is going to be + * smaller than the slice spacing itself. + * + * @param autoDisable + */ + public void setAutomaticallyDisableSliceSpacing(boolean autoDisable) { + mAutomaticallyDisableSliceSpacing = autoDisable; + } + + /** + * When enabled, slice spacing will be 0.0 when the smallest value is going to be + * smaller than the slice spacing itself. + * + * @return + */ + @Override + public boolean isAutomaticallyDisableSliceSpacingEnabled() { + return mAutomaticallyDisableSliceSpacing; + } + + /** + * sets the distance the highlighted piechart-slice of this DataSet is + * "shifted" away from the center of the chart, default 12f + * + * @param shift + */ + public void setSelectionShift(float shift) { + mShift = Utils.convertDpToPixel(shift); + } + + @Override + public float getSelectionShift() { + return mShift; + } + + @Override + public ValuePosition getXValuePosition() { + return mXValuePosition; + } + + public void setXValuePosition(ValuePosition xValuePosition) { + this.mXValuePosition = xValuePosition; + } + + @Override + public ValuePosition getYValuePosition() { + return mYValuePosition; + } + + public void setYValuePosition(ValuePosition yValuePosition) { + this.mYValuePosition = yValuePosition; + } + + /** + * This method is deprecated. + * Use isUseValueColorForLineEnabled() instead. + */ + @Deprecated + public boolean isUsingSliceColorAsValueLineColor() { + return isUseValueColorForLineEnabled(); + } + + /** + * This method is deprecated. + * Use setUseValueColorForLine(...) instead. + * + * @param enabled + */ + @Deprecated + public void setUsingSliceColorAsValueLineColor(boolean enabled) { + setUseValueColorForLine(enabled); + } + + /** + * When valuePosition is OutsideSlice, indicates line color + */ + @Override + public int getValueLineColor() { + return mValueLineColor; + } + + public void setValueLineColor(int valueLineColor) { + this.mValueLineColor = valueLineColor; + } + + @Override + public boolean isUseValueColorForLineEnabled() + { + return mUseValueColorForLine; + } + + public void setUseValueColorForLine(boolean enabled) + { + mUseValueColorForLine = enabled; + } + + /** + * When valuePosition is OutsideSlice, indicates line width + */ + @Override + public float getValueLineWidth() { + return mValueLineWidth; + } + + public void setValueLineWidth(float valueLineWidth) { + this.mValueLineWidth = valueLineWidth; + } + + /** + * When valuePosition is OutsideSlice, indicates offset as percentage out of the slice size + */ + @Override + public float getValueLinePart1OffsetPercentage() { + return mValueLinePart1OffsetPercentage; + } + + public void setValueLinePart1OffsetPercentage(float valueLinePart1OffsetPercentage) { + this.mValueLinePart1OffsetPercentage = valueLinePart1OffsetPercentage; + } + + /** + * When valuePosition is OutsideSlice, indicates length of first half of the line + */ + @Override + public float getValueLinePart1Length() { + return mValueLinePart1Length; + } + + public void setValueLinePart1Length(float valueLinePart1Length) { + this.mValueLinePart1Length = valueLinePart1Length; + } + + /** + * When valuePosition is OutsideSlice, indicates length of second half of the line + */ + @Override + public float getValueLinePart2Length() { + return mValueLinePart2Length; + } + + public void setValueLinePart2Length(float valueLinePart2Length) { + this.mValueLinePart2Length = valueLinePart2Length; + } + + /** + * When valuePosition is OutsideSlice, this allows variable line length + */ + @Override + public boolean isValueLineVariableLength() { + return mValueLineVariableLength; + } + + public void setValueLineVariableLength(boolean valueLineVariableLength) { + this.mValueLineVariableLength = valueLineVariableLength; + } + + /** Gets the color for the highlighted sector */ + @Override + @Nullable + public Integer getHighlightColor() + { + return mHighlightColor; + } + + /** Sets the color for the highlighted sector (null for using entry color) */ + public void setHighlightColor(@Nullable Integer color) + { + this.mHighlightColor = color; + } + + + public enum ValuePosition { + INSIDE_SLICE, + OUTSIDE_SLICE + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/PieEntry.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/PieEntry.java new file mode 100644 index 0000000..65741ef --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/PieEntry.java @@ -0,0 +1,86 @@ +package com.github.mikephil.charting.data; + +import android.annotation.SuppressLint; +import android.graphics.drawable.Drawable; +import android.util.Log; + +/** + * @author Philipp Jahoda + */ +@SuppressLint("ParcelCreator") +public class PieEntry extends Entry { + + private String label; + + public PieEntry(float value) { + super(0f, value); + } + + public PieEntry(float value, Object data) { + super(0f, value, data); + } + + public PieEntry(float value, Drawable icon) { + super(0f, value, icon); + } + + public PieEntry(float value, Drawable icon, Object data) { + super(0f, value, icon, data); + } + + public PieEntry(float value, String label) { + super(0f, value); + this.label = label; + } + + public PieEntry(float value, String label, Object data) { + super(0f, value, data); + this.label = label; + } + + public PieEntry(float value, String label, Drawable icon) { + super(0f, value, icon); + this.label = label; + } + + public PieEntry(float value, String label, Drawable icon, Object data) { + super(0f, value, icon, data); + this.label = label; + } + + /** + * This is the same as getY(). Returns the value of the PieEntry. + * + * @return + */ + public float getValue() { + return getY(); + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + @Deprecated + @Override + public void setX(float x) { + super.setX(x); + Log.i("DEPRECATED", "Pie entries do not have x values"); + } + + @Deprecated + @Override + public float getX() { + Log.i("DEPRECATED", "Pie entries do not have x values"); + return super.getX(); + } + + public PieEntry copy() { + PieEntry e = new PieEntry(getY(), label, getData()); + return e; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/RadarData.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/RadarData.java new file mode 100644 index 0000000..0c1dbe5 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/RadarData.java @@ -0,0 +1,58 @@ + +package com.github.mikephil.charting.data; + +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.datasets.IRadarDataSet; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Data container for the RadarChart. + * + * @author Philipp Jahoda + */ +public class RadarData extends ChartData { + + private List mLabels; + + public RadarData() { + super(); + } + + public RadarData(List dataSets) { + super(dataSets); + } + + public RadarData(IRadarDataSet... dataSets) { + super(dataSets); + } + + /** + * Sets the labels that should be drawn around the RadarChart at the end of each web line. + * + * @param labels + */ + public void setLabels(List labels) { + this.mLabels = labels; + } + + /** + * Sets the labels that should be drawn around the RadarChart at the end of each web line. + * + * @param labels + */ + public void setLabels(String... labels) { + this.mLabels = Arrays.asList(labels); + } + + public List getLabels() { + return mLabels; + } + + @Override + public Entry getEntryForHighlight(Highlight highlight) { + return getDataSetByIndex(highlight.getDataSetIndex()).getEntryForIndex((int) highlight.getX()); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/RadarDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/RadarDataSet.java new file mode 100644 index 0000000..8a9740b --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/RadarDataSet.java @@ -0,0 +1,122 @@ + +package com.github.mikephil.charting.data; + +import android.graphics.Color; + +import com.github.mikephil.charting.interfaces.datasets.IRadarDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; + +import java.util.ArrayList; +import java.util.List; + +public class RadarDataSet extends LineRadarDataSet implements IRadarDataSet { + + /// flag indicating whether highlight circle should be drawn or not + protected boolean mDrawHighlightCircleEnabled = false; + + protected int mHighlightCircleFillColor = Color.WHITE; + + /// The stroke color for highlight circle. + /// If Utils.COLOR_NONE, the color of the dataset is taken. + protected int mHighlightCircleStrokeColor = ColorTemplate.COLOR_NONE; + + protected int mHighlightCircleStrokeAlpha = (int) (0.3 * 255); + protected float mHighlightCircleInnerRadius = 3.0f; + protected float mHighlightCircleOuterRadius = 4.0f; + protected float mHighlightCircleStrokeWidth = 2.0f; + + public RadarDataSet(List yVals, String label) { + super(yVals, label); + } + + /// Returns true if highlight circle should be drawn, false if not + @Override + public boolean isDrawHighlightCircleEnabled() { + return mDrawHighlightCircleEnabled; + } + + /// Sets whether highlight circle should be drawn or not + @Override + public void setDrawHighlightCircleEnabled(boolean enabled) { + mDrawHighlightCircleEnabled = enabled; + } + + @Override + public int getHighlightCircleFillColor() { + return mHighlightCircleFillColor; + } + + public void setHighlightCircleFillColor(int color) { + mHighlightCircleFillColor = color; + } + + /// Returns the stroke color for highlight circle. + /// If Utils.COLOR_NONE, the color of the dataset is taken. + @Override + public int getHighlightCircleStrokeColor() { + return mHighlightCircleStrokeColor; + } + + /// Sets the stroke color for highlight circle. + /// Set to Utils.COLOR_NONE in order to use the color of the dataset; + public void setHighlightCircleStrokeColor(int color) { + mHighlightCircleStrokeColor = color; + } + + @Override + public int getHighlightCircleStrokeAlpha() { + return mHighlightCircleStrokeAlpha; + } + + public void setHighlightCircleStrokeAlpha(int alpha) { + mHighlightCircleStrokeAlpha = alpha; + } + + @Override + public float getHighlightCircleInnerRadius() { + return mHighlightCircleInnerRadius; + } + + public void setHighlightCircleInnerRadius(float radius) { + mHighlightCircleInnerRadius = radius; + } + + @Override + public float getHighlightCircleOuterRadius() { + return mHighlightCircleOuterRadius; + } + + public void setHighlightCircleOuterRadius(float radius) { + mHighlightCircleOuterRadius = radius; + } + + @Override + public float getHighlightCircleStrokeWidth() { + return mHighlightCircleStrokeWidth; + } + + public void setHighlightCircleStrokeWidth(float strokeWidth) { + mHighlightCircleStrokeWidth = strokeWidth; + } + + @Override + public DataSet copy() { + List entries = new ArrayList(); + for (int i = 0; i < mEntries.size(); i++) { + entries.add(mEntries.get(i).copy()); + } + RadarDataSet copied = new RadarDataSet(entries, getLabel()); + copy(copied); + return copied; + } + + protected void copy(RadarDataSet radarDataSet) { + super.copy(radarDataSet); + radarDataSet.mDrawHighlightCircleEnabled = mDrawHighlightCircleEnabled; + radarDataSet.mHighlightCircleFillColor = mHighlightCircleFillColor; + radarDataSet.mHighlightCircleInnerRadius = mHighlightCircleInnerRadius; + radarDataSet.mHighlightCircleStrokeAlpha = mHighlightCircleStrokeAlpha; + radarDataSet.mHighlightCircleStrokeColor = mHighlightCircleStrokeColor; + radarDataSet.mHighlightCircleStrokeWidth = mHighlightCircleStrokeWidth; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/RadarEntry.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/RadarEntry.java new file mode 100644 index 0000000..02fdce7 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/RadarEntry.java @@ -0,0 +1,44 @@ +package com.github.mikephil.charting.data; + +import android.annotation.SuppressLint; + +/** + * Created by philipp on 13/06/16. + */ +@SuppressLint("ParcelCreator") +public class RadarEntry extends Entry { + + public RadarEntry(float value) { + super(0f, value); + } + + public RadarEntry(float value, Object data) { + super(0f, value, data); + } + + /** + * This is the same as getY(). Returns the value of the RadarEntry. + * + * @return + */ + public float getValue() { + return getY(); + } + + public RadarEntry copy() { + RadarEntry e = new RadarEntry(getY(), getData()); + return e; + } + + @Deprecated + @Override + public void setX(float x) { + super.setX(x); + } + + @Deprecated + @Override + public float getX() { + return super.getX(); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/ScatterData.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/ScatterData.java new file mode 100644 index 0000000..ba14236 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/ScatterData.java @@ -0,0 +1,40 @@ + +package com.github.mikephil.charting.data; + +import com.github.mikephil.charting.interfaces.datasets.IScatterDataSet; + +import java.util.List; + +public class ScatterData extends BarLineScatterCandleBubbleData { + + public ScatterData() { + super(); + } + + public ScatterData(List dataSets) { + super(dataSets); + } + + public ScatterData(IScatterDataSet... dataSets) { + super(dataSets); + } + + /** + * Returns the maximum shape-size across all DataSets. + * + * @return + */ + public float getGreatestShapeSize() { + + float max = 0f; + + for (IScatterDataSet set : mDataSets) { + float size = set.getScatterShapeSize(); + + if (size > max) + max = size; + } + + return max; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/ScatterDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/ScatterDataSet.java new file mode 100644 index 0000000..85ed808 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/ScatterDataSet.java @@ -0,0 +1,157 @@ + +package com.github.mikephil.charting.data; + +import com.github.mikephil.charting.charts.ScatterChart; +import com.github.mikephil.charting.interfaces.datasets.IScatterDataSet; +import com.github.mikephil.charting.renderer.scatter.ChevronDownShapeRenderer; +import com.github.mikephil.charting.renderer.scatter.ChevronUpShapeRenderer; +import com.github.mikephil.charting.renderer.scatter.CircleShapeRenderer; +import com.github.mikephil.charting.renderer.scatter.CrossShapeRenderer; +import com.github.mikephil.charting.renderer.scatter.IShapeRenderer; +import com.github.mikephil.charting.renderer.scatter.SquareShapeRenderer; +import com.github.mikephil.charting.renderer.scatter.TriangleShapeRenderer; +import com.github.mikephil.charting.renderer.scatter.XShapeRenderer; +import com.github.mikephil.charting.utils.ColorTemplate; + +import java.util.ArrayList; +import java.util.List; + +public class ScatterDataSet extends LineScatterCandleRadarDataSet implements IScatterDataSet { + + /** + * the size the scattershape will have, in density pixels + */ + private float mShapeSize = 15f; + + /** + * Renderer responsible for rendering this DataSet, default: square + */ + protected IShapeRenderer mShapeRenderer = new SquareShapeRenderer(); + + /** + * The radius of the hole in the shape (applies to Square, Circle and Triangle) + * - default: 0.0 + */ + private float mScatterShapeHoleRadius = 0f; + + /** + * Color for the hole in the shape. + * Setting to `ColorTemplate.COLOR_NONE` will behave as transparent. + * - default: ColorTemplate.COLOR_NONE + */ + private int mScatterShapeHoleColor = ColorTemplate.COLOR_NONE; + + public ScatterDataSet(List yVals, String label) { + super(yVals, label); + } + + @Override + public DataSet copy() { + List entries = new ArrayList(); + for (int i = 0; i < mEntries.size(); i++) { + entries.add(mEntries.get(i).copy()); + } + ScatterDataSet copied = new ScatterDataSet(entries, getLabel()); + copy(copied); + return copied; + } + + protected void copy(ScatterDataSet scatterDataSet) { + super.copy(scatterDataSet); + scatterDataSet.mShapeSize = mShapeSize; + scatterDataSet.mShapeRenderer = mShapeRenderer; + scatterDataSet.mScatterShapeHoleRadius = mScatterShapeHoleRadius; + scatterDataSet.mScatterShapeHoleColor = mScatterShapeHoleColor; + } + + /** + * Sets the size in density pixels the drawn scattershape will have. This + * only applies for non custom shapes. + * + * @param size + */ + public void setScatterShapeSize(float size) { + mShapeSize = size; + } + + @Override + public float getScatterShapeSize() { + return mShapeSize; + } + + /** + * Sets the ScatterShape this DataSet should be drawn with. This will search for an available IShapeRenderer and set this + * renderer for the DataSet. + * + * @param shape + */ + public void setScatterShape(ScatterChart.ScatterShape shape) { + mShapeRenderer = getRendererForShape(shape); + } + + /** + * Sets a new IShapeRenderer responsible for drawing this DataSet. + * This can also be used to set a custom IShapeRenderer aside from the default ones. + * + * @param shapeRenderer + */ + public void setShapeRenderer(IShapeRenderer shapeRenderer) { + mShapeRenderer = shapeRenderer; + } + + @Override + public IShapeRenderer getShapeRenderer() { + return mShapeRenderer; + } + + /** + * Sets the radius of the hole in the shape (applies to Square, Circle and Triangle) + * Set this to <= 0 to remove holes. + * + * @param holeRadius + */ + public void setScatterShapeHoleRadius(float holeRadius) { + mScatterShapeHoleRadius = holeRadius; + } + + @Override + public float getScatterShapeHoleRadius() { + return mScatterShapeHoleRadius; + } + + /** + * Sets the color for the hole in the shape. + * + * @param holeColor + */ + public void setScatterShapeHoleColor(int holeColor) { + mScatterShapeHoleColor = holeColor; + } + + @Override + public int getScatterShapeHoleColor() { + return mScatterShapeHoleColor; + } + + public static IShapeRenderer getRendererForShape(ScatterChart.ScatterShape shape) { + + switch (shape) { + case SQUARE: + return new SquareShapeRenderer(); + case CIRCLE: + return new CircleShapeRenderer(); + case TRIANGLE: + return new TriangleShapeRenderer(); + case CROSS: + return new CrossShapeRenderer(); + case X: + return new XShapeRenderer(); + case CHEVRON_UP: + return new ChevronUpShapeRenderer(); + case CHEVRON_DOWN: + return new ChevronDownShapeRenderer(); + } + + return null; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/filter/Approximator.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/filter/Approximator.java new file mode 100644 index 0000000..542188e --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/filter/Approximator.java @@ -0,0 +1,102 @@ + +package com.github.mikephil.charting.data.filter; + +import android.annotation.TargetApi; +import android.os.Build; + +import java.util.Arrays; + +/** + * Implemented according to Wiki-Pseudocode {@link} + * http://en.wikipedia.org/wiki/Ramer�Douglas�Peucker_algorithm + * + * @author Philipp Baldauf & Phliipp Jahoda + */ +public class Approximator { + + @TargetApi(Build.VERSION_CODES.GINGERBREAD) + public float[] reduceWithDouglasPeucker(float[] points, float tolerance) { + + int greatestIndex = 0; + float greatestDistance = 0f; + + Line line = new Line(points[0], points[1], points[points.length - 2], points[points.length - 1]); + + for (int i = 2; i < points.length - 2; i += 2) { + + float distance = line.distance(points[i], points[i + 1]); + + if (distance > greatestDistance) { + greatestDistance = distance; + greatestIndex = i; + } + } + + if (greatestDistance > tolerance) { + + float[] reduced1 = reduceWithDouglasPeucker(Arrays.copyOfRange(points, 0, greatestIndex + 2), tolerance); + float[] reduced2 = reduceWithDouglasPeucker(Arrays.copyOfRange(points, greatestIndex, points.length), + tolerance); + + float[] result1 = reduced1; + float[] result2 = Arrays.copyOfRange(reduced2, 2, reduced2.length); + + return concat(result1, result2); + } else { + return line.getPoints(); + } + } + + /** + * Combine arrays. + * + * @param arrays + * @return + */ + float[] concat(float[]... arrays) { + int length = 0; + for (float[] array : arrays) { + length += array.length; + } + float[] result = new float[length]; + int pos = 0; + for (float[] array : arrays) { + for (float element : array) { + result[pos] = element; + pos++; + } + } + return result; + } + + private class Line { + + private float[] points; + + private float sxey; + private float exsy; + + private float dx; + private float dy; + + private float length; + + public Line(float x1, float y1, float x2, float y2) { + dx = x1 - x2; + dy = y1 - y2; + sxey = x1 * y2; + exsy = x2 * y1; + length = (float) Math.sqrt(dx * dx + dy * dy); + + points = new float[]{x1, y1, x2, y2}; + } + + public float distance(float x, float y) { + return Math.abs(dy * x - dx * y + sxey - exsy) / length; + } + + public float[] getPoints() { + return points; + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/data/filter/ApproximatorN.java b/MPChartLib/src/main/java/com/github/mikephil/charting/data/filter/ApproximatorN.java new file mode 100644 index 0000000..9351341 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/data/filter/ApproximatorN.java @@ -0,0 +1,146 @@ + +package com.github.mikephil.charting.data.filter; + +import java.util.ArrayList; + +/** + * Implemented according to modified Douglas Peucker {@link} + * http://psimpl.sourceforge.net/douglas-peucker.html + */ +public class ApproximatorN +{ + public float[] reduceWithDouglasPeucker(float[] points, float resultCount) { + + int pointCount = points.length / 2; + + // if a shape has 2 or less points it cannot be reduced + if (resultCount <= 2 || resultCount >= pointCount) + return points; + + boolean[] keep = new boolean[pointCount]; + + // first and last always stay + keep[0] = true; + keep[pointCount - 1] = true; + + int currentStoredPoints = 2; + + ArrayList queue = new ArrayList<>(); + Line line = new Line(0, pointCount - 1, points); + queue.add(line); + + do { + line = queue.remove(queue.size() - 1); + + // store the key + keep[line.index] = true; + + // check point count tolerance + currentStoredPoints += 1; + + if (currentStoredPoints == resultCount) + break; + + // split the polyline at the key and recurse + Line left = new Line(line.start, line.index, points); + if (left.index > 0) { + int insertionIndex = insertionIndex(left, queue); + queue.add(insertionIndex, left); + } + + Line right = new Line(line.index, line.end, points); + if (right.index > 0) { + int insertionIndex = insertionIndex(right, queue); + queue.add(insertionIndex, right); + } + } while (queue.isEmpty()); + + float[] reducedEntries = new float[currentStoredPoints * 2]; + + for (int i = 0, i2 = 0, r2 = 0; i < currentStoredPoints; i++, r2 += 2) { + if (keep[i]) { + reducedEntries[i2++] = points[r2]; + reducedEntries[i2++] = points[r2 + 1]; + } + } + + return reducedEntries; + } + + private static float distanceToLine( + float ptX, float ptY, float[] + fromLinePoint1, float[] fromLinePoint2) { + float dx = fromLinePoint2[0] - fromLinePoint1[0]; + float dy = fromLinePoint2[1] - fromLinePoint1[1]; + + float dividend = Math.abs( + dy * ptX - + dx * ptY - + fromLinePoint1[0] * fromLinePoint2[1] + + fromLinePoint2[0] * fromLinePoint1[1]); + double divisor = Math.sqrt(dx * dx + dy * dy); + + return (float)(dividend / divisor); + } + + private static class Line { + int start; + int end; + + float distance = 0; + int index = 0; + + Line(int start, int end, float[] points) { + this.start = start; + this.end = end; + + float[] startPoint = new float[]{points[start * 2], points[start * 2 + 1]}; + float[] endPoint = new float[]{points[end * 2], points[end * 2 + 1]}; + + if (end <= start + 1) return; + + for (int i = start + 1, i2 = i * 2; i < end; i++, i2 += 2) { + float distance = distanceToLine( + points[i2], points[i2 + 1], + startPoint, endPoint); + + if (distance > this.distance) { + this.index = i; + this.distance = distance; + } + } + } + + boolean equals(final Line rhs) { + return (start == rhs.start) && (end == rhs.end) && index == rhs.index; + } + + boolean lessThan(final Line rhs) { + return distance < rhs.distance; + } + } + + private static int insertionIndex(Line line, ArrayList queue) { + int min = 0; + int max = queue.size(); + + while (!queue.isEmpty()) { + int midIndex = min + (max - min) / 2; + Line midLine = queue.get(midIndex); + + if (midLine.equals(line)) { + return midIndex; + } + else if (line.lessThan(midLine)) { + // perform search in left half + max = midIndex; + } + else { + // perform search in right half + min = midIndex + 1; + } + } + + return min; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/exception/DrawingDataSetNotCreatedException.java b/MPChartLib/src/main/java/com/github/mikephil/charting/exception/DrawingDataSetNotCreatedException.java new file mode 100644 index 0000000..d6c1237 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/exception/DrawingDataSetNotCreatedException.java @@ -0,0 +1,14 @@ +package com.github.mikephil.charting.exception; + +public class DrawingDataSetNotCreatedException extends RuntimeException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public DrawingDataSetNotCreatedException() { + super("Have to create a new drawing set first. Call ChartData's createNewDrawingDataSet() method"); + } + +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/ColorFormatter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/ColorFormatter.java new file mode 100644 index 0000000..2db66fd --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/ColorFormatter.java @@ -0,0 +1,24 @@ +package com.github.mikephil.charting.formatter; + +import com.github.mikephil.charting.data.DataSet; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; + +/** + * Interface that can be used to return a customized color instead of setting + * colors via the setColor(...) method of the DataSet. + * + * @author Philipp Jahoda + */ +public interface ColorFormatter { + + /** + * Returns the color to be used for the given Entry at the given index (in the entries array) + * + * @param index index in the entries array + * @param e the entry to color + * @param set the DataSet the entry belongs to + * @return + */ + int getColor(int index, Entry e, IDataSet set); +} \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/DefaultAxisValueFormatter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/DefaultAxisValueFormatter.java new file mode 100644 index 0000000..552c150 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/DefaultAxisValueFormatter.java @@ -0,0 +1,56 @@ +package com.github.mikephil.charting.formatter; + +import com.github.mikephil.charting.components.AxisBase; + +import java.text.DecimalFormat; + +/** + * Created by philipp on 02/06/16. + */ +public class DefaultAxisValueFormatter implements IAxisValueFormatter +{ + + /** + * decimalformat for formatting + */ + protected DecimalFormat mFormat; + + /** + * the number of decimal digits this formatter uses + */ + protected int digits = 0; + + /** + * Constructor that specifies to how many digits the value should be + * formatted. + * + * @param digits + */ + public DefaultAxisValueFormatter(int digits) { + this.digits = digits; + + StringBuffer b = new StringBuffer(); + for (int i = 0; i < digits; i++) { + if (i == 0) + b.append("."); + b.append("0"); + } + + mFormat = new DecimalFormat("###,###,###,##0" + b.toString()); + } + + @Override + public String getFormattedValue(float value, AxisBase axis) { + // avoid memory allocations here (for performance) + return mFormat.format(value); + } + + /** + * Returns the number of decimal digits this formatter uses or -1, if unspecified. + * + * @return + */ + public int getDecimalDigits() { + return digits; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/DefaultFillFormatter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/DefaultFillFormatter.java new file mode 100644 index 0000000..851faeb --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/DefaultFillFormatter.java @@ -0,0 +1,45 @@ +package com.github.mikephil.charting.formatter; + + +import com.github.mikephil.charting.data.LineData; +import com.github.mikephil.charting.interfaces.dataprovider.LineDataProvider; +import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; + +/** + * Default formatter that calculates the position of the filled line. + * + * @author Philipp Jahoda + */ +public class DefaultFillFormatter implements IFillFormatter +{ + + @Override + public float getFillLinePosition(ILineDataSet dataSet, LineDataProvider dataProvider) { + + float fillMin = 0f; + float chartMaxY = dataProvider.getYChartMax(); + float chartMinY = dataProvider.getYChartMin(); + + LineData data = dataProvider.getLineData(); + + if (dataSet.getYMax() > 0 && dataSet.getYMin() < 0) { + fillMin = 0f; + } else { + + float max, min; + + if (data.getYMax() > 0) + max = 0f; + else + max = chartMaxY; + if (data.getYMin() < 0) + min = 0f; + else + min = chartMinY; + + fillMin = dataSet.getYMin() >= 0 ? min : max; + } + + return fillMin; + } +} \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/DefaultValueFormatter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/DefaultValueFormatter.java new file mode 100644 index 0000000..e2fea4b --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/DefaultValueFormatter.java @@ -0,0 +1,71 @@ + +package com.github.mikephil.charting.formatter; + +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.text.DecimalFormat; + +/** + * Default formatter used for formatting values inside the chart. Uses a DecimalFormat with + * pre-calculated number of digits (depending on max and min value). + * + * @author Philipp Jahoda + */ +public class DefaultValueFormatter implements IValueFormatter +{ + + /** + * DecimalFormat for formatting + */ + protected DecimalFormat mFormat; + + protected int mDecimalDigits; + + /** + * Constructor that specifies to how many digits the value should be + * formatted. + * + * @param digits + */ + public DefaultValueFormatter(int digits) { + setup(digits); + } + + /** + * Sets up the formatter with a given number of decimal digits. + * + * @param digits + */ + public void setup(int digits) { + + this.mDecimalDigits = digits; + + StringBuffer b = new StringBuffer(); + for (int i = 0; i < digits; i++) { + if (i == 0) + b.append("."); + b.append("0"); + } + + mFormat = new DecimalFormat("###,###,###,##0" + b.toString()); + } + + @Override + public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) { + + // put more logic here ... + // avoid memory allocations here (for performance reasons) + + return mFormat.format(value); + } + + /** + * Returns the number of decimal digits this formatter uses. + * + * @return + */ + public int getDecimalDigits() { + return mDecimalDigits; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IAxisValueFormatter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IAxisValueFormatter.java new file mode 100644 index 0000000..51939b5 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IAxisValueFormatter.java @@ -0,0 +1,23 @@ +package com.github.mikephil.charting.formatter; + +import com.github.mikephil.charting.components.AxisBase; + +/** + * Created by Philipp Jahoda on 20/09/15. + * Custom formatter interface that allows formatting of + * axis labels before they are being drawn. + */ +public interface IAxisValueFormatter +{ + + /** + * Called when a value from an axis is to be formatted + * before being drawn. For performance reasons, avoid excessive calculations + * and memory allocations inside this method. + * + * @param value the value to be formatted + * @param axis the axis the value belongs to + * @return + */ + String getFormattedValue(float value, AxisBase axis); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IFillFormatter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IFillFormatter.java new file mode 100644 index 0000000..e096cc6 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IFillFormatter.java @@ -0,0 +1,24 @@ +package com.github.mikephil.charting.formatter; + +import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; +import com.github.mikephil.charting.interfaces.dataprovider.LineDataProvider; + +/** + * Interface for providing a custom logic to where the filling line of a LineDataSet + * should end. This of course only works if setFillEnabled(...) is set to true. + * + * @author Philipp Jahoda + */ +public interface IFillFormatter +{ + + /** + * Returns the vertical (y-axis) position where the filled-line of the + * LineDataSet should end. + * + * @param dataSet the ILineDataSet that is currently drawn + * @param dataProvider + * @return + */ + float getFillLinePosition(ILineDataSet dataSet, LineDataProvider dataProvider); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IValueFormatter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IValueFormatter.java new file mode 100644 index 0000000..75d2363 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IValueFormatter.java @@ -0,0 +1,29 @@ +package com.github.mikephil.charting.formatter; + +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Interface that allows custom formatting of all values inside the chart before they are + * being drawn to the screen. Simply create your own formatting class and let + * it implement IValueFormatter. Then override the getFormattedValue(...) method + * and return whatever you want. + * + * @author Philipp Jahoda + */ +public interface IValueFormatter +{ + + /** + * Called when a value (from labels inside the chart) is formatted + * before being drawn. For performance reasons, avoid excessive calculations + * and memory allocations inside this method. + * + * @param value the value to be formatted + * @param entry the entry the value belongs to - in e.g. BarChart, this is of class BarEntry + * @param dataSetIndex the index of the DataSet the entry in focus belongs to + * @param viewPortHandler provides information about the current chart state (scale, translation, ...) + * @return the formatted label ready for being drawn + */ + String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IndexAxisValueFormatter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IndexAxisValueFormatter.java new file mode 100644 index 0000000..07349a6 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/IndexAxisValueFormatter.java @@ -0,0 +1,69 @@ + +package com.github.mikephil.charting.formatter; + +import com.github.mikephil.charting.components.AxisBase; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.text.DecimalFormat; +import java.util.Arrays; +import java.util.Collection; + +/** + * This formatter is used for passing an array of x-axis labels, on whole x steps. + */ +public class IndexAxisValueFormatter implements IAxisValueFormatter +{ + private String[] mValues = new String[] {}; + private int mValueCount = 0; + + /** + * An empty constructor. + * Use `setValues` to set the axis labels. + */ + public IndexAxisValueFormatter() { + } + + /** + * Constructor that specifies axis labels. + * + * @param values The values string array + */ + public IndexAxisValueFormatter(String[] values) { + if (values != null) + setValues(values); + } + + /** + * Constructor that specifies axis labels. + * + * @param values The values string array + */ + public IndexAxisValueFormatter(Collection values) { + if (values != null) + setValues(values.toArray(new String[values.size()])); + } + + public String getFormattedValue(float value, AxisBase axis) { + int index = Math.round(value); + + if (index < 0 || index >= mValueCount || index != (int)value) + return ""; + + return mValues[index]; + } + + public String[] getValues() + { + return mValues; + } + + public void setValues(String[] values) + { + if (values == null) + values = new String[] {}; + + this.mValues = values; + this.mValueCount = values.length; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/LargeValueFormatter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/LargeValueFormatter.java new file mode 100644 index 0000000..211401a --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/LargeValueFormatter.java @@ -0,0 +1,103 @@ + +package com.github.mikephil.charting.formatter; + +import com.github.mikephil.charting.components.AxisBase; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.text.DecimalFormat; + +/** + * Predefined value-formatter that formats large numbers in a pretty way. + * Outputs: 856 = 856; 1000 = 1k; 5821 = 5.8k; 10500 = 10k; 101800 = 102k; + * 2000000 = 2m; 7800000 = 7.8m; 92150000 = 92m; 123200000 = 123m; 9999999 = + * 10m; 1000000000 = 1b; Special thanks to Roman Gromov + * (https://github.com/romangromov) for this piece of code. + * + * @author Philipp Jahoda + * @author Oleksandr Tyshkovets + */ +public class LargeValueFormatter implements IValueFormatter, IAxisValueFormatter +{ + + private String[] mSuffix = new String[]{ + "", "k", "m", "b", "t" + }; + private int mMaxLength = 5; + private DecimalFormat mFormat; + private String mText = ""; + + public LargeValueFormatter() { + mFormat = new DecimalFormat("###E00"); + } + + /** + * Creates a formatter that appends a specified text to the result string + * + * @param appendix a text that will be appended + */ + public LargeValueFormatter(String appendix) { + this(); + mText = appendix; + } + + // IValueFormatter + @Override + public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) { + return makePretty(value) + mText; + } + + // IAxisValueFormatter + @Override + public String getFormattedValue(float value, AxisBase axis) { + return makePretty(value) + mText; + } + + /** + * Set an appendix text to be added at the end of the formatted value. + * + * @param appendix + */ + public void setAppendix(String appendix) { + this.mText = appendix; + } + + /** + * Set custom suffix to be appended after the values. + * Default suffix: ["", "k", "m", "b", "t"] + * + * @param suffix new suffix + */ + public void setSuffix(String[] suffix) { + this.mSuffix = suffix; + } + + public void setMaxLength(int maxLength) { + this.mMaxLength = maxLength; + } + + /** + * Formats each number properly. Special thanks to Roman Gromov + * (https://github.com/romangromov) for this piece of code. + */ + private String makePretty(double number) { + + String r = mFormat.format(number); + + int numericValue1 = Character.getNumericValue(r.charAt(r.length() - 1)); + int numericValue2 = Character.getNumericValue(r.charAt(r.length() - 2)); + int combined = Integer.valueOf(numericValue2 + "" + numericValue1); + + r = r.replaceAll("E[0-9][0-9]", mSuffix[combined / 3]); + + while (r.length() > mMaxLength || r.matches("[0-9]+\\.[a-z]")) { + r = r.substring(0, r.length() - 2) + r.substring(r.length() - 1); + } + + return r; + } + + public int getDecimalDigits() { + return 0; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/PercentFormatter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/PercentFormatter.java new file mode 100644 index 0000000..de8a102 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/PercentFormatter.java @@ -0,0 +1,49 @@ + +package com.github.mikephil.charting.formatter; + +import com.github.mikephil.charting.components.AxisBase; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.text.DecimalFormat; + +/** + * This IValueFormatter is just for convenience and simply puts a "%" sign after + * each value. (Recommeded for PieChart) + * + * @author Philipp Jahoda + */ +public class PercentFormatter implements IValueFormatter, IAxisValueFormatter +{ + + protected DecimalFormat mFormat; + + public PercentFormatter() { + mFormat = new DecimalFormat("###,###,##0.0"); + } + + /** + * Allow a custom decimalformat + * + * @param format + */ + public PercentFormatter(DecimalFormat format) { + this.mFormat = format; + } + + // IValueFormatter + @Override + public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) { + return mFormat.format(value) + " %"; + } + + // IAxisValueFormatter + @Override + public String getFormattedValue(float value, AxisBase axis) { + return mFormat.format(value) + " %"; + } + + public int getDecimalDigits() { + return 1; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/StackedValueFormatter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/StackedValueFormatter.java new file mode 100644 index 0000000..0e83516 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/formatter/StackedValueFormatter.java @@ -0,0 +1,75 @@ +package com.github.mikephil.charting.formatter; + +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.text.DecimalFormat; + +/** + * Created by Philipp Jahoda on 28/01/16. + *

+ * A formatter specifically for stacked BarChart that allows to specify whether the all stack values + * or just the top value should be drawn. + */ +public class StackedValueFormatter implements IValueFormatter +{ + + /** + * if true, all stack values of the stacked bar entry are drawn, else only top + */ + private boolean mDrawWholeStack; + + /** + * a string that should be appended behind the value + */ + private String mAppendix; + + private DecimalFormat mFormat; + + /** + * Constructor. + * + * @param drawWholeStack if true, all stack values of the stacked bar entry are drawn, else only top + * @param appendix a string that should be appended behind the value + * @param decimals the number of decimal digits to use + */ + public StackedValueFormatter(boolean drawWholeStack, String appendix, int decimals) { + this.mDrawWholeStack = drawWholeStack; + this.mAppendix = appendix; + + StringBuffer b = new StringBuffer(); + for (int i = 0; i < decimals; i++) { + if (i == 0) + b.append("."); + b.append("0"); + } + + this.mFormat = new DecimalFormat("###,###,###,##0" + b.toString()); + } + + @Override + public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) { + + if (!mDrawWholeStack && entry instanceof BarEntry) { + + BarEntry barEntry = (BarEntry) entry; + float[] vals = barEntry.getYVals(); + + if (vals != null) { + + // find out if we are on top of the stack + if (vals[vals.length - 1] == value) { + + // return the "sum" across all stack values + return mFormat.format(barEntry.getY()) + mAppendix; + } else { + return ""; // return empty + } + } + } + + // return the "proposed" value + return mFormat.format(value) + mAppendix; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/BarHighlighter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/BarHighlighter.java new file mode 100644 index 0000000..af83a45 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/BarHighlighter.java @@ -0,0 +1,163 @@ +package com.github.mikephil.charting.highlight; + +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.data.BarLineScatterCandleBubbleData; +import com.github.mikephil.charting.interfaces.dataprovider.BarDataProvider; +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; +import com.github.mikephil.charting.utils.MPPointD; + +/** + * Created by Philipp Jahoda on 22/07/15. + */ +public class BarHighlighter extends ChartHighlighter { + + public BarHighlighter(BarDataProvider chart) { + super(chart); + } + + @Override + public Highlight getHighlight(float x, float y) { + Highlight high = super.getHighlight(x, y); + + if(high == null) { + return null; + } + + MPPointD pos = getValsForTouch(x, y); + + BarData barData = mChart.getBarData(); + + IBarDataSet set = barData.getDataSetByIndex(high.getDataSetIndex()); + if (set.isStacked()) { + + return getStackedHighlight(high, + set, + (float) pos.x, + (float) pos.y); + } + + MPPointD.recycleInstance(pos); + + return high; + } + + /** + * This method creates the Highlight object that also indicates which value of a stacked BarEntry has been + * selected. + * + * @param high the Highlight to work with looking for stacked values + * @param set + * @param xVal + * @param yVal + * @return + */ + public Highlight getStackedHighlight(Highlight high, IBarDataSet set, float xVal, float yVal) { + + BarEntry entry = set.getEntryForXValue(xVal, yVal); + + if (entry == null) + return null; + + // not stacked + if (entry.getYVals() == null) { + return high; + } else { + Range[] ranges = entry.getRanges(); + + if (ranges.length > 0) { + int stackIndex = getClosestStackIndex(ranges, yVal); + + MPPointD pixels = mChart.getTransformer(set.getAxisDependency()).getPixelForValues(high.getX(), ranges[stackIndex].to); + + Highlight stackedHigh = new Highlight( + entry.getX(), + entry.getY(), + (float) pixels.x, + (float) pixels.y, + high.getDataSetIndex(), + stackIndex, + high.getAxis() + ); + + MPPointD.recycleInstance(pixels); + + return stackedHigh; + } + } + + return null; + } + + /** + * Returns the index of the closest value inside the values array / ranges (stacked barchart) to the value + * given as + * a parameter. + * + * @param ranges + * @param value + * @return + */ + protected int getClosestStackIndex(Range[] ranges, float value) { + + if (ranges == null || ranges.length == 0) + return 0; + + int stackIndex = 0; + + for (Range range : ranges) { + if (range.contains(value)) + return stackIndex; + else + stackIndex++; + } + + int length = Math.max(ranges.length - 1, 0); + + return (value > ranges[length].to) ? length : 0; + } + +// /** +// * Splits up the stack-values of the given bar-entry into Range objects. +// * +// * @param entry +// * @return +// */ +// protected Range[] getRanges(BarEntry entry) { +// +// float[] values = entry.getYVals(); +// +// if (values == null || values.length == 0) +// return new Range[0]; +// +// Range[] ranges = new Range[values.length]; +// +// float negRemain = -entry.getNegativeSum(); +// float posRemain = 0f; +// +// for (int i = 0; i < ranges.length; i++) { +// +// float value = values[i]; +// +// if (value < 0) { +// ranges[i] = new Range(negRemain, negRemain + Math.abs(value)); +// negRemain += Math.abs(value); +// } else { +// ranges[i] = new Range(posRemain, posRemain + value); +// posRemain += value; +// } +// } +// +// return ranges; +// } + + @Override + protected float getDistance(float x1, float y1, float x2, float y2) { + return Math.abs(x1 - x2); + } + + @Override + protected BarLineScatterCandleBubbleData getData() { + return mChart.getBarData(); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/ChartHighlighter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/ChartHighlighter.java new file mode 100644 index 0000000..f889bf1 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/ChartHighlighter.java @@ -0,0 +1,246 @@ +package com.github.mikephil.charting.highlight; + +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.data.BarLineScatterCandleBubbleData; +import com.github.mikephil.charting.data.DataSet; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.interfaces.dataprovider.BarLineScatterCandleBubbleDataProvider; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.utils.MPPointD; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Philipp Jahoda on 21/07/15. + */ +public class ChartHighlighter implements IHighlighter +{ + + /** + * instance of the data-provider + */ + protected T mChart; + + /** + * buffer for storing previously highlighted values + */ + protected List mHighlightBuffer = new ArrayList(); + + public ChartHighlighter(T chart) { + this.mChart = chart; + } + + @Override + public Highlight getHighlight(float x, float y) { + + MPPointD pos = getValsForTouch(x, y); + float xVal = (float) pos.x; + MPPointD.recycleInstance(pos); + + Highlight high = getHighlightForX(xVal, x, y); + return high; + } + + /** + * Returns a recyclable MPPointD instance. + * Returns the corresponding xPos for a given touch-position in pixels. + * + * @param x + * @param y + * @return + */ + protected MPPointD getValsForTouch(float x, float y) { + + // take any transformer to determine the x-axis value + MPPointD pos = mChart.getTransformer(YAxis.AxisDependency.LEFT).getValuesByTouchPoint(x, y); + return pos; + } + + /** + * Returns the corresponding Highlight for a given xVal and x- and y-touch position in pixels. + * + * @param xVal + * @param x + * @param y + * @return + */ + protected Highlight getHighlightForX(float xVal, float x, float y) { + + List closestValues = getHighlightsAtXValue(xVal, x, y); + + if(closestValues.isEmpty()) { + return null; + } + + float leftAxisMinDist = getMinimumDistance(closestValues, y, YAxis.AxisDependency.LEFT); + float rightAxisMinDist = getMinimumDistance(closestValues, y, YAxis.AxisDependency.RIGHT); + + YAxis.AxisDependency axis = leftAxisMinDist < rightAxisMinDist ? YAxis.AxisDependency.LEFT : YAxis.AxisDependency.RIGHT; + + Highlight detail = getClosestHighlightByPixel(closestValues, x, y, axis, mChart.getMaxHighlightDistance()); + + return detail; + } + + /** + * Returns the minimum distance from a touch value (in pixels) to the + * closest value (in pixels) that is displayed in the chart. + * + * @param closestValues + * @param pos + * @param axis + * @return + */ + protected float getMinimumDistance(List closestValues, float pos, YAxis.AxisDependency axis) { + + float distance = Float.MAX_VALUE; + + for (int i = 0; i < closestValues.size(); i++) { + + Highlight high = closestValues.get(i); + + if (high.getAxis() == axis) { + + float tempDistance = Math.abs(getHighlightPos(high) - pos); + if (tempDistance < distance) { + distance = tempDistance; + } + } + } + + return distance; + } + + protected float getHighlightPos(Highlight h) { + return h.getYPx(); + } + + /** + * Returns a list of Highlight objects representing the entries closest to the given xVal. + * The returned list contains two objects per DataSet (closest rounding up, closest rounding down). + * + * @param xVal the transformed x-value of the x-touch position + * @param x touch position + * @param y touch position + * @return + */ + protected List getHighlightsAtXValue(float xVal, float x, float y) { + + mHighlightBuffer.clear(); + + BarLineScatterCandleBubbleData data = getData(); + + if (data == null) + return mHighlightBuffer; + + for (int i = 0, dataSetCount = data.getDataSetCount(); i < dataSetCount; i++) { + + IDataSet dataSet = data.getDataSetByIndex(i); + + // don't include DataSets that cannot be highlighted + if (!dataSet.isHighlightEnabled()) + continue; + + mHighlightBuffer.addAll(buildHighlights(dataSet, i, xVal, DataSet.Rounding.CLOSEST)); + } + + return mHighlightBuffer; + } + + /** + * An array of `Highlight` objects corresponding to the selected xValue and dataSetIndex. + * + * @param set + * @param dataSetIndex + * @param xVal + * @param rounding + * @return + */ + protected List buildHighlights(IDataSet set, int dataSetIndex, float xVal, DataSet.Rounding rounding) { + + ArrayList highlights = new ArrayList<>(); + + //noinspection unchecked + List entries = set.getEntriesForXValue(xVal); + if (entries.size() == 0) { + // Try to find closest x-value and take all entries for that x-value + final Entry closest = set.getEntryForXValue(xVal, Float.NaN, rounding); + if (closest != null) + { + //noinspection unchecked + entries = set.getEntriesForXValue(closest.getX()); + } + } + + if (entries.size() == 0) + return highlights; + + for (Entry e : entries) { + MPPointD pixels = mChart.getTransformer( + set.getAxisDependency()).getPixelForValues(e.getX(), e.getY()); + + highlights.add(new Highlight( + e.getX(), e.getY(), + (float) pixels.x, (float) pixels.y, + dataSetIndex, set.getAxisDependency())); + } + + return highlights; + } + + /** + * Returns the Highlight of the DataSet that contains the closest value on the + * y-axis. + * + * @param closestValues contains two Highlight objects per DataSet closest to the selected x-position (determined by + * rounding up an down) + * @param x + * @param y + * @param axis the closest axis + * @param minSelectionDistance + * @return + */ + public Highlight getClosestHighlightByPixel(List closestValues, float x, float y, + YAxis.AxisDependency axis, float minSelectionDistance) { + + Highlight closest = null; + float distance = minSelectionDistance; + + for (int i = 0; i < closestValues.size(); i++) { + + Highlight high = closestValues.get(i); + + if (axis == null || high.getAxis() == axis) { + + float cDistance = getDistance(x, y, high.getXPx(), high.getYPx()); + + if (cDistance < distance) { + closest = high; + distance = cDistance; + } + } + } + + return closest; + } + + /** + * Calculates the distance between the two given points. + * + * @param x1 + * @param y1 + * @param x2 + * @param y2 + * @return + */ + protected float getDistance(float x1, float y1, float x2, float y2) { + //return Math.abs(y1 - y2); + //return Math.abs(x1 - x2); + return (float) Math.hypot(x1 - x2, y1 - y2); + } + + protected BarLineScatterCandleBubbleData getData() { + return mChart.getData(); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/CombinedHighlighter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/CombinedHighlighter.java new file mode 100644 index 0000000..76788af --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/CombinedHighlighter.java @@ -0,0 +1,93 @@ +package com.github.mikephil.charting.highlight; + +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.BarLineScatterCandleBubbleData; +import com.github.mikephil.charting.data.ChartData; +import com.github.mikephil.charting.data.DataSet; +import com.github.mikephil.charting.interfaces.dataprovider.BarDataProvider; +import com.github.mikephil.charting.interfaces.dataprovider.CombinedDataProvider; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; + +import java.util.List; + +/** + * Created by Philipp Jahoda on 12/09/15. + */ +public class CombinedHighlighter extends ChartHighlighter implements IHighlighter +{ + + /** + * bar highlighter for supporting stacked highlighting + */ + protected BarHighlighter barHighlighter; + + public CombinedHighlighter(CombinedDataProvider chart, BarDataProvider barChart) { + super(chart); + + // if there is BarData, create a BarHighlighter + barHighlighter = barChart.getBarData() == null ? null : new BarHighlighter(barChart); + } + + @Override + protected List getHighlightsAtXValue(float xVal, float x, float y) { + + mHighlightBuffer.clear(); + + List dataObjects = mChart.getCombinedData().getAllData(); + + for (int i = 0; i < dataObjects.size(); i++) { + + ChartData dataObject = dataObjects.get(i); + + // in case of BarData, let the BarHighlighter take over + if (barHighlighter != null && dataObject instanceof BarData) { + Highlight high = barHighlighter.getHighlight(x, y); + + if (high != null) { + high.setDataIndex(i); + mHighlightBuffer.add(high); + } + } else { + + for (int j = 0, dataSetCount = dataObject.getDataSetCount(); j < dataSetCount; j++) { + + IDataSet dataSet = dataObjects.get(i).getDataSetByIndex(j); + + // don't include datasets that cannot be highlighted + if (!dataSet.isHighlightEnabled()) + continue; + + List highs = buildHighlights(dataSet, j, xVal, DataSet.Rounding.CLOSEST); + for (Highlight high : highs) + { + high.setDataIndex(i); + mHighlightBuffer.add(high); + } + } + } + } + + return mHighlightBuffer; + } + +// protected Highlight getClosest(float x, float y, Highlight... highs) { +// +// Highlight closest = null; +// float minDistance = Float.MAX_VALUE; +// +// for (Highlight high : highs) { +// +// if (high == null) +// continue; +// +// float tempDistance = getDistance(x, y, high.getXPx(), high.getYPx()); +// +// if (tempDistance < minDistance) { +// minDistance = tempDistance; +// closest = high; +// } +// } +// +// return closest; +// } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/Highlight.java b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/Highlight.java new file mode 100644 index 0000000..62307cb --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/Highlight.java @@ -0,0 +1,243 @@ + +package com.github.mikephil.charting.highlight; + +import com.github.mikephil.charting.components.YAxis; + +/** + * Contains information needed to determine the highlighted value. + * + * @author Philipp Jahoda + */ +public class Highlight { + + /** + * the x-value of the highlighted value + */ + private float mX = Float.NaN; + + /** + * the y-value of the highlighted value + */ + private float mY = Float.NaN; + + /** + * the x-pixel of the highlight + */ + private float mXPx; + + /** + * the y-pixel of the highlight + */ + private float mYPx; + + /** + * the index of the data object - in case it refers to more than one + */ + private int mDataIndex = -1; + + /** + * the index of the dataset the highlighted value is in + */ + private int mDataSetIndex; + + /** + * index which value of a stacked bar entry is highlighted, default -1 + */ + private int mStackIndex = -1; + + /** + * the axis the highlighted value belongs to + */ + private YAxis.AxisDependency axis; + + /** + * the x-position (pixels) on which this highlight object was last drawn + */ + private float mDrawX; + + /** + * the y-position (pixels) on which this highlight object was last drawn + */ + private float mDrawY; + + public Highlight(float x, float y, int dataSetIndex, int dataIndex) { + this.mX = x; + this.mY = y; + this.mDataSetIndex = dataSetIndex; + this.mDataIndex = dataIndex; + } + + public Highlight(float x, float y, int dataSetIndex) { + this.mX = x; + this.mY = y; + this.mDataSetIndex = dataSetIndex; + this.mDataIndex = -1; + } + + public Highlight(float x, int dataSetIndex, int stackIndex) { + this(x, Float.NaN, dataSetIndex); + this.mStackIndex = stackIndex; + } + + /** + * constructor + * + * @param x the x-value of the highlighted value + * @param y the y-value of the highlighted value + * @param dataSetIndex the index of the DataSet the highlighted value belongs to + */ + public Highlight(float x, float y, float xPx, float yPx, int dataSetIndex, YAxis.AxisDependency axis) { + this.mX = x; + this.mY = y; + this.mXPx = xPx; + this.mYPx = yPx; + this.mDataSetIndex = dataSetIndex; + this.axis = axis; + } + + /** + * Constructor, only used for stacked-barchart. + * + * @param x the index of the highlighted value on the x-axis + * @param y the y-value of the highlighted value + * @param dataSetIndex the index of the DataSet the highlighted value belongs to + * @param stackIndex references which value of a stacked-bar entry has been + * selected + */ + public Highlight(float x, float y, float xPx, float yPx, int dataSetIndex, int stackIndex, YAxis.AxisDependency axis) { + this(x, y, xPx, yPx, dataSetIndex, axis); + this.mStackIndex = stackIndex; + } + + /** + * returns the x-value of the highlighted value + * + * @return + */ + public float getX() { + return mX; + } + + /** + * returns the y-value of the highlighted value + * + * @return + */ + public float getY() { + return mY; + } + + /** + * returns the x-position of the highlight in pixels + */ + public float getXPx() { + return mXPx; + } + + /** + * returns the y-position of the highlight in pixels + */ + public float getYPx() { + return mYPx; + } + + /** + * the index of the data object - in case it refers to more than one + * + * @return + */ + public int getDataIndex() { + return mDataIndex; + } + + public void setDataIndex(int mDataIndex) { + this.mDataIndex = mDataIndex; + } + + /** + * returns the index of the DataSet the highlighted value is in + * + * @return + */ + public int getDataSetIndex() { + return mDataSetIndex; + } + + /** + * Only needed if a stacked-barchart entry was highlighted. References the + * selected value within the stacked-entry. + * + * @return + */ + public int getStackIndex() { + return mStackIndex; + } + + public boolean isStacked() { + return mStackIndex >= 0; + } + + /** + * Returns the axis the highlighted value belongs to. + * + * @return + */ + public YAxis.AxisDependency getAxis() { + return axis; + } + + /** + * Sets the x- and y-position (pixels) where this highlight was last drawn. + * + * @param x + * @param y + */ + public void setDraw(float x, float y) { + this.mDrawX = x; + this.mDrawY = y; + } + + /** + * Returns the x-position in pixels where this highlight object was last drawn. + * + * @return + */ + public float getDrawX() { + return mDrawX; + } + + /** + * Returns the y-position in pixels where this highlight object was last drawn. + * + * @return + */ + public float getDrawY() { + return mDrawY; + } + + /** + * Returns true if this highlight object is equal to the other (compares + * xIndex and dataSetIndex) + * + * @param h + * @return + */ + public boolean equalTo(Highlight h) { + + if (h == null) + return false; + else { + if (this.mDataSetIndex == h.mDataSetIndex && this.mX == h.mX + && this.mStackIndex == h.mStackIndex && this.mDataIndex == h.mDataIndex) + return true; + else + return false; + } + } + + @Override + public String toString() { + return "Highlight, x: " + mX + ", y: " + mY + ", dataSetIndex: " + mDataSetIndex + + ", stackIndex (only stacked barentry): " + mStackIndex; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/HorizontalBarHighlighter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/HorizontalBarHighlighter.java new file mode 100644 index 0000000..d96298e --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/HorizontalBarHighlighter.java @@ -0,0 +1,85 @@ +package com.github.mikephil.charting.highlight; + +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.DataSet; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.interfaces.dataprovider.BarDataProvider; +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.utils.MPPointD; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Philipp Jahoda on 22/07/15. + */ +public class HorizontalBarHighlighter extends BarHighlighter { + + public HorizontalBarHighlighter(BarDataProvider chart) { + super(chart); + } + + @Override + public Highlight getHighlight(float x, float y) { + + BarData barData = mChart.getBarData(); + + MPPointD pos = getValsForTouch(y, x); + + Highlight high = getHighlightForX((float) pos.y, y, x); + if (high == null) + return null; + + IBarDataSet set = barData.getDataSetByIndex(high.getDataSetIndex()); + if (set.isStacked()) { + + return getStackedHighlight(high, + set, + (float) pos.y, + (float) pos.x); + } + + MPPointD.recycleInstance(pos); + + return high; + } + + @Override + protected List buildHighlights(IDataSet set, int dataSetIndex, float xVal, DataSet.Rounding rounding) { + + ArrayList highlights = new ArrayList<>(); + + //noinspection unchecked + List entries = set.getEntriesForXValue(xVal); + if (entries.size() == 0) { + // Try to find closest x-value and take all entries for that x-value + final Entry closest = set.getEntryForXValue(xVal, Float.NaN, rounding); + if (closest != null) + { + //noinspection unchecked + entries = set.getEntriesForXValue(closest.getX()); + } + } + + if (entries.size() == 0) + return highlights; + + for (Entry e : entries) { + MPPointD pixels = mChart.getTransformer( + set.getAxisDependency()).getPixelForValues(e.getY(), e.getX()); + + highlights.add(new Highlight( + e.getX(), e.getY(), + (float) pixels.x, (float) pixels.y, + dataSetIndex, set.getAxisDependency())); + } + + return highlights; + } + + @Override + protected float getDistance(float x1, float y1, float x2, float y2) { + return Math.abs(y1 - y2); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/IHighlighter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/IHighlighter.java new file mode 100644 index 0000000..d0ca0cf --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/IHighlighter.java @@ -0,0 +1,17 @@ +package com.github.mikephil.charting.highlight; + +/** + * Created by philipp on 10/06/16. + */ +public interface IHighlighter +{ + + /** + * Returns a Highlight object corresponding to the given x- and y- touch positions in pixels. + * + * @param x + * @param y + * @return + */ + Highlight getHighlight(float x, float y); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/PieHighlighter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/PieHighlighter.java new file mode 100644 index 0000000..b4c5a1b --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/PieHighlighter.java @@ -0,0 +1,25 @@ +package com.github.mikephil.charting.highlight; + +import com.github.mikephil.charting.charts.PieChart; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.interfaces.datasets.IPieDataSet; + +/** + * Created by philipp on 12/06/16. + */ +public class PieHighlighter extends PieRadarHighlighter { + + public PieHighlighter(PieChart chart) { + super(chart); + } + + @Override + protected Highlight getClosestHighlight(int index, float x, float y) { + + IPieDataSet set = mChart.getData().getDataSet(); + + final Entry entry = set.getEntryForIndex(index); + + return new Highlight(index, entry.getY(), x, y, 0, set.getAxisDependency()); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/PieRadarHighlighter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/PieRadarHighlighter.java new file mode 100644 index 0000000..a19906d --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/PieRadarHighlighter.java @@ -0,0 +1,66 @@ +package com.github.mikephil.charting.highlight; + +import com.github.mikephil.charting.charts.PieChart; +import com.github.mikephil.charting.charts.PieRadarChartBase; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by philipp on 12/06/16. + */ +public abstract class PieRadarHighlighter implements IHighlighter +{ + + protected T mChart; + + /** + * buffer for storing previously highlighted values + */ + protected List mHighlightBuffer = new ArrayList(); + + public PieRadarHighlighter(T chart) { + this.mChart = chart; + } + + @Override + public Highlight getHighlight(float x, float y) { + + float touchDistanceToCenter = mChart.distanceToCenter(x, y); + + // check if a slice was touched + if (touchDistanceToCenter > mChart.getRadius()) { + + // if no slice was touched, highlight nothing + return null; + + } else { + + float angle = mChart.getAngleForPoint(x, y); + + if (mChart instanceof PieChart) { + angle /= mChart.getAnimator().getPhaseY(); + } + + int index = mChart.getIndexForAngle(angle); + + // check if the index could be found + if (index < 0 || index >= mChart.getData().getMaxEntryCountSet().getEntryCount()) { + return null; + + } else { + return getClosestHighlight(index, x, y); + } + } + } + + /** + * Returns the closest Highlight object of the given objects based on the touch position inside the chart. + * + * @param index + * @param x + * @param y + * @return + */ + protected abstract Highlight getClosestHighlight(int index, float x, float y); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/RadarHighlighter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/RadarHighlighter.java new file mode 100644 index 0000000..3c4f6d0 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/RadarHighlighter.java @@ -0,0 +1,79 @@ +package com.github.mikephil.charting.highlight; + +import com.github.mikephil.charting.charts.RadarChart; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; + +import java.util.List; + +/** + * Created by philipp on 12/06/16. + */ +public class RadarHighlighter extends PieRadarHighlighter { + + public RadarHighlighter(RadarChart chart) { + super(chart); + } + + @Override + protected Highlight getClosestHighlight(int index, float x, float y) { + + List highlights = getHighlightsAtIndex(index); + + float distanceToCenter = mChart.distanceToCenter(x, y) / mChart.getFactor(); + + Highlight closest = null; + float distance = Float.MAX_VALUE; + + for (int i = 0; i < highlights.size(); i++) { + + Highlight high = highlights.get(i); + + float cdistance = Math.abs(high.getY() - distanceToCenter); + if (cdistance < distance) { + closest = high; + distance = cdistance; + } + } + + return closest; + } + /** + * Returns an array of Highlight objects for the given index. The Highlight + * objects give information about the value at the selected index and the + * DataSet it belongs to. INFORMATION: This method does calculations at + * runtime. Do not over-use in performance critical situations. + * + * @param index + * @return + */ + protected List getHighlightsAtIndex(int index) { + + mHighlightBuffer.clear(); + + float phaseX = mChart.getAnimator().getPhaseX(); + float phaseY = mChart.getAnimator().getPhaseY(); + float sliceangle = mChart.getSliceAngle(); + float factor = mChart.getFactor(); + + MPPointF pOut = MPPointF.getInstance(0,0); + for (int i = 0; i < mChart.getData().getDataSetCount(); i++) { + + IDataSet dataSet = mChart.getData().getDataSetByIndex(i); + + final Entry entry = dataSet.getEntryForIndex(index); + + float y = (entry.getY() - mChart.getYChartMin()); + + Utils.getPosition( + mChart.getCenterOffsets(), y * factor * phaseY, + sliceangle * index * phaseX + mChart.getRotationAngle(), pOut); + + mHighlightBuffer.add(new Highlight(index, entry.getY(), pOut.x, pOut.y, i, dataSet.getAxisDependency())); + } + + return mHighlightBuffer; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/Range.java b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/Range.java new file mode 100644 index 0000000..96dd592 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/Range.java @@ -0,0 +1,38 @@ +package com.github.mikephil.charting.highlight; + +/** + * Created by Philipp Jahoda on 24/07/15. Class that represents the range of one value in a stacked bar entry. e.g. + * stack values are -10, 5, 20 -> then ranges are (-10 - 0, 0 - 5, 5 - 25). + */ +public final class Range { + + public float from; + public float to; + + public Range(float from, float to) { + this.from = from; + this.to = to; + } + + /** + * Returns true if this range contains (if the value is in between) the given value, false if not. + * + * @param value + * @return + */ + public boolean contains(float value) { + + if (value > from && value <= to) + return true; + else + return false; + } + + public boolean isLarger(float value) { + return value > to; + } + + public boolean isSmaller(float value) { + return value < from; + } +} \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/BarDataProvider.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/BarDataProvider.java new file mode 100644 index 0000000..9dfee07 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/BarDataProvider.java @@ -0,0 +1,11 @@ +package com.github.mikephil.charting.interfaces.dataprovider; + +import com.github.mikephil.charting.data.BarData; + +public interface BarDataProvider extends BarLineScatterCandleBubbleDataProvider { + + BarData getBarData(); + boolean isDrawBarShadowEnabled(); + boolean isDrawValueAboveBarEnabled(); + boolean isHighlightFullBarEnabled(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/BarLineScatterCandleBubbleDataProvider.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/BarLineScatterCandleBubbleDataProvider.java new file mode 100644 index 0000000..68beeb8 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/BarLineScatterCandleBubbleDataProvider.java @@ -0,0 +1,16 @@ +package com.github.mikephil.charting.interfaces.dataprovider; + +import com.github.mikephil.charting.components.YAxis.AxisDependency; +import com.github.mikephil.charting.data.BarLineScatterCandleBubbleData; +import com.github.mikephil.charting.utils.Transformer; + +public interface BarLineScatterCandleBubbleDataProvider extends ChartInterface { + + Transformer getTransformer(AxisDependency axis); + boolean isInverted(AxisDependency axis); + + float getLowestVisibleX(); + float getHighestVisibleX(); + + BarLineScatterCandleBubbleData getData(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/BubbleDataProvider.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/BubbleDataProvider.java new file mode 100644 index 0000000..82ee30a --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/BubbleDataProvider.java @@ -0,0 +1,8 @@ +package com.github.mikephil.charting.interfaces.dataprovider; + +import com.github.mikephil.charting.data.BubbleData; + +public interface BubbleDataProvider extends BarLineScatterCandleBubbleDataProvider { + + BubbleData getBubbleData(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/CandleDataProvider.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/CandleDataProvider.java new file mode 100644 index 0000000..357403f --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/CandleDataProvider.java @@ -0,0 +1,8 @@ +package com.github.mikephil.charting.interfaces.dataprovider; + +import com.github.mikephil.charting.data.CandleData; + +public interface CandleDataProvider extends BarLineScatterCandleBubbleDataProvider { + + CandleData getCandleData(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/ChartInterface.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/ChartInterface.java new file mode 100644 index 0000000..219b46b --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/ChartInterface.java @@ -0,0 +1,69 @@ +package com.github.mikephil.charting.interfaces.dataprovider; + +import android.graphics.RectF; + +import com.github.mikephil.charting.data.ChartData; +import com.github.mikephil.charting.formatter.IValueFormatter; +import com.github.mikephil.charting.utils.MPPointF; + +/** + * Interface that provides everything there is to know about the dimensions, + * bounds, and range of the chart. + * + * @author Philipp Jahoda + */ +public interface ChartInterface { + + /** + * Returns the minimum x value of the chart, regardless of zoom or translation. + * + * @return + */ + float getXChartMin(); + + /** + * Returns the maximum x value of the chart, regardless of zoom or translation. + * + * @return + */ + float getXChartMax(); + + float getXRange(); + + /** + * Returns the minimum y value of the chart, regardless of zoom or translation. + * + * @return + */ + float getYChartMin(); + + /** + * Returns the maximum y value of the chart, regardless of zoom or translation. + * + * @return + */ + float getYChartMax(); + + /** + * Returns the maximum distance in scren dp a touch can be away from an entry to cause it to get highlighted. + * + * @return + */ + float getMaxHighlightDistance(); + + int getWidth(); + + int getHeight(); + + MPPointF getCenterOfView(); + + MPPointF getCenterOffsets(); + + RectF getContentRect(); + + IValueFormatter getDefaultValueFormatter(); + + ChartData getData(); + + int getMaxVisibleCount(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/CombinedDataProvider.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/CombinedDataProvider.java new file mode 100644 index 0000000..574d26a --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/CombinedDataProvider.java @@ -0,0 +1,11 @@ +package com.github.mikephil.charting.interfaces.dataprovider; + +import com.github.mikephil.charting.data.CombinedData; + +/** + * Created by philipp on 11/06/16. + */ +public interface CombinedDataProvider extends LineDataProvider, BarDataProvider, BubbleDataProvider, CandleDataProvider, ScatterDataProvider { + + CombinedData getCombinedData(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/LineDataProvider.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/LineDataProvider.java new file mode 100644 index 0000000..5c23ac2 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/LineDataProvider.java @@ -0,0 +1,11 @@ +package com.github.mikephil.charting.interfaces.dataprovider; + +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.data.LineData; + +public interface LineDataProvider extends BarLineScatterCandleBubbleDataProvider { + + LineData getLineData(); + + YAxis getAxis(YAxis.AxisDependency dependency); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/ScatterDataProvider.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/ScatterDataProvider.java new file mode 100644 index 0000000..b58d5af --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/dataprovider/ScatterDataProvider.java @@ -0,0 +1,8 @@ +package com.github.mikephil.charting.interfaces.dataprovider; + +import com.github.mikephil.charting.data.ScatterData; + +public interface ScatterDataProvider extends BarLineScatterCandleBubbleDataProvider { + + ScatterData getScatterData(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IBarDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IBarDataSet.java new file mode 100644 index 0000000..5e82a48 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IBarDataSet.java @@ -0,0 +1,71 @@ +package com.github.mikephil.charting.interfaces.datasets; + +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.utils.Fill; + +import java.util.List; + +/** + * Created by philipp on 21/10/15. + */ +public interface IBarDataSet extends IBarLineScatterCandleBubbleDataSet { + + List getFills(); + + Fill getFill(int index); + + /** + * Returns true if this DataSet is stacked (stacksize > 1) or not. + * + * @return + */ + boolean isStacked(); + + /** + * Returns the maximum number of bars that can be stacked upon another in + * this DataSet. This should return 1 for non stacked bars, and > 1 for stacked bars. + * + * @return + */ + int getStackSize(); + + /** + * Returns the color used for drawing the bar-shadows. The bar shadows is a + * surface behind the bar that indicates the maximum value. + * + * @return + */ + int getBarShadowColor(); + + /** + * Returns the width used for drawing borders around the bars. + * If borderWidth == 0, no border will be drawn. + * + * @return + */ + float getBarBorderWidth(); + + /** + * Returns the color drawing borders around the bars. + * + * @return + */ + int getBarBorderColor(); + + /** + * Returns the alpha value (transparency) that is used for drawing the + * highlight indicator. + * + * @return + */ + int getHighLightAlpha(); + + + /** + * Returns the labels used for the different value-stacks in the legend. + * This is only relevant for stacked bar entries. + * + * @return + */ + String[] getStackLabels(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IBarLineScatterCandleBubbleDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IBarLineScatterCandleBubbleDataSet.java new file mode 100644 index 0000000..0f9454b --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IBarLineScatterCandleBubbleDataSet.java @@ -0,0 +1,16 @@ +package com.github.mikephil.charting.interfaces.datasets; + +import com.github.mikephil.charting.data.Entry; + +/** + * Created by philipp on 21/10/15. + */ +public interface IBarLineScatterCandleBubbleDataSet extends IDataSet { + + /** + * Returns the color that is used for drawing the highlight indicators. + * + * @return + */ + int getHighLightColor(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IBubbleDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IBubbleDataSet.java new file mode 100644 index 0000000..e284aac --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IBubbleDataSet.java @@ -0,0 +1,27 @@ +package com.github.mikephil.charting.interfaces.datasets; + +import com.github.mikephil.charting.data.BubbleEntry; + +/** + * Created by philipp on 21/10/15. + */ +public interface IBubbleDataSet extends IBarLineScatterCandleBubbleDataSet { + + /** + * Sets the width of the circle that surrounds the bubble when highlighted, + * in dp. + * + * @param width + */ + void setHighlightCircleWidth(float width); + + float getMaxSize(); + + boolean isNormalizeSizeEnabled(); + + /** + * Returns the width of the highlight-circle that surrounds the bubble + * @return + */ + float getHighlightCircleWidth(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ICandleDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ICandleDataSet.java new file mode 100644 index 0000000..1d004ed --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ICandleDataSet.java @@ -0,0 +1,85 @@ +package com.github.mikephil.charting.interfaces.datasets; + +import android.graphics.Paint; + +import com.github.mikephil.charting.data.CandleEntry; + +/** + * Created by philipp on 21/10/15. + */ +public interface ICandleDataSet extends ILineScatterCandleRadarDataSet { + + /** + * Returns the space that is left out on the left and right side of each + * candle. + * + * @return + */ + float getBarSpace(); + + /** + * Returns whether the candle bars should show? + * When false, only "ticks" will show + * + * - default: true + * + * @return + */ + boolean getShowCandleBar(); + + /** + * Returns the width of the candle-shadow-line in pixels. + * + * @return + */ + float getShadowWidth(); + + /** + * Returns shadow color for all entries + * + * @return + */ + int getShadowColor(); + + /** + * Returns the neutral color (for open == close) + * + * @return + */ + int getNeutralColor(); + + /** + * Returns the increasing color (for open < close). + * + * @return + */ + int getIncreasingColor(); + + /** + * Returns the decreasing color (for open > close). + * + * @return + */ + int getDecreasingColor(); + + /** + * Returns paint style when open < close + * + * @return + */ + Paint.Style getIncreasingPaintStyle(); + + /** + * Returns paint style when open > close + * + * @return + */ + Paint.Style getDecreasingPaintStyle(); + + /** + * Is the shadow color same as the candle color? + * + * @return + */ + boolean getShadowColorSameAsCandle(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IDataSet.java new file mode 100644 index 0000000..fd8af70 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IDataSet.java @@ -0,0 +1,486 @@ +package com.github.mikephil.charting.interfaces.datasets; + +import android.graphics.DashPathEffect; +import android.graphics.PointF; +import android.graphics.Typeface; + +import com.github.mikephil.charting.components.Legend; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.data.DataSet; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.formatter.IValueFormatter; +import com.github.mikephil.charting.utils.MPPointF; + +import java.util.List; + +/** + * Created by Philipp Jahoda on 21/10/15. + */ +public interface IDataSet { + + /** ###### ###### DATA RELATED METHODS ###### ###### */ + + /** + * returns the minimum y-value this DataSet holds + * + * @return + */ + float getYMin(); + + /** + * returns the maximum y-value this DataSet holds + * + * @return + */ + float getYMax(); + + /** + * returns the minimum x-value this DataSet holds + * + * @return + */ + float getXMin(); + + /** + * returns the maximum x-value this DataSet holds + * + * @return + */ + float getXMax(); + + /** + * Returns the number of y-values this DataSet represents -> the size of the y-values array + * -> yvals.size() + * + * @return + */ + int getEntryCount(); + + /** + * Calculates the minimum and maximum x and y values (mXMin, mXMax, mYMin, mYMax). + */ + void calcMinMax(); + + /** + * Calculates the min and max y-values from the Entry closest to the given fromX to the Entry closest to the given toX value. + * This is only needed for the autoScaleMinMax feature. + * + * @param fromX + * @param toX + */ + void calcMinMaxY(float fromX, float toX); + + /** + * Returns the first Entry object found at the given x-value with binary + * search. + * If the no Entry at the specified x-value is found, this method + * returns the Entry at the closest x-value according to the rounding. + * INFORMATION: This method does calculations at runtime. Do + * not over-use in performance critical situations. + * + * @param xValue the x-value + * @param closestToY If there are multiple y-values for the specified x-value, + * @param rounding determine whether to round up/down/closest + * if there is no Entry matching the provided x-value + * @return + * + * + */ + T getEntryForXValue(float xValue, float closestToY, DataSet.Rounding rounding); + + /** + * Returns the first Entry object found at the given x-value with binary + * search. + * If the no Entry at the specified x-value is found, this method + * returns the Entry at the closest x-value. + * INFORMATION: This method does calculations at runtime. Do + * not over-use in performance critical situations. + * + * + * @param xValue the x-value + * @param closestToY If there are multiple y-values for the specified x-value, + * @return + */ + T getEntryForXValue(float xValue, float closestToY); + + /** + * Returns all Entry objects found at the given x-value with binary + * search. An empty array if no Entry object at that x-value. + * INFORMATION: This method does calculations at runtime. Do + * not over-use in performance critical situations. + * + * @param xValue + * @return + */ + List getEntriesForXValue(float xValue); + + /** + * Returns the Entry object found at the given index (NOT xIndex) in the values array. + * + * @param index + * @return + */ + T getEntryForIndex(int index); + + /** + * Returns the first Entry index found at the given x-value with binary + * search. + * If the no Entry at the specified x-value is found, this method + * returns the Entry at the closest x-value according to the rounding. + * INFORMATION: This method does calculations at runtime. Do + * not over-use in performance critical situations. + * + * @param xValue the x-value + * @param closestToY If there are multiple y-values for the specified x-value, + * @param rounding determine whether to round up/down/closest + * if there is no Entry matching the provided x-value + * @return + */ + int getEntryIndex(float xValue, float closestToY, DataSet.Rounding rounding); + + /** + * Returns the position of the provided entry in the DataSets Entry array. + * Returns -1 if doesn't exist. + * + * @param e + * @return + */ + int getEntryIndex(T e); + + + /** + * This method returns the actual + * index in the Entry array of the DataSet for a given xIndex. IMPORTANT: This method does + * calculations at runtime, do not over-use in performance critical + * situations. + * + * @param xIndex + * @return + */ + int getIndexInEntries(int xIndex); + + /** + * Adds an Entry to the DataSet dynamically. + * Entries are added to the end of the list. + * This will also recalculate the current minimum and maximum + * values of the DataSet and the value-sum. + * + * @param e + */ + boolean addEntry(T e); + + + /** + * Adds an Entry to the DataSet dynamically. + * Entries are added to their appropriate index in the values array respective to their x-position. + * This will also recalculate the current minimum and maximum + * values of the DataSet and the value-sum. + * + * @param e + */ + void addEntryOrdered(T e); + + /** + * Removes the first Entry (at index 0) of this DataSet from the entries array. + * Returns true if successful, false if not. + * + * @return + */ + boolean removeFirst(); + + /** + * Removes the last Entry (at index size-1) of this DataSet from the entries array. + * Returns true if successful, false if not. + * + * @return + */ + boolean removeLast(); + + /** + * Removes an Entry from the DataSets entries array. This will also + * recalculate the current minimum and maximum values of the DataSet and the + * value-sum. Returns true if an Entry was removed, false if no Entry could + * be removed. + * + * @param e + */ + boolean removeEntry(T e); + + /** + * Removes the Entry object closest to the given x-value from the DataSet. + * Returns true if an Entry was removed, false if no Entry could be removed. + * + * @param xValue + */ + boolean removeEntryByXValue(float xValue); + + /** + * Removes the Entry object at the given index in the values array from the DataSet. + * Returns true if an Entry was removed, false if no Entry could be removed. + * + * @param index + * @return + */ + boolean removeEntry(int index); + + /** + * Checks if this DataSet contains the specified Entry. Returns true if so, + * false if not. NOTE: Performance is pretty bad on this one, do not + * over-use in performance critical situations. + * + * @param entry + * @return + */ + boolean contains(T entry); + + /** + * Removes all values from this DataSet and does all necessary recalculations. + */ + void clear(); + + + /** ###### ###### STYLING RELATED (& OTHER) METHODS ###### ###### */ + + /** + * Returns the label string that describes the DataSet. + * + * @return + */ + String getLabel(); + + /** + * Sets the label string that describes the DataSet. + * + * @param label + */ + void setLabel(String label); + + /** + * Returns the axis this DataSet should be plotted against. + * + * @return + */ + YAxis.AxisDependency getAxisDependency(); + + /** + * Set the y-axis this DataSet should be plotted against (either LEFT or + * RIGHT). Default: LEFT + * + * @param dependency + */ + void setAxisDependency(YAxis.AxisDependency dependency); + + /** + * returns all the colors that are set for this DataSet + * + * @return + */ + List getColors(); + + /** + * Returns the first color (index 0) of the colors-array this DataSet + * contains. This is only used for performance reasons when only one color is in the colors array (size == 1) + * + * @return + */ + int getColor(); + + /** + * Returns the color at the given index of the DataSet's color array. + * Performs a IndexOutOfBounds check by modulus. + * + * @param index + * @return + */ + int getColor(int index); + + /** + * returns true if highlighting of values is enabled, false if not + * + * @return + */ + boolean isHighlightEnabled(); + + /** + * If set to true, value highlighting is enabled which means that values can + * be highlighted programmatically or by touch gesture. + * + * @param enabled + */ + void setHighlightEnabled(boolean enabled); + + /** + * Sets the formatter to be used for drawing the values inside the chart. If + * no formatter is set, the chart will automatically determine a reasonable + * formatting (concerning decimals) for all the values that are drawn inside + * the chart. Use chart.getDefaultValueFormatter() to use the formatter + * calculated by the chart. + * + * @param f + */ + void setValueFormatter(IValueFormatter f); + + /** + * Returns the formatter used for drawing the values inside the chart. + * + * @return + */ + IValueFormatter getValueFormatter(); + + /** + * Returns true if the valueFormatter object of this DataSet is null. + * + * @return + */ + boolean needsFormatter(); + + /** + * Sets the color the value-labels of this DataSet should have. + * + * @param color + */ + void setValueTextColor(int color); + + /** + * Sets a list of colors to be used as the colors for the drawn values. + * + * @param colors + */ + void setValueTextColors(List colors); + + /** + * Sets a Typeface for the value-labels of this DataSet. + * + * @param tf + */ + void setValueTypeface(Typeface tf); + + /** + * Sets the text-size of the value-labels of this DataSet in dp. + * + * @param size + */ + void setValueTextSize(float size); + + /** + * Returns only the first color of all colors that are set to be used for the values. + * + * @return + */ + int getValueTextColor(); + + /** + * Returns the color at the specified index that is used for drawing the values inside the chart. + * Uses modulus internally. + * + * @param index + * @return + */ + int getValueTextColor(int index); + + /** + * Returns the typeface that is used for drawing the values inside the chart + * + * @return + */ + Typeface getValueTypeface(); + + /** + * Returns the text size that is used for drawing the values inside the chart + * + * @return + */ + float getValueTextSize(); + + /** + * The form to draw for this dataset in the legend. + *

+ * Return `DEFAULT` to use the default legend form. + */ + Legend.LegendForm getForm(); + + /** + * The form size to draw for this dataset in the legend. + *

+ * Return `Float.NaN` to use the default legend form size. + */ + float getFormSize(); + + /** + * The line width for drawing the form of this dataset in the legend + *

+ * Return `Float.NaN` to use the default legend form line width. + */ + float getFormLineWidth(); + + /** + * The line dash path effect used for shapes that consist of lines. + *

+ * Return `null` to use the default legend form line dash effect. + */ + DashPathEffect getFormLineDashEffect(); + + /** + * set this to true to draw y-values on the chart. + * + * NOTE (for bar and line charts): if `maxVisibleCount` is reached, no values will be drawn even + * if this is enabled + * @param enabled + */ + void setDrawValues(boolean enabled); + + /** + * Returns true if y-value drawing is enabled, false if not + * + * @return + */ + boolean isDrawValuesEnabled(); + + /** + * Set this to true to draw y-icons on the chart. + * + * NOTE (for bar and line charts): if `maxVisibleCount` is reached, no icons will be drawn even + * if this is enabled + * + * @param enabled + */ + void setDrawIcons(boolean enabled); + + /** + * Returns true if y-icon drawing is enabled, false if not + * + * @return + */ + boolean isDrawIconsEnabled(); + + /** + * Offset of icons drawn on the chart. + * + * For all charts except Pie and Radar it will be ordinary (x offset,y offset). + * + * For Pie and Radar chart it will be (y offset, distance from center offset); so if you want icon to be rendered under value, you should increase X component of CGPoint, and if you want icon to be rendered closet to center, you should decrease height component of CGPoint. + * @param offset + */ + void setIconsOffset(MPPointF offset); + + /** + * Get the offset for drawing icons. + */ + MPPointF getIconsOffset(); + + /** + * Set the visibility of this DataSet. If not visible, the DataSet will not + * be drawn to the chart upon refreshing it. + * + * @param visible + */ + void setVisible(boolean visible); + + /** + * Returns true if this DataSet is visible inside the chart, or false if it + * is currently hidden. + * + * @return + */ + boolean isVisible(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ILineDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ILineDataSet.java new file mode 100644 index 0000000..3f534fe --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ILineDataSet.java @@ -0,0 +1,103 @@ +package com.github.mikephil.charting.interfaces.datasets; + +import android.graphics.DashPathEffect; + +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.LineDataSet; +import com.github.mikephil.charting.formatter.IFillFormatter; + +/** + * Created by Philpp Jahoda on 21/10/15. + */ +public interface ILineDataSet extends ILineRadarDataSet { + + /** + * Returns the drawing mode for this line dataset + * + * @return + */ + LineDataSet.Mode getMode(); + + /** + * Returns the intensity of the cubic lines (the effect intensity). + * Max = 1f = very cubic, Min = 0.05f = low cubic effect, Default: 0.2f + * + * @return + */ + float getCubicIntensity(); + + @Deprecated + boolean isDrawCubicEnabled(); + + @Deprecated + boolean isDrawSteppedEnabled(); + + /** + * Returns the size of the drawn circles. + */ + float getCircleRadius(); + + /** + * Returns the hole radius of the drawn circles. + */ + float getCircleHoleRadius(); + + /** + * Returns the color at the given index of the DataSet's circle-color array. + * Performs a IndexOutOfBounds check by modulus. + * + * @param index + * @return + */ + int getCircleColor(int index); + + /** + * Returns the number of colors in this DataSet's circle-color array. + * + * @return + */ + int getCircleColorCount(); + + /** + * Returns true if drawing circles for this DataSet is enabled, false if not + * + * @return + */ + boolean isDrawCirclesEnabled(); + + /** + * Returns the color of the inner circle (the circle-hole). + * + * @return + */ + int getCircleHoleColor(); + + /** + * Returns true if drawing the circle-holes is enabled, false if not. + * + * @return + */ + boolean isDrawCircleHoleEnabled(); + + /** + * Returns the DashPathEffect that is used for drawing the lines. + * + * @return + */ + DashPathEffect getDashPathEffect(); + + /** + * Returns true if the dashed-line effect is enabled, false if not. + * If the DashPathEffect object is null, also return false here. + * + * @return + */ + boolean isDashedLineEnabled(); + + /** + * Returns the IFillFormatter that is set for this DataSet. + * + * @return + */ + IFillFormatter getFillFormatter(); +} \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ILineRadarDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ILineRadarDataSet.java new file mode 100644 index 0000000..ce89822 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ILineRadarDataSet.java @@ -0,0 +1,58 @@ +package com.github.mikephil.charting.interfaces.datasets; + +import android.graphics.drawable.Drawable; + +import com.github.mikephil.charting.data.Entry; + +/** + * Created by Philipp Jahoda on 21/10/15. + */ +public interface ILineRadarDataSet extends ILineScatterCandleRadarDataSet { + + /** + * Returns the color that is used for filling the line surface area. + * + * @return + */ + int getFillColor(); + + /** + * Returns the drawable used for filling the area below the line. + * + * @return + */ + Drawable getFillDrawable(); + + /** + * Returns the alpha value that is used for filling the line surface, + * default: 85 + * + * @return + */ + int getFillAlpha(); + + /** + * Returns the stroke-width of the drawn line + * + * @return + */ + float getLineWidth(); + + /** + * Returns true if filled drawing is enabled, false if not + * + * @return + */ + boolean isDrawFilledEnabled(); + + /** + * Set to true if the DataSet should be drawn filled (surface), and not just + * as a line, disabling this will give great performance boost. Please note that this method + * uses the canvas.clipPath(...) method for drawing the filled area. + * For devices with API level < 18 (Android 4.3), hardware acceleration of the chart should + * be turned off. Default: false + * + * @param enabled + */ + void setDrawFilled(boolean enabled); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ILineScatterCandleRadarDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ILineScatterCandleRadarDataSet.java new file mode 100644 index 0000000..9ab6802 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/ILineScatterCandleRadarDataSet.java @@ -0,0 +1,35 @@ +package com.github.mikephil.charting.interfaces.datasets; + +import android.graphics.DashPathEffect; + +import com.github.mikephil.charting.data.Entry; + +/** + * Created by Philipp Jahoda on 21/10/15. + */ +public interface ILineScatterCandleRadarDataSet extends IBarLineScatterCandleBubbleDataSet { + + /** + * Returns true if vertical highlight indicator lines are enabled (drawn) + * @return + */ + boolean isVerticalHighlightIndicatorEnabled(); + + /** + * Returns true if vertical highlight indicator lines are enabled (drawn) + * @return + */ + boolean isHorizontalHighlightIndicatorEnabled(); + + /** + * Returns the line-width in which highlight lines are to be drawn. + * @return + */ + float getHighlightLineWidth(); + + /** + * Returns the DashPathEffect that is used for highlighting. + * @return + */ + DashPathEffect getDashPathEffectHighlight(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IPieDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IPieDataSet.java new file mode 100644 index 0000000..b228fca --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IPieDataSet.java @@ -0,0 +1,82 @@ +package com.github.mikephil.charting.interfaces.datasets; + +import androidx.annotation.Nullable; + +import com.github.mikephil.charting.data.PieDataSet; +import com.github.mikephil.charting.data.PieEntry; + +/** + * Created by Philipp Jahoda on 03/11/15. + */ +public interface IPieDataSet extends IDataSet { + + /** + * Returns the space that is set to be between the piechart-slices of this + * DataSet, in pixels. + * + * @return + */ + float getSliceSpace(); + + /** + * When enabled, slice spacing will be 0.0 when the smallest value is going to be + * smaller than the slice spacing itself. + * + * @return + */ + boolean isAutomaticallyDisableSliceSpacingEnabled(); + + /** + * Returns the distance a highlighted piechart slice is "shifted" away from + * the chart-center in dp. + * + * @return + */ + float getSelectionShift(); + + PieDataSet.ValuePosition getXValuePosition(); + PieDataSet.ValuePosition getYValuePosition(); + + /** + * When valuePosition is OutsideSlice, indicates line color + * */ + int getValueLineColor(); + + /** + * When valuePosition is OutsideSlice and enabled, line will have the same color as the slice + * */ + boolean isUseValueColorForLineEnabled(); + + /** + * When valuePosition is OutsideSlice, indicates line width + * */ + float getValueLineWidth(); + + /** + * When valuePosition is OutsideSlice, indicates offset as percentage out of the slice size + * */ + float getValueLinePart1OffsetPercentage(); + + /** + * When valuePosition is OutsideSlice, indicates length of first half of the line + * */ + float getValueLinePart1Length(); + + /** + * When valuePosition is OutsideSlice, indicates length of second half of the line + * */ + float getValueLinePart2Length(); + + /** + * When valuePosition is OutsideSlice, this allows variable line length + * */ + boolean isValueLineVariableLength(); + + /** + * Gets the color for the highlighted sector + * */ + @Nullable + Integer getHighlightColor(); + +} + diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IRadarDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IRadarDataSet.java new file mode 100644 index 0000000..8af00d5 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IRadarDataSet.java @@ -0,0 +1,30 @@ +package com.github.mikephil.charting.interfaces.datasets; + +import com.github.mikephil.charting.data.RadarEntry; + +/** + * Created by Philipp Jahoda on 03/11/15. + */ +public interface IRadarDataSet extends ILineRadarDataSet { + + /// flag indicating whether highlight circle should be drawn or not + boolean isDrawHighlightCircleEnabled(); + + /// Sets whether highlight circle should be drawn or not + void setDrawHighlightCircleEnabled(boolean enabled); + + int getHighlightCircleFillColor(); + + /// The stroke color for highlight circle. + /// If Utils.COLOR_NONE, the color of the dataset is taken. + int getHighlightCircleStrokeColor(); + + int getHighlightCircleStrokeAlpha(); + + float getHighlightCircleInnerRadius(); + + float getHighlightCircleOuterRadius(); + + float getHighlightCircleStrokeWidth(); + +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IScatterDataSet.java b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IScatterDataSet.java new file mode 100644 index 0000000..ac61227 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/interfaces/datasets/IScatterDataSet.java @@ -0,0 +1,38 @@ +package com.github.mikephil.charting.interfaces.datasets; + +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.renderer.scatter.IShapeRenderer; + +/** + * Created by philipp on 21/10/15. + */ +public interface IScatterDataSet extends ILineScatterCandleRadarDataSet { + + /** + * Returns the currently set scatter shape size + * + * @return + */ + float getScatterShapeSize(); + + /** + * Returns radius of the hole in the shape + * + * @return + */ + float getScatterShapeHoleRadius(); + + /** + * Returns the color for the hole in the shape + * + * @return + */ + int getScatterShapeHoleColor(); + + /** + * Returns the IShapeRenderer responsible for rendering this DataSet. + * + * @return + */ + IShapeRenderer getShapeRenderer(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/AnimatedMoveViewJob.java b/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/AnimatedMoveViewJob.java new file mode 100644 index 0000000..8f953a0 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/AnimatedMoveViewJob.java @@ -0,0 +1,65 @@ +package com.github.mikephil.charting.jobs; + +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.view.View; + +import com.github.mikephil.charting.utils.ObjectPool; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by Philipp Jahoda on 19/02/16. + */ +@SuppressLint("NewApi") +public class AnimatedMoveViewJob extends AnimatedViewPortJob { + + private static ObjectPool pool; + + static { + pool = ObjectPool.create(4, new AnimatedMoveViewJob(null,0,0,null,null,0,0,0)); + pool.setReplenishPercentage(0.5f); + } + + public static AnimatedMoveViewJob getInstance(ViewPortHandler viewPortHandler, float xValue, float yValue, Transformer trans, View v, float xOrigin, float yOrigin, long duration){ + AnimatedMoveViewJob result = pool.get(); + result.mViewPortHandler = viewPortHandler; + result.xValue = xValue; + result.yValue = yValue; + result.mTrans = trans; + result.view = v; + result.xOrigin = xOrigin; + result.yOrigin = yOrigin; + //result.resetAnimator(); + result.animator.setDuration(duration); + return result; + } + + public static void recycleInstance(AnimatedMoveViewJob instance){ + pool.recycle(instance); + } + + + public AnimatedMoveViewJob(ViewPortHandler viewPortHandler, float xValue, float yValue, Transformer trans, View v, float xOrigin, float yOrigin, long duration) { + super(viewPortHandler, xValue, yValue, trans, v, xOrigin, yOrigin, duration); + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + + pts[0] = xOrigin + (xValue - xOrigin) * phase; + pts[1] = yOrigin + (yValue - yOrigin) * phase; + + mTrans.pointValuesToPixel(pts); + mViewPortHandler.centerViewPort(pts, view); + } + + public void recycleSelf(){ + recycleInstance(this); + } + + @Override + protected ObjectPool.Poolable instantiate() { + return new AnimatedMoveViewJob(null,0,0,null,null,0,0,0); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/AnimatedViewPortJob.java b/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/AnimatedViewPortJob.java new file mode 100644 index 0000000..f8b520a --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/AnimatedViewPortJob.java @@ -0,0 +1,99 @@ +package com.github.mikephil.charting.jobs; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.view.View; + +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by Philipp Jahoda on 19/02/16. + */ +@SuppressLint("NewApi") +public abstract class AnimatedViewPortJob extends ViewPortJob implements ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener { + + protected ObjectAnimator animator; + + protected float phase; + + protected float xOrigin; + protected float yOrigin; + + public AnimatedViewPortJob(ViewPortHandler viewPortHandler, float xValue, float yValue, Transformer trans, View v, float xOrigin, float yOrigin, long duration) { + super(viewPortHandler, xValue, yValue, trans, v); + this.xOrigin = xOrigin; + this.yOrigin = yOrigin; + animator = ObjectAnimator.ofFloat(this, "phase", 0f, 1f); + animator.setDuration(duration); + animator.addUpdateListener(this); + animator.addListener(this); + } + + @SuppressLint("NewApi") + @Override + public void run() { + animator.start(); + } + + public float getPhase() { + return phase; + } + + public void setPhase(float phase) { + this.phase = phase; + } + + public float getXOrigin() { + return xOrigin; + } + + public float getYOrigin() { + return yOrigin; + } + + public abstract void recycleSelf(); + + protected void resetAnimator(){ + animator.removeAllListeners(); + animator.removeAllUpdateListeners(); + animator.reverse(); + animator.addUpdateListener(this); + animator.addListener(this); + } + + @Override + public void onAnimationStart(Animator animation) { + + } + + @Override + public void onAnimationEnd(Animator animation) { + try{ + recycleSelf(); + }catch (IllegalArgumentException e){ + // don't worry about it. + } + } + + @Override + public void onAnimationCancel(Animator animation) { + try{ + recycleSelf(); + }catch (IllegalArgumentException e){ + // don't worry about it. + } + } + + @Override + public void onAnimationRepeat(Animator animation) { + + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/AnimatedZoomJob.java b/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/AnimatedZoomJob.java new file mode 100644 index 0000000..e5e4c41 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/AnimatedZoomJob.java @@ -0,0 +1,119 @@ +package com.github.mikephil.charting.jobs; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.graphics.Matrix; +import android.view.View; + +import com.github.mikephil.charting.charts.BarLineChartBase; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.utils.ObjectPool; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by Philipp Jahoda on 19/02/16. + */ +@SuppressLint("NewApi") +public class AnimatedZoomJob extends AnimatedViewPortJob implements Animator.AnimatorListener { + + private static ObjectPool pool; + + static { + pool = ObjectPool.create(8, new AnimatedZoomJob(null,null,null,null,0,0,0,0,0,0,0,0,0,0)); + } + + public static AnimatedZoomJob getInstance(ViewPortHandler viewPortHandler, View v, Transformer trans, YAxis axis, float xAxisRange, float scaleX, float scaleY, float xOrigin, float yOrigin, float zoomCenterX, float zoomCenterY, float zoomOriginX, float zoomOriginY, long duration) { + AnimatedZoomJob result = pool.get(); + result.mViewPortHandler = viewPortHandler; + result.xValue = scaleX; + result.yValue = scaleY; + result.mTrans = trans; + result.view = v; + result.xOrigin = xOrigin; + result.yOrigin = yOrigin; + result.yAxis = axis; + result.xAxisRange = xAxisRange; + result.resetAnimator(); + result.animator.setDuration(duration); + return result; + } + + protected float zoomOriginX; + protected float zoomOriginY; + + protected float zoomCenterX; + protected float zoomCenterY; + + protected YAxis yAxis; + + protected float xAxisRange; + + @SuppressLint("NewApi") + public AnimatedZoomJob(ViewPortHandler viewPortHandler, View v, Transformer trans, YAxis axis, float xAxisRange, float scaleX, float scaleY, float xOrigin, float yOrigin, float zoomCenterX, float zoomCenterY, float zoomOriginX, float zoomOriginY, long duration) { + super(viewPortHandler, scaleX, scaleY, trans, v, xOrigin, yOrigin, duration); + + this.zoomCenterX = zoomCenterX; + this.zoomCenterY = zoomCenterY; + this.zoomOriginX = zoomOriginX; + this.zoomOriginY = zoomOriginY; + this.animator.addListener(this); + this.yAxis = axis; + this.xAxisRange = xAxisRange; + } + + protected Matrix mOnAnimationUpdateMatrixBuffer = new Matrix(); + @Override + public void onAnimationUpdate(ValueAnimator animation) { + + float scaleX = xOrigin + (xValue - xOrigin) * phase; + float scaleY = yOrigin + (yValue - yOrigin) * phase; + + Matrix save = mOnAnimationUpdateMatrixBuffer; + mViewPortHandler.setZoom(scaleX, scaleY, save); + mViewPortHandler.refresh(save, view, false); + + float valsInView = yAxis.mAxisRange / mViewPortHandler.getScaleY(); + float xsInView = xAxisRange / mViewPortHandler.getScaleX(); + + pts[0] = zoomOriginX + ((zoomCenterX - xsInView / 2f) - zoomOriginX) * phase; + pts[1] = zoomOriginY + ((zoomCenterY + valsInView / 2f) - zoomOriginY) * phase; + + mTrans.pointValuesToPixel(pts); + + mViewPortHandler.translate(pts, save); + mViewPortHandler.refresh(save, view, true); + } + + @Override + public void onAnimationEnd(Animator animation) { + ((BarLineChartBase) view).calculateOffsets(); + view.postInvalidate(); + } + + @Override + public void onAnimationCancel(Animator animation) { + + } + + @Override + public void onAnimationRepeat(Animator animation) { + + } + + @Override + public void recycleSelf() { + + } + + @Override + public void onAnimationStart(Animator animation) { + + } + + @Override + protected ObjectPool.Poolable instantiate() { + return new AnimatedZoomJob(null,null,null,null,0,0,0,0,0,0,0,0,0,0); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/MoveViewJob.java b/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/MoveViewJob.java new file mode 100644 index 0000000..46b56b1 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/MoveViewJob.java @@ -0,0 +1,56 @@ + +package com.github.mikephil.charting.jobs; + +import android.view.View; + +import com.github.mikephil.charting.utils.ObjectPool; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by Philipp Jahoda on 19/02/16. + */ +public class MoveViewJob extends ViewPortJob { + + private static ObjectPool pool; + + static { + pool = ObjectPool.create(2, new MoveViewJob(null,0,0,null,null)); + pool.setReplenishPercentage(0.5f); + } + + public static MoveViewJob getInstance(ViewPortHandler viewPortHandler, float xValue, float yValue, Transformer trans, View v){ + MoveViewJob result = pool.get(); + result.mViewPortHandler = viewPortHandler; + result.xValue = xValue; + result.yValue = yValue; + result.mTrans = trans; + result.view = v; + return result; + } + + public static void recycleInstance(MoveViewJob instance){ + pool.recycle(instance); + } + + public MoveViewJob(ViewPortHandler viewPortHandler, float xValue, float yValue, Transformer trans, View v) { + super(viewPortHandler, xValue, yValue, trans, v); + } + + @Override + public void run() { + + pts[0] = xValue; + pts[1] = yValue; + + mTrans.pointValuesToPixel(pts); + mViewPortHandler.centerViewPort(pts, view); + + this.recycleInstance(this); + } + + @Override + protected ObjectPool.Poolable instantiate() { + return new MoveViewJob(mViewPortHandler, xValue, yValue, mTrans, view); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/ViewPortJob.java b/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/ViewPortJob.java new file mode 100644 index 0000000..c424e4b --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/ViewPortJob.java @@ -0,0 +1,47 @@ + +package com.github.mikephil.charting.jobs; + +import android.view.View; + +import com.github.mikephil.charting.utils.ObjectPool; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Runnable that is used for viewport modifications since they cannot be + * executed at any time. This can be used to delay the execution of viewport + * modifications until the onSizeChanged(...) method of the chart-view is called. + * This is especially important if viewport modifying methods are called on the chart + * directly after initialization. + * + * @author Philipp Jahoda + */ +public abstract class ViewPortJob extends ObjectPool.Poolable implements Runnable { + + protected float[] pts = new float[2]; + + protected ViewPortHandler mViewPortHandler; + protected float xValue = 0f; + protected float yValue = 0f; + protected Transformer mTrans; + protected View view; + + public ViewPortJob(ViewPortHandler viewPortHandler, float xValue, float yValue, + Transformer trans, View v) { + + this.mViewPortHandler = viewPortHandler; + this.xValue = xValue; + this.yValue = yValue; + this.mTrans = trans; + this.view = v; + + } + + public float getXValue() { + return xValue; + } + + public float getYValue() { + return yValue; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/ZoomJob.java b/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/ZoomJob.java new file mode 100644 index 0000000..c39586c --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/jobs/ZoomJob.java @@ -0,0 +1,87 @@ + +package com.github.mikephil.charting.jobs; + +import android.graphics.Matrix; +import android.view.View; + +import com.github.mikephil.charting.charts.BarLineChartBase; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.utils.ObjectPool; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by Philipp Jahoda on 19/02/16. + */ +public class ZoomJob extends ViewPortJob { + + private static ObjectPool pool; + + static { + pool = ObjectPool.create(1, new ZoomJob(null, 0, 0, 0, 0, null, null, null)); + pool.setReplenishPercentage(0.5f); + } + + public static ZoomJob getInstance(ViewPortHandler viewPortHandler, float scaleX, float scaleY, float xValue, float yValue, + Transformer trans, YAxis.AxisDependency axis, View v) { + ZoomJob result = pool.get(); + result.xValue = xValue; + result.yValue = yValue; + result.scaleX = scaleX; + result.scaleY = scaleY; + result.mViewPortHandler = viewPortHandler; + result.mTrans = trans; + result.axisDependency = axis; + result.view = v; + return result; + } + + public static void recycleInstance(ZoomJob instance) { + pool.recycle(instance); + } + + protected float scaleX; + protected float scaleY; + + protected YAxis.AxisDependency axisDependency; + + public ZoomJob(ViewPortHandler viewPortHandler, float scaleX, float scaleY, float xValue, float yValue, Transformer trans, + YAxis.AxisDependency axis, View v) { + super(viewPortHandler, xValue, yValue, trans, v); + + this.scaleX = scaleX; + this.scaleY = scaleY; + this.axisDependency = axis; + } + + protected Matrix mRunMatrixBuffer = new Matrix(); + + @Override + public void run() { + + Matrix save = mRunMatrixBuffer; + mViewPortHandler.zoom(scaleX, scaleY, save); + mViewPortHandler.refresh(save, view, false); + + float yValsInView = ((BarLineChartBase) view).getAxis(axisDependency).mAxisRange / mViewPortHandler.getScaleY(); + float xValsInView = ((BarLineChartBase) view).getXAxis().mAxisRange / mViewPortHandler.getScaleX(); + + pts[0] = xValue - xValsInView / 2f; + pts[1] = yValue + yValsInView / 2f; + + mTrans.pointValuesToPixel(pts); + + mViewPortHandler.translate(pts, save); + mViewPortHandler.refresh(save, view, false); + + ((BarLineChartBase) view).calculateOffsets(); + view.postInvalidate(); + + recycleInstance(this); + } + + @Override + protected ObjectPool.Poolable instantiate() { + return new ZoomJob(null, 0, 0, 0, 0, null, null, null); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/listener/BarLineChartTouchListener.java b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/BarLineChartTouchListener.java new file mode 100644 index 0000000..5685d32 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/BarLineChartTouchListener.java @@ -0,0 +1,698 @@ +package com.github.mikephil.charting.listener; + +import android.annotation.SuppressLint; +import android.graphics.Matrix; +import android.graphics.PointF; +import android.util.Log; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.animation.AnimationUtils; + +import com.github.mikephil.charting.charts.BarLineChartBase; +import com.github.mikephil.charting.charts.HorizontalBarChart; +import com.github.mikephil.charting.data.BarLineScatterCandleBubbleData; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.datasets.IBarLineScatterCandleBubbleDataSet; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * TouchListener for Bar-, Line-, Scatter- and CandleStickChart with handles all + * touch interaction. Longpress == Zoom out. Double-Tap == Zoom in. + * + * @author Philipp Jahoda + */ +public class BarLineChartTouchListener extends ChartTouchListener>>> { + + /** + * the original touch-matrix from the chart + */ + private Matrix mMatrix = new Matrix(); + + /** + * matrix for saving the original matrix state + */ + private Matrix mSavedMatrix = new Matrix(); + + /** + * point where the touch action started + */ + private MPPointF mTouchStartPoint = MPPointF.getInstance(0,0); + + /** + * center between two pointers (fingers on the display) + */ + private MPPointF mTouchPointCenter = MPPointF.getInstance(0,0); + + private float mSavedXDist = 1f; + private float mSavedYDist = 1f; + private float mSavedDist = 1f; + + private IDataSet mClosestDataSetToTouch; + + /** + * used for tracking velocity of dragging + */ + private VelocityTracker mVelocityTracker; + + private long mDecelerationLastTime = 0; + private MPPointF mDecelerationCurrentPoint = MPPointF.getInstance(0,0); + private MPPointF mDecelerationVelocity = MPPointF.getInstance(0,0); + + /** + * the distance of movement that will be counted as a drag + */ + private float mDragTriggerDist; + + /** + * the minimum distance between the pointers that will trigger a zoom gesture + */ + private float mMinScalePointerDistance; + + /** + * Constructor with initialization parameters. + * + * @param chart instance of the chart + * @param touchMatrix the touch-matrix of the chart + * @param dragTriggerDistance the minimum movement distance that will be interpreted as a "drag" gesture in dp (3dp equals + * to about 9 pixels on a 5.5" FHD screen) + */ + public BarLineChartTouchListener(BarLineChartBase>> chart, Matrix touchMatrix, float dragTriggerDistance) { + super(chart); + this.mMatrix = touchMatrix; + + this.mDragTriggerDist = Utils.convertDpToPixel(dragTriggerDistance); + + this.mMinScalePointerDistance = Utils.convertDpToPixel(3.5f); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouch(View v, MotionEvent event) { + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(event); + + if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + if (mTouchMode == NONE) { + mGestureDetector.onTouchEvent(event); + } + + if (!mChart.isDragEnabled() && (!mChart.isScaleXEnabled() && !mChart.isScaleYEnabled())) + return true; + + // Handle touch events here... + switch (event.getAction() & MotionEvent.ACTION_MASK) { + + case MotionEvent.ACTION_DOWN: + + startAction(event); + + stopDeceleration(); + + saveTouchStart(event); + + break; + + case MotionEvent.ACTION_POINTER_DOWN: + + if (event.getPointerCount() >= 2) { + + mChart.disableScroll(); + + saveTouchStart(event); + + // get the distance between the pointers on the x-axis + mSavedXDist = getXDist(event); + + // get the distance between the pointers on the y-axis + mSavedYDist = getYDist(event); + + // get the total distance between the pointers + mSavedDist = spacing(event); + + if (mSavedDist > 10f) { + + if (mChart.isPinchZoomEnabled()) { + mTouchMode = PINCH_ZOOM; + } else { + if (mChart.isScaleXEnabled() != mChart.isScaleYEnabled()) { + mTouchMode = mChart.isScaleXEnabled() ? X_ZOOM : Y_ZOOM; + } else { + mTouchMode = mSavedXDist > mSavedYDist ? X_ZOOM : Y_ZOOM; + } + } + } + + // determine the touch-pointer center + midPoint(mTouchPointCenter, event); + } + break; + + case MotionEvent.ACTION_MOVE: + + if (mTouchMode == DRAG) { + + mChart.disableScroll(); + + float x = mChart.isDragXEnabled() ? event.getX() - mTouchStartPoint.x : 0.f; + float y = mChart.isDragYEnabled() ? event.getY() - mTouchStartPoint.y : 0.f; + + performDrag(event, x, y); + + } else if (mTouchMode == X_ZOOM || mTouchMode == Y_ZOOM || mTouchMode == PINCH_ZOOM) { + + mChart.disableScroll(); + + if (mChart.isScaleXEnabled() || mChart.isScaleYEnabled()) + performZoom(event); + + } else if (mTouchMode == NONE + && Math.abs(distance(event.getX(), mTouchStartPoint.x, event.getY(), + mTouchStartPoint.y)) > mDragTriggerDist) { + + if (mChart.isDragEnabled()) { + + boolean shouldPan = !mChart.isFullyZoomedOut() || + !mChart.hasNoDragOffset(); + + if (shouldPan) { + + float distanceX = Math.abs(event.getX() - mTouchStartPoint.x); + float distanceY = Math.abs(event.getY() - mTouchStartPoint.y); + + // Disable dragging in a direction that's disallowed + if ((mChart.isDragXEnabled() || distanceY >= distanceX) && + (mChart.isDragYEnabled() || distanceY <= distanceX)) { + + mLastGesture = ChartGesture.DRAG; + mTouchMode = DRAG; + } + + } else { + + if (mChart.isHighlightPerDragEnabled()) { + mLastGesture = ChartGesture.DRAG; + + if (mChart.isHighlightPerDragEnabled()) + performHighlightDrag(event); + } + } + + } + + } + break; + + case MotionEvent.ACTION_UP: + + final VelocityTracker velocityTracker = mVelocityTracker; + final int pointerId = event.getPointerId(0); + velocityTracker.computeCurrentVelocity(1000, Utils.getMaximumFlingVelocity()); + final float velocityY = velocityTracker.getYVelocity(pointerId); + final float velocityX = velocityTracker.getXVelocity(pointerId); + + if (Math.abs(velocityX) > Utils.getMinimumFlingVelocity() || + Math.abs(velocityY) > Utils.getMinimumFlingVelocity()) { + + if (mTouchMode == DRAG && mChart.isDragDecelerationEnabled()) { + + stopDeceleration(); + + mDecelerationLastTime = AnimationUtils.currentAnimationTimeMillis(); + + mDecelerationCurrentPoint.x = event.getX(); + mDecelerationCurrentPoint.y = event.getY(); + + mDecelerationVelocity.x = velocityX; + mDecelerationVelocity.y = velocityY; + + Utils.postInvalidateOnAnimation(mChart); // This causes computeScroll to fire, recommended for this by + // Google + } + } + + if (mTouchMode == X_ZOOM || + mTouchMode == Y_ZOOM || + mTouchMode == PINCH_ZOOM || + mTouchMode == POST_ZOOM) { + + // Range might have changed, which means that Y-axis labels + // could have changed in size, affecting Y-axis size. + // So we need to recalculate offsets. + mChart.calculateOffsets(); + mChart.postInvalidate(); + } + + mTouchMode = NONE; + mChart.enableScroll(); + + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + + endAction(event); + + break; + case MotionEvent.ACTION_POINTER_UP: + Utils.velocityTrackerPointerUpCleanUpIfNecessary(event, mVelocityTracker); + + mTouchMode = POST_ZOOM; + break; + + case MotionEvent.ACTION_CANCEL: + + mTouchMode = NONE; + endAction(event); + break; + } + + // perform the transformation, update the chart + mMatrix = mChart.getViewPortHandler().refresh(mMatrix, mChart, true); + + return true; // indicate event was handled + } + + /** + * ################ ################ ################ ################ + */ + /** BELOW CODE PERFORMS THE ACTUAL TOUCH ACTIONS */ + + /** + * Saves the current Matrix state and the touch-start point. + * + * @param event + */ + private void saveTouchStart(MotionEvent event) { + + mSavedMatrix.set(mMatrix); + mTouchStartPoint.x = event.getX(); + mTouchStartPoint.y = event.getY(); + + mClosestDataSetToTouch = mChart.getDataSetByTouchPoint(event.getX(), event.getY()); + } + + /** + * Performs all necessary operations needed for dragging. + * + * @param event + */ + private void performDrag(MotionEvent event, float distanceX, float distanceY) { + + mLastGesture = ChartGesture.DRAG; + + mMatrix.set(mSavedMatrix); + + OnChartGestureListener l = mChart.getOnChartGestureListener(); + + // check if axis is inverted + if (inverted()) { + + // if there is an inverted horizontalbarchart + if (mChart instanceof HorizontalBarChart) { + distanceX = -distanceX; + } else { + distanceY = -distanceY; + } + } + + mMatrix.postTranslate(distanceX, distanceY); + + if (l != null) + l.onChartTranslate(event, distanceX, distanceY); + } + + /** + * Performs the all operations necessary for pinch and axis zoom. + * + * @param event + */ + private void performZoom(MotionEvent event) { + + if (event.getPointerCount() >= 2) { // two finger zoom + + OnChartGestureListener l = mChart.getOnChartGestureListener(); + + // get the distance between the pointers of the touch event + float totalDist = spacing(event); + + if (totalDist > mMinScalePointerDistance) { + + // get the translation + MPPointF t = getTrans(mTouchPointCenter.x, mTouchPointCenter.y); + ViewPortHandler h = mChart.getViewPortHandler(); + + // take actions depending on the activated touch mode + if (mTouchMode == PINCH_ZOOM) { + + mLastGesture = ChartGesture.PINCH_ZOOM; + + float scale = totalDist / mSavedDist; // total scale + + boolean isZoomingOut = (scale < 1); + + boolean canZoomMoreX = isZoomingOut ? + h.canZoomOutMoreX() : + h.canZoomInMoreX(); + + boolean canZoomMoreY = isZoomingOut ? + h.canZoomOutMoreY() : + h.canZoomInMoreY(); + + float scaleX = (mChart.isScaleXEnabled()) ? scale : 1f; + float scaleY = (mChart.isScaleYEnabled()) ? scale : 1f; + + if (canZoomMoreY || canZoomMoreX) { + + mMatrix.set(mSavedMatrix); + mMatrix.postScale(scaleX, scaleY, t.x, t.y); + + if (l != null) + l.onChartScale(event, scaleX, scaleY); + } + + } else if (mTouchMode == X_ZOOM && mChart.isScaleXEnabled()) { + + mLastGesture = ChartGesture.X_ZOOM; + + float xDist = getXDist(event); + float scaleX = xDist / mSavedXDist; // x-axis scale + + boolean isZoomingOut = (scaleX < 1); + boolean canZoomMoreX = isZoomingOut ? + h.canZoomOutMoreX() : + h.canZoomInMoreX(); + + if (canZoomMoreX) { + + mMatrix.set(mSavedMatrix); + mMatrix.postScale(scaleX, 1f, t.x, t.y); + + if (l != null) + l.onChartScale(event, scaleX, 1f); + } + + } else if (mTouchMode == Y_ZOOM && mChart.isScaleYEnabled()) { + + mLastGesture = ChartGesture.Y_ZOOM; + + float yDist = getYDist(event); + float scaleY = yDist / mSavedYDist; // y-axis scale + + boolean isZoomingOut = (scaleY < 1); + boolean canZoomMoreY = isZoomingOut ? + h.canZoomOutMoreY() : + h.canZoomInMoreY(); + + if (canZoomMoreY) { + + mMatrix.set(mSavedMatrix); + mMatrix.postScale(1f, scaleY, t.x, t.y); + + if (l != null) + l.onChartScale(event, 1f, scaleY); + } + } + + MPPointF.recycleInstance(t); + } + } + } + + /** + * Highlights upon dragging, generates callbacks for the selection-listener. + * + * @param e + */ + private void performHighlightDrag(MotionEvent e) { + + Highlight h = mChart.getHighlightByTouchPoint(e.getX(), e.getY()); + + if (h != null && !h.equalTo(mLastHighlighted)) { + mLastHighlighted = h; + mChart.highlightValue(h, true); + } + } + + /** + * ################ ################ ################ ################ + */ + /** DOING THE MATH BELOW ;-) */ + + + /** + * Determines the center point between two pointer touch points. + * + * @param point + * @param event + */ + private static void midPoint(MPPointF point, MotionEvent event) { + float x = event.getX(0) + event.getX(1); + float y = event.getY(0) + event.getY(1); + point.x = (x / 2f); + point.y = (y / 2f); + } + + /** + * returns the distance between two pointer touch points + * + * @param event + * @return + */ + private static float spacing(MotionEvent event) { + float x = event.getX(0) - event.getX(1); + float y = event.getY(0) - event.getY(1); + return (float) Math.sqrt(x * x + y * y); + } + + /** + * calculates the distance on the x-axis between two pointers (fingers on + * the display) + * + * @param e + * @return + */ + private static float getXDist(MotionEvent e) { + float x = Math.abs(e.getX(0) - e.getX(1)); + return x; + } + + /** + * calculates the distance on the y-axis between two pointers (fingers on + * the display) + * + * @param e + * @return + */ + private static float getYDist(MotionEvent e) { + float y = Math.abs(e.getY(0) - e.getY(1)); + return y; + } + + /** + * Returns a recyclable MPPointF instance. + * returns the correct translation depending on the provided x and y touch + * points + * + * @param x + * @param y + * @return + */ + public MPPointF getTrans(float x, float y) { + + ViewPortHandler vph = mChart.getViewPortHandler(); + + float xTrans = x - vph.offsetLeft(); + float yTrans = 0f; + + // check if axis is inverted + if (inverted()) { + yTrans = -(y - vph.offsetTop()); + } else { + yTrans = -(mChart.getMeasuredHeight() - y - vph.offsetBottom()); + } + + return MPPointF.getInstance(xTrans, yTrans); + } + + /** + * Returns true if the current touch situation should be interpreted as inverted, false if not. + * + * @return + */ + private boolean inverted() { + return (mClosestDataSetToTouch == null && mChart.isAnyAxisInverted()) || (mClosestDataSetToTouch != null + && mChart.isInverted(mClosestDataSetToTouch.getAxisDependency())); + } + + /** + * ################ ################ ################ ################ + */ + /** GETTERS AND GESTURE RECOGNITION BELOW */ + + /** + * returns the matrix object the listener holds + * + * @return + */ + public Matrix getMatrix() { + return mMatrix; + } + + /** + * Sets the minimum distance that will be interpreted as a "drag" by the chart in dp. + * Default: 3dp + * + * @param dragTriggerDistance + */ + public void setDragTriggerDist(float dragTriggerDistance) { + this.mDragTriggerDist = Utils.convertDpToPixel(dragTriggerDistance); + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + + mLastGesture = ChartGesture.DOUBLE_TAP; + + OnChartGestureListener l = mChart.getOnChartGestureListener(); + + if (l != null) { + l.onChartDoubleTapped(e); + } + + // check if double-tap zooming is enabled + if (mChart.isDoubleTapToZoomEnabled() && mChart.getData().getEntryCount() > 0) { + + MPPointF trans = getTrans(e.getX(), e.getY()); + + float scaleX = mChart.isScaleXEnabled() ? 1.4f : 1f; + float scaleY = mChart.isScaleYEnabled() ? 1.4f : 1f; + + mChart.zoom(scaleX, scaleY, trans.x, trans.y); + + if (mChart.isLogEnabled()) + Log.i("BarlineChartTouch", "Double-Tap, Zooming In, x: " + trans.x + ", y: " + + trans.y); + + if (l != null) { + l.onChartScale(e, scaleX, scaleY); + } + + MPPointF.recycleInstance(trans); + } + + return super.onDoubleTap(e); + } + + @Override + public void onLongPress(MotionEvent e) { + + mLastGesture = ChartGesture.LONG_PRESS; + + OnChartGestureListener l = mChart.getOnChartGestureListener(); + + if (l != null) { + + l.onChartLongPressed(e); + } + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + + mLastGesture = ChartGesture.SINGLE_TAP; + + OnChartGestureListener l = mChart.getOnChartGestureListener(); + + if (l != null) { + l.onChartSingleTapped(e); + } + + if (!mChart.isHighlightPerTapEnabled()) { + return false; + } + + Highlight h = mChart.getHighlightByTouchPoint(e.getX(), e.getY()); + performHighlight(h, e); + + return super.onSingleTapUp(e); + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + + mLastGesture = ChartGesture.FLING; + + OnChartGestureListener l = mChart.getOnChartGestureListener(); + + if (l != null) { + l.onChartFling(e1, e2, velocityX, velocityY); + } + + return super.onFling(e1, e2, velocityX, velocityY); + } + + public void stopDeceleration() { + mDecelerationVelocity.x = 0; + mDecelerationVelocity.y = 0; + } + + public void computeScroll() { + + if (mDecelerationVelocity.x == 0.f && mDecelerationVelocity.y == 0.f) + return; // There's no deceleration in progress + + final long currentTime = AnimationUtils.currentAnimationTimeMillis(); + + mDecelerationVelocity.x *= mChart.getDragDecelerationFrictionCoef(); + mDecelerationVelocity.y *= mChart.getDragDecelerationFrictionCoef(); + + final float timeInterval = (float) (currentTime - mDecelerationLastTime) / 1000.f; + + float distanceX = mDecelerationVelocity.x * timeInterval; + float distanceY = mDecelerationVelocity.y * timeInterval; + + mDecelerationCurrentPoint.x += distanceX; + mDecelerationCurrentPoint.y += distanceY; + + MotionEvent event = MotionEvent.obtain(currentTime, currentTime, MotionEvent.ACTION_MOVE, mDecelerationCurrentPoint.x, + mDecelerationCurrentPoint.y, 0); + + float dragDistanceX = mChart.isDragXEnabled() ? mDecelerationCurrentPoint.x - mTouchStartPoint.x : 0.f; + float dragDistanceY = mChart.isDragYEnabled() ? mDecelerationCurrentPoint.y - mTouchStartPoint.y : 0.f; + + performDrag(event, dragDistanceX, dragDistanceY); + + event.recycle(); + mMatrix = mChart.getViewPortHandler().refresh(mMatrix, mChart, false); + + mDecelerationLastTime = currentTime; + + if (Math.abs(mDecelerationVelocity.x) >= 0.01 || Math.abs(mDecelerationVelocity.y) >= 0.01) + Utils.postInvalidateOnAnimation(mChart); // This causes computeScroll to fire, recommended for this by Google + else { + // Range might have changed, which means that Y-axis labels + // could have changed in size, affecting Y-axis size. + // So we need to recalculate offsets. + mChart.calculateOffsets(); + mChart.postInvalidate(); + + stopDeceleration(); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/listener/ChartTouchListener.java b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/ChartTouchListener.java new file mode 100644 index 0000000..75c8e86 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/ChartTouchListener.java @@ -0,0 +1,143 @@ +package com.github.mikephil.charting.listener; + +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; + +import com.github.mikephil.charting.charts.Chart; +import com.github.mikephil.charting.highlight.Highlight; + +/** + * Created by philipp on 12/06/15. + */ +public abstract class ChartTouchListener> extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { + + public enum ChartGesture { + NONE, DRAG, X_ZOOM, Y_ZOOM, PINCH_ZOOM, ROTATE, SINGLE_TAP, DOUBLE_TAP, LONG_PRESS, FLING + } + + /** + * the last touch gesture that has been performed + **/ + protected ChartGesture mLastGesture = ChartGesture.NONE; + + // states + protected static final int NONE = 0; + protected static final int DRAG = 1; + protected static final int X_ZOOM = 2; + protected static final int Y_ZOOM = 3; + protected static final int PINCH_ZOOM = 4; + protected static final int POST_ZOOM = 5; + protected static final int ROTATE = 6; + + /** + * integer field that holds the current touch-state + */ + protected int mTouchMode = NONE; + + /** + * the last highlighted object (via touch) + */ + protected Highlight mLastHighlighted; + + /** + * the gesturedetector used for detecting taps and longpresses, ... + */ + protected GestureDetector mGestureDetector; + + /** + * the chart the listener represents + */ + protected T mChart; + + public ChartTouchListener(T chart) { + this.mChart = chart; + + mGestureDetector = new GestureDetector(chart.getContext(), this); + } + + /** + * Calls the OnChartGestureListener to do the start callback + * + * @param me + */ + public void startAction(MotionEvent me) { + + OnChartGestureListener l = mChart.getOnChartGestureListener(); + + if (l != null) + l.onChartGestureStart(me, mLastGesture); + } + + /** + * Calls the OnChartGestureListener to do the end callback + * + * @param me + */ + public void endAction(MotionEvent me) { + + OnChartGestureListener l = mChart.getOnChartGestureListener(); + + if (l != null) + l.onChartGestureEnd(me, mLastGesture); + } + + /** + * Sets the last value that was highlighted via touch. + * + * @param high + */ + public void setLastHighlighted(Highlight high) { + mLastHighlighted = high; + } + + /** + * returns the touch mode the listener is currently in + * + * @return + */ + public int getTouchMode() { + return mTouchMode; + } + + /** + * Returns the last gesture that has been performed on the chart. + * + * @return + */ + public ChartGesture getLastGesture() { + return mLastGesture; + } + + + /** + * Perform a highlight operation. + * + * @param e + */ + protected void performHighlight(Highlight h, MotionEvent e) { + + if (h == null || h.equalTo(mLastHighlighted)) { + mChart.highlightValue(null, true); + mLastHighlighted = null; + } else { + mChart.highlightValue(h, true); + mLastHighlighted = h; + } + } + + /** + * returns the distance between two points + * + * @param eventX + * @param startX + * @param eventY + * @param startY + * @return + */ + protected static float distance(float eventX, float startX, float eventY, float startY) { + float dx = eventX - startX; + float dy = eventY - startY; + return (float) Math.sqrt(dx * dx + dy * dy); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnChartGestureListener.java b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnChartGestureListener.java new file mode 100644 index 0000000..da0c5ed --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnChartGestureListener.java @@ -0,0 +1,76 @@ +package com.github.mikephil.charting.listener; + +import android.view.MotionEvent; + +/** + * Listener for callbacks when doing gestures on the chart. + * + * @author Philipp Jahoda + */ +public interface OnChartGestureListener { + + /** + * Callbacks when a touch-gesture has started on the chart (ACTION_DOWN) + * + * @param me + * @param lastPerformedGesture + */ + void onChartGestureStart(MotionEvent me, ChartTouchListener.ChartGesture lastPerformedGesture); + + /** + * Callbacks when a touch-gesture has ended on the chart (ACTION_UP, ACTION_CANCEL) + * + * @param me + * @param lastPerformedGesture + */ + void onChartGestureEnd(MotionEvent me, ChartTouchListener.ChartGesture lastPerformedGesture); + + /** + * Callbacks when the chart is longpressed. + * + * @param me + */ + void onChartLongPressed(MotionEvent me); + + /** + * Callbacks when the chart is double-tapped. + * + * @param me + */ + void onChartDoubleTapped(MotionEvent me); + + /** + * Callbacks when the chart is single-tapped. + * + * @param me + */ + void onChartSingleTapped(MotionEvent me); + + /** + * Callbacks then a fling gesture is made on the chart. + * + * @param me1 + * @param me2 + * @param velocityX + * @param velocityY + */ + void onChartFling(MotionEvent me1, MotionEvent me2, float velocityX, float velocityY); + + /** + * Callbacks when the chart is scaled / zoomed via pinch zoom / double-tap gesture. + * + * @param me + * @param scaleX scalefactor on the x-axis + * @param scaleY scalefactor on the y-axis + */ + void onChartScale(MotionEvent me, float scaleX, float scaleY); + + /** + * Callbacks when the chart is moved / translated via drag gesture. + * + * @param me + * @param dX translation distance on the x-axis + * @param dY translation distance on the y-axis + */ + void onChartTranslate(MotionEvent me, float dX, float dY); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnChartValueSelectedListener.java b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnChartValueSelectedListener.java new file mode 100644 index 0000000..7f50232 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnChartValueSelectedListener.java @@ -0,0 +1,27 @@ +package com.github.mikephil.charting.listener; + +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.highlight.Highlight; + +/** + * Listener for callbacks when selecting values inside the chart by + * touch-gesture. + * + * @author Philipp Jahoda + */ +public interface OnChartValueSelectedListener { + + /** + * Called when a value has been selected inside the chart. + * + * @param e The selected Entry + * @param h The corresponding highlight object that contains information + * about the highlighted position such as dataSetIndex, ... + */ + void onValueSelected(Entry e, Highlight h); + + /** + * Called when nothing has been selected or an "un-select" has been made. + */ + void onNothingSelected(); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnDrawLineChartTouchListener.java b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnDrawLineChartTouchListener.java new file mode 100644 index 0000000..ca3a67f --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnDrawLineChartTouchListener.java @@ -0,0 +1,15 @@ +package com.github.mikephil.charting.listener; + +import android.view.GestureDetector.SimpleOnGestureListener; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; + +public class OnDrawLineChartTouchListener extends SimpleOnGestureListener implements OnTouchListener { + + @Override + public boolean onTouch(View v, MotionEvent event) { + return false; + } + +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnDrawListener.java b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnDrawListener.java new file mode 100644 index 0000000..5890350 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/OnDrawListener.java @@ -0,0 +1,38 @@ +package com.github.mikephil.charting.listener; + +import com.github.mikephil.charting.data.DataSet; +import com.github.mikephil.charting.data.Entry; + +/** + * Listener for callbacks when drawing on the chart. + * + * @author Philipp + * + */ +public interface OnDrawListener { + + /** + * Called whenever an entry is added with the finger. Note this is also called for entries that are generated by the + * library, when the touch gesture is too fast and skips points. + * + * @param entry + * the last drawn entry + */ + void onEntryAdded(Entry entry); + + /** + * Called whenever an entry is moved by the user after beeing highlighted + * + * @param entry + */ + void onEntryMoved(Entry entry); + + /** + * Called when drawing finger is lifted and the draw is finished. + * + * @param dataSet + * the last drawn DataSet + */ + void onDrawFinished(DataSet dataSet); + +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/listener/PieRadarChartTouchListener.java b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/PieRadarChartTouchListener.java new file mode 100644 index 0000000..d3527f9 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/listener/PieRadarChartTouchListener.java @@ -0,0 +1,288 @@ + +package com.github.mikephil.charting.listener; + +import android.annotation.SuppressLint; +import android.graphics.PointF; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.AnimationUtils; + +import com.github.mikephil.charting.charts.PieRadarChartBase; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; + +import java.util.ArrayList; + +/** + * Touchlistener for the PieChart. + * + * @author Philipp Jahoda + */ +public class PieRadarChartTouchListener extends ChartTouchListener> { + + private MPPointF mTouchStartPoint = MPPointF.getInstance(0,0); + + /** + * the angle where the dragging started + */ + private float mStartAngle = 0f; + + private ArrayList _velocitySamples = new ArrayList(); + + private long mDecelerationLastTime = 0; + private float mDecelerationAngularVelocity = 0.f; + + public PieRadarChartTouchListener(PieRadarChartBase chart) { + super(chart); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouch(View v, MotionEvent event) { + + if (mGestureDetector.onTouchEvent(event)) + return true; + + // if rotation by touch is enabled + // TODO: Also check if the pie itself is being touched, rather than the entire chart area + if (mChart.isRotationEnabled()) { + + float x = event.getX(); + float y = event.getY(); + + switch (event.getAction()) { + + case MotionEvent.ACTION_DOWN: + + startAction(event); + + stopDeceleration(); + + resetVelocity(); + + if (mChart.isDragDecelerationEnabled()) + sampleVelocity(x, y); + + setGestureStartAngle(x, y); + mTouchStartPoint.x = x; + mTouchStartPoint.y = y; + + break; + case MotionEvent.ACTION_MOVE: + + if (mChart.isDragDecelerationEnabled()) + sampleVelocity(x, y); + + if (mTouchMode == NONE + && distance(x, mTouchStartPoint.x, y, mTouchStartPoint.y) + > Utils.convertDpToPixel(8f)) { + mLastGesture = ChartGesture.ROTATE; + mTouchMode = ROTATE; + mChart.disableScroll(); + } else if (mTouchMode == ROTATE) { + updateGestureRotation(x, y); + mChart.invalidate(); + } + + endAction(event); + + break; + case MotionEvent.ACTION_UP: + + if (mChart.isDragDecelerationEnabled()) { + + stopDeceleration(); + + sampleVelocity(x, y); + + mDecelerationAngularVelocity = calculateVelocity(); + + if (mDecelerationAngularVelocity != 0.f) { + mDecelerationLastTime = AnimationUtils.currentAnimationTimeMillis(); + + Utils.postInvalidateOnAnimation(mChart); // This causes computeScroll to fire, recommended for this by Google + } + } + + mChart.enableScroll(); + mTouchMode = NONE; + + endAction(event); + + break; + } + } + + return true; + } + + @Override + public void onLongPress(MotionEvent me) { + + mLastGesture = ChartGesture.LONG_PRESS; + + OnChartGestureListener l = mChart.getOnChartGestureListener(); + + if (l != null) { + l.onChartLongPressed(me); + } + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + return true; + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + + mLastGesture = ChartGesture.SINGLE_TAP; + + OnChartGestureListener l = mChart.getOnChartGestureListener(); + + if (l != null) { + l.onChartSingleTapped(e); + } + + if(!mChart.isHighlightPerTapEnabled()) { + return false; + } + + Highlight high = mChart.getHighlightByTouchPoint(e.getX(), e.getY()); + performHighlight(high, e); + + return true; + } + + private void resetVelocity() { + _velocitySamples.clear(); + } + + private void sampleVelocity(float touchLocationX, float touchLocationY) { + + long currentTime = AnimationUtils.currentAnimationTimeMillis(); + + _velocitySamples.add(new AngularVelocitySample(currentTime, mChart.getAngleForPoint(touchLocationX, touchLocationY))); + + // Remove samples older than our sample time - 1 seconds + for (int i = 0, count = _velocitySamples.size(); i < count - 2; i++) { + if (currentTime - _velocitySamples.get(i).time > 1000) { + _velocitySamples.remove(0); + i--; + count--; + } else { + break; + } + } + } + + private float calculateVelocity() { + + if (_velocitySamples.isEmpty()) + return 0.f; + + AngularVelocitySample firstSample = _velocitySamples.get(0); + AngularVelocitySample lastSample = _velocitySamples.get(_velocitySamples.size() - 1); + + // Look for a sample that's closest to the latest sample, but not the same, so we can deduce the direction + AngularVelocitySample beforeLastSample = firstSample; + for (int i = _velocitySamples.size() - 1; i >= 0; i--) { + beforeLastSample = _velocitySamples.get(i); + if (beforeLastSample.angle != lastSample.angle) { + break; + } + } + + // Calculate the sampling time + float timeDelta = (lastSample.time - firstSample.time) / 1000.f; + if (timeDelta == 0.f) { + timeDelta = 0.1f; + } + + // Calculate clockwise/ccw by choosing two values that should be closest to each other, + // so if the angles are two far from each other we know they are inverted "for sure" + boolean clockwise = lastSample.angle >= beforeLastSample.angle; + if (Math.abs(lastSample.angle - beforeLastSample.angle) > 270.0) { + clockwise = !clockwise; + } + + // Now if the "gesture" is over a too big of an angle - then we know the angles are inverted, and we need to move them closer to each other from both sides of the 360.0 wrapping point + if (lastSample.angle - firstSample.angle > 180.0) { + firstSample.angle += 360.0; + } else if (firstSample.angle - lastSample.angle > 180.0) { + lastSample.angle += 360.0; + } + + // The velocity + float velocity = Math.abs((lastSample.angle - firstSample.angle) / timeDelta); + + // Direction? + if (!clockwise) { + velocity = -velocity; + } + + return velocity; + } + + /** + * sets the starting angle of the rotation, this is only used by the touch + * listener, x and y is the touch position + * + * @param x + * @param y + */ + public void setGestureStartAngle(float x, float y) { + mStartAngle = mChart.getAngleForPoint(x, y) - mChart.getRawRotationAngle(); + } + + /** + * updates the view rotation depending on the given touch position, also + * takes the starting angle into consideration + * + * @param x + * @param y + */ + public void updateGestureRotation(float x, float y) { + mChart.setRotationAngle(mChart.getAngleForPoint(x, y) - mStartAngle); + } + + /** + * Sets the deceleration-angular-velocity to 0f + */ + public void stopDeceleration() { + mDecelerationAngularVelocity = 0.f; + } + + public void computeScroll() { + + if (mDecelerationAngularVelocity == 0.f) + return; // There's no deceleration in progress + + final long currentTime = AnimationUtils.currentAnimationTimeMillis(); + + mDecelerationAngularVelocity *= mChart.getDragDecelerationFrictionCoef(); + + final float timeInterval = (float) (currentTime - mDecelerationLastTime) / 1000.f; + + mChart.setRotationAngle(mChart.getRotationAngle() + mDecelerationAngularVelocity * timeInterval); + + mDecelerationLastTime = currentTime; + + if (Math.abs(mDecelerationAngularVelocity) >= 0.001) + Utils.postInvalidateOnAnimation(mChart); // This causes computeScroll to fire, recommended for this by Google + else + stopDeceleration(); + } + + private class AngularVelocitySample { + + public long time; + public float angle; + + public AngularVelocitySample(long time, float angle) { + this.time = time; + this.angle = angle; + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/matrix/Vector3.java b/MPChartLib/src/main/java/com/github/mikephil/charting/matrix/Vector3.java new file mode 100644 index 0000000..6ff9a62 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/matrix/Vector3.java @@ -0,0 +1,136 @@ + +package com.github.mikephil.charting.matrix; + +/** + * Simple 3D vector class. Handles basic vector math for 3D vectors. + */ +public final class Vector3 { + public float x; + public float y; + public float z; + + public static final Vector3 ZERO = new Vector3(0, 0, 0); + public static final Vector3 UNIT_X = new Vector3(1, 0, 0); + public static final Vector3 UNIT_Y = new Vector3(0, 1, 0); + public static final Vector3 UNIT_Z = new Vector3(0, 0, 1); + + public Vector3() { + } + + public Vector3(float[] array) + { + set(array[0], array[1], array[2]); + } + + public Vector3(float xValue, float yValue, float zValue) { + set(xValue, yValue, zValue); + } + + public Vector3(Vector3 other) { + set(other); + } + + public final void add(Vector3 other) { + x += other.x; + y += other.y; + z += other.z; + } + + public final void add(float otherX, float otherY, float otherZ) { + x += otherX; + y += otherY; + z += otherZ; + } + + public final void subtract(Vector3 other) { + x -= other.x; + y -= other.y; + z -= other.z; + } + + public final void subtractMultiple(Vector3 other, float multiplicator) + { + x -= other.x * multiplicator; + y -= other.y * multiplicator; + z -= other.z * multiplicator; + } + + public final void multiply(float magnitude) { + x *= magnitude; + y *= magnitude; + z *= magnitude; + } + + public final void multiply(Vector3 other) { + x *= other.x; + y *= other.y; + z *= other.z; + } + + public final void divide(float magnitude) { + if (magnitude != 0.0f) { + x /= magnitude; + y /= magnitude; + z /= magnitude; + } + } + + public final void set(Vector3 other) { + x = other.x; + y = other.y; + z = other.z; + } + + public final void set(float xValue, float yValue, float zValue) { + x = xValue; + y = yValue; + z = zValue; + } + + public final float dot(Vector3 other) { + return (x * other.x) + (y * other.y) + (z * other.z); + } + + public final Vector3 cross(Vector3 other) { + return new Vector3(y * other.z - z * other.y, + z * other.x - x * other.z, + x * other.y - y * other.x); + } + + public final float length() { + return (float) Math.sqrt(length2()); + } + + public final float length2() { + return (x * x) + (y * y) + (z * z); + } + + public final float distance2(Vector3 other) { + float dx = x - other.x; + float dy = y - other.y; + float dz = z - other.z; + return (dx * dx) + (dy * dy) + (dz * dz); + } + + public final float normalize() { + final float magnitude = length(); + + // TODO: I'm choosing safety over speed here. + if (magnitude != 0.0f) { + x /= magnitude; + y /= magnitude; + z /= magnitude; + } + + return magnitude; + } + + public final void zero() { + set(0.0f, 0.0f, 0.0f); + } + + public final boolean pointsInSameDirection(Vector3 other) { + return this.dot(other) > 0; + } + +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/model/GradientColor.java b/MPChartLib/src/main/java/com/github/mikephil/charting/model/GradientColor.java new file mode 100644 index 0000000..b5c8715 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/model/GradientColor.java @@ -0,0 +1,69 @@ +package com.github.mikephil.charting.model; + +import com.github.mikephil.charting.utils.Fill; + +/** + * Deprecated. Use `Fill` + */ +@Deprecated +public class GradientColor extends Fill +{ + /** + * Deprecated. Use `Fill.getGradientColors()` + */ + @Deprecated + public int getStartColor() + { + return getGradientColors()[0]; + } + + /** + * Deprecated. Use `Fill.setGradientColors(...)` + */ + @Deprecated + public void setStartColor(int startColor) + { + if (getGradientColors() == null || getGradientColors().length != 2) + { + setGradientColors(new int[]{ + startColor, + getGradientColors() != null && getGradientColors().length > 1 + ? getGradientColors()[1] + : 0 + }); + } else + { + getGradientColors()[0] = startColor; + } + } + + /** + * Deprecated. Use `Fill.getGradientColors()` + */ + @Deprecated + public int getEndColor() + { + return getGradientColors()[1]; + } + + /** + * Deprecated. Use `Fill.setGradientColors(...)` + */ + @Deprecated + public void setEndColor(int endColor) + { + if (getGradientColors() == null || getGradientColors().length != 2) + { + setGradientColors(new int[]{ + getGradientColors() != null && getGradientColors().length > 0 + ? getGradientColors()[0] + : 0, + endColor + }); + } else + { + getGradientColors()[1] = endColor; + } + } + +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/AxisRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/AxisRenderer.java new file mode 100644 index 0000000..72ea2d1 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/AxisRenderer.java @@ -0,0 +1,293 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Style; + +import com.github.mikephil.charting.components.AxisBase; +import com.github.mikephil.charting.utils.MPPointD; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Baseclass of all axis renderers. + * + * @author Philipp Jahoda + */ +public abstract class AxisRenderer extends Renderer { + + /** base axis this axis renderer works with */ + protected AxisBase mAxis; + + /** transformer to transform values to screen pixels and return */ + protected Transformer mTrans; + + /** + * paint object for the grid lines + */ + protected Paint mGridPaint; + + /** + * paint for the x-label values + */ + protected Paint mAxisLabelPaint; + + /** + * paint for the line surrounding the chart + */ + protected Paint mAxisLinePaint; + + /** + * paint used for the limit lines + */ + protected Paint mLimitLinePaint; + + public AxisRenderer(ViewPortHandler viewPortHandler, Transformer trans, AxisBase axis) { + super(viewPortHandler); + + this.mTrans = trans; + this.mAxis = axis; + + if(mViewPortHandler != null) { + + mAxisLabelPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + mGridPaint = new Paint(); + mGridPaint.setColor(Color.GRAY); + mGridPaint.setStrokeWidth(1f); + mGridPaint.setStyle(Style.STROKE); + mGridPaint.setAlpha(90); + + mAxisLinePaint = new Paint(); + mAxisLinePaint.setColor(Color.BLACK); + mAxisLinePaint.setStrokeWidth(1f); + mAxisLinePaint.setStyle(Style.STROKE); + + mLimitLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mLimitLinePaint.setStyle(Paint.Style.STROKE); + } + } + + /** + * Returns the Paint object used for drawing the axis (labels). + * + * @return + */ + public Paint getPaintAxisLabels() { + return mAxisLabelPaint; + } + + /** + * Returns the Paint object that is used for drawing the grid-lines of the + * axis. + * + * @return + */ + public Paint getPaintGrid() { + return mGridPaint; + } + + /** + * Returns the Paint object that is used for drawing the axis-line that goes + * alongside the axis. + * + * @return + */ + public Paint getPaintAxisLine() { + return mAxisLinePaint; + } + + /** + * Returns the Transformer object used for transforming the axis values. + * + * @return + */ + public Transformer getTransformer() { + return mTrans; + } + + /** + * Computes the axis values. + * + * @param min - the minimum value in the data object for this axis + * @param max - the maximum value in the data object for this axis + */ + public void computeAxis(float min, float max, boolean inverted) { + + // calculate the starting and entry point of the y-labels (depending on + // zoom / contentrect bounds) + if (mViewPortHandler != null && mViewPortHandler.contentWidth() > 10 && !mViewPortHandler.isFullyZoomedOutY()) { + + MPPointD p1 = mTrans.getValuesByTouchPoint(mViewPortHandler.contentLeft(), mViewPortHandler.contentTop()); + MPPointD p2 = mTrans.getValuesByTouchPoint(mViewPortHandler.contentLeft(), mViewPortHandler.contentBottom()); + + if (!inverted) { + + min = (float) p2.y; + max = (float) p1.y; + } else { + + min = (float) p1.y; + max = (float) p2.y; + } + + MPPointD.recycleInstance(p1); + MPPointD.recycleInstance(p2); + } + + computeAxisValues(min, max); + } + + /** + * Sets up the axis values. Computes the desired number of labels between the two given extremes. + * + * @return + */ + protected void computeAxisValues(float min, float max) { + + float yMin = min; + float yMax = max; + + int labelCount = mAxis.getLabelCount(); + double range = Math.abs(yMax - yMin); + + if (labelCount == 0 || range <= 0 || Double.isInfinite(range)) { + mAxis.mEntries = new float[]{}; + mAxis.mCenteredEntries = new float[]{}; + mAxis.mEntryCount = 0; + return; + } + + // Find out how much spacing (in y value space) between axis values + double rawInterval = range / labelCount; + double interval = Utils.roundToNextSignificant(rawInterval); + + // If granularity is enabled, then do not allow the interval to go below specified granularity. + // This is used to avoid repeated values when rounding values for display. + if (mAxis.isGranularityEnabled()) + interval = interval < mAxis.getGranularity() ? mAxis.getGranularity() : interval; + + // Normalize interval + double intervalMagnitude = Utils.roundToNextSignificant(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 + // if it's 0.0 after floor(), we use the old value + interval = Math.floor(10.0 * intervalMagnitude) == 0.0 + ? interval + : Math.floor(10.0 * intervalMagnitude); + + } + + int n = mAxis.isCenterAxisLabelsEnabled() ? 1 : 0; + + // force label count + if (mAxis.isForceLabelsEnabled()) { + + interval = (float) range / (float) (labelCount - 1); + mAxis.mEntryCount = labelCount; + + if (mAxis.mEntries.length < labelCount) { + // Ensure stops contains at least numStops elements. + mAxis.mEntries = new float[labelCount]; + } + + float v = min; + + for (int i = 0; i < labelCount; i++) { + mAxis.mEntries[i] = v; + v += interval; + } + + n = labelCount; + + // no forced count + } else { + + double first = interval == 0.0 ? 0.0 : Math.ceil(yMin / interval) * interval; + if(mAxis.isCenterAxisLabelsEnabled()) { + first -= interval; + } + + double last = interval == 0.0 ? 0.0 : Utils.nextUp(Math.floor(yMax / interval) * interval); + + double f; + int i; + + if (interval != 0.0 && last != first) { + for (f = first; f <= last; f += interval) { + ++n; + } + } + else if (last == first && n == 0) { + n = 1; + } + + mAxis.mEntryCount = n; + + if (mAxis.mEntries.length < n) { + // Ensure stops contains at least numStops elements. + mAxis.mEntries = new float[n]; + } + + for (f = first, i = 0; i < n; f += interval, ++i) { + + if (f == 0.0) // Fix for negative zero case (Where value == -0.0, and 0.0 == -0.0) + f = 0.0; + + mAxis.mEntries[i] = (float) f; + } + } + + // set decimals + if (interval < 1) { + mAxis.mDecimals = (int) Math.ceil(-Math.log10(interval)); + } else { + mAxis.mDecimals = 0; + } + + if (mAxis.isCenterAxisLabelsEnabled()) { + + if (mAxis.mCenteredEntries.length < n) { + mAxis.mCenteredEntries = new float[n]; + } + + float offset = (float)interval / 2f; + + for (int i = 0; i < n; i++) { + mAxis.mCenteredEntries[i] = mAxis.mEntries[i] + offset; + } + } + } + + /** + * Draws the axis labels to the screen. + * + * @param c + */ + public abstract void renderAxisLabels(Canvas c); + + /** + * Draws the grid lines belonging to the axis. + * + * @param c + */ + public abstract void renderGridLines(Canvas c); + + /** + * Draws the line that goes alongside the axis. + * + * @param c + */ + public abstract void renderAxisLine(Canvas c); + + /** + * Draws the LimitLines associated with this axis to the screen. + * + * @param c + */ + public abstract void renderLimitLines(Canvas c); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BarChartRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BarChartRenderer.java new file mode 100644 index 0000000..1656a3a --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BarChartRenderer.java @@ -0,0 +1,498 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.buffer.BarBuffer; +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.highlight.Range; +import com.github.mikephil.charting.interfaces.dataprovider.BarDataProvider; +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; +import com.github.mikephil.charting.utils.Fill; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.util.List; + +public class BarChartRenderer extends BarLineScatterCandleBubbleRenderer { + + protected BarDataProvider mChart; + + /** + * the rect object that is used for drawing the bars + */ + protected RectF mBarRect = new RectF(); + + protected BarBuffer[] mBarBuffers; + + protected Paint mShadowPaint; + protected Paint mBarBorderPaint; + + public BarChartRenderer(BarDataProvider chart, ChartAnimator animator, + ViewPortHandler viewPortHandler) { + super(animator, viewPortHandler); + this.mChart = chart; + + mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mHighlightPaint.setStyle(Paint.Style.FILL); + mHighlightPaint.setColor(Color.rgb(0, 0, 0)); + // set alpha after color + mHighlightPaint.setAlpha(120); + + mShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mShadowPaint.setStyle(Paint.Style.FILL); + + mBarBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mBarBorderPaint.setStyle(Paint.Style.STROKE); + } + + @Override + public void initBuffers() { + + BarData barData = mChart.getBarData(); + mBarBuffers = new BarBuffer[barData.getDataSetCount()]; + + for (int i = 0; i < mBarBuffers.length; i++) { + IBarDataSet set = barData.getDataSetByIndex(i); + mBarBuffers[i] = new BarBuffer(set.getEntryCount() * 4 * (set.isStacked() ? set.getStackSize() : 1), + barData.getDataSetCount(), set.isStacked()); + } + } + + @Override + public void drawData(Canvas c) { + + BarData barData = mChart.getBarData(); + + for (int i = 0; i < barData.getDataSetCount(); i++) { + + IBarDataSet set = barData.getDataSetByIndex(i); + + if (set.isVisible()) { + drawDataSet(c, set, i); + } + } + } + + private RectF mBarShadowRectBuffer = new RectF(); + + protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) { + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + mBarBorderPaint.setColor(dataSet.getBarBorderColor()); + mBarBorderPaint.setStrokeWidth(Utils.convertDpToPixel(dataSet.getBarBorderWidth())); + + final boolean drawBorder = dataSet.getBarBorderWidth() > 0.f; + + float phaseX = mAnimator.getPhaseX(); + float phaseY = mAnimator.getPhaseY(); + + // draw the bar shadow before the values + if (mChart.isDrawBarShadowEnabled()) { + mShadowPaint.setColor(dataSet.getBarShadowColor()); + + BarData barData = mChart.getBarData(); + + final float barWidth = barData.getBarWidth(); + final float barWidthHalf = barWidth / 2.0f; + float x; + + for (int i = 0, count = Math.min((int)(Math.ceil((float)(dataSet.getEntryCount()) * phaseX)), dataSet.getEntryCount()); + i < count; + i++) { + + BarEntry e = dataSet.getEntryForIndex(i); + + x = e.getX(); + + mBarShadowRectBuffer.left = x - barWidthHalf; + mBarShadowRectBuffer.right = x + barWidthHalf; + + trans.rectValueToPixel(mBarShadowRectBuffer); + + if (!mViewPortHandler.isInBoundsLeft(mBarShadowRectBuffer.right)) + continue; + + if (!mViewPortHandler.isInBoundsRight(mBarShadowRectBuffer.left)) + break; + + mBarShadowRectBuffer.top = mViewPortHandler.contentTop(); + mBarShadowRectBuffer.bottom = mViewPortHandler.contentBottom(); + + c.drawRect(mBarShadowRectBuffer, mShadowPaint); + } + } + + // initialize the buffer + BarBuffer buffer = mBarBuffers[index]; + buffer.setPhases(phaseX, phaseY); + buffer.setDataSet(index); + buffer.setInverted(mChart.isInverted(dataSet.getAxisDependency())); + buffer.setBarWidth(mChart.getBarData().getBarWidth()); + + buffer.feed(dataSet); + + trans.pointValuesToPixel(buffer.buffer); + + final boolean isCustomFill = dataSet.getFills() != null && !dataSet.getFills().isEmpty(); + final boolean isSingleColor = dataSet.getColors().size() == 1; + final boolean isInverted = mChart.isInverted(dataSet.getAxisDependency()); + + if (isSingleColor) { + mRenderPaint.setColor(dataSet.getColor()); + } + + for (int j = 0, pos = 0; j < buffer.size(); j += 4, pos++) { + + if (!mViewPortHandler.isInBoundsLeft(buffer.buffer[j + 2])) + continue; + + if (!mViewPortHandler.isInBoundsRight(buffer.buffer[j])) + break; + + if (!isSingleColor) { + // Set the color for the currently drawn value. If the index + // is out of bounds, reuse colors. + mRenderPaint.setColor(dataSet.getColor(pos)); + } + + if (isCustomFill) { + dataSet.getFill(pos) + .fillRect( + c, mRenderPaint, + buffer.buffer[j], + buffer.buffer[j + 1], + buffer.buffer[j + 2], + buffer.buffer[j + 3], + isInverted ? Fill.Direction.DOWN : Fill.Direction.UP); + } + else { + c.drawRect(buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], + buffer.buffer[j + 3], mRenderPaint); + } + + if (drawBorder) { + c.drawRect(buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], + buffer.buffer[j + 3], mBarBorderPaint); + } + } + } + + protected void prepareBarHighlight(float x, float y1, float y2, float barWidthHalf, Transformer trans) { + + float left = x - barWidthHalf; + float right = x + barWidthHalf; + float top = y1; + float bottom = y2; + + mBarRect.set(left, top, right, bottom); + + trans.rectToPixelPhase(mBarRect, mAnimator.getPhaseY()); + } + + @Override + public void drawValues(Canvas c) { + + // if values are drawn + if (isDrawingValuesAllowed(mChart)) { + + List dataSets = mChart.getBarData().getDataSets(); + + final float valueOffsetPlus = Utils.convertDpToPixel(4.5f); + float posOffset = 0f; + float negOffset = 0f; + boolean drawValueAboveBar = mChart.isDrawValueAboveBarEnabled(); + + for (int i = 0; i < mChart.getBarData().getDataSetCount(); i++) { + + IBarDataSet dataSet = dataSets.get(i); + + if (!shouldDrawValues(dataSet)) + continue; + + // apply the text-styling defined by the DataSet + applyValueTextStyle(dataSet); + + boolean isInverted = mChart.isInverted(dataSet.getAxisDependency()); + + // calculate the correct offset depending on the draw position of + // the value + float valueTextHeight = Utils.calcTextHeight(mValuePaint, "8"); + posOffset = (drawValueAboveBar ? -valueOffsetPlus : valueTextHeight + valueOffsetPlus); + negOffset = (drawValueAboveBar ? valueTextHeight + valueOffsetPlus : -valueOffsetPlus); + + if (isInverted) { + posOffset = -posOffset - valueTextHeight; + negOffset = -negOffset - valueTextHeight; + } + + // get the buffer + BarBuffer buffer = mBarBuffers[i]; + + final float phaseY = mAnimator.getPhaseY(); + + MPPointF iconsOffset = MPPointF.getInstance(dataSet.getIconsOffset()); + iconsOffset.x = Utils.convertDpToPixel(iconsOffset.x); + iconsOffset.y = Utils.convertDpToPixel(iconsOffset.y); + + // if only single values are drawn (sum) + if (!dataSet.isStacked()) { + + for (int j = 0; j < buffer.buffer.length * mAnimator.getPhaseX(); j += 4) { + + float x = (buffer.buffer[j] + buffer.buffer[j + 2]) / 2f; + + if (!mViewPortHandler.isInBoundsRight(x)) + break; + + if (!mViewPortHandler.isInBoundsY(buffer.buffer[j + 1]) + || !mViewPortHandler.isInBoundsLeft(x)) + continue; + + BarEntry entry = dataSet.getEntryForIndex(j / 4); + float val = entry.getY(); + + if (dataSet.isDrawValuesEnabled()) { + drawValue(c, dataSet.getValueFormatter(), val, entry, i, x, + val >= 0 ? + (buffer.buffer[j + 1] + posOffset) : + (buffer.buffer[j + 3] + negOffset), + dataSet.getValueTextColor(j / 4)); + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + + Drawable icon = entry.getIcon(); + + float px = x; + float py = val >= 0 ? + (buffer.buffer[j + 1] + posOffset) : + (buffer.buffer[j + 3] + negOffset); + + px += iconsOffset.x; + py += iconsOffset.y; + + Utils.drawImage( + c, + icon, + (int)px, + (int)py, + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight()); + } + } + + // if we have stacks + } else { + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + int bufferIndex = 0; + int index = 0; + + while (index < dataSet.getEntryCount() * mAnimator.getPhaseX()) { + + BarEntry entry = dataSet.getEntryForIndex(index); + + float[] vals = entry.getYVals(); + float x = (buffer.buffer[bufferIndex] + buffer.buffer[bufferIndex + 2]) / 2f; + + int color = dataSet.getValueTextColor(index); + + // we still draw stacked bars, but there is one + // non-stacked + // in between + if (vals == null) { + + if (!mViewPortHandler.isInBoundsRight(x)) + break; + + if (!mViewPortHandler.isInBoundsY(buffer.buffer[bufferIndex + 1]) + || !mViewPortHandler.isInBoundsLeft(x)) + continue; + + if (dataSet.isDrawValuesEnabled()) { + drawValue(c, dataSet.getValueFormatter(), entry.getY(), entry, i, x, + buffer.buffer[bufferIndex + 1] + + (entry.getY() >= 0 ? posOffset : negOffset), + color); + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + + Drawable icon = entry.getIcon(); + + float px = x; + float py = buffer.buffer[bufferIndex + 1] + + (entry.getY() >= 0 ? posOffset : negOffset); + + px += iconsOffset.x; + py += iconsOffset.y; + + Utils.drawImage( + c, + icon, + (int)px, + (int)py, + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight()); + } + + // draw stack values + } else { + + float[] transformed = new float[vals.length * 2]; + + float posY = 0f; + float negY = -entry.getNegativeSum(); + + for (int k = 0, idx = 0; k < transformed.length; k += 2, idx++) { + + float value = vals[idx]; + float y; + + if (value == 0.0f && (posY == 0.0f || negY == 0.0f)) { + // Take care of the situation of a 0.0 value, which overlaps a non-zero bar + y = value; + } else if (value >= 0.0f) { + posY += value; + y = posY; + } else { + y = negY; + negY -= value; + } + + transformed[k + 1] = y * phaseY; + } + + trans.pointValuesToPixel(transformed); + + for (int k = 0; k < transformed.length; k += 2) { + + final float val = vals[k / 2]; + final boolean drawBelow = + (val == 0.0f && negY == 0.0f && posY > 0.0f) || + val < 0.0f; + float y = transformed[k + 1] + + (drawBelow ? negOffset : posOffset); + + if (!mViewPortHandler.isInBoundsRight(x)) + break; + + if (!mViewPortHandler.isInBoundsY(y) + || !mViewPortHandler.isInBoundsLeft(x)) + continue; + + if (dataSet.isDrawValuesEnabled()) { + drawValue(c, + dataSet.getValueFormatter(), + vals[k / 2], + entry, + i, + x, + y, + color); + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + + Drawable icon = entry.getIcon(); + + Utils.drawImage( + c, + icon, + (int)(x + iconsOffset.x), + (int)(y + iconsOffset.y), + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight()); + } + } + } + + bufferIndex = vals == null ? bufferIndex + 4 : bufferIndex + 4 * vals.length; + index++; + } + } + + MPPointF.recycleInstance(iconsOffset); + } + } + } + + @Override + public void drawHighlighted(Canvas c, Highlight[] indices) { + + BarData barData = mChart.getBarData(); + + for (Highlight high : indices) { + + IBarDataSet set = barData.getDataSetByIndex(high.getDataSetIndex()); + + if (set == null || !set.isHighlightEnabled()) + continue; + + BarEntry e = set.getEntryForXValue(high.getX(), high.getY()); + + if (!isInBoundsX(e, set)) + continue; + + Transformer trans = mChart.getTransformer(set.getAxisDependency()); + + mHighlightPaint.setColor(set.getHighLightColor()); + mHighlightPaint.setAlpha(set.getHighLightAlpha()); + + boolean isStack = (high.getStackIndex() >= 0 && e.isStacked()) ? true : false; + + final float y1; + final float y2; + + if (isStack) { + + if(mChart.isHighlightFullBarEnabled()) { + + y1 = e.getPositiveSum(); + y2 = -e.getNegativeSum(); + + } else { + + Range range = e.getRanges()[high.getStackIndex()]; + + y1 = range.from; + y2 = range.to; + } + + } else { + y1 = e.getY(); + y2 = 0.f; + } + + prepareBarHighlight(e.getX(), y1, y2, barData.getBarWidth() / 2f, trans); + + setHighlightDrawPos(high, mBarRect); + + c.drawRect(mBarRect, mHighlightPaint); + } + } + + /** + * Sets the drawing position of the highlight object based on the riven bar-rect. + * @param high + */ + protected void setHighlightDrawPos(Highlight high, RectF bar) { + high.setDraw(bar.centerX(), bar.top); + } + + @Override + public void drawExtras(Canvas c) { + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BarLineScatterCandleBubbleRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BarLineScatterCandleBubbleRenderer.java new file mode 100644 index 0000000..0659918 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BarLineScatterCandleBubbleRenderer.java @@ -0,0 +1,96 @@ +package com.github.mikephil.charting.renderer; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.data.DataSet; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.interfaces.dataprovider.BarLineScatterCandleBubbleDataProvider; +import com.github.mikephil.charting.interfaces.datasets.IBarLineScatterCandleBubbleDataSet; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by Philipp Jahoda on 09/06/16. + */ +public abstract class BarLineScatterCandleBubbleRenderer extends DataRenderer { + + /** + * buffer for storing the current minimum and maximum visible x + */ + protected XBounds mXBounds = new XBounds(); + + public BarLineScatterCandleBubbleRenderer(ChartAnimator animator, ViewPortHandler viewPortHandler) { + super(animator, viewPortHandler); + } + + /** + * Returns true if the DataSet values should be drawn, false if not. + * + * @param set + * @return + */ + protected boolean shouldDrawValues(IDataSet set) { + return set.isVisible() && (set.isDrawValuesEnabled() || set.isDrawIconsEnabled()); + } + + /** + * Checks if the provided entry object is in bounds for drawing considering the current animation phase. + * + * @param e + * @param set + * @return + */ + protected boolean isInBoundsX(Entry e, IBarLineScatterCandleBubbleDataSet set) { + + if (e == null) + return false; + + float entryIndex = set.getEntryIndex(e); + + if (e == null || entryIndex >= set.getEntryCount() * mAnimator.getPhaseX()) { + return false; + } else { + return true; + } + } + + /** + * Class representing the bounds of the current viewport in terms of indices in the values array of a DataSet. + */ + protected class XBounds { + + /** + * minimum visible entry index + */ + public int min; + + /** + * maximum visible entry index + */ + public int max; + + /** + * range of visible entry indices + */ + public int range; + + /** + * Calculates the minimum and maximum x values as well as the range between them. + * + * @param chart + * @param dataSet + */ + public void set(BarLineScatterCandleBubbleDataProvider chart, IBarLineScatterCandleBubbleDataSet dataSet) { + float phaseX = Math.max(0.f, Math.min(1.f, mAnimator.getPhaseX())); + + float low = chart.getLowestVisibleX(); + float high = chart.getHighestVisibleX(); + + Entry entryFrom = dataSet.getEntryForXValue(low, Float.NaN, DataSet.Rounding.DOWN); + Entry entryTo = dataSet.getEntryForXValue(high, Float.NaN, DataSet.Rounding.UP); + + min = entryFrom == null ? 0 : dataSet.getEntryIndex(entryFrom); + max = entryTo == null ? 0 : dataSet.getEntryIndex(entryTo); + range = (int) ((max - min) * phaseX); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BubbleChartRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BubbleChartRenderer.java new file mode 100644 index 0000000..5ce19c2 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BubbleChartRenderer.java @@ -0,0 +1,274 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint.Style; +import android.graphics.drawable.Drawable; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.data.BubbleData; +import com.github.mikephil.charting.data.BubbleEntry; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.dataprovider.BubbleDataProvider; +import com.github.mikephil.charting.interfaces.datasets.IBubbleDataSet; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.util.List; + +/** + * Bubble chart implementation: Copyright 2015 Pierre-Marc Airoldi Licensed + * under Apache License 2.0 Ported by Daniel Cohen Gindi + */ +public class BubbleChartRenderer extends BarLineScatterCandleBubbleRenderer { + + protected BubbleDataProvider mChart; + + public BubbleChartRenderer(BubbleDataProvider chart, ChartAnimator animator, + ViewPortHandler viewPortHandler) { + super(animator, viewPortHandler); + mChart = chart; + + mRenderPaint.setStyle(Style.FILL); + + mHighlightPaint.setStyle(Style.STROKE); + mHighlightPaint.setStrokeWidth(Utils.convertDpToPixel(1.5f)); + } + + @Override + public void initBuffers() { + + } + + @Override + public void drawData(Canvas c) { + + BubbleData bubbleData = mChart.getBubbleData(); + + for (IBubbleDataSet set : bubbleData.getDataSets()) { + + if (set.isVisible()) + drawDataSet(c, set); + } + } + + private float[] sizeBuffer = new float[4]; + private float[] pointBuffer = new float[2]; + + protected float getShapeSize(float entrySize, float maxSize, float reference, boolean normalizeSize) { + final float factor = normalizeSize ? ((maxSize == 0f) ? 1f : (float) Math.sqrt(entrySize / maxSize)) : + entrySize; + final float shapeSize = reference * factor; + return shapeSize; + } + + protected void drawDataSet(Canvas c, IBubbleDataSet dataSet) { + + if (dataSet.getEntryCount() < 1) + return; + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + float phaseY = mAnimator.getPhaseY(); + + mXBounds.set(mChart, dataSet); + + sizeBuffer[0] = 0f; + sizeBuffer[2] = 1f; + + trans.pointValuesToPixel(sizeBuffer); + + boolean normalizeSize = dataSet.isNormalizeSizeEnabled(); + + // calcualte the full width of 1 step on the x-axis + final float maxBubbleWidth = Math.abs(sizeBuffer[2] - sizeBuffer[0]); + final float maxBubbleHeight = Math.abs(mViewPortHandler.contentBottom() - mViewPortHandler.contentTop()); + final float referenceSize = Math.min(maxBubbleHeight, maxBubbleWidth); + + for (int j = mXBounds.min; j <= mXBounds.range + mXBounds.min; j++) { + + final BubbleEntry entry = dataSet.getEntryForIndex(j); + + pointBuffer[0] = entry.getX(); + pointBuffer[1] = (entry.getY()) * phaseY; + trans.pointValuesToPixel(pointBuffer); + + float shapeHalf = getShapeSize(entry.getSize(), dataSet.getMaxSize(), referenceSize, normalizeSize) / 2f; + + if (!mViewPortHandler.isInBoundsTop(pointBuffer[1] + shapeHalf) + || !mViewPortHandler.isInBoundsBottom(pointBuffer[1] - shapeHalf)) + continue; + + if (!mViewPortHandler.isInBoundsLeft(pointBuffer[0] + shapeHalf)) + continue; + + if (!mViewPortHandler.isInBoundsRight(pointBuffer[0] - shapeHalf)) + break; + + final int color = dataSet.getColor(j); + + mRenderPaint.setColor(color); + c.drawCircle(pointBuffer[0], pointBuffer[1], shapeHalf, mRenderPaint); + } + } + + @Override + public void drawValues(Canvas c) { + + BubbleData bubbleData = mChart.getBubbleData(); + + if (bubbleData == null) + return; + + // if values are drawn + if (isDrawingValuesAllowed(mChart)) { + + final List dataSets = bubbleData.getDataSets(); + + float lineHeight = Utils.calcTextHeight(mValuePaint, "1"); + + for (int i = 0; i < dataSets.size(); i++) { + + IBubbleDataSet dataSet = dataSets.get(i); + + if (!shouldDrawValues(dataSet) || dataSet.getEntryCount() < 1) + continue; + + // apply the text-styling defined by the DataSet + applyValueTextStyle(dataSet); + + final float phaseX = Math.max(0.f, Math.min(1.f, mAnimator.getPhaseX())); + final float phaseY = mAnimator.getPhaseY(); + + mXBounds.set(mChart, dataSet); + + final float[] positions = mChart.getTransformer(dataSet.getAxisDependency()) + .generateTransformedValuesBubble(dataSet, phaseY, mXBounds.min, mXBounds.max); + + final float alpha = phaseX == 1 ? phaseY : phaseX; + + MPPointF iconsOffset = MPPointF.getInstance(dataSet.getIconsOffset()); + iconsOffset.x = Utils.convertDpToPixel(iconsOffset.x); + iconsOffset.y = Utils.convertDpToPixel(iconsOffset.y); + + for (int j = 0; j < positions.length; j += 2) { + + int valueTextColor = dataSet.getValueTextColor(j / 2 + mXBounds.min); + valueTextColor = Color.argb(Math.round(255.f * alpha), Color.red(valueTextColor), + Color.green(valueTextColor), Color.blue(valueTextColor)); + + float x = positions[j]; + float y = positions[j + 1]; + + if (!mViewPortHandler.isInBoundsRight(x)) + break; + + if ((!mViewPortHandler.isInBoundsLeft(x) || !mViewPortHandler.isInBoundsY(y))) + continue; + + BubbleEntry entry = dataSet.getEntryForIndex(j / 2 + mXBounds.min); + + if (dataSet.isDrawValuesEnabled()) { + drawValue(c, dataSet.getValueFormatter(), entry.getSize(), entry, i, x, + y + (0.5f * lineHeight), valueTextColor); + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + + Drawable icon = entry.getIcon(); + + Utils.drawImage( + c, + icon, + (int)(x + iconsOffset.x), + (int)(y + iconsOffset.y), + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight()); + } + } + + MPPointF.recycleInstance(iconsOffset); + } + } + } + + @Override + public void drawExtras(Canvas c) { + } + + private float[] _hsvBuffer = new float[3]; + + @Override + public void drawHighlighted(Canvas c, Highlight[] indices) { + + BubbleData bubbleData = mChart.getBubbleData(); + + float phaseY = mAnimator.getPhaseY(); + + for (Highlight high : indices) { + + IBubbleDataSet set = bubbleData.getDataSetByIndex(high.getDataSetIndex()); + + if (set == null || !set.isHighlightEnabled()) + continue; + + final BubbleEntry entry = set.getEntryForXValue(high.getX(), high.getY()); + + if (entry.getY() != high.getY()) + continue; + + if (!isInBoundsX(entry, set)) + continue; + + Transformer trans = mChart.getTransformer(set.getAxisDependency()); + + sizeBuffer[0] = 0f; + sizeBuffer[2] = 1f; + + trans.pointValuesToPixel(sizeBuffer); + + boolean normalizeSize = set.isNormalizeSizeEnabled(); + + // calcualte the full width of 1 step on the x-axis + final float maxBubbleWidth = Math.abs(sizeBuffer[2] - sizeBuffer[0]); + final float maxBubbleHeight = Math.abs( + mViewPortHandler.contentBottom() - mViewPortHandler.contentTop()); + final float referenceSize = Math.min(maxBubbleHeight, maxBubbleWidth); + + pointBuffer[0] = entry.getX(); + pointBuffer[1] = (entry.getY()) * phaseY; + trans.pointValuesToPixel(pointBuffer); + + high.setDraw(pointBuffer[0], pointBuffer[1]); + + float shapeHalf = getShapeSize(entry.getSize(), + set.getMaxSize(), + referenceSize, + normalizeSize) / 2f; + + if (!mViewPortHandler.isInBoundsTop(pointBuffer[1] + shapeHalf) + || !mViewPortHandler.isInBoundsBottom(pointBuffer[1] - shapeHalf)) + continue; + + if (!mViewPortHandler.isInBoundsLeft(pointBuffer[0] + shapeHalf)) + continue; + + if (!mViewPortHandler.isInBoundsRight(pointBuffer[0] - shapeHalf)) + break; + + final int originalColor = set.getColor((int) entry.getX()); + + Color.RGBToHSV(Color.red(originalColor), Color.green(originalColor), + Color.blue(originalColor), _hsvBuffer); + _hsvBuffer[2] *= 0.5f; + final int color = Color.HSVToColor(Color.alpha(originalColor), _hsvBuffer); + + mHighlightPaint.setColor(color); + mHighlightPaint.setStrokeWidth(set.getHighlightCircleWidth()); + c.drawCircle(pointBuffer[0], pointBuffer[1], shapeHalf, mHighlightPaint); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/CandleStickChartRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/CandleStickChartRenderer.java new file mode 100644 index 0000000..991b702 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/CandleStickChartRenderer.java @@ -0,0 +1,363 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.data.CandleData; +import com.github.mikephil.charting.data.CandleEntry; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.dataprovider.CandleDataProvider; +import com.github.mikephil.charting.interfaces.datasets.ICandleDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.MPPointD; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.util.List; + +public class CandleStickChartRenderer extends LineScatterCandleRadarRenderer { + + protected CandleDataProvider mChart; + + private float[] mShadowBuffers = new float[8]; + private float[] mBodyBuffers = new float[4]; + private float[] mRangeBuffers = new float[4]; + private float[] mOpenBuffers = new float[4]; + private float[] mCloseBuffers = new float[4]; + + public CandleStickChartRenderer(CandleDataProvider chart, ChartAnimator animator, + ViewPortHandler viewPortHandler) { + super(animator, viewPortHandler); + mChart = chart; + } + + @Override + public void initBuffers() { + + } + + @Override + public void drawData(Canvas c) { + + CandleData candleData = mChart.getCandleData(); + + for (ICandleDataSet set : candleData.getDataSets()) { + + if (set.isVisible()) + drawDataSet(c, set); + } + } + + @SuppressWarnings("ResourceAsColor") + protected void drawDataSet(Canvas c, ICandleDataSet dataSet) { + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + float phaseY = mAnimator.getPhaseY(); + float barSpace = dataSet.getBarSpace(); + boolean showCandleBar = dataSet.getShowCandleBar(); + + mXBounds.set(mChart, dataSet); + + mRenderPaint.setStrokeWidth(dataSet.getShadowWidth()); + + // draw the body + for (int j = mXBounds.min; j <= mXBounds.range + mXBounds.min; j++) { + + // get the entry + CandleEntry e = dataSet.getEntryForIndex(j); + + if (e == null) + continue; + + final float xPos = e.getX(); + + final float open = e.getOpen(); + final float close = e.getClose(); + final float high = e.getHigh(); + final float low = e.getLow(); + + if (showCandleBar) { + // calculate the shadow + + mShadowBuffers[0] = xPos; + mShadowBuffers[2] = xPos; + mShadowBuffers[4] = xPos; + mShadowBuffers[6] = xPos; + + if (open > close) { + mShadowBuffers[1] = high * phaseY; + mShadowBuffers[3] = open * phaseY; + mShadowBuffers[5] = low * phaseY; + mShadowBuffers[7] = close * phaseY; + } else if (open < close) { + mShadowBuffers[1] = high * phaseY; + mShadowBuffers[3] = close * phaseY; + mShadowBuffers[5] = low * phaseY; + mShadowBuffers[7] = open * phaseY; + } else { + mShadowBuffers[1] = high * phaseY; + mShadowBuffers[3] = open * phaseY; + mShadowBuffers[5] = low * phaseY; + mShadowBuffers[7] = mShadowBuffers[3]; + } + + trans.pointValuesToPixel(mShadowBuffers); + + // draw the shadows + + if (dataSet.getShadowColorSameAsCandle()) { + + if (open > close) + mRenderPaint.setColor( + dataSet.getDecreasingColor() == ColorTemplate.COLOR_NONE ? + dataSet.getColor(j) : + dataSet.getDecreasingColor() + ); + + else if (open < close) + mRenderPaint.setColor( + dataSet.getIncreasingColor() == ColorTemplate.COLOR_NONE ? + dataSet.getColor(j) : + dataSet.getIncreasingColor() + ); + + else + mRenderPaint.setColor( + dataSet.getNeutralColor() == ColorTemplate.COLOR_NONE ? + dataSet.getColor(j) : + dataSet.getNeutralColor() + ); + + } else { + mRenderPaint.setColor( + dataSet.getShadowColor() == ColorTemplate.COLOR_NONE ? + dataSet.getColor(j) : + dataSet.getShadowColor() + ); + } + + mRenderPaint.setStyle(Paint.Style.STROKE); + + c.drawLines(mShadowBuffers, mRenderPaint); + + // calculate the body + + mBodyBuffers[0] = xPos - 0.5f + barSpace; + mBodyBuffers[1] = close * phaseY; + mBodyBuffers[2] = (xPos + 0.5f - barSpace); + mBodyBuffers[3] = open * phaseY; + + trans.pointValuesToPixel(mBodyBuffers); + + // draw body differently for increasing and decreasing entry + if (open > close) { // decreasing + + if (dataSet.getDecreasingColor() == ColorTemplate.COLOR_NONE) { + mRenderPaint.setColor(dataSet.getColor(j)); + } else { + mRenderPaint.setColor(dataSet.getDecreasingColor()); + } + + mRenderPaint.setStyle(dataSet.getDecreasingPaintStyle()); + + c.drawRect( + mBodyBuffers[0], mBodyBuffers[3], + mBodyBuffers[2], mBodyBuffers[1], + mRenderPaint); + + } else if (open < close) { + + if (dataSet.getIncreasingColor() == ColorTemplate.COLOR_NONE) { + mRenderPaint.setColor(dataSet.getColor(j)); + } else { + mRenderPaint.setColor(dataSet.getIncreasingColor()); + } + + mRenderPaint.setStyle(dataSet.getIncreasingPaintStyle()); + + c.drawRect( + mBodyBuffers[0], mBodyBuffers[1], + mBodyBuffers[2], mBodyBuffers[3], + mRenderPaint); + } else { // equal values + + if (dataSet.getNeutralColor() == ColorTemplate.COLOR_NONE) { + mRenderPaint.setColor(dataSet.getColor(j)); + } else { + mRenderPaint.setColor(dataSet.getNeutralColor()); + } + + c.drawLine( + mBodyBuffers[0], mBodyBuffers[1], + mBodyBuffers[2], mBodyBuffers[3], + mRenderPaint); + } + } else { + + mRangeBuffers[0] = xPos; + mRangeBuffers[1] = high * phaseY; + mRangeBuffers[2] = xPos; + mRangeBuffers[3] = low * phaseY; + + mOpenBuffers[0] = xPos - 0.5f + barSpace; + mOpenBuffers[1] = open * phaseY; + mOpenBuffers[2] = xPos; + mOpenBuffers[3] = open * phaseY; + + mCloseBuffers[0] = xPos + 0.5f - barSpace; + mCloseBuffers[1] = close * phaseY; + mCloseBuffers[2] = xPos; + mCloseBuffers[3] = close * phaseY; + + trans.pointValuesToPixel(mRangeBuffers); + trans.pointValuesToPixel(mOpenBuffers); + trans.pointValuesToPixel(mCloseBuffers); + + // draw the ranges + int barColor; + + if (open > close) + barColor = dataSet.getDecreasingColor() == ColorTemplate.COLOR_NONE + ? dataSet.getColor(j) + : dataSet.getDecreasingColor(); + else if (open < close) + barColor = dataSet.getIncreasingColor() == ColorTemplate.COLOR_NONE + ? dataSet.getColor(j) + : dataSet.getIncreasingColor(); + else + barColor = dataSet.getNeutralColor() == ColorTemplate.COLOR_NONE + ? dataSet.getColor(j) + : dataSet.getNeutralColor(); + + mRenderPaint.setColor(barColor); + c.drawLine( + mRangeBuffers[0], mRangeBuffers[1], + mRangeBuffers[2], mRangeBuffers[3], + mRenderPaint); + c.drawLine( + mOpenBuffers[0], mOpenBuffers[1], + mOpenBuffers[2], mOpenBuffers[3], + mRenderPaint); + c.drawLine( + mCloseBuffers[0], mCloseBuffers[1], + mCloseBuffers[2], mCloseBuffers[3], + mRenderPaint); + } + } + } + + @Override + public void drawValues(Canvas c) { + + // if values are drawn + if (isDrawingValuesAllowed(mChart)) { + + List dataSets = mChart.getCandleData().getDataSets(); + + for (int i = 0; i < dataSets.size(); i++) { + + ICandleDataSet dataSet = dataSets.get(i); + + if (!shouldDrawValues(dataSet) || dataSet.getEntryCount() < 1) + continue; + + // apply the text-styling defined by the DataSet + applyValueTextStyle(dataSet); + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + mXBounds.set(mChart, dataSet); + + float[] positions = trans.generateTransformedValuesCandle( + dataSet, mAnimator.getPhaseX(), mAnimator.getPhaseY(), mXBounds.min, mXBounds.max); + + float yOffset = Utils.convertDpToPixel(5f); + + MPPointF iconsOffset = MPPointF.getInstance(dataSet.getIconsOffset()); + iconsOffset.x = Utils.convertDpToPixel(iconsOffset.x); + iconsOffset.y = Utils.convertDpToPixel(iconsOffset.y); + + for (int j = 0; j < positions.length; j += 2) { + + float x = positions[j]; + float y = positions[j + 1]; + + if (!mViewPortHandler.isInBoundsRight(x)) + break; + + if (!mViewPortHandler.isInBoundsLeft(x) || !mViewPortHandler.isInBoundsY(y)) + continue; + + CandleEntry entry = dataSet.getEntryForIndex(j / 2 + mXBounds.min); + + if (dataSet.isDrawValuesEnabled()) { + drawValue(c, + dataSet.getValueFormatter(), + entry.getHigh(), + entry, + i, + x, + y - yOffset, + dataSet + .getValueTextColor(j / 2)); + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + + Drawable icon = entry.getIcon(); + + Utils.drawImage( + c, + icon, + (int)(x + iconsOffset.x), + (int)(y + iconsOffset.y), + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight()); + } + } + + MPPointF.recycleInstance(iconsOffset); + } + } + } + + @Override + public void drawExtras(Canvas c) { + } + + @Override + public void drawHighlighted(Canvas c, Highlight[] indices) { + + CandleData candleData = mChart.getCandleData(); + + for (Highlight high : indices) { + + ICandleDataSet set = candleData.getDataSetByIndex(high.getDataSetIndex()); + + if (set == null || !set.isHighlightEnabled()) + continue; + + CandleEntry e = set.getEntryForXValue(high.getX(), high.getY()); + + if (!isInBoundsX(e, set)) + continue; + + float lowValue = e.getLow() * mAnimator.getPhaseY(); + float highValue = e.getHigh() * mAnimator.getPhaseY(); + float y = (lowValue + highValue) / 2f; + + MPPointD pix = mChart.getTransformer(set.getAxisDependency()).getPixelForValues(e.getX(), y); + + high.setDraw((float) pix.x, (float) pix.y); + + // draw the lines + drawHighlightLines(c, (float) pix.x, (float) pix.y, set); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/CombinedChartRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/CombinedChartRenderer.java new file mode 100644 index 0000000..6d0d4d3 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/CombinedChartRenderer.java @@ -0,0 +1,167 @@ +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.charts.Chart; +import com.github.mikephil.charting.charts.CombinedChart; +import com.github.mikephil.charting.charts.CombinedChart.DrawOrder; +import com.github.mikephil.charting.data.ChartData; +import com.github.mikephil.charting.data.CombinedData; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.dataprovider.BarLineScatterCandleBubbleDataProvider; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +/** + * Renderer class that is responsible for rendering multiple different data-types. + */ +public class CombinedChartRenderer extends DataRenderer { + + /** + * all rederers for the different kinds of data this combined-renderer can draw + */ + protected List mRenderers = new ArrayList(5); + + protected WeakReference mChart; + + public CombinedChartRenderer(CombinedChart chart, ChartAnimator animator, ViewPortHandler viewPortHandler) { + super(animator, viewPortHandler); + mChart = new WeakReference(chart); + createRenderers(); + } + + /** + * Creates the renderers needed for this combined-renderer in the required order. Also takes the DrawOrder into + * consideration. + */ + public void createRenderers() { + + mRenderers.clear(); + + CombinedChart chart = (CombinedChart)mChart.get(); + if (chart == null) + return; + + DrawOrder[] orders = chart.getDrawOrder(); + + for (DrawOrder order : orders) { + + switch (order) { + case BAR: + if (chart.getBarData() != null) + mRenderers.add(new BarChartRenderer(chart, mAnimator, mViewPortHandler)); + break; + case BUBBLE: + if (chart.getBubbleData() != null) + mRenderers.add(new BubbleChartRenderer(chart, mAnimator, mViewPortHandler)); + break; + case LINE: + if (chart.getLineData() != null) + mRenderers.add(new LineChartRenderer(chart, mAnimator, mViewPortHandler)); + break; + case CANDLE: + if (chart.getCandleData() != null) + mRenderers.add(new CandleStickChartRenderer(chart, mAnimator, mViewPortHandler)); + break; + case SCATTER: + if (chart.getScatterData() != null) + mRenderers.add(new ScatterChartRenderer(chart, mAnimator, mViewPortHandler)); + break; + } + } + } + + @Override + public void initBuffers() { + + for (DataRenderer renderer : mRenderers) + renderer.initBuffers(); + } + + @Override + public void drawData(Canvas c) { + + for (DataRenderer renderer : mRenderers) + renderer.drawData(c); + } + + @Override + public void drawValues(Canvas c) { + + for (DataRenderer renderer : mRenderers) + renderer.drawValues(c); + } + + @Override + public void drawExtras(Canvas c) { + + for (DataRenderer renderer : mRenderers) + renderer.drawExtras(c); + } + + protected List mHighlightBuffer = new ArrayList(); + + @Override + public void drawHighlighted(Canvas c, Highlight[] indices) { + + Chart chart = mChart.get(); + if (chart == null) return; + + for (DataRenderer renderer : mRenderers) { + ChartData data = null; + + if (renderer instanceof BarChartRenderer) + data = ((BarChartRenderer)renderer).mChart.getBarData(); + else if (renderer instanceof LineChartRenderer) + data = ((LineChartRenderer)renderer).mChart.getLineData(); + else if (renderer instanceof CandleStickChartRenderer) + data = ((CandleStickChartRenderer)renderer).mChart.getCandleData(); + else if (renderer instanceof ScatterChartRenderer) + data = ((ScatterChartRenderer)renderer).mChart.getScatterData(); + else if (renderer instanceof BubbleChartRenderer) + data = ((BubbleChartRenderer)renderer).mChart.getBubbleData(); + + int dataIndex = data == null ? -1 + : ((CombinedData)chart.getData()).getAllData().indexOf(data); + + mHighlightBuffer.clear(); + + for (Highlight h : indices) { + if (h.getDataIndex() == dataIndex || h.getDataIndex() == -1) + mHighlightBuffer.add(h); + } + + renderer.drawHighlighted(c, mHighlightBuffer.toArray(new Highlight[mHighlightBuffer.size()])); + } + } + + /** + * Returns the sub-renderer object at the specified index. + * + * @param index + * @return + */ + public DataRenderer getSubRenderer(int index) { + if (index >= mRenderers.size() || index < 0) + return null; + else + return mRenderers.get(index); + } + + /** + * Returns all sub-renderers. + * + * @return + */ + public List getSubRenderers() { + return mRenderers; + } + + public void setSubRenderers(List renderers) { + this.mRenderers = renderers; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/DataRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/DataRenderer.java new file mode 100644 index 0000000..e8e5446 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/DataRenderer.java @@ -0,0 +1,169 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Paint.Style; +import android.graphics.drawable.Drawable; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.formatter.IValueFormatter; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.dataprovider.ChartInterface; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Superclass of all render classes for the different data types (line, bar, ...). + * + * @author Philipp Jahoda + */ +public abstract class DataRenderer extends Renderer { + + /** + * the animator object used to perform animations on the chart data + */ + protected ChartAnimator mAnimator; + + /** + * main paint object used for rendering + */ + protected Paint mRenderPaint; + + /** + * paint used for highlighting values + */ + protected Paint mHighlightPaint; + + protected Paint mDrawPaint; + + /** + * paint object for drawing values (text representing values of chart + * entries) + */ + protected Paint mValuePaint; + + public DataRenderer(ChartAnimator animator, ViewPortHandler viewPortHandler) { + super(viewPortHandler); + this.mAnimator = animator; + + mRenderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mRenderPaint.setStyle(Style.FILL); + + mDrawPaint = new Paint(Paint.DITHER_FLAG); + + mValuePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mValuePaint.setColor(Color.rgb(63, 63, 63)); + mValuePaint.setTextAlign(Align.CENTER); + mValuePaint.setTextSize(Utils.convertDpToPixel(9f)); + + mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mHighlightPaint.setStyle(Paint.Style.STROKE); + mHighlightPaint.setStrokeWidth(2f); + mHighlightPaint.setColor(Color.rgb(255, 187, 115)); + } + + protected boolean isDrawingValuesAllowed(ChartInterface chart) { + return chart.getData().getEntryCount() < chart.getMaxVisibleCount() + * mViewPortHandler.getScaleX(); + } + + /** + * Returns the Paint object this renderer uses for drawing the values + * (value-text). + * + * @return + */ + public Paint getPaintValues() { + return mValuePaint; + } + + /** + * Returns the Paint object this renderer uses for drawing highlight + * indicators. + * + * @return + */ + public Paint getPaintHighlight() { + return mHighlightPaint; + } + + /** + * Returns the Paint object used for rendering. + * + * @return + */ + public Paint getPaintRender() { + return mRenderPaint; + } + + /** + * Applies the required styling (provided by the DataSet) to the value-paint + * object. + * + * @param set + */ + protected void applyValueTextStyle(IDataSet set) { + + mValuePaint.setTypeface(set.getValueTypeface()); + mValuePaint.setTextSize(set.getValueTextSize()); + } + + /** + * Initializes the buffers used for rendering with a new size. Since this + * method performs memory allocations, it should only be called if + * necessary. + */ + public abstract void initBuffers(); + + /** + * Draws the actual data in form of lines, bars, ... depending on Renderer subclass. + * + * @param c + */ + public abstract void drawData(Canvas c); + + /** + * Loops over all Entrys and draws their values. + * + * @param c + */ + public abstract void drawValues(Canvas c); + + /** + * Draws the value of the given entry by using the provided IValueFormatter. + * + * @param c canvas + * @param formatter formatter for custom value-formatting + * @param value the value to be drawn + * @param entry the entry the value belongs to + * @param dataSetIndex the index of the DataSet the drawn Entry belongs to + * @param x position + * @param y position + * @param color + */ + public void drawValue(Canvas c, IValueFormatter formatter, float value, Entry entry, int dataSetIndex, float x, float y, int color) { + mValuePaint.setColor(color); + c.drawText(formatter.getFormattedValue(value, entry, dataSetIndex, mViewPortHandler), x, y, mValuePaint); + } + + /** + * Draws any kind of additional information (e.g. line-circles). + * + * @param c + */ + public abstract void drawExtras(Canvas c); + + /** + * Draws all highlight indicators for the values that are currently highlighted. + * + * @param c + * @param indices the highlighted values + */ + public abstract void drawHighlighted(Canvas c, Highlight[] indices); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/HorizontalBarChartRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/HorizontalBarChartRenderer.java new file mode 100644 index 0000000..0cd7234 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/HorizontalBarChartRenderer.java @@ -0,0 +1,443 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Paint.Align; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.buffer.BarBuffer; +import com.github.mikephil.charting.buffer.HorizontalBarBuffer; +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.formatter.IValueFormatter; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.dataprovider.BarDataProvider; +import com.github.mikephil.charting.interfaces.dataprovider.ChartInterface; +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; +import com.github.mikephil.charting.utils.Fill; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.util.List; + +/** + * Renderer for the HorizontalBarChart. + * + * @author Philipp Jahoda + */ +public class HorizontalBarChartRenderer extends BarChartRenderer { + + public HorizontalBarChartRenderer(BarDataProvider chart, ChartAnimator animator, + ViewPortHandler viewPortHandler) { + super(chart, animator, viewPortHandler); + + mValuePaint.setTextAlign(Align.LEFT); + } + + @Override + public void initBuffers() { + + BarData barData = mChart.getBarData(); + mBarBuffers = new HorizontalBarBuffer[barData.getDataSetCount()]; + + for (int i = 0; i < mBarBuffers.length; i++) { + IBarDataSet set = barData.getDataSetByIndex(i); + mBarBuffers[i] = new HorizontalBarBuffer(set.getEntryCount() * 4 * (set.isStacked() ? set.getStackSize() : 1), + barData.getDataSetCount(), set.isStacked()); + } + } + + private RectF mBarShadowRectBuffer = new RectF(); + + @Override + protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) { + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + mBarBorderPaint.setColor(dataSet.getBarBorderColor()); + mBarBorderPaint.setStrokeWidth(Utils.convertDpToPixel(dataSet.getBarBorderWidth())); + + final boolean drawBorder = dataSet.getBarBorderWidth() > 0.f; + + float phaseX = mAnimator.getPhaseX(); + float phaseY = mAnimator.getPhaseY(); + + // draw the bar shadow before the values + if (mChart.isDrawBarShadowEnabled()) { + mShadowPaint.setColor(dataSet.getBarShadowColor()); + + BarData barData = mChart.getBarData(); + + final float barWidth = barData.getBarWidth(); + final float barWidthHalf = barWidth / 2.0f; + float x; + + for (int i = 0, count = Math.min((int)(Math.ceil((float)(dataSet.getEntryCount()) * phaseX)), dataSet.getEntryCount()); + i < count; + i++) { + + BarEntry e = dataSet.getEntryForIndex(i); + + x = e.getX(); + + mBarShadowRectBuffer.top = x - barWidthHalf; + mBarShadowRectBuffer.bottom = x + barWidthHalf; + + trans.rectValueToPixel(mBarShadowRectBuffer); + + if (!mViewPortHandler.isInBoundsTop(mBarShadowRectBuffer.bottom)) + continue; + + if (!mViewPortHandler.isInBoundsBottom(mBarShadowRectBuffer.top)) + break; + + mBarShadowRectBuffer.left = mViewPortHandler.contentLeft(); + mBarShadowRectBuffer.right = mViewPortHandler.contentRight(); + + c.drawRect(mBarShadowRectBuffer, mShadowPaint); + } + } + + // initialize the buffer + BarBuffer buffer = mBarBuffers[index]; + buffer.setPhases(phaseX, phaseY); + buffer.setDataSet(index); + buffer.setInverted(mChart.isInverted(dataSet.getAxisDependency())); + buffer.setBarWidth(mChart.getBarData().getBarWidth()); + + buffer.feed(dataSet); + + trans.pointValuesToPixel(buffer.buffer); + + final boolean isCustomFill = dataSet.getFills() != null && !dataSet.getFills().isEmpty(); + final boolean isSingleColor = dataSet.getColors().size() == 1; + final boolean isInverted = mChart.isInverted(dataSet.getAxisDependency()); + + if (isSingleColor) { + mRenderPaint.setColor(dataSet.getColor()); + } + + for (int j = 0, pos = 0; j < buffer.size(); j += 4, pos++) { + + if (!mViewPortHandler.isInBoundsTop(buffer.buffer[j + 3])) + break; + + if (!mViewPortHandler.isInBoundsBottom(buffer.buffer[j + 1])) + continue; + + if (!isSingleColor) { + // Set the color for the currently drawn value. If the index + // is out of bounds, reuse colors. + mRenderPaint.setColor(dataSet.getColor(j / 4)); + } + + if (isCustomFill) { + dataSet.getFill(pos) + .fillRect( + c, mRenderPaint, + buffer.buffer[j], + buffer.buffer[j + 1], + buffer.buffer[j + 2], + buffer.buffer[j + 3], + isInverted ? Fill.Direction.LEFT : Fill.Direction.RIGHT); + } + else { + c.drawRect(buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], + buffer.buffer[j + 3], mRenderPaint); + } + + if (drawBorder) { + c.drawRect(buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], + buffer.buffer[j + 3], mBarBorderPaint); + } + } + } + + @Override + public void drawValues(Canvas c) { + // if values are drawn + if (isDrawingValuesAllowed(mChart)) { + + List dataSets = mChart.getBarData().getDataSets(); + + final float valueOffsetPlus = Utils.convertDpToPixel(5f); + float posOffset = 0f; + float negOffset = 0f; + final boolean drawValueAboveBar = mChart.isDrawValueAboveBarEnabled(); + + for (int i = 0; i < mChart.getBarData().getDataSetCount(); i++) { + + IBarDataSet dataSet = dataSets.get(i); + + if (!shouldDrawValues(dataSet)) + continue; + + boolean isInverted = mChart.isInverted(dataSet.getAxisDependency()); + + // apply the text-styling defined by the DataSet + applyValueTextStyle(dataSet); + final float halfTextHeight = Utils.calcTextHeight(mValuePaint, "10") / 2f; + + IValueFormatter formatter = dataSet.getValueFormatter(); + + // get the buffer + BarBuffer buffer = mBarBuffers[i]; + + final float phaseY = mAnimator.getPhaseY(); + + MPPointF iconsOffset = MPPointF.getInstance(dataSet.getIconsOffset()); + iconsOffset.x = Utils.convertDpToPixel(iconsOffset.x); + iconsOffset.y = Utils.convertDpToPixel(iconsOffset.y); + + // if only single values are drawn (sum) + if (!dataSet.isStacked()) { + + for (int j = 0; j < buffer.buffer.length * mAnimator.getPhaseX(); j += 4) { + + float y = (buffer.buffer[j + 1] + buffer.buffer[j + 3]) / 2f; + + if (!mViewPortHandler.isInBoundsTop(buffer.buffer[j + 1])) + break; + + if (!mViewPortHandler.isInBoundsX(buffer.buffer[j])) + continue; + + if (!mViewPortHandler.isInBoundsBottom(buffer.buffer[j + 1])) + continue; + + BarEntry entry = dataSet.getEntryForIndex(j / 4); + float val = entry.getY(); + String formattedValue = formatter.getFormattedValue(val, entry, i, mViewPortHandler); + + // calculate the correct offset depending on the draw position of the value + float valueTextWidth = Utils.calcTextWidth(mValuePaint, formattedValue); + posOffset = (drawValueAboveBar ? valueOffsetPlus : -(valueTextWidth + valueOffsetPlus)); + negOffset = (drawValueAboveBar ? -(valueTextWidth + valueOffsetPlus) : valueOffsetPlus) + - (buffer.buffer[j + 2] - buffer.buffer[j]); + + if (isInverted) { + posOffset = -posOffset - valueTextWidth; + negOffset = -negOffset - valueTextWidth; + } + + if (dataSet.isDrawValuesEnabled()) { + drawValue(c, + formattedValue, + buffer.buffer[j + 2] + (val >= 0 ? posOffset : negOffset), + y + halfTextHeight, + dataSet.getValueTextColor(j / 2)); + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + + Drawable icon = entry.getIcon(); + + float px = buffer.buffer[j + 2] + (val >= 0 ? posOffset : negOffset); + float py = y; + + px += iconsOffset.x; + py += iconsOffset.y; + + Utils.drawImage( + c, + icon, + (int)px, + (int)py, + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight()); + } + } + + // if each value of a potential stack should be drawn + } else { + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + int bufferIndex = 0; + int index = 0; + + while (index < dataSet.getEntryCount() * mAnimator.getPhaseX()) { + + BarEntry entry = dataSet.getEntryForIndex(index); + + int color = dataSet.getValueTextColor(index); + float[] vals = entry.getYVals(); + + // we still draw stacked bars, but there is one + // non-stacked + // in between + if (vals == null) { + + if (!mViewPortHandler.isInBoundsTop(buffer.buffer[bufferIndex + 1])) + break; + + if (!mViewPortHandler.isInBoundsX(buffer.buffer[bufferIndex])) + continue; + + if (!mViewPortHandler.isInBoundsBottom(buffer.buffer[bufferIndex + 1])) + continue; + + float val = entry.getY(); + String formattedValue = formatter.getFormattedValue(val, + entry, i, mViewPortHandler); + + // calculate the correct offset depending on the draw position of the value + float valueTextWidth = Utils.calcTextWidth(mValuePaint, formattedValue); + posOffset = (drawValueAboveBar ? valueOffsetPlus : -(valueTextWidth + valueOffsetPlus)); + negOffset = (drawValueAboveBar ? -(valueTextWidth + valueOffsetPlus) : valueOffsetPlus); + + if (isInverted) { + posOffset = -posOffset - valueTextWidth; + negOffset = -negOffset - valueTextWidth; + } + + if (dataSet.isDrawValuesEnabled()) { + drawValue(c, formattedValue, + buffer.buffer[bufferIndex + 2] + + (entry.getY() >= 0 ? posOffset : negOffset), + buffer.buffer[bufferIndex + 1] + halfTextHeight, color); + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + + Drawable icon = entry.getIcon(); + + float px = buffer.buffer[bufferIndex + 2] + + (entry.getY() >= 0 ? posOffset : negOffset); + float py = buffer.buffer[bufferIndex + 1]; + + px += iconsOffset.x; + py += iconsOffset.y; + + Utils.drawImage( + c, + icon, + (int)px, + (int)py, + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight()); + } + + } else { + + float[] transformed = new float[vals.length * 2]; + + float posY = 0f; + float negY = -entry.getNegativeSum(); + + for (int k = 0, idx = 0; k < transformed.length; k += 2, idx++) { + + float value = vals[idx]; + float y; + + if (value == 0.0f && (posY == 0.0f || negY == 0.0f)) { + // Take care of the situation of a 0.0 value, which overlaps a non-zero bar + y = value; + } else if (value >= 0.0f) { + posY += value; + y = posY; + } else { + y = negY; + negY -= value; + } + + transformed[k] = y * phaseY; + } + + trans.pointValuesToPixel(transformed); + + for (int k = 0; k < transformed.length; k += 2) { + + final float val = vals[k / 2]; + String formattedValue = formatter.getFormattedValue(val, + entry, i, mViewPortHandler); + + // calculate the correct offset depending on the draw position of the value + float valueTextWidth = Utils.calcTextWidth(mValuePaint, formattedValue); + posOffset = (drawValueAboveBar ? valueOffsetPlus : -(valueTextWidth + valueOffsetPlus)); + negOffset = (drawValueAboveBar ? -(valueTextWidth + valueOffsetPlus) : valueOffsetPlus); + + if (isInverted) { + posOffset = -posOffset - valueTextWidth; + negOffset = -negOffset - valueTextWidth; + } + + final boolean drawBelow = + (val == 0.0f && negY == 0.0f && posY > 0.0f) || + val < 0.0f; + + float x = transformed[k] + + (drawBelow ? negOffset : posOffset); + float y = (buffer.buffer[bufferIndex + 1] + buffer.buffer[bufferIndex + 3]) / 2f; + + if (!mViewPortHandler.isInBoundsTop(y)) + break; + + if (!mViewPortHandler.isInBoundsX(x)) + continue; + + if (!mViewPortHandler.isInBoundsBottom(y)) + continue; + + if (dataSet.isDrawValuesEnabled()) { + drawValue(c, formattedValue, x, y + halfTextHeight, color); + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + + Drawable icon = entry.getIcon(); + + Utils.drawImage( + c, + icon, + (int)(x + iconsOffset.x), + (int)(y + iconsOffset.y), + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight()); + } + } + } + + bufferIndex = vals == null ? bufferIndex + 4 : bufferIndex + 4 * vals.length; + index++; + } + } + + MPPointF.recycleInstance(iconsOffset); + } + } + } + + protected void drawValue(Canvas c, String valueText, float x, float y, int color) { + mValuePaint.setColor(color); + c.drawText(valueText, x, y, mValuePaint); + } + + @Override + protected void prepareBarHighlight(float x, float y1, float y2, float barWidthHalf, Transformer trans) { + + float top = x - barWidthHalf; + float bottom = x + barWidthHalf; + float left = y1; + float right = y2; + + mBarRect.set(left, top, right, bottom); + + trans.rectToPixelPhaseHorizontal(mBarRect, mAnimator.getPhaseY()); + } + + @Override + protected void setHighlightDrawPos(Highlight high, RectF bar) { + high.setDraw(bar.centerY(), bar.right); + } + + @Override + protected boolean isDrawingValuesAllowed(ChartInterface chart) { + return chart.getData().getEntryCount() < chart.getMaxVisibleCount() + * mViewPortHandler.getScaleY(); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LegendRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LegendRenderer.java new file mode 100644 index 0000000..5d49580 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LegendRenderer.java @@ -0,0 +1,570 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.DashPathEffect; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Path; +import android.graphics.Typeface; + +import com.github.mikephil.charting.components.Legend; +import com.github.mikephil.charting.components.LegendEntry; +import com.github.mikephil.charting.data.ChartData; +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; +import com.github.mikephil.charting.interfaces.datasets.ICandleDataSet; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.interfaces.datasets.IPieDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.FSize; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class LegendRenderer extends Renderer { + + /** + * paint for the legend labels + */ + protected Paint mLegendLabelPaint; + + /** + * paint used for the legend forms + */ + protected Paint mLegendFormPaint; + + /** + * the legend object this renderer renders + */ + protected Legend mLegend; + + public LegendRenderer(ViewPortHandler viewPortHandler, Legend legend) { + super(viewPortHandler); + + this.mLegend = legend; + + mLegendLabelPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mLegendLabelPaint.setTextSize(Utils.convertDpToPixel(9f)); + mLegendLabelPaint.setTextAlign(Align.LEFT); + + mLegendFormPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mLegendFormPaint.setStyle(Paint.Style.FILL); + } + + /** + * Returns the Paint object used for drawing the Legend labels. + * + * @return + */ + public Paint getLabelPaint() { + return mLegendLabelPaint; + } + + /** + * Returns the Paint object used for drawing the Legend forms. + * + * @return + */ + public Paint getFormPaint() { + return mLegendFormPaint; + } + + + protected List computedEntries = new ArrayList<>(16); + + /** + * Prepares the legend and calculates all needed forms, labels and colors. + * + * @param data + */ + public void computeLegend(ChartData data) { + + if (!mLegend.isLegendCustom()) { + + computedEntries.clear(); + + // loop for building up the colors and labels used in the legend + for (int i = 0; i < data.getDataSetCount(); i++) { + + IDataSet dataSet = data.getDataSetByIndex(i); + if (dataSet == null) continue; + + List clrs = dataSet.getColors(); + int entryCount = dataSet.getEntryCount(); + + // if we have a barchart with stacked bars + if (dataSet instanceof IBarDataSet && ((IBarDataSet) dataSet).isStacked()) { + + IBarDataSet bds = (IBarDataSet) dataSet; + String[] sLabels = bds.getStackLabels(); + + int minEntries = Math.min(clrs.size(), bds.getStackSize()); + + for (int j = 0; j < minEntries; j++) { + String label; + if (sLabels.length > 0) { + int labelIndex = j % minEntries; + label = labelIndex < sLabels.length ? sLabels[labelIndex] : null; + } else { + label = null; + } + + computedEntries.add(new LegendEntry( + label, + dataSet.getForm(), + dataSet.getFormSize(), + dataSet.getFormLineWidth(), + dataSet.getFormLineDashEffect(), + clrs.get(j) + )); + } + + if (bds.getLabel() != null) { + // add the legend description label + computedEntries.add(new LegendEntry( + dataSet.getLabel(), + Legend.LegendForm.NONE, + Float.NaN, + Float.NaN, + null, + ColorTemplate.COLOR_NONE + )); + } + + } else if (dataSet instanceof IPieDataSet) { + + IPieDataSet pds = (IPieDataSet) dataSet; + + for (int j = 0; j < clrs.size() && j < entryCount; j++) { + + computedEntries.add(new LegendEntry( + pds.getEntryForIndex(j).getLabel(), + dataSet.getForm(), + dataSet.getFormSize(), + dataSet.getFormLineWidth(), + dataSet.getFormLineDashEffect(), + clrs.get(j) + )); + } + + if (pds.getLabel() != null) { + // add the legend description label + computedEntries.add(new LegendEntry( + dataSet.getLabel(), + Legend.LegendForm.NONE, + Float.NaN, + Float.NaN, + null, + ColorTemplate.COLOR_NONE + )); + } + + } else if (dataSet instanceof ICandleDataSet && ((ICandleDataSet) dataSet).getDecreasingColor() != + ColorTemplate.COLOR_NONE) { + + int decreasingColor = ((ICandleDataSet) dataSet).getDecreasingColor(); + int increasingColor = ((ICandleDataSet) dataSet).getIncreasingColor(); + + computedEntries.add(new LegendEntry( + null, + dataSet.getForm(), + dataSet.getFormSize(), + dataSet.getFormLineWidth(), + dataSet.getFormLineDashEffect(), + decreasingColor + )); + + computedEntries.add(new LegendEntry( + dataSet.getLabel(), + dataSet.getForm(), + dataSet.getFormSize(), + dataSet.getFormLineWidth(), + dataSet.getFormLineDashEffect(), + increasingColor + )); + + } else { // all others + + for (int j = 0; j < clrs.size() && j < entryCount; j++) { + + String label; + + // if multiple colors are set for a DataSet, group them + if (j < clrs.size() - 1 && j < entryCount - 1) { + label = null; + } else { // add label to the last entry + label = data.getDataSetByIndex(i).getLabel(); + } + + computedEntries.add(new LegendEntry( + label, + dataSet.getForm(), + dataSet.getFormSize(), + dataSet.getFormLineWidth(), + dataSet.getFormLineDashEffect(), + clrs.get(j) + )); + } + } + } + + if (mLegend.getExtraEntries() != null) { + Collections.addAll(computedEntries, mLegend.getExtraEntries()); + } + + mLegend.setEntries(computedEntries); + } + + Typeface tf = mLegend.getTypeface(); + + if (tf != null) + mLegendLabelPaint.setTypeface(tf); + + mLegendLabelPaint.setTextSize(mLegend.getTextSize()); + mLegendLabelPaint.setColor(mLegend.getTextColor()); + + // calculate all dimensions of the mLegend + mLegend.calculateDimensions(mLegendLabelPaint, mViewPortHandler); + } + + protected Paint.FontMetrics legendFontMetrics = new Paint.FontMetrics(); + + public void renderLegend(Canvas c) { + + if (!mLegend.isEnabled()) + return; + + Typeface tf = mLegend.getTypeface(); + + if (tf != null) + mLegendLabelPaint.setTypeface(tf); + + mLegendLabelPaint.setTextSize(mLegend.getTextSize()); + mLegendLabelPaint.setColor(mLegend.getTextColor()); + + float labelLineHeight = Utils.getLineHeight(mLegendLabelPaint, legendFontMetrics); + float labelLineSpacing = Utils.getLineSpacing(mLegendLabelPaint, legendFontMetrics) + + Utils.convertDpToPixel(mLegend.getYEntrySpace()); + float formYOffset = labelLineHeight - Utils.calcTextHeight(mLegendLabelPaint, "ABC") / 2.f; + + LegendEntry[] entries = mLegend.getEntries(); + + float formToTextSpace = Utils.convertDpToPixel(mLegend.getFormToTextSpace()); + float xEntrySpace = Utils.convertDpToPixel(mLegend.getXEntrySpace()); + Legend.LegendOrientation orientation = mLegend.getOrientation(); + Legend.LegendHorizontalAlignment horizontalAlignment = mLegend.getHorizontalAlignment(); + Legend.LegendVerticalAlignment verticalAlignment = mLegend.getVerticalAlignment(); + Legend.LegendDirection direction = mLegend.getDirection(); + float defaultFormSize = Utils.convertDpToPixel(mLegend.getFormSize()); + + // space between the entries + float stackSpace = Utils.convertDpToPixel(mLegend.getStackSpace()); + + float yoffset = mLegend.getYOffset(); + float xoffset = mLegend.getXOffset(); + float originPosX = 0.f; + + switch (horizontalAlignment) { + case LEFT: + + if (orientation == Legend.LegendOrientation.VERTICAL) + originPosX = xoffset; + else + originPosX = mViewPortHandler.contentLeft() + xoffset; + + if (direction == Legend.LegendDirection.RIGHT_TO_LEFT) + originPosX += mLegend.mNeededWidth; + + break; + + case RIGHT: + + if (orientation == Legend.LegendOrientation.VERTICAL) + originPosX = mViewPortHandler.getChartWidth() - xoffset; + else + originPosX = mViewPortHandler.contentRight() - xoffset; + + if (direction == Legend.LegendDirection.LEFT_TO_RIGHT) + originPosX -= mLegend.mNeededWidth; + + break; + + case CENTER: + + if (orientation == Legend.LegendOrientation.VERTICAL) + originPosX = mViewPortHandler.getChartWidth() / 2.f; + else + originPosX = mViewPortHandler.contentLeft() + + mViewPortHandler.contentWidth() / 2.f; + + originPosX += (direction == Legend.LegendDirection.LEFT_TO_RIGHT + ? +xoffset + : -xoffset); + + // Horizontally layed out legends do the center offset on a line basis, + // So here we offset the vertical ones only. + if (orientation == Legend.LegendOrientation.VERTICAL) { + originPosX += (direction == Legend.LegendDirection.LEFT_TO_RIGHT + ? -mLegend.mNeededWidth / 2.0 + xoffset + : mLegend.mNeededWidth / 2.0 - xoffset); + } + + break; + } + + switch (orientation) { + case HORIZONTAL: { + + List calculatedLineSizes = mLegend.getCalculatedLineSizes(); + List calculatedLabelSizes = mLegend.getCalculatedLabelSizes(); + List calculatedLabelBreakPoints = mLegend.getCalculatedLabelBreakPoints(); + + float posX = originPosX; + float posY = 0.f; + + switch (verticalAlignment) { + case TOP: + posY = yoffset; + break; + + case BOTTOM: + posY = mViewPortHandler.getChartHeight() - yoffset - mLegend.mNeededHeight; + break; + + case CENTER: + posY = (mViewPortHandler.getChartHeight() - mLegend.mNeededHeight) / 2.f + yoffset; + break; + } + + int lineIndex = 0; + + for (int i = 0, count = entries.length; i < count; i++) { + + LegendEntry e = entries[i]; + boolean drawingForm = e.form != Legend.LegendForm.NONE; + float formSize = Float.isNaN(e.formSize) ? defaultFormSize : Utils.convertDpToPixel(e.formSize); + + if (i < calculatedLabelBreakPoints.size() && calculatedLabelBreakPoints.get(i)) { + posX = originPosX; + posY += labelLineHeight + labelLineSpacing; + } + + if (posX == originPosX && + horizontalAlignment == Legend.LegendHorizontalAlignment.CENTER && + lineIndex < calculatedLineSizes.size()) { + posX += (direction == Legend.LegendDirection.RIGHT_TO_LEFT + ? calculatedLineSizes.get(lineIndex).width + : -calculatedLineSizes.get(lineIndex).width) / 2.f; + lineIndex++; + } + + boolean isStacked = e.label == null; // grouped forms have null labels + + if (drawingForm) { + if (direction == Legend.LegendDirection.RIGHT_TO_LEFT) + posX -= formSize; + + drawForm(c, posX, posY + formYOffset, e, mLegend); + + if (direction == Legend.LegendDirection.LEFT_TO_RIGHT) + posX += formSize; + } + + if (!isStacked) { + if (drawingForm) + posX += direction == Legend.LegendDirection.RIGHT_TO_LEFT ? -formToTextSpace : + formToTextSpace; + + if (direction == Legend.LegendDirection.RIGHT_TO_LEFT) + posX -= calculatedLabelSizes.get(i).width; + + drawLabel(c, posX, posY + labelLineHeight, e.label); + + if (direction == Legend.LegendDirection.LEFT_TO_RIGHT) + posX += calculatedLabelSizes.get(i).width; + + posX += direction == Legend.LegendDirection.RIGHT_TO_LEFT ? -xEntrySpace : xEntrySpace; + } else + posX += direction == Legend.LegendDirection.RIGHT_TO_LEFT ? -stackSpace : stackSpace; + } + + break; + } + + case VERTICAL: { + // contains the stacked legend size in pixels + float stack = 0f; + boolean wasStacked = false; + float posY = 0.f; + + switch (verticalAlignment) { + case TOP: + posY = (horizontalAlignment == Legend.LegendHorizontalAlignment.CENTER + ? 0.f + : mViewPortHandler.contentTop()); + posY += yoffset; + break; + + case BOTTOM: + posY = (horizontalAlignment == Legend.LegendHorizontalAlignment.CENTER + ? mViewPortHandler.getChartHeight() + : mViewPortHandler.contentBottom()); + posY -= mLegend.mNeededHeight + yoffset; + break; + + case CENTER: + posY = mViewPortHandler.getChartHeight() / 2.f + - mLegend.mNeededHeight / 2.f + + mLegend.getYOffset(); + break; + } + + for (int i = 0; i < entries.length; i++) { + + LegendEntry e = entries[i]; + boolean drawingForm = e.form != Legend.LegendForm.NONE; + float formSize = Float.isNaN(e.formSize) ? defaultFormSize : Utils.convertDpToPixel(e.formSize); + + float posX = originPosX; + + if (drawingForm) { + if (direction == Legend.LegendDirection.LEFT_TO_RIGHT) + posX += stack; + else + posX -= formSize - stack; + + drawForm(c, posX, posY + formYOffset, e, mLegend); + + if (direction == Legend.LegendDirection.LEFT_TO_RIGHT) + posX += formSize; + } + + if (e.label != null) { + + if (drawingForm && !wasStacked) + posX += direction == Legend.LegendDirection.LEFT_TO_RIGHT ? formToTextSpace + : -formToTextSpace; + else if (wasStacked) + posX = originPosX; + + if (direction == Legend.LegendDirection.RIGHT_TO_LEFT) + posX -= Utils.calcTextWidth(mLegendLabelPaint, e.label); + + if (!wasStacked) { + drawLabel(c, posX, posY + labelLineHeight, e.label); + } else { + posY += labelLineHeight + labelLineSpacing; + drawLabel(c, posX, posY + labelLineHeight, e.label); + } + + // make a step down + posY += labelLineHeight + labelLineSpacing; + stack = 0f; + } else { + stack += formSize + stackSpace; + wasStacked = true; + } + } + + break; + + } + } + } + + private Path mLineFormPath = new Path(); + + /** + * Draws the Legend-form at the given position with the color at the given + * index. + * + * @param c canvas to draw with + * @param x position + * @param y position + * @param entry the entry to render + * @param legend the legend context + */ + protected void drawForm( + Canvas c, + float x, float y, + LegendEntry entry, + Legend legend) { + + if (entry.formColor == ColorTemplate.COLOR_SKIP || + entry.formColor == ColorTemplate.COLOR_NONE || + entry.formColor == 0) + return; + + int restoreCount = c.save(); + + Legend.LegendForm form = entry.form; + if (form == Legend.LegendForm.DEFAULT) + form = legend.getForm(); + + mLegendFormPaint.setColor(entry.formColor); + + final float formSize = Utils.convertDpToPixel( + Float.isNaN(entry.formSize) + ? legend.getFormSize() + : entry.formSize); + final float half = formSize / 2f; + + switch (form) { + case NONE: + // Do nothing + break; + + case EMPTY: + // Do not draw, but keep space for the form + break; + + case DEFAULT: + case CIRCLE: + mLegendFormPaint.setStyle(Paint.Style.FILL); + c.drawCircle(x + half, y, half, mLegendFormPaint); + break; + + case SQUARE: + mLegendFormPaint.setStyle(Paint.Style.FILL); + c.drawRect(x, y - half, x + formSize, y + half, mLegendFormPaint); + break; + + case LINE: + { + final float formLineWidth = Utils.convertDpToPixel( + Float.isNaN(entry.formLineWidth) + ? legend.getFormLineWidth() + : entry.formLineWidth); + final DashPathEffect formLineDashEffect = entry.formLineDashEffect == null + ? legend.getFormLineDashEffect() + : entry.formLineDashEffect; + mLegendFormPaint.setStyle(Paint.Style.STROKE); + mLegendFormPaint.setStrokeWidth(formLineWidth); + mLegendFormPaint.setPathEffect(formLineDashEffect); + + mLineFormPath.reset(); + mLineFormPath.moveTo(x, y); + mLineFormPath.lineTo(x + formSize, y); + c.drawPath(mLineFormPath, mLegendFormPaint); + } + break; + } + + c.restoreToCount(restoreCount); + } + + /** + * Draws the provided label at the given position. + * + * @param c canvas to draw with + * @param x + * @param y + * @param label the label to draw + */ + protected void drawLabel(Canvas c, float x, float y, String label) { + c.drawText(label, x, y, mLegendLabelPaint); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineChartRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineChartRenderer.java new file mode 100644 index 0000000..a86c16f --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineChartRenderer.java @@ -0,0 +1,863 @@ +package com.github.mikephil.charting.renderer; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.drawable.Drawable; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.charts.LineChart; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.LineData; +import com.github.mikephil.charting.data.LineDataSet; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.dataprovider.LineDataProvider; +import com.github.mikephil.charting.interfaces.datasets.IDataSet; +import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.MPPointD; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.List; + +public class LineChartRenderer extends LineRadarRenderer { + + protected LineDataProvider mChart; + + /** + * paint for the inner circle of the value indicators + */ + protected Paint mCirclePaintInner; + + /** + * Bitmap object used for drawing the paths (otherwise they are too long if + * rendered directly on the canvas) + */ + protected WeakReference mDrawBitmap; + + /** + * on this canvas, the paths are rendered, it is initialized with the + * pathBitmap + */ + protected Canvas mBitmapCanvas; + + /** + * the bitmap configuration to be used + */ + protected Bitmap.Config mBitmapConfig = Bitmap.Config.ARGB_8888; + + protected Path cubicPath = new Path(); + protected Path cubicFillPath = new Path(); + + public LineChartRenderer(LineDataProvider chart, ChartAnimator animator, + ViewPortHandler viewPortHandler) { + super(animator, viewPortHandler); + mChart = chart; + + mCirclePaintInner = new Paint(Paint.ANTI_ALIAS_FLAG); + mCirclePaintInner.setStyle(Paint.Style.FILL); + mCirclePaintInner.setColor(Color.WHITE); + } + + @Override + public void initBuffers() { + } + + @Override + public void drawData(Canvas c) { + + int width = (int) mViewPortHandler.getChartWidth(); + int height = (int) mViewPortHandler.getChartHeight(); + + Bitmap drawBitmap = mDrawBitmap == null ? null : mDrawBitmap.get(); + + if (drawBitmap == null + || (drawBitmap.getWidth() != width) + || (drawBitmap.getHeight() != height)) { + + if (width > 0 && height > 0) { + drawBitmap = Bitmap.createBitmap(width, height, mBitmapConfig); + mDrawBitmap = new WeakReference<>(drawBitmap); + mBitmapCanvas = new Canvas(drawBitmap); + } else + return; + } + + drawBitmap.eraseColor(Color.TRANSPARENT); + + LineData lineData = mChart.getLineData(); + + for (ILineDataSet set : lineData.getDataSets()) { + + if (set.isVisible()) + drawDataSet(c, set); + } + + c.drawBitmap(drawBitmap, 0, 0, mRenderPaint); + } + + protected void drawDataSet(Canvas c, ILineDataSet dataSet) { + + if (dataSet.getEntryCount() < 1) + return; + + mRenderPaint.setStrokeWidth(dataSet.getLineWidth()); + mRenderPaint.setPathEffect(dataSet.getDashPathEffect()); + + switch (dataSet.getMode()) { + default: + case LINEAR: + case STEPPED: + drawLinear(c, dataSet); + break; + + case CUBIC_BEZIER: + drawCubicBezier(dataSet); + break; + + case HORIZONTAL_BEZIER: + drawHorizontalBezier(dataSet); + break; + } + + mRenderPaint.setPathEffect(null); + } + + protected void drawHorizontalBezier(ILineDataSet dataSet) { + + float phaseY = mAnimator.getPhaseY(); + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + mXBounds.set(mChart, dataSet); + + cubicPath.reset(); + + if (mXBounds.range >= 1) { + + Entry prev = dataSet.getEntryForIndex(mXBounds.min); + Entry cur = prev; + + // let the spline start + cubicPath.moveTo(cur.getX(), cur.getY() * phaseY); + + for (int j = mXBounds.min + 1; j <= mXBounds.range + mXBounds.min; j++) { + + prev = cur; + cur = dataSet.getEntryForIndex(j); + + final float cpx = (prev.getX()) + + (cur.getX() - prev.getX()) / 2.0f; + + cubicPath.cubicTo( + cpx, prev.getY() * phaseY, + cpx, cur.getY() * phaseY, + cur.getX(), cur.getY() * phaseY); + } + } + + // if filled is enabled, close the path + if (dataSet.isDrawFilledEnabled()) { + + cubicFillPath.reset(); + cubicFillPath.addPath(cubicPath); + // create a new path, this is bad for performance + drawCubicFill(mBitmapCanvas, dataSet, cubicFillPath, trans, mXBounds); + } + + mRenderPaint.setColor(dataSet.getColor()); + + mRenderPaint.setStyle(Paint.Style.STROKE); + + trans.pathValueToPixel(cubicPath); + + mBitmapCanvas.drawPath(cubicPath, mRenderPaint); + + mRenderPaint.setPathEffect(null); + } + + protected void drawCubicBezier(ILineDataSet dataSet) { + + float phaseY = mAnimator.getPhaseY(); + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + mXBounds.set(mChart, dataSet); + + float intensity = dataSet.getCubicIntensity(); + + cubicPath.reset(); + + if (mXBounds.range >= 1) { + + float prevDx = 0f; + float prevDy = 0f; + float curDx = 0f; + float curDy = 0f; + + // Take an extra point from the left, and an extra from the right. + // That's because we need 4 points for a cubic bezier (cubic=4), otherwise we get lines moving and doing weird stuff on the edges of the chart. + // So in the starting `prev` and `cur`, go -2, -1 + // And in the `lastIndex`, add +1 + + final int firstIndex = mXBounds.min + 1; + final int lastIndex = mXBounds.min + mXBounds.range; + + Entry prevPrev; + Entry prev = dataSet.getEntryForIndex(Math.max(firstIndex - 2, 0)); + Entry cur = dataSet.getEntryForIndex(Math.max(firstIndex - 1, 0)); + Entry next = cur; + int nextIndex = -1; + + if (cur == null) return; + + // let the spline start + cubicPath.moveTo(cur.getX(), cur.getY() * phaseY); + + for (int j = mXBounds.min + 1; j <= mXBounds.range + mXBounds.min; j++) { + + prevPrev = prev; + prev = cur; + cur = nextIndex == j ? next : dataSet.getEntryForIndex(j); + + nextIndex = j + 1 < dataSet.getEntryCount() ? j + 1 : j; + next = dataSet.getEntryForIndex(nextIndex); + + prevDx = (cur.getX() - prevPrev.getX()) * intensity; + prevDy = (cur.getY() - prevPrev.getY()) * intensity; + curDx = (next.getX() - prev.getX()) * intensity; + curDy = (next.getY() - prev.getY()) * intensity; + + cubicPath.cubicTo(prev.getX() + prevDx, (prev.getY() + prevDy) * phaseY, + cur.getX() - curDx, + (cur.getY() - curDy) * phaseY, cur.getX(), cur.getY() * phaseY); + } + } + + // if filled is enabled, close the path + if (dataSet.isDrawFilledEnabled()) { + + cubicFillPath.reset(); + cubicFillPath.addPath(cubicPath); + + drawCubicFill(mBitmapCanvas, dataSet, cubicFillPath, trans, mXBounds); + } + + mRenderPaint.setColor(dataSet.getColor()); + + mRenderPaint.setStyle(Paint.Style.STROKE); + + trans.pathValueToPixel(cubicPath); + + mBitmapCanvas.drawPath(cubicPath, mRenderPaint); + + mRenderPaint.setPathEffect(null); + } + + protected void drawCubicFill(Canvas c, ILineDataSet dataSet, Path spline, Transformer trans, XBounds bounds) { + + float fillMin = dataSet.getFillFormatter() + .getFillLinePosition(dataSet, mChart); + + spline.lineTo(dataSet.getEntryForIndex(bounds.min + bounds.range).getX(), fillMin); + spline.lineTo(dataSet.getEntryForIndex(bounds.min).getX(), fillMin); + spline.close(); + + trans.pathValueToPixel(spline); + + final Drawable drawable = dataSet.getFillDrawable(); + if (drawable != null) { + + drawFilledPath(c, spline, drawable); + } else { + + drawFilledPath(c, spline, dataSet.getFillColor(), dataSet.getFillAlpha()); + } + } + + private float[] mLineBuffer = new float[4]; + + /** + * Draws a normal line. + * + * @param c + * @param dataSet + */ + protected void drawLinear(Canvas c, ILineDataSet dataSet) { + + int entryCount = dataSet.getEntryCount(); + + final boolean isDrawSteppedEnabled = dataSet.isDrawSteppedEnabled(); + final int pointsPerEntryPair = isDrawSteppedEnabled ? 4 : 2; + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + float phaseY = mAnimator.getPhaseY(); + + mRenderPaint.setStyle(Paint.Style.STROKE); + + Canvas canvas = null; + + // if the data-set is dashed, draw on bitmap-canvas + if (dataSet.isDashedLineEnabled()) { + canvas = mBitmapCanvas; + } else { + canvas = c; + } + + mXBounds.set(mChart, dataSet); + + // if drawing filled is enabled + if (dataSet.isDrawFilledEnabled() && entryCount > 0) { + drawLinearFill(c, dataSet, trans, mXBounds); + } + + // more than 1 color + if (dataSet.getColors().size() > 1) { + + int numberOfFloats = pointsPerEntryPair * 2; + + if (mLineBuffer.length <= numberOfFloats) + mLineBuffer = new float[numberOfFloats * 2]; + + int max = mXBounds.min + mXBounds.range; + + for (int j = mXBounds.min; j < max; j++) { + + Entry e = dataSet.getEntryForIndex(j); + if (e == null) continue; + + mLineBuffer[0] = e.getX(); + mLineBuffer[1] = e.getY() * phaseY; + + if (j < mXBounds.max) { + + e = dataSet.getEntryForIndex(j + 1); + + if (e == null) break; + + if (isDrawSteppedEnabled) { + mLineBuffer[2] = e.getX(); + mLineBuffer[3] = mLineBuffer[1]; + mLineBuffer[4] = mLineBuffer[2]; + mLineBuffer[5] = mLineBuffer[3]; + mLineBuffer[6] = e.getX(); + mLineBuffer[7] = e.getY() * phaseY; + } else { + mLineBuffer[2] = e.getX(); + mLineBuffer[3] = e.getY() * phaseY; + } + + } else { + mLineBuffer[2] = mLineBuffer[0]; + mLineBuffer[3] = mLineBuffer[1]; + } + + // Determine the start and end coordinates of the line, and make sure they differ. + float firstCoordinateX = mLineBuffer[0]; + float firstCoordinateY = mLineBuffer[1]; + float lastCoordinateX = mLineBuffer[numberOfFloats - 2]; + float lastCoordinateY = mLineBuffer[numberOfFloats - 1]; + + if (firstCoordinateX == lastCoordinateX && + firstCoordinateY == lastCoordinateY) + continue; + + trans.pointValuesToPixel(mLineBuffer); + + if (!mViewPortHandler.isInBoundsRight(firstCoordinateX)) + break; + + // make sure the lines don't do shitty things outside + // bounds + if (!mViewPortHandler.isInBoundsLeft(lastCoordinateX) || + !mViewPortHandler.isInBoundsTop(Math.max(firstCoordinateY, lastCoordinateY)) || + !mViewPortHandler.isInBoundsBottom(Math.min(firstCoordinateY, lastCoordinateY))) + continue; + + // get the color that is set for this line-segment + mRenderPaint.setColor(dataSet.getColor(j)); + + canvas.drawLines(mLineBuffer, 0, pointsPerEntryPair * 2, mRenderPaint); + } + + } else { // only one color per dataset + + if (mLineBuffer.length < Math.max((entryCount) * pointsPerEntryPair, pointsPerEntryPair) * 2) + mLineBuffer = new float[Math.max((entryCount) * pointsPerEntryPair, pointsPerEntryPair) * 4]; + + Entry e1, e2; + + e1 = dataSet.getEntryForIndex(mXBounds.min); + + if (e1 != null) { + + int j = 0; + for (int x = mXBounds.min; x <= mXBounds.range + mXBounds.min; x++) { + + e1 = dataSet.getEntryForIndex(x == 0 ? 0 : (x - 1)); + e2 = dataSet.getEntryForIndex(x); + + if (e1 == null || e2 == null) continue; + + mLineBuffer[j++] = e1.getX(); + mLineBuffer[j++] = e1.getY() * phaseY; + + if (isDrawSteppedEnabled) { + mLineBuffer[j++] = e2.getX(); + mLineBuffer[j++] = e1.getY() * phaseY; + mLineBuffer[j++] = e2.getX(); + mLineBuffer[j++] = e1.getY() * phaseY; + } + + mLineBuffer[j++] = e2.getX(); + mLineBuffer[j++] = e2.getY() * phaseY; + } + + if (j > 0) { + trans.pointValuesToPixel(mLineBuffer); + + final int size = Math.max((mXBounds.range + 1) * pointsPerEntryPair, pointsPerEntryPair) * 2; + + mRenderPaint.setColor(dataSet.getColor()); + + canvas.drawLines(mLineBuffer, 0, size, mRenderPaint); + } + } + } + + mRenderPaint.setPathEffect(null); + } + + protected Path mGenerateFilledPathBuffer = new Path(); + + /** + * Draws a filled linear path on the canvas. + * + * @param c + * @param dataSet + * @param trans + * @param bounds + */ + protected void drawLinearFill(Canvas c, ILineDataSet dataSet, Transformer trans, XBounds bounds) { + + final Path filled = mGenerateFilledPathBuffer; + + final int startingIndex = bounds.min; + final int endingIndex = bounds.range + bounds.min; + final int indexInterval = 128; + + int currentStartIndex = 0; + int currentEndIndex = indexInterval; + int iterations = 0; + + // Doing this iteratively in order to avoid OutOfMemory errors that can happen on large bounds sets. + do { + currentStartIndex = startingIndex + (iterations * indexInterval); + currentEndIndex = currentStartIndex + indexInterval; + currentEndIndex = currentEndIndex > endingIndex ? endingIndex : currentEndIndex; + + if (currentStartIndex <= currentEndIndex) { + generateFilledPath(dataSet, currentStartIndex, currentEndIndex, filled); + + trans.pathValueToPixel(filled); + + final Drawable drawable = dataSet.getFillDrawable(); + if (drawable != null) { + + drawFilledPath(c, filled, drawable); + } else { + + drawFilledPath(c, filled, dataSet.getFillColor(), dataSet.getFillAlpha()); + } + } + + iterations++; + + } while (currentStartIndex <= currentEndIndex); + + } + + /** + * Generates a path that is used for filled drawing. + * + * @param dataSet The dataset from which to read the entries. + * @param startIndex The index from which to start reading the dataset + * @param endIndex The index from which to stop reading the dataset + * @param outputPath The path object that will be assigned the chart data. + * @return + */ + private void generateFilledPath(final ILineDataSet dataSet, final int startIndex, final int endIndex, final Path outputPath) { + + final float fillMin = dataSet.getFillFormatter().getFillLinePosition(dataSet, mChart); + final float phaseY = mAnimator.getPhaseY(); + final boolean isDrawSteppedEnabled = dataSet.getMode() == LineDataSet.Mode.STEPPED; + + final Path filled = outputPath; + filled.reset(); + + final Entry entry = dataSet.getEntryForIndex(startIndex); + + filled.moveTo(entry.getX(), fillMin); + filled.lineTo(entry.getX(), entry.getY() * phaseY); + + // create a new path + Entry currentEntry = null; + Entry previousEntry = entry; + for (int x = startIndex + 1; x <= endIndex; x++) { + + currentEntry = dataSet.getEntryForIndex(x); + + if (isDrawSteppedEnabled) { + filled.lineTo(currentEntry.getX(), previousEntry.getY() * phaseY); + } + + filled.lineTo(currentEntry.getX(), currentEntry.getY() * phaseY); + + previousEntry = currentEntry; + } + + // close up + if (currentEntry != null) { + filled.lineTo(currentEntry.getX(), fillMin); + } + + filled.close(); + } + + @Override + public void drawValues(Canvas c) { + + if (isDrawingValuesAllowed(mChart)) { + + List dataSets = mChart.getLineData().getDataSets(); + + for (int i = 0; i < dataSets.size(); i++) { + + ILineDataSet dataSet = dataSets.get(i); + + if (!shouldDrawValues(dataSet) || dataSet.getEntryCount() < 1) + continue; + + // apply the text-styling defined by the DataSet + applyValueTextStyle(dataSet); + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + // make sure the values do not interfear with the circles + int valOffset = (int) (dataSet.getCircleRadius() * 1.75f); + + if (!dataSet.isDrawCirclesEnabled()) + valOffset = valOffset / 2; + + mXBounds.set(mChart, dataSet); + + float[] positions = trans.generateTransformedValuesLine(dataSet, mAnimator.getPhaseX(), mAnimator + .getPhaseY(), mXBounds.min, mXBounds.max); + + MPPointF iconsOffset = MPPointF.getInstance(dataSet.getIconsOffset()); + iconsOffset.x = Utils.convertDpToPixel(iconsOffset.x); + iconsOffset.y = Utils.convertDpToPixel(iconsOffset.y); + + for (int j = 0; j < positions.length; j += 2) { + + float x = positions[j]; + float y = positions[j + 1]; + + if (!mViewPortHandler.isInBoundsRight(x)) + break; + + if (!mViewPortHandler.isInBoundsLeft(x) || !mViewPortHandler.isInBoundsY(y)) + continue; + + Entry entry = dataSet.getEntryForIndex(j / 2 + mXBounds.min); + + if (dataSet.isDrawValuesEnabled()) { + drawValue(c, dataSet.getValueFormatter(), entry.getY(), entry, i, x, + y - valOffset, dataSet.getValueTextColor(j / 2)); + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + + Drawable icon = entry.getIcon(); + + Utils.drawImage( + c, + icon, + (int)(x + iconsOffset.x), + (int)(y + iconsOffset.y), + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight()); + } + } + + MPPointF.recycleInstance(iconsOffset); + } + } + } + + @Override + public void drawExtras(Canvas c) { + drawCircles(c); + } + + /** + * cache for the circle bitmaps of all datasets + */ + private HashMap mImageCaches = new HashMap<>(); + + /** + * buffer for drawing the circles + */ + private float[] mCirclesBuffer = new float[2]; + + protected void drawCircles(Canvas c) { + + mRenderPaint.setStyle(Paint.Style.FILL); + + float phaseY = mAnimator.getPhaseY(); + + mCirclesBuffer[0] = 0; + mCirclesBuffer[1] = 0; + + List dataSets = mChart.getLineData().getDataSets(); + + for (int i = 0; i < dataSets.size(); i++) { + + ILineDataSet dataSet = dataSets.get(i); + + if (!dataSet.isVisible() || !dataSet.isDrawCirclesEnabled() || + dataSet.getEntryCount() == 0) + continue; + + mCirclePaintInner.setColor(dataSet.getCircleHoleColor()); + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + mXBounds.set(mChart, dataSet); + + float circleRadius = dataSet.getCircleRadius(); + float circleHoleRadius = dataSet.getCircleHoleRadius(); + boolean drawCircleHole = dataSet.isDrawCircleHoleEnabled() && + circleHoleRadius < circleRadius && + circleHoleRadius > 0.f; + boolean drawTransparentCircleHole = drawCircleHole && + dataSet.getCircleHoleColor() == ColorTemplate.COLOR_NONE; + + DataSetImageCache imageCache; + + if (mImageCaches.containsKey(dataSet)) { + imageCache = mImageCaches.get(dataSet); + } else { + imageCache = new DataSetImageCache(); + mImageCaches.put(dataSet, imageCache); + } + + boolean changeRequired = imageCache.init(dataSet); + + // only fill the cache with new bitmaps if a change is required + if (changeRequired) { + imageCache.fill(dataSet, drawCircleHole, drawTransparentCircleHole); + } + + int boundsRangeCount = mXBounds.range + mXBounds.min; + + for (int j = mXBounds.min; j <= boundsRangeCount; j++) { + + Entry e = dataSet.getEntryForIndex(j); + + if (e == null) break; + + mCirclesBuffer[0] = e.getX(); + mCirclesBuffer[1] = e.getY() * phaseY; + + trans.pointValuesToPixel(mCirclesBuffer); + + if (!mViewPortHandler.isInBoundsRight(mCirclesBuffer[0])) + break; + + if (!mViewPortHandler.isInBoundsLeft(mCirclesBuffer[0]) || + !mViewPortHandler.isInBoundsY(mCirclesBuffer[1])) + continue; + + Bitmap circleBitmap = imageCache.getBitmap(j); + + if (circleBitmap != null) { + c.drawBitmap(circleBitmap, mCirclesBuffer[0] - circleRadius, mCirclesBuffer[1] - circleRadius, null); + } + } + } + } + + @Override + public void drawHighlighted(Canvas c, Highlight[] indices) { + + LineData lineData = mChart.getLineData(); + + for (Highlight high : indices) { + + ILineDataSet set = lineData.getDataSetByIndex(high.getDataSetIndex()); + + if (set == null || !set.isHighlightEnabled()) + continue; + + Entry e = set.getEntryForXValue(high.getX(), high.getY()); + + if (!isInBoundsX(e, set)) + continue; + + MPPointD pix = mChart.getTransformer(set.getAxisDependency()).getPixelForValues(e.getX(), e.getY() * mAnimator + .getPhaseY()); + + high.setDraw((float) pix.x, (float) pix.y); + + // draw the lines + drawHighlightLines(c, (float) pix.x, (float) pix.y, set); + } + } + + /** + * Sets the Bitmap.Config to be used by this renderer. + * Default: Bitmap.Config.ARGB_8888 + * Use Bitmap.Config.ARGB_4444 to consume less memory. + * + * @param config + */ + public void setBitmapConfig(Bitmap.Config config) { + mBitmapConfig = config; + releaseBitmap(); + } + + /** + * Returns the Bitmap.Config that is used by this renderer. + * + * @return + */ + public Bitmap.Config getBitmapConfig() { + return mBitmapConfig; + } + + /** + * Releases the drawing bitmap. This should be called when {@link LineChart#onDetachedFromWindow()}. + */ + public void releaseBitmap() { + if (mBitmapCanvas != null) { + mBitmapCanvas.setBitmap(null); + mBitmapCanvas = null; + } + if (mDrawBitmap != null) { + Bitmap drawBitmap = mDrawBitmap.get(); + if (drawBitmap != null) { + drawBitmap.recycle(); + } + mDrawBitmap.clear(); + mDrawBitmap = null; + } + } + + private class DataSetImageCache { + + private Path mCirclePathBuffer = new Path(); + + private Bitmap[] circleBitmaps; + + /** + * Sets up the cache, returns true if a change of cache was required. + * + * @param set + * @return + */ + protected boolean init(ILineDataSet set) { + + int size = set.getCircleColorCount(); + boolean changeRequired = false; + + if (circleBitmaps == null) { + circleBitmaps = new Bitmap[size]; + changeRequired = true; + } else if (circleBitmaps.length != size) { + circleBitmaps = new Bitmap[size]; + changeRequired = true; + } + + return changeRequired; + } + + /** + * Fills the cache with bitmaps for the given dataset. + * + * @param set + * @param drawCircleHole + * @param drawTransparentCircleHole + */ + protected void fill(ILineDataSet set, boolean drawCircleHole, boolean drawTransparentCircleHole) { + + int colorCount = set.getCircleColorCount(); + float circleRadius = set.getCircleRadius(); + float circleHoleRadius = set.getCircleHoleRadius(); + + for (int i = 0; i < colorCount; i++) { + + Bitmap.Config conf = Bitmap.Config.ARGB_4444; + Bitmap circleBitmap = Bitmap.createBitmap((int) (circleRadius * 2.1), (int) (circleRadius * 2.1), conf); + + Canvas canvas = new Canvas(circleBitmap); + circleBitmaps[i] = circleBitmap; + mRenderPaint.setColor(set.getCircleColor(i)); + + if (drawTransparentCircleHole) { + // Begin path for circle with hole + mCirclePathBuffer.reset(); + + mCirclePathBuffer.addCircle( + circleRadius, + circleRadius, + circleRadius, + Path.Direction.CW); + + // Cut hole in path + mCirclePathBuffer.addCircle( + circleRadius, + circleRadius, + circleHoleRadius, + Path.Direction.CCW); + + // Fill in-between + canvas.drawPath(mCirclePathBuffer, mRenderPaint); + } else { + + canvas.drawCircle( + circleRadius, + circleRadius, + circleRadius, + mRenderPaint); + + if (drawCircleHole) { + canvas.drawCircle( + circleRadius, + circleRadius, + circleHoleRadius, + mCirclePaintInner); + } + } + } + } + + /** + * Returns the cached Bitmap at the given index. + * + * @param index + * @return + */ + protected Bitmap getBitmap(int index) { + return circleBitmaps[index % circleBitmaps.length]; + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineRadarRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineRadarRenderer.java new file mode 100644 index 0000000..288eff6 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineRadarRenderer.java @@ -0,0 +1,95 @@ +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.drawable.Drawable; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by Philipp Jahoda on 25/01/16. + */ +public abstract class LineRadarRenderer extends LineScatterCandleRadarRenderer { + + public LineRadarRenderer(ChartAnimator animator, ViewPortHandler viewPortHandler) { + super(animator, viewPortHandler); + } + + /** + * Draws the provided path in filled mode with the provided drawable. + * + * @param c + * @param filledPath + * @param drawable + */ + protected void drawFilledPath(Canvas c, Path filledPath, Drawable drawable) { + + if (clipPathSupported()) { + + int save = c.save(); + c.clipPath(filledPath); + + drawable.setBounds((int) mViewPortHandler.contentLeft(), + (int) mViewPortHandler.contentTop(), + (int) mViewPortHandler.contentRight(), + (int) mViewPortHandler.contentBottom()); + drawable.draw(c); + + c.restoreToCount(save); + } else { + throw new RuntimeException("Fill-drawables not (yet) supported below API level 18, " + + "this code was run on API level " + Utils.getSDKInt() + "."); + } + } + + /** + * Draws the provided path in filled mode with the provided color and alpha. + * Special thanks to Angelo Suzuki (https://github.com/tinsukE) for this. + * + * @param c + * @param filledPath + * @param fillColor + * @param fillAlpha + */ + protected void drawFilledPath(Canvas c, Path filledPath, int fillColor, int fillAlpha) { + + int color = (fillAlpha << 24) | (fillColor & 0xffffff); + + if (clipPathSupported()) { + + int save = c.save(); + + c.clipPath(filledPath); + + c.drawColor(color); + c.restoreToCount(save); + } else { + + // save + Paint.Style previous = mRenderPaint.getStyle(); + int previousColor = mRenderPaint.getColor(); + + // set + mRenderPaint.setStyle(Paint.Style.FILL); + mRenderPaint.setColor(color); + + c.drawPath(filledPath, mRenderPaint); + + // restore + mRenderPaint.setColor(previousColor); + mRenderPaint.setStyle(previous); + } + } + + /** + * Clip path with hardware acceleration only working properly on API level 18 and above. + * + * @return + */ + private boolean clipPathSupported() { + return Utils.getSDKInt() >= 18; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineScatterCandleRadarRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineScatterCandleRadarRenderer.java new file mode 100644 index 0000000..53f54cb --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineScatterCandleRadarRenderer.java @@ -0,0 +1,63 @@ +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Path; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.interfaces.datasets.ILineScatterCandleRadarDataSet; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by Philipp Jahoda on 11/07/15. + */ +public abstract class LineScatterCandleRadarRenderer extends BarLineScatterCandleBubbleRenderer { + + /** + * path that is used for drawing highlight-lines (drawLines(...) cannot be used because of dashes) + */ + private Path mHighlightLinePath = new Path(); + + public LineScatterCandleRadarRenderer(ChartAnimator animator, ViewPortHandler viewPortHandler) { + super(animator, viewPortHandler); + } + + /** + * Draws vertical & horizontal highlight-lines if enabled. + * + * @param c + * @param x x-position of the highlight line intersection + * @param y y-position of the highlight line intersection + * @param set the currently drawn dataset + */ + protected void drawHighlightLines(Canvas c, float x, float y, ILineScatterCandleRadarDataSet set) { + + // set color and stroke-width + mHighlightPaint.setColor(set.getHighLightColor()); + mHighlightPaint.setStrokeWidth(set.getHighlightLineWidth()); + + // draw highlighted lines (if enabled) + mHighlightPaint.setPathEffect(set.getDashPathEffectHighlight()); + + // draw vertical highlight lines + if (set.isVerticalHighlightIndicatorEnabled()) { + + // create vertical path + mHighlightLinePath.reset(); + mHighlightLinePath.moveTo(x, mViewPortHandler.contentTop()); + mHighlightLinePath.lineTo(x, mViewPortHandler.contentBottom()); + + c.drawPath(mHighlightLinePath, mHighlightPaint); + } + + // draw horizontal highlight lines + if (set.isHorizontalHighlightIndicatorEnabled()) { + + // create horizontal path + mHighlightLinePath.reset(); + mHighlightLinePath.moveTo(mViewPortHandler.contentLeft(), y); + mHighlightLinePath.lineTo(mViewPortHandler.contentRight(), y); + + c.drawPath(mHighlightLinePath, mHighlightPaint); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/PieChartRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/PieChartRenderer.java new file mode 100644 index 0000000..f35c775 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/PieChartRenderer.java @@ -0,0 +1,1070 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Paint.Style; +import android.graphics.Path; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.charts.LineChart; +import com.github.mikephil.charting.charts.PieChart; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.PieData; +import com.github.mikephil.charting.data.PieDataSet; +import com.github.mikephil.charting.data.PieEntry; +import com.github.mikephil.charting.formatter.IValueFormatter; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.datasets.IPieDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.lang.ref.WeakReference; +import java.util.List; + +public class PieChartRenderer extends DataRenderer { + + protected PieChart mChart; + + /** + * paint for the hole in the center of the pie chart and the transparent + * circle + */ + protected Paint mHolePaint; + protected Paint mTransparentCirclePaint; + protected Paint mValueLinePaint; + + /** + * paint object for the text that can be displayed in the center of the + * chart + */ + private TextPaint mCenterTextPaint; + + /** + * paint object used for drwing the slice-text + */ + private Paint mEntryLabelsPaint; + + private StaticLayout mCenterTextLayout; + private CharSequence mCenterTextLastValue; + private RectF mCenterTextLastBounds = new RectF(); + private RectF[] mRectBuffer = {new RectF(), new RectF(), new RectF()}; + + /** + * Bitmap for drawing the center hole + */ + protected WeakReference mDrawBitmap; + + protected Canvas mBitmapCanvas; + + public PieChartRenderer(PieChart chart, ChartAnimator animator, + ViewPortHandler viewPortHandler) { + super(animator, viewPortHandler); + mChart = chart; + + mHolePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mHolePaint.setColor(Color.WHITE); + mHolePaint.setStyle(Style.FILL); + + mTransparentCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mTransparentCirclePaint.setColor(Color.WHITE); + mTransparentCirclePaint.setStyle(Style.FILL); + mTransparentCirclePaint.setAlpha(105); + + mCenterTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); + mCenterTextPaint.setColor(Color.BLACK); + mCenterTextPaint.setTextSize(Utils.convertDpToPixel(12f)); + + mValuePaint.setTextSize(Utils.convertDpToPixel(13f)); + mValuePaint.setColor(Color.WHITE); + mValuePaint.setTextAlign(Align.CENTER); + + mEntryLabelsPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mEntryLabelsPaint.setColor(Color.WHITE); + mEntryLabelsPaint.setTextAlign(Align.CENTER); + mEntryLabelsPaint.setTextSize(Utils.convertDpToPixel(13f)); + + mValueLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mValueLinePaint.setStyle(Style.STROKE); + } + + public Paint getPaintHole() { + return mHolePaint; + } + + public Paint getPaintTransparentCircle() { + return mTransparentCirclePaint; + } + + public TextPaint getPaintCenterText() { + return mCenterTextPaint; + } + + public Paint getPaintEntryLabels() { + return mEntryLabelsPaint; + } + + @Override + public void initBuffers() { + // TODO Auto-generated method stub + } + + @Override + public void drawData(Canvas c) { + + int width = (int) mViewPortHandler.getChartWidth(); + int height = (int) mViewPortHandler.getChartHeight(); + + Bitmap drawBitmap = mDrawBitmap == null ? null : mDrawBitmap.get(); + + if (drawBitmap == null + || (drawBitmap.getWidth() != width) + || (drawBitmap.getHeight() != height)) { + + if (width > 0 && height > 0) { + drawBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444); + mDrawBitmap = new WeakReference<>(drawBitmap); + mBitmapCanvas = new Canvas(drawBitmap); + } else + return; + } + + drawBitmap.eraseColor(Color.TRANSPARENT); + + PieData pieData = mChart.getData(); + + for (IPieDataSet set : pieData.getDataSets()) { + + if (set.isVisible() && set.getEntryCount() > 0) + drawDataSet(c, set); + } + } + + private Path mPathBuffer = new Path(); + private RectF mInnerRectBuffer = new RectF(); + + protected float calculateMinimumRadiusForSpacedSlice( + MPPointF center, + float radius, + float angle, + float arcStartPointX, + float arcStartPointY, + float startAngle, + float sweepAngle) { + final float angleMiddle = startAngle + sweepAngle / 2.f; + + // Other point of the arc + float arcEndPointX = center.x + radius * (float) Math.cos((startAngle + sweepAngle) * Utils.FDEG2RAD); + float arcEndPointY = center.y + radius * (float) Math.sin((startAngle + sweepAngle) * Utils.FDEG2RAD); + + // Middle point on the arc + float arcMidPointX = center.x + radius * (float) Math.cos(angleMiddle * Utils.FDEG2RAD); + float arcMidPointY = center.y + radius * (float) Math.sin(angleMiddle * Utils.FDEG2RAD); + + // This is the base of the contained triangle + double basePointsDistance = Math.sqrt( + Math.pow(arcEndPointX - arcStartPointX, 2) + + Math.pow(arcEndPointY - arcStartPointY, 2)); + + // After reducing space from both sides of the "slice", + // the angle of the contained triangle should stay the same. + // So let's find out the height of that triangle. + float containedTriangleHeight = (float) (basePointsDistance / 2.0 * + Math.tan((180.0 - angle) / 2.0 * Utils.DEG2RAD)); + + // Now we subtract that from the radius + float spacedRadius = radius - containedTriangleHeight; + + // And now subtract the height of the arc that's between the triangle and the outer circle + spacedRadius -= Math.sqrt( + Math.pow(arcMidPointX - (arcEndPointX + arcStartPointX) / 2.f, 2) + + Math.pow(arcMidPointY - (arcEndPointY + arcStartPointY) / 2.f, 2)); + + return spacedRadius; + } + + /** + * Calculates the sliceSpace to use based on visible values and their size compared to the set sliceSpace. + * + * @param dataSet + * @return + */ + protected float getSliceSpace(IPieDataSet dataSet) { + + if (!dataSet.isAutomaticallyDisableSliceSpacingEnabled()) + return dataSet.getSliceSpace(); + + float spaceSizeRatio = dataSet.getSliceSpace() / mViewPortHandler.getSmallestContentExtension(); + float minValueRatio = dataSet.getYMin() / mChart.getData().getYValueSum() * 2; + + float sliceSpace = spaceSizeRatio > minValueRatio ? 0f : dataSet.getSliceSpace(); + + return sliceSpace; + } + + protected void drawDataSet(Canvas c, IPieDataSet dataSet) { + + float angle = 0; + float rotationAngle = mChart.getRotationAngle(); + + float phaseX = mAnimator.getPhaseX(); + float phaseY = mAnimator.getPhaseY(); + + final RectF circleBox = mChart.getCircleBox(); + + final int entryCount = dataSet.getEntryCount(); + final float[] drawAngles = mChart.getDrawAngles(); + final MPPointF center = mChart.getCenterCircleBox(); + final float radius = mChart.getRadius(); + final boolean drawInnerArc = mChart.isDrawHoleEnabled() && !mChart.isDrawSlicesUnderHoleEnabled(); + final float userInnerRadius = drawInnerArc + ? radius * (mChart.getHoleRadius() / 100.f) + : 0.f; + final float roundedRadius = (radius - (radius * mChart.getHoleRadius() / 100f)) / 2f; + final RectF roundedCircleBox = new RectF(); + final boolean drawRoundedSlices = drawInnerArc && mChart.isDrawRoundedSlicesEnabled(); + + int visibleAngleCount = 0; + for (int j = 0; j < entryCount; j++) { + // draw only if the value is greater than zero + if ((Math.abs(dataSet.getEntryForIndex(j).getY()) > Utils.FLOAT_EPSILON)) { + visibleAngleCount++; + } + } + + final float sliceSpace = visibleAngleCount <= 1 ? 0.f : getSliceSpace(dataSet); + + for (int j = 0; j < entryCount; j++) { + + float sliceAngle = drawAngles[j]; + float innerRadius = userInnerRadius; + + Entry e = dataSet.getEntryForIndex(j); + + // draw only if the value is greater than zero + if (!(Math.abs(e.getY()) > Utils.FLOAT_EPSILON)) { + angle += sliceAngle * phaseX; + continue; + } + + // Don't draw if it's highlighted, unless the chart uses rounded slices + if (dataSet.isHighlightEnabled() && mChart.needsHighlight(j) && !drawRoundedSlices) { + angle += sliceAngle * phaseX; + continue; + } + + final boolean accountForSliceSpacing = sliceSpace > 0.f && sliceAngle <= 180.f; + + mRenderPaint.setColor(dataSet.getColor(j)); + + final float sliceSpaceAngleOuter = visibleAngleCount == 1 ? + 0.f : + sliceSpace / (Utils.FDEG2RAD * radius); + final float startAngleOuter = rotationAngle + (angle + sliceSpaceAngleOuter / 2.f) * phaseY; + float sweepAngleOuter = (sliceAngle - sliceSpaceAngleOuter) * phaseY; + if (sweepAngleOuter < 0.f) { + sweepAngleOuter = 0.f; + } + + mPathBuffer.reset(); + + if (drawRoundedSlices) { + float x = center.x + (radius - roundedRadius) * (float) Math.cos(startAngleOuter * Utils.FDEG2RAD); + float y = center.y + (radius - roundedRadius) * (float) Math.sin(startAngleOuter * Utils.FDEG2RAD); + roundedCircleBox.set(x - roundedRadius, y - roundedRadius, x + roundedRadius, y + roundedRadius); + } + + float arcStartPointX = center.x + radius * (float) Math.cos(startAngleOuter * Utils.FDEG2RAD); + float arcStartPointY = center.y + radius * (float) Math.sin(startAngleOuter * Utils.FDEG2RAD); + + if (sweepAngleOuter >= 360.f && sweepAngleOuter % 360f <= Utils.FLOAT_EPSILON) { + // Android is doing "mod 360" + mPathBuffer.addCircle(center.x, center.y, radius, Path.Direction.CW); + } else { + + if (drawRoundedSlices) { + mPathBuffer.arcTo(roundedCircleBox, startAngleOuter + 180, -180); + } + + mPathBuffer.arcTo( + circleBox, + startAngleOuter, + sweepAngleOuter + ); + } + + // API < 21 does not receive floats in addArc, but a RectF + mInnerRectBuffer.set( + center.x - innerRadius, + center.y - innerRadius, + center.x + innerRadius, + center.y + innerRadius); + + if (drawInnerArc && (innerRadius > 0.f || accountForSliceSpacing)) { + + if (accountForSliceSpacing) { + float minSpacedRadius = + calculateMinimumRadiusForSpacedSlice( + center, radius, + sliceAngle * phaseY, + arcStartPointX, arcStartPointY, + startAngleOuter, + sweepAngleOuter); + + if (minSpacedRadius < 0.f) + minSpacedRadius = -minSpacedRadius; + + innerRadius = Math.max(innerRadius, minSpacedRadius); + } + + final float sliceSpaceAngleInner = visibleAngleCount == 1 || innerRadius == 0.f ? + 0.f : + sliceSpace / (Utils.FDEG2RAD * innerRadius); + final float startAngleInner = rotationAngle + (angle + sliceSpaceAngleInner / 2.f) * phaseY; + float sweepAngleInner = (sliceAngle - sliceSpaceAngleInner) * phaseY; + if (sweepAngleInner < 0.f) { + sweepAngleInner = 0.f; + } + final float endAngleInner = startAngleInner + sweepAngleInner; + + if (sweepAngleOuter >= 360.f && sweepAngleOuter % 360f <= Utils.FLOAT_EPSILON) { + // Android is doing "mod 360" + mPathBuffer.addCircle(center.x, center.y, innerRadius, Path.Direction.CCW); + } else { + + if (drawRoundedSlices) { + float x = center.x + (radius - roundedRadius) * (float) Math.cos(endAngleInner * Utils.FDEG2RAD); + float y = center.y + (radius - roundedRadius) * (float) Math.sin(endAngleInner * Utils.FDEG2RAD); + roundedCircleBox.set(x - roundedRadius, y - roundedRadius, x + roundedRadius, y + roundedRadius); + mPathBuffer.arcTo(roundedCircleBox, endAngleInner, 180); + } else + mPathBuffer.lineTo( + center.x + innerRadius * (float) Math.cos(endAngleInner * Utils.FDEG2RAD), + center.y + innerRadius * (float) Math.sin(endAngleInner * Utils.FDEG2RAD)); + + mPathBuffer.arcTo( + mInnerRectBuffer, + endAngleInner, + -sweepAngleInner + ); + } + } else { + + if (sweepAngleOuter % 360f > Utils.FLOAT_EPSILON) { + if (accountForSliceSpacing) { + + float angleMiddle = startAngleOuter + sweepAngleOuter / 2.f; + + float sliceSpaceOffset = + calculateMinimumRadiusForSpacedSlice( + center, + radius, + sliceAngle * phaseY, + arcStartPointX, + arcStartPointY, + startAngleOuter, + sweepAngleOuter); + + float arcEndPointX = center.x + + sliceSpaceOffset * (float) Math.cos(angleMiddle * Utils.FDEG2RAD); + float arcEndPointY = center.y + + sliceSpaceOffset * (float) Math.sin(angleMiddle * Utils.FDEG2RAD); + + mPathBuffer.lineTo( + arcEndPointX, + arcEndPointY); + + } else { + mPathBuffer.lineTo( + center.x, + center.y); + } + } + + } + + mPathBuffer.close(); + + mBitmapCanvas.drawPath(mPathBuffer, mRenderPaint); + + angle += sliceAngle * phaseX; + } + + MPPointF.recycleInstance(center); + } + + @Override + public void drawValues(Canvas c) { + + MPPointF center = mChart.getCenterCircleBox(); + + // get whole the radius + float radius = mChart.getRadius(); + float rotationAngle = mChart.getRotationAngle(); + float[] drawAngles = mChart.getDrawAngles(); + float[] absoluteAngles = mChart.getAbsoluteAngles(); + + float phaseX = mAnimator.getPhaseX(); + float phaseY = mAnimator.getPhaseY(); + + final float roundedRadius = (radius - (radius * mChart.getHoleRadius() / 100f)) / 2f; + final float holeRadiusPercent = mChart.getHoleRadius() / 100.f; + float labelRadiusOffset = radius / 10f * 3.6f; + + if (mChart.isDrawHoleEnabled()) { + labelRadiusOffset = (radius - (radius * holeRadiusPercent)) / 2f; + + if (!mChart.isDrawSlicesUnderHoleEnabled() && mChart.isDrawRoundedSlicesEnabled()) { + // Add curved circle slice and spacing to rotation angle, so that it sits nicely inside + rotationAngle += roundedRadius * 360 / (Math.PI * 2 * radius); + } + } + + final float labelRadius = radius - labelRadiusOffset; + + PieData data = mChart.getData(); + List dataSets = data.getDataSets(); + + float yValueSum = data.getYValueSum(); + + boolean drawEntryLabels = mChart.isDrawEntryLabelsEnabled(); + + float angle; + int xIndex = 0; + + c.save(); + + float offset = Utils.convertDpToPixel(5.f); + + for (int i = 0; i < dataSets.size(); i++) { + + IPieDataSet dataSet = dataSets.get(i); + + final boolean drawValues = dataSet.isDrawValuesEnabled(); + + if (!drawValues && !drawEntryLabels) + continue; + + final PieDataSet.ValuePosition xValuePosition = dataSet.getXValuePosition(); + final PieDataSet.ValuePosition yValuePosition = dataSet.getYValuePosition(); + + // apply the text-styling defined by the DataSet + applyValueTextStyle(dataSet); + + float lineHeight = Utils.calcTextHeight(mValuePaint, "Q") + + Utils.convertDpToPixel(4f); + + IValueFormatter formatter = dataSet.getValueFormatter(); + + int entryCount = dataSet.getEntryCount(); + + boolean isUseValueColorForLineEnabled = dataSet.isUseValueColorForLineEnabled(); + int valueLineColor = dataSet.getValueLineColor(); + + mValueLinePaint.setStrokeWidth(Utils.convertDpToPixel(dataSet.getValueLineWidth())); + + final float sliceSpace = getSliceSpace(dataSet); + + MPPointF iconsOffset = MPPointF.getInstance(dataSet.getIconsOffset()); + iconsOffset.x = Utils.convertDpToPixel(iconsOffset.x); + iconsOffset.y = Utils.convertDpToPixel(iconsOffset.y); + + for (int j = 0; j < entryCount; j++) { + + PieEntry entry = dataSet.getEntryForIndex(j); + + if (xIndex == 0) + angle = 0.f; + else + angle = absoluteAngles[xIndex - 1] * phaseX; + + final float sliceAngle = drawAngles[xIndex]; + final float sliceSpaceMiddleAngle = sliceSpace / (Utils.FDEG2RAD * labelRadius); + + // offset needed to center the drawn text in the slice + final float angleOffset = (sliceAngle - sliceSpaceMiddleAngle / 2.f) / 2.f; + + angle = angle + angleOffset; + + final float transformedAngle = rotationAngle + angle * phaseY; + + float value = mChart.isUsePercentValuesEnabled() ? entry.getY() + / yValueSum * 100f : entry.getY(); + String entryLabel = entry.getLabel(); + + final float sliceXBase = (float) Math.cos(transformedAngle * Utils.FDEG2RAD); + final float sliceYBase = (float) Math.sin(transformedAngle * Utils.FDEG2RAD); + + final boolean drawXOutside = drawEntryLabels && + xValuePosition == PieDataSet.ValuePosition.OUTSIDE_SLICE; + final boolean drawYOutside = drawValues && + yValuePosition == PieDataSet.ValuePosition.OUTSIDE_SLICE; + final boolean drawXInside = drawEntryLabels && + xValuePosition == PieDataSet.ValuePosition.INSIDE_SLICE; + final boolean drawYInside = drawValues && + yValuePosition == PieDataSet.ValuePosition.INSIDE_SLICE; + + if (drawXOutside || drawYOutside) { + + final float valueLineLength1 = dataSet.getValueLinePart1Length(); + final float valueLineLength2 = dataSet.getValueLinePart2Length(); + final float valueLinePart1OffsetPercentage = dataSet.getValueLinePart1OffsetPercentage() / 100.f; + + float pt2x, pt2y; + float labelPtx, labelPty; + + float line1Radius; + + if (mChart.isDrawHoleEnabled()) + line1Radius = (radius - (radius * holeRadiusPercent)) + * valueLinePart1OffsetPercentage + + (radius * holeRadiusPercent); + else + line1Radius = radius * valueLinePart1OffsetPercentage; + + final float polyline2Width = dataSet.isValueLineVariableLength() + ? labelRadius * valueLineLength2 * (float) Math.abs(Math.sin( + transformedAngle * Utils.FDEG2RAD)) + : labelRadius * valueLineLength2; + + final float pt0x = line1Radius * sliceXBase + center.x; + final float pt0y = line1Radius * sliceYBase + center.y; + + final float pt1x = labelRadius * (1 + valueLineLength1) * sliceXBase + center.x; + final float pt1y = labelRadius * (1 + valueLineLength1) * sliceYBase + center.y; + + if (transformedAngle % 360.0 >= 90.0 && transformedAngle % 360.0 <= 270.0) { + pt2x = pt1x - polyline2Width; + pt2y = pt1y; + + mValuePaint.setTextAlign(Align.RIGHT); + + if(drawXOutside) + mEntryLabelsPaint.setTextAlign(Align.RIGHT); + + labelPtx = pt2x - offset; + labelPty = pt2y; + } else { + pt2x = pt1x + polyline2Width; + pt2y = pt1y; + mValuePaint.setTextAlign(Align.LEFT); + + if(drawXOutside) + mEntryLabelsPaint.setTextAlign(Align.LEFT); + + labelPtx = pt2x + offset; + labelPty = pt2y; + } + + int lineColor = ColorTemplate.COLOR_NONE; + + if (isUseValueColorForLineEnabled) + lineColor = dataSet.getColor(j); + else if (valueLineColor != ColorTemplate.COLOR_NONE) + lineColor = valueLineColor; + + if (lineColor != ColorTemplate.COLOR_NONE) { + mValueLinePaint.setColor(lineColor); + c.drawLine(pt0x, pt0y, pt1x, pt1y, mValueLinePaint); + c.drawLine(pt1x, pt1y, pt2x, pt2y, mValueLinePaint); + } + + // draw everything, depending on settings + if (drawXOutside && drawYOutside) { + + drawValue(c, + formatter, + value, + entry, + 0, + labelPtx, + labelPty, + dataSet.getValueTextColor(j)); + + if (j < data.getEntryCount() && entryLabel != null) { + drawEntryLabel(c, entryLabel, labelPtx, labelPty + lineHeight); + } + + } else if (drawXOutside) { + if (j < data.getEntryCount() && entryLabel != null) { + drawEntryLabel(c, entryLabel, labelPtx, labelPty + lineHeight / 2.f); + } + } else if (drawYOutside) { + + drawValue(c, formatter, value, entry, 0, labelPtx, labelPty + lineHeight / 2.f, dataSet + .getValueTextColor(j)); + } + } + + if (drawXInside || drawYInside) { + // calculate the text position + float x = labelRadius * sliceXBase + center.x; + float y = labelRadius * sliceYBase + center.y; + + mValuePaint.setTextAlign(Align.CENTER); + + // draw everything, depending on settings + if (drawXInside && drawYInside) { + + drawValue(c, formatter, value, entry, 0, x, y, dataSet.getValueTextColor(j)); + + if (j < data.getEntryCount() && entryLabel != null) { + drawEntryLabel(c, entryLabel, x, y + lineHeight); + } + + } else if (drawXInside) { + if (j < data.getEntryCount() && entryLabel != null) { + drawEntryLabel(c, entryLabel, x, y + lineHeight / 2f); + } + } else if (drawYInside) { + + drawValue(c, formatter, value, entry, 0, x, y + lineHeight / 2f, dataSet.getValueTextColor(j)); + } + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + + Drawable icon = entry.getIcon(); + + float x = (labelRadius + iconsOffset.y) * sliceXBase + center.x; + float y = (labelRadius + iconsOffset.y) * sliceYBase + center.y; + y += iconsOffset.x; + + Utils.drawImage( + c, + icon, + (int)x, + (int)y, + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight()); + } + + xIndex++; + } + + MPPointF.recycleInstance(iconsOffset); + } + MPPointF.recycleInstance(center); + c.restore(); + } + + /** + * Draws an entry label at the specified position. + * + * @param c + * @param label + * @param x + * @param y + */ + protected void drawEntryLabel(Canvas c, String label, float x, float y) { + c.drawText(label, x, y, mEntryLabelsPaint); + } + + @Override + public void drawExtras(Canvas c) { + drawHole(c); + c.drawBitmap(mDrawBitmap.get(), 0, 0, null); + drawCenterText(c); + } + + private Path mHoleCirclePath = new Path(); + + /** + * draws the hole in the center of the chart and the transparent circle / + * hole + */ + protected void drawHole(Canvas c) { + + if (mChart.isDrawHoleEnabled() && mBitmapCanvas != null) { + + float radius = mChart.getRadius(); + float holeRadius = radius * (mChart.getHoleRadius() / 100); + MPPointF center = mChart.getCenterCircleBox(); + + if (Color.alpha(mHolePaint.getColor()) > 0) { + // draw the hole-circle + mBitmapCanvas.drawCircle( + center.x, center.y, + holeRadius, mHolePaint); + } + + // only draw the circle if it can be seen (not covered by the hole) + if (Color.alpha(mTransparentCirclePaint.getColor()) > 0 && + mChart.getTransparentCircleRadius() > mChart.getHoleRadius()) { + + int alpha = mTransparentCirclePaint.getAlpha(); + float secondHoleRadius = radius * (mChart.getTransparentCircleRadius() / 100); + + mTransparentCirclePaint.setAlpha((int) ((float) alpha * mAnimator.getPhaseX() * mAnimator.getPhaseY())); + + // draw the transparent-circle + mHoleCirclePath.reset(); + mHoleCirclePath.addCircle(center.x, center.y, secondHoleRadius, Path.Direction.CW); + mHoleCirclePath.addCircle(center.x, center.y, holeRadius, Path.Direction.CCW); + mBitmapCanvas.drawPath(mHoleCirclePath, mTransparentCirclePaint); + + // reset alpha + mTransparentCirclePaint.setAlpha(alpha); + } + MPPointF.recycleInstance(center); + } + } + + protected Path mDrawCenterTextPathBuffer = new Path(); + /** + * draws the description text in the center of the pie chart makes most + * sense when center-hole is enabled + */ + protected void drawCenterText(Canvas c) { + + CharSequence centerText = mChart.getCenterText(); + + if (mChart.isDrawCenterTextEnabled() && centerText != null) { + + MPPointF center = mChart.getCenterCircleBox(); + MPPointF offset = mChart.getCenterTextOffset(); + + float x = center.x + offset.x; + float y = center.y + offset.y; + + float innerRadius = mChart.isDrawHoleEnabled() && !mChart.isDrawSlicesUnderHoleEnabled() + ? mChart.getRadius() * (mChart.getHoleRadius() / 100f) + : mChart.getRadius(); + + RectF holeRect = mRectBuffer[0]; + holeRect.left = x - innerRadius; + holeRect.top = y - innerRadius; + holeRect.right = x + innerRadius; + holeRect.bottom = y + innerRadius; + RectF boundingRect = mRectBuffer[1]; + boundingRect.set(holeRect); + + float radiusPercent = mChart.getCenterTextRadiusPercent() / 100f; + if (radiusPercent > 0.0) { + boundingRect.inset( + (boundingRect.width() - boundingRect.width() * radiusPercent) / 2.f, + (boundingRect.height() - boundingRect.height() * radiusPercent) / 2.f + ); + } + + if (!centerText.equals(mCenterTextLastValue) || !boundingRect.equals(mCenterTextLastBounds)) { + + // Next time we won't recalculate StaticLayout... + mCenterTextLastBounds.set(boundingRect); + mCenterTextLastValue = centerText; + + float width = mCenterTextLastBounds.width(); + + // If width is 0, it will crash. Always have a minimum of 1 + mCenterTextLayout = new StaticLayout(centerText, 0, centerText.length(), + mCenterTextPaint, + (int) Math.max(Math.ceil(width), 1.f), + Layout.Alignment.ALIGN_CENTER, 1.f, 0.f, false); + } + + //float layoutWidth = Utils.getStaticLayoutMaxWidth(mCenterTextLayout); + float layoutHeight = mCenterTextLayout.getHeight(); + + c.save(); + if (Build.VERSION.SDK_INT >= 18) { + Path path = mDrawCenterTextPathBuffer; + path.reset(); + path.addOval(holeRect, Path.Direction.CW); + c.clipPath(path); + } + + c.translate(boundingRect.left, boundingRect.top + (boundingRect.height() - layoutHeight) / 2.f); + mCenterTextLayout.draw(c); + + c.restore(); + + MPPointF.recycleInstance(center); + MPPointF.recycleInstance(offset); + } + } + + protected RectF mDrawHighlightedRectF = new RectF(); + @Override + public void drawHighlighted(Canvas c, Highlight[] indices) { + + /* Skip entirely if using rounded circle slices, because it doesn't make sense to highlight + * in this way. + * TODO: add support for changing slice color with highlighting rather than only shifting the slice + */ + + final boolean drawInnerArc = mChart.isDrawHoleEnabled() && !mChart.isDrawSlicesUnderHoleEnabled(); + if (drawInnerArc && mChart.isDrawRoundedSlicesEnabled()) + return; + + float phaseX = mAnimator.getPhaseX(); + float phaseY = mAnimator.getPhaseY(); + + float angle; + float rotationAngle = mChart.getRotationAngle(); + + float[] drawAngles = mChart.getDrawAngles(); + float[] absoluteAngles = mChart.getAbsoluteAngles(); + final MPPointF center = mChart.getCenterCircleBox(); + final float radius = mChart.getRadius(); + final float userInnerRadius = drawInnerArc + ? radius * (mChart.getHoleRadius() / 100.f) + : 0.f; + + final RectF highlightedCircleBox = mDrawHighlightedRectF; + highlightedCircleBox.set(0,0,0,0); + + for (int i = 0; i < indices.length; i++) { + + // get the index to highlight + int index = (int) indices[i].getX(); + + if (index >= drawAngles.length) + continue; + + IPieDataSet set = mChart.getData() + .getDataSetByIndex(indices[i].getDataSetIndex()); + + if (set == null || !set.isHighlightEnabled()) + continue; + + final int entryCount = set.getEntryCount(); + int visibleAngleCount = 0; + for (int j = 0; j < entryCount; j++) { + // draw only if the value is greater than zero + if ((Math.abs(set.getEntryForIndex(j).getY()) > Utils.FLOAT_EPSILON)) { + visibleAngleCount++; + } + } + + if (index == 0) + angle = 0.f; + else + angle = absoluteAngles[index - 1] * phaseX; + + final float sliceSpace = visibleAngleCount <= 1 ? 0.f : set.getSliceSpace(); + + float sliceAngle = drawAngles[index]; + float innerRadius = userInnerRadius; + + float shift = set.getSelectionShift(); + final float highlightedRadius = radius + shift; + highlightedCircleBox.set(mChart.getCircleBox()); + highlightedCircleBox.inset(-shift, -shift); + + final boolean accountForSliceSpacing = sliceSpace > 0.f && sliceAngle <= 180.f; + + Integer highlightColor = set.getHighlightColor(); + if (highlightColor == null) + highlightColor = set.getColor(index); + mRenderPaint.setColor(highlightColor); + + final float sliceSpaceAngleOuter = visibleAngleCount == 1 ? + 0.f : + sliceSpace / (Utils.FDEG2RAD * radius); + + final float sliceSpaceAngleShifted = visibleAngleCount == 1 ? + 0.f : + sliceSpace / (Utils.FDEG2RAD * highlightedRadius); + + final float startAngleOuter = rotationAngle + (angle + sliceSpaceAngleOuter / 2.f) * phaseY; + float sweepAngleOuter = (sliceAngle - sliceSpaceAngleOuter) * phaseY; + if (sweepAngleOuter < 0.f) { + sweepAngleOuter = 0.f; + } + + final float startAngleShifted = rotationAngle + (angle + sliceSpaceAngleShifted / 2.f) * phaseY; + float sweepAngleShifted = (sliceAngle - sliceSpaceAngleShifted) * phaseY; + if (sweepAngleShifted < 0.f) { + sweepAngleShifted = 0.f; + } + + mPathBuffer.reset(); + + if (sweepAngleOuter >= 360.f && sweepAngleOuter % 360f <= Utils.FLOAT_EPSILON) { + // Android is doing "mod 360" + mPathBuffer.addCircle(center.x, center.y, highlightedRadius, Path.Direction.CW); + } else { + + mPathBuffer.moveTo( + center.x + highlightedRadius * (float) Math.cos(startAngleShifted * Utils.FDEG2RAD), + center.y + highlightedRadius * (float) Math.sin(startAngleShifted * Utils.FDEG2RAD)); + + mPathBuffer.arcTo( + highlightedCircleBox, + startAngleShifted, + sweepAngleShifted + ); + } + + float sliceSpaceRadius = 0.f; + if (accountForSliceSpacing) { + sliceSpaceRadius = + calculateMinimumRadiusForSpacedSlice( + center, radius, + sliceAngle * phaseY, + center.x + radius * (float) Math.cos(startAngleOuter * Utils.FDEG2RAD), + center.y + radius * (float) Math.sin(startAngleOuter * Utils.FDEG2RAD), + startAngleOuter, + sweepAngleOuter); + } + + // API < 21 does not receive floats in addArc, but a RectF + mInnerRectBuffer.set( + center.x - innerRadius, + center.y - innerRadius, + center.x + innerRadius, + center.y + innerRadius); + + if (drawInnerArc && + (innerRadius > 0.f || accountForSliceSpacing)) { + + if (accountForSliceSpacing) { + float minSpacedRadius = sliceSpaceRadius; + + if (minSpacedRadius < 0.f) + minSpacedRadius = -minSpacedRadius; + + innerRadius = Math.max(innerRadius, minSpacedRadius); + } + + final float sliceSpaceAngleInner = visibleAngleCount == 1 || innerRadius == 0.f ? + 0.f : + sliceSpace / (Utils.FDEG2RAD * innerRadius); + final float startAngleInner = rotationAngle + (angle + sliceSpaceAngleInner / 2.f) * phaseY; + float sweepAngleInner = (sliceAngle - sliceSpaceAngleInner) * phaseY; + if (sweepAngleInner < 0.f) { + sweepAngleInner = 0.f; + } + final float endAngleInner = startAngleInner + sweepAngleInner; + + if (sweepAngleOuter >= 360.f && sweepAngleOuter % 360f <= Utils.FLOAT_EPSILON) { + // Android is doing "mod 360" + mPathBuffer.addCircle(center.x, center.y, innerRadius, Path.Direction.CCW); + } else { + + mPathBuffer.lineTo( + center.x + innerRadius * (float) Math.cos(endAngleInner * Utils.FDEG2RAD), + center.y + innerRadius * (float) Math.sin(endAngleInner * Utils.FDEG2RAD)); + + mPathBuffer.arcTo( + mInnerRectBuffer, + endAngleInner, + -sweepAngleInner + ); + } + } else { + + if (sweepAngleOuter % 360f > Utils.FLOAT_EPSILON) { + + if (accountForSliceSpacing) { + final float angleMiddle = startAngleOuter + sweepAngleOuter / 2.f; + + final float arcEndPointX = center.x + + sliceSpaceRadius * (float) Math.cos(angleMiddle * Utils.FDEG2RAD); + final float arcEndPointY = center.y + + sliceSpaceRadius * (float) Math.sin(angleMiddle * Utils.FDEG2RAD); + + mPathBuffer.lineTo( + arcEndPointX, + arcEndPointY); + + } else { + + mPathBuffer.lineTo( + center.x, + center.y); + } + + } + + } + + mPathBuffer.close(); + + mBitmapCanvas.drawPath(mPathBuffer, mRenderPaint); + } + + MPPointF.recycleInstance(center); + } + + /** + * This gives all pie-slices a rounded edge. + * + * @param c + */ + protected void drawRoundedSlices(Canvas c) { + + if (!mChart.isDrawRoundedSlicesEnabled()) + return; + + IPieDataSet dataSet = mChart.getData().getDataSet(); + + if (!dataSet.isVisible()) + return; + + float phaseX = mAnimator.getPhaseX(); + float phaseY = mAnimator.getPhaseY(); + + MPPointF center = mChart.getCenterCircleBox(); + float r = mChart.getRadius(); + + // calculate the radius of the "slice-circle" + float circleRadius = (r - (r * mChart.getHoleRadius() / 100f)) / 2f; + + float[] drawAngles = mChart.getDrawAngles(); + float angle = mChart.getRotationAngle(); + + for (int j = 0; j < dataSet.getEntryCount(); j++) { + + float sliceAngle = drawAngles[j]; + + Entry e = dataSet.getEntryForIndex(j); + + // draw only if the value is greater than zero + if ((Math.abs(e.getY()) > Utils.FLOAT_EPSILON)) { + + float x = (float) ((r - circleRadius) + * Math.cos(Math.toRadians((angle + sliceAngle) + * phaseY)) + center.x); + float y = (float) ((r - circleRadius) + * Math.sin(Math.toRadians((angle + sliceAngle) + * phaseY)) + center.y); + + mRenderPaint.setColor(dataSet.getColor(j)); + mBitmapCanvas.drawCircle(x, y, circleRadius, mRenderPaint); + } + + angle += sliceAngle * phaseX; + } + MPPointF.recycleInstance(center); + } + + /** + * Releases the drawing bitmap. This should be called when {@link LineChart#onDetachedFromWindow()}. + */ + public void releaseBitmap() { + if (mBitmapCanvas != null) { + mBitmapCanvas.setBitmap(null); + mBitmapCanvas = null; + } + if (mDrawBitmap != null) { + Bitmap drawBitmap = mDrawBitmap.get(); + if (drawBitmap != null) { + drawBitmap.recycle(); + } + mDrawBitmap.clear(); + mDrawBitmap = null; + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/RadarChartRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/RadarChartRenderer.java new file mode 100644 index 0000000..dbf0e8f --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/RadarChartRenderer.java @@ -0,0 +1,398 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.drawable.Drawable; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.charts.RadarChart; +import com.github.mikephil.charting.data.RadarData; +import com.github.mikephil.charting.data.RadarEntry; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.datasets.IRadarDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +public class RadarChartRenderer extends LineRadarRenderer { + + protected RadarChart mChart; + + /** + * paint for drawing the web + */ + protected Paint mWebPaint; + protected Paint mHighlightCirclePaint; + + public RadarChartRenderer(RadarChart chart, ChartAnimator animator, + ViewPortHandler viewPortHandler) { + super(animator, viewPortHandler); + mChart = chart; + + mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mHighlightPaint.setStyle(Paint.Style.STROKE); + mHighlightPaint.setStrokeWidth(2f); + mHighlightPaint.setColor(Color.rgb(255, 187, 115)); + + mWebPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mWebPaint.setStyle(Paint.Style.STROKE); + + mHighlightCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + } + + public Paint getWebPaint() { + return mWebPaint; + } + + @Override + public void initBuffers() { + // TODO Auto-generated method stub + + } + + @Override + public void drawData(Canvas c) { + + RadarData radarData = mChart.getData(); + + int mostEntries = radarData.getMaxEntryCountSet().getEntryCount(); + + for (IRadarDataSet set : radarData.getDataSets()) { + + if (set.isVisible()) { + drawDataSet(c, set, mostEntries); + } + } + } + + protected Path mDrawDataSetSurfacePathBuffer = new Path(); + /** + * Draws the RadarDataSet + * + * @param c + * @param dataSet + * @param mostEntries the entry count of the dataset with the most entries + */ + protected void drawDataSet(Canvas c, IRadarDataSet dataSet, int mostEntries) { + + float phaseX = mAnimator.getPhaseX(); + float phaseY = mAnimator.getPhaseY(); + + float sliceangle = mChart.getSliceAngle(); + + // calculate the factor that is needed for transforming the value to + // pixels + float factor = mChart.getFactor(); + + MPPointF center = mChart.getCenterOffsets(); + MPPointF pOut = MPPointF.getInstance(0,0); + Path surface = mDrawDataSetSurfacePathBuffer; + surface.reset(); + + boolean hasMovedToPoint = false; + + for (int j = 0; j < dataSet.getEntryCount(); j++) { + + mRenderPaint.setColor(dataSet.getColor(j)); + + RadarEntry e = dataSet.getEntryForIndex(j); + + Utils.getPosition( + center, + (e.getY() - mChart.getYChartMin()) * factor * phaseY, + sliceangle * j * phaseX + mChart.getRotationAngle(), pOut); + + if (Float.isNaN(pOut.x)) + continue; + + if (!hasMovedToPoint) { + surface.moveTo(pOut.x, pOut.y); + hasMovedToPoint = true; + } else + surface.lineTo(pOut.x, pOut.y); + } + + if (dataSet.getEntryCount() > mostEntries) { + // if this is not the largest set, draw a line to the center before closing + surface.lineTo(center.x, center.y); + } + + surface.close(); + + if (dataSet.isDrawFilledEnabled()) { + + final Drawable drawable = dataSet.getFillDrawable(); + if (drawable != null) { + + drawFilledPath(c, surface, drawable); + } else { + + drawFilledPath(c, surface, dataSet.getFillColor(), dataSet.getFillAlpha()); + } + } + + mRenderPaint.setStrokeWidth(dataSet.getLineWidth()); + mRenderPaint.setStyle(Paint.Style.STROKE); + + // draw the line (only if filled is disabled or alpha is below 255) + if (!dataSet.isDrawFilledEnabled() || dataSet.getFillAlpha() < 255) + c.drawPath(surface, mRenderPaint); + + MPPointF.recycleInstance(center); + MPPointF.recycleInstance(pOut); + } + + @Override + public void drawValues(Canvas c) { + + float phaseX = mAnimator.getPhaseX(); + float phaseY = mAnimator.getPhaseY(); + + float sliceangle = mChart.getSliceAngle(); + + // calculate the factor that is needed for transforming the value to + // pixels + float factor = mChart.getFactor(); + + MPPointF center = mChart.getCenterOffsets(); + MPPointF pOut = MPPointF.getInstance(0,0); + MPPointF pIcon = MPPointF.getInstance(0,0); + + float yoffset = Utils.convertDpToPixel(5f); + + for (int i = 0; i < mChart.getData().getDataSetCount(); i++) { + + IRadarDataSet dataSet = mChart.getData().getDataSetByIndex(i); + + if (!shouldDrawValues(dataSet)) + continue; + + // apply the text-styling defined by the DataSet + applyValueTextStyle(dataSet); + + MPPointF iconsOffset = MPPointF.getInstance(dataSet.getIconsOffset()); + iconsOffset.x = Utils.convertDpToPixel(iconsOffset.x); + iconsOffset.y = Utils.convertDpToPixel(iconsOffset.y); + + for (int j = 0; j < dataSet.getEntryCount(); j++) { + + RadarEntry entry = dataSet.getEntryForIndex(j); + + Utils.getPosition( + center, + (entry.getY() - mChart.getYChartMin()) * factor * phaseY, + sliceangle * j * phaseX + mChart.getRotationAngle(), + pOut); + + if (dataSet.isDrawValuesEnabled()) { + drawValue(c, + dataSet.getValueFormatter(), + entry.getY(), + entry, + i, + pOut.x, + pOut.y - yoffset, + dataSet.getValueTextColor + (j)); + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + + Drawable icon = entry.getIcon(); + + Utils.getPosition( + center, + (entry.getY()) * factor * phaseY + iconsOffset.y, + sliceangle * j * phaseX + mChart.getRotationAngle(), + pIcon); + + //noinspection SuspiciousNameCombination + pIcon.y += iconsOffset.x; + + Utils.drawImage( + c, + icon, + (int)pIcon.x, + (int)pIcon.y, + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight()); + } + } + + MPPointF.recycleInstance(iconsOffset); + } + + MPPointF.recycleInstance(center); + MPPointF.recycleInstance(pOut); + MPPointF.recycleInstance(pIcon); + } + + @Override + public void drawExtras(Canvas c) { + drawWeb(c); + } + + protected void drawWeb(Canvas c) { + + float sliceangle = mChart.getSliceAngle(); + + // calculate the factor that is needed for transforming the value to + // pixels + float factor = mChart.getFactor(); + float rotationangle = mChart.getRotationAngle(); + + MPPointF center = mChart.getCenterOffsets(); + + // draw the web lines that come from the center + mWebPaint.setStrokeWidth(mChart.getWebLineWidth()); + mWebPaint.setColor(mChart.getWebColor()); + mWebPaint.setAlpha(mChart.getWebAlpha()); + + final int xIncrements = 1 + mChart.getSkipWebLineCount(); + int maxEntryCount = mChart.getData().getMaxEntryCountSet().getEntryCount(); + + MPPointF p = MPPointF.getInstance(0,0); + for (int i = 0; i < maxEntryCount; i += xIncrements) { + + Utils.getPosition( + center, + mChart.getYRange() * factor, + sliceangle * i + rotationangle, + p); + + c.drawLine(center.x, center.y, p.x, p.y, mWebPaint); + } + MPPointF.recycleInstance(p); + + // draw the inner-web + mWebPaint.setStrokeWidth(mChart.getWebLineWidthInner()); + mWebPaint.setColor(mChart.getWebColorInner()); + mWebPaint.setAlpha(mChart.getWebAlpha()); + + int labelCount = mChart.getYAxis().mEntryCount; + + MPPointF p1out = MPPointF.getInstance(0,0); + MPPointF p2out = MPPointF.getInstance(0,0); + for (int j = 0; j < labelCount; j++) { + + for (int i = 0; i < mChart.getData().getEntryCount(); i++) { + + float r = (mChart.getYAxis().mEntries[j] - mChart.getYChartMin()) * factor; + + Utils.getPosition(center, r, sliceangle * i + rotationangle, p1out); + Utils.getPosition(center, r, sliceangle * (i + 1) + rotationangle, p2out); + + c.drawLine(p1out.x, p1out.y, p2out.x, p2out.y, mWebPaint); + + + } + } + MPPointF.recycleInstance(p1out); + MPPointF.recycleInstance(p2out); + } + + @Override + public void drawHighlighted(Canvas c, Highlight[] indices) { + + float sliceangle = mChart.getSliceAngle(); + + // calculate the factor that is needed for transforming the value to + // pixels + float factor = mChart.getFactor(); + + MPPointF center = mChart.getCenterOffsets(); + MPPointF pOut = MPPointF.getInstance(0,0); + + RadarData radarData = mChart.getData(); + + for (Highlight high : indices) { + + IRadarDataSet set = radarData.getDataSetByIndex(high.getDataSetIndex()); + + if (set == null || !set.isHighlightEnabled()) + continue; + + RadarEntry e = set.getEntryForIndex((int) high.getX()); + + if (!isInBoundsX(e, set)) + continue; + + float y = (e.getY() - mChart.getYChartMin()); + + Utils.getPosition(center, + y * factor * mAnimator.getPhaseY(), + sliceangle * high.getX() * mAnimator.getPhaseX() + mChart.getRotationAngle(), + pOut); + + high.setDraw(pOut.x, pOut.y); + + // draw the lines + drawHighlightLines(c, pOut.x, pOut.y, set); + + if (set.isDrawHighlightCircleEnabled()) { + + if (!Float.isNaN(pOut.x) && !Float.isNaN(pOut.y)) { + + int strokeColor = set.getHighlightCircleStrokeColor(); + if (strokeColor == ColorTemplate.COLOR_NONE) { + strokeColor = set.getColor(0); + } + + if (set.getHighlightCircleStrokeAlpha() < 255) { + strokeColor = ColorTemplate.colorWithAlpha(strokeColor, set.getHighlightCircleStrokeAlpha()); + } + + drawHighlightCircle(c, + pOut, + set.getHighlightCircleInnerRadius(), + set.getHighlightCircleOuterRadius(), + set.getHighlightCircleFillColor(), + strokeColor, + set.getHighlightCircleStrokeWidth()); + } + } + } + + MPPointF.recycleInstance(center); + MPPointF.recycleInstance(pOut); + } + + protected Path mDrawHighlightCirclePathBuffer = new Path(); + public void drawHighlightCircle(Canvas c, + MPPointF point, + float innerRadius, + float outerRadius, + int fillColor, + int strokeColor, + float strokeWidth) { + c.save(); + + outerRadius = Utils.convertDpToPixel(outerRadius); + innerRadius = Utils.convertDpToPixel(innerRadius); + + if (fillColor != ColorTemplate.COLOR_NONE) { + Path p = mDrawHighlightCirclePathBuffer; + p.reset(); + p.addCircle(point.x, point.y, outerRadius, Path.Direction.CW); + if (innerRadius > 0.f) { + p.addCircle(point.x, point.y, innerRadius, Path.Direction.CCW); + } + mHighlightCirclePaint.setColor(fillColor); + mHighlightCirclePaint.setStyle(Paint.Style.FILL); + c.drawPath(p, mHighlightCirclePaint); + } + + if (strokeColor != ColorTemplate.COLOR_NONE) { + mHighlightCirclePaint.setColor(strokeColor); + mHighlightCirclePaint.setStyle(Paint.Style.STROKE); + mHighlightCirclePaint.setStrokeWidth(Utils.convertDpToPixel(strokeWidth)); + c.drawCircle(point.x, point.y, outerRadius, mHighlightCirclePaint); + } + + c.restore(); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/Renderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/Renderer.java new file mode 100644 index 0000000..90f4968 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/Renderer.java @@ -0,0 +1,21 @@ + +package com.github.mikephil.charting.renderer; + +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Abstract baseclass of all Renderers. + * + * @author Philipp Jahoda + */ +public abstract class Renderer { + + /** + * the component that handles the drawing area of the chart and it's offsets + */ + protected ViewPortHandler mViewPortHandler; + + public Renderer(ViewPortHandler viewPortHandler) { + this.mViewPortHandler = viewPortHandler; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/ScatterChartRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/ScatterChartRenderer.java new file mode 100644 index 0000000..ccd077e --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/ScatterChartRenderer.java @@ -0,0 +1,197 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.util.Log; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.ScatterData; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.interfaces.dataprovider.ScatterDataProvider; +import com.github.mikephil.charting.interfaces.datasets.IScatterDataSet; +import com.github.mikephil.charting.renderer.scatter.IShapeRenderer; +import com.github.mikephil.charting.utils.MPPointD; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.util.List; + +public class ScatterChartRenderer extends LineScatterCandleRadarRenderer { + + protected ScatterDataProvider mChart; + + public ScatterChartRenderer(ScatterDataProvider chart, ChartAnimator animator, ViewPortHandler viewPortHandler) { + super(animator, viewPortHandler); + mChart = chart; + } + + @Override + public void initBuffers() { + } + + @Override + public void drawData(Canvas c) { + + ScatterData scatterData = mChart.getScatterData(); + + for (IScatterDataSet set : scatterData.getDataSets()) { + + if (set.isVisible()) + drawDataSet(c, set); + } + } + + float[] mPixelBuffer = new float[2]; + + protected void drawDataSet(Canvas c, IScatterDataSet dataSet) { + + if (dataSet.getEntryCount() < 1) + return; + + ViewPortHandler viewPortHandler = mViewPortHandler; + + Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + + float phaseY = mAnimator.getPhaseY(); + + IShapeRenderer renderer = dataSet.getShapeRenderer(); + if (renderer == null) { + Log.i("MISSING", "There's no IShapeRenderer specified for ScatterDataSet"); + return; + } + + int max = (int)(Math.min( + Math.ceil((float)dataSet.getEntryCount() * mAnimator.getPhaseX()), + (float)dataSet.getEntryCount())); + + for (int i = 0; i < max; i++) { + + Entry e = dataSet.getEntryForIndex(i); + + mPixelBuffer[0] = e.getX(); + mPixelBuffer[1] = e.getY() * phaseY; + + trans.pointValuesToPixel(mPixelBuffer); + + if (!viewPortHandler.isInBoundsRight(mPixelBuffer[0])) + break; + + if (!viewPortHandler.isInBoundsLeft(mPixelBuffer[0]) + || !viewPortHandler.isInBoundsY(mPixelBuffer[1])) + continue; + + mRenderPaint.setColor(dataSet.getColor(i / 2)); + renderer.renderShape( + c, dataSet, mViewPortHandler, + mPixelBuffer[0], mPixelBuffer[1], + mRenderPaint); + } + } + + @Override + public void drawValues(Canvas c) { + + // if values are drawn + if (isDrawingValuesAllowed(mChart)) { + + List dataSets = mChart.getScatterData().getDataSets(); + + for (int i = 0; i < mChart.getScatterData().getDataSetCount(); i++) { + + IScatterDataSet dataSet = dataSets.get(i); + + if (!shouldDrawValues(dataSet) || dataSet.getEntryCount() < 1) + continue; + + // apply the text-styling defined by the DataSet + applyValueTextStyle(dataSet); + + mXBounds.set(mChart, dataSet); + + float[] positions = mChart.getTransformer(dataSet.getAxisDependency()) + .generateTransformedValuesScatter(dataSet, + mAnimator.getPhaseX(), mAnimator.getPhaseY(), mXBounds.min, mXBounds.max); + + float shapeSize = Utils.convertDpToPixel(dataSet.getScatterShapeSize()); + + MPPointF iconsOffset = MPPointF.getInstance(dataSet.getIconsOffset()); + iconsOffset.x = Utils.convertDpToPixel(iconsOffset.x); + iconsOffset.y = Utils.convertDpToPixel(iconsOffset.y); + + for (int j = 0; j < positions.length; j += 2) { + + if (!mViewPortHandler.isInBoundsRight(positions[j])) + break; + + // make sure the lines don't do shitty things outside bounds + if ((!mViewPortHandler.isInBoundsLeft(positions[j]) + || !mViewPortHandler.isInBoundsY(positions[j + 1]))) + continue; + + Entry entry = dataSet.getEntryForIndex(j / 2 + mXBounds.min); + + if (dataSet.isDrawValuesEnabled()) { + drawValue(c, + dataSet.getValueFormatter(), + entry.getY(), + entry, + i, + positions[j], + positions[j + 1] - shapeSize, + dataSet.getValueTextColor(j / 2 + mXBounds.min)); + } + + if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { + + Drawable icon = entry.getIcon(); + + Utils.drawImage( + c, + icon, + (int)(positions[j] + iconsOffset.x), + (int)(positions[j + 1] + iconsOffset.y), + icon.getIntrinsicWidth(), + icon.getIntrinsicHeight()); + } + } + + MPPointF.recycleInstance(iconsOffset); + } + } + } + + @Override + public void drawExtras(Canvas c) { + } + + @Override + public void drawHighlighted(Canvas c, Highlight[] indices) { + + ScatterData scatterData = mChart.getScatterData(); + + for (Highlight high : indices) { + + IScatterDataSet set = scatterData.getDataSetByIndex(high.getDataSetIndex()); + + if (set == null || !set.isHighlightEnabled()) + continue; + + final Entry e = set.getEntryForXValue(high.getX(), high.getY()); + + if (!isInBoundsX(e, set)) + continue; + + MPPointD pix = mChart.getTransformer(set.getAxisDependency()).getPixelForValues(e.getX(), e.getY() * mAnimator + .getPhaseY()); + + high.setDraw((float) pix.x, (float) pix.y); + + // draw the lines + drawHighlightLines(c, (float) pix.x, (float) pix.y, set); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/XAxisRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/XAxisRenderer.java new file mode 100644 index 0000000..8adb56c --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/XAxisRenderer.java @@ -0,0 +1,401 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Path; +import android.graphics.RectF; + +import com.github.mikephil.charting.components.LimitLine; +import com.github.mikephil.charting.components.XAxis; +import com.github.mikephil.charting.components.XAxis.XAxisPosition; +import com.github.mikephil.charting.utils.FSize; +import com.github.mikephil.charting.utils.MPPointD; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.util.List; + +public class XAxisRenderer extends AxisRenderer { + + protected XAxis mXAxis; + + public XAxisRenderer(ViewPortHandler viewPortHandler, XAxis xAxis, Transformer trans) { + super(viewPortHandler, trans, xAxis); + + this.mXAxis = xAxis; + + mAxisLabelPaint.setColor(Color.BLACK); + mAxisLabelPaint.setTextAlign(Align.CENTER); + mAxisLabelPaint.setTextSize(Utils.convertDpToPixel(10f)); + } + + protected void setupGridPaint() { + mGridPaint.setColor(mXAxis.getGridColor()); + mGridPaint.setStrokeWidth(mXAxis.getGridLineWidth()); + mGridPaint.setPathEffect(mXAxis.getGridDashPathEffect()); + } + + @Override + public void computeAxis(float min, float max, boolean inverted) { + + // calculate the starting and entry point of the y-labels (depending on + // zoom / contentrect bounds) + if (mViewPortHandler.contentWidth() > 10 && !mViewPortHandler.isFullyZoomedOutX()) { + + MPPointD p1 = mTrans.getValuesByTouchPoint(mViewPortHandler.contentLeft(), mViewPortHandler.contentTop()); + MPPointD p2 = mTrans.getValuesByTouchPoint(mViewPortHandler.contentRight(), mViewPortHandler.contentTop()); + + if (inverted) { + + min = (float) p2.x; + max = (float) p1.x; + } else { + + min = (float) p1.x; + max = (float) p2.x; + } + + MPPointD.recycleInstance(p1); + MPPointD.recycleInstance(p2); + } + + computeAxisValues(min, max); + } + + @Override + protected void computeAxisValues(float min, float max) { + super.computeAxisValues(min, max); + + computeSize(); + } + + protected void computeSize() { + + String longest = mXAxis.getLongestLabel(); + + mAxisLabelPaint.setTypeface(mXAxis.getTypeface()); + mAxisLabelPaint.setTextSize(mXAxis.getTextSize()); + + final FSize labelSize = Utils.calcTextSize(mAxisLabelPaint, longest); + + final float labelWidth = labelSize.width; + final float labelHeight = Utils.calcTextHeight(mAxisLabelPaint, "Q"); + + final FSize labelRotatedSize = Utils.getSizeOfRotatedRectangleByDegrees( + labelWidth, + labelHeight, + mXAxis.getLabelRotationAngle()); + + + mXAxis.mLabelWidth = Math.round(labelWidth); + mXAxis.mLabelHeight = Math.round(labelHeight); + mXAxis.mLabelRotatedWidth = Math.round(labelRotatedSize.width); + mXAxis.mLabelRotatedHeight = Math.round(labelRotatedSize.height); + + FSize.recycleInstance(labelRotatedSize); + FSize.recycleInstance(labelSize); + } + + @Override + public void renderAxisLabels(Canvas c) { + + if (!mXAxis.isEnabled() || !mXAxis.isDrawLabelsEnabled()) + return; + + float yoffset = mXAxis.getYOffset(); + + mAxisLabelPaint.setTypeface(mXAxis.getTypeface()); + mAxisLabelPaint.setTextSize(mXAxis.getTextSize()); + mAxisLabelPaint.setColor(mXAxis.getTextColor()); + + MPPointF pointF = MPPointF.getInstance(0,0); + if (mXAxis.getPosition() == XAxisPosition.TOP) { + pointF.x = 0.5f; + pointF.y = 1.0f; + drawLabels(c, mViewPortHandler.contentTop() - yoffset, pointF); + + } else if (mXAxis.getPosition() == XAxisPosition.TOP_INSIDE) { + pointF.x = 0.5f; + pointF.y = 1.0f; + drawLabels(c, mViewPortHandler.contentTop() + yoffset + mXAxis.mLabelRotatedHeight, pointF); + + } else if (mXAxis.getPosition() == XAxisPosition.BOTTOM) { + pointF.x = 0.5f; + pointF.y = 0.0f; + drawLabels(c, mViewPortHandler.contentBottom() + yoffset, pointF); + + } else if (mXAxis.getPosition() == XAxisPosition.BOTTOM_INSIDE) { + pointF.x = 0.5f; + pointF.y = 0.0f; + drawLabels(c, mViewPortHandler.contentBottom() - yoffset - mXAxis.mLabelRotatedHeight, pointF); + + } else { // BOTH SIDED + pointF.x = 0.5f; + pointF.y = 1.0f; + drawLabels(c, mViewPortHandler.contentTop() - yoffset, pointF); + pointF.x = 0.5f; + pointF.y = 0.0f; + drawLabels(c, mViewPortHandler.contentBottom() + yoffset, pointF); + } + MPPointF.recycleInstance(pointF); + } + + @Override + public void renderAxisLine(Canvas c) { + + if (!mXAxis.isDrawAxisLineEnabled() || !mXAxis.isEnabled()) + return; + + mAxisLinePaint.setColor(mXAxis.getAxisLineColor()); + mAxisLinePaint.setStrokeWidth(mXAxis.getAxisLineWidth()); + mAxisLinePaint.setPathEffect(mXAxis.getAxisLineDashPathEffect()); + + if (mXAxis.getPosition() == XAxisPosition.TOP + || mXAxis.getPosition() == XAxisPosition.TOP_INSIDE + || mXAxis.getPosition() == XAxisPosition.BOTH_SIDED) { + c.drawLine(mViewPortHandler.contentLeft(), + mViewPortHandler.contentTop(), mViewPortHandler.contentRight(), + mViewPortHandler.contentTop(), mAxisLinePaint); + } + + if (mXAxis.getPosition() == XAxisPosition.BOTTOM + || mXAxis.getPosition() == XAxisPosition.BOTTOM_INSIDE + || mXAxis.getPosition() == XAxisPosition.BOTH_SIDED) { + c.drawLine(mViewPortHandler.contentLeft(), + mViewPortHandler.contentBottom(), mViewPortHandler.contentRight(), + mViewPortHandler.contentBottom(), mAxisLinePaint); + } + } + + /** + * draws the x-labels on the specified y-position + * + * @param pos + */ + protected void drawLabels(Canvas c, float pos, MPPointF anchor) { + + final float labelRotationAngleDegrees = mXAxis.getLabelRotationAngle(); + boolean centeringEnabled = mXAxis.isCenterAxisLabelsEnabled(); + + float[] positions = new float[mXAxis.mEntryCount * 2]; + + for (int i = 0; i < positions.length; i += 2) { + + // only fill x values + if (centeringEnabled) { + positions[i] = mXAxis.mCenteredEntries[i / 2]; + } else { + positions[i] = mXAxis.mEntries[i / 2]; + } + } + + mTrans.pointValuesToPixel(positions); + + for (int i = 0; i < positions.length; i += 2) { + + float x = positions[i]; + + if (mViewPortHandler.isInBoundsX(x)) { + + String label = mXAxis.getValueFormatter().getFormattedValue(mXAxis.mEntries[i / 2], mXAxis); + + if (mXAxis.isAvoidFirstLastClippingEnabled()) { + + // avoid clipping of the last + if (i / 2 == mXAxis.mEntryCount - 1 && mXAxis.mEntryCount > 1) { + float width = Utils.calcTextWidth(mAxisLabelPaint, label); + + if (width > mViewPortHandler.offsetRight() * 2 + && x + width > mViewPortHandler.getChartWidth()) + x -= width / 2; + + // avoid clipping of the first + } else if (i == 0) { + + float width = Utils.calcTextWidth(mAxisLabelPaint, label); + x += width / 2; + } + } + + drawLabel(c, label, x, pos, anchor, labelRotationAngleDegrees); + } + } + } + + protected void drawLabel(Canvas c, String formattedLabel, float x, float y, MPPointF anchor, float angleDegrees) { + Utils.drawXAxisValue(c, formattedLabel, x, y, mAxisLabelPaint, anchor, angleDegrees); + } + protected Path mRenderGridLinesPath = new Path(); + protected float[] mRenderGridLinesBuffer = new float[2]; + @Override + public void renderGridLines(Canvas c) { + + if (!mXAxis.isDrawGridLinesEnabled() || !mXAxis.isEnabled()) + return; + + int clipRestoreCount = c.save(); + c.clipRect(getGridClippingRect()); + + if(mRenderGridLinesBuffer.length != mAxis.mEntryCount * 2){ + mRenderGridLinesBuffer = new float[mXAxis.mEntryCount * 2]; + } + float[] positions = mRenderGridLinesBuffer; + + for (int i = 0; i < positions.length; i += 2) { + positions[i] = mXAxis.mEntries[i / 2]; + positions[i + 1] = mXAxis.mEntries[i / 2]; + } + + mTrans.pointValuesToPixel(positions); + + setupGridPaint(); + + Path gridLinePath = mRenderGridLinesPath; + gridLinePath.reset(); + + for (int i = 0; i < positions.length; i += 2) { + + drawGridLine(c, positions[i], positions[i + 1], gridLinePath); + } + + c.restoreToCount(clipRestoreCount); + } + + protected RectF mGridClippingRect = new RectF(); + + public RectF getGridClippingRect() { + mGridClippingRect.set(mViewPortHandler.getContentRect()); + mGridClippingRect.inset(-mAxis.getGridLineWidth(), 0.f); + return mGridClippingRect; + } + + /** + * Draws the grid line at the specified position using the provided path. + * + * @param c + * @param x + * @param y + * @param gridLinePath + */ + protected void drawGridLine(Canvas c, float x, float y, Path gridLinePath) { + + gridLinePath.moveTo(x, mViewPortHandler.contentBottom()); + gridLinePath.lineTo(x, mViewPortHandler.contentTop()); + + // draw a path because lines don't support dashing on lower android versions + c.drawPath(gridLinePath, mGridPaint); + + gridLinePath.reset(); + } + + protected float[] mRenderLimitLinesBuffer = new float[2]; + protected RectF mLimitLineClippingRect = new RectF(); + + /** + * Draws the LimitLines associated with this axis to the screen. + * + * @param c + */ + @Override + public void renderLimitLines(Canvas c) { + + List limitLines = mXAxis.getLimitLines(); + + if (limitLines == null || limitLines.size() <= 0) + return; + + float[] position = mRenderLimitLinesBuffer; + position[0] = 0; + position[1] = 0; + + for (int i = 0; i < limitLines.size(); i++) { + + LimitLine l = limitLines.get(i); + + if (!l.isEnabled()) + continue; + + int clipRestoreCount = c.save(); + mLimitLineClippingRect.set(mViewPortHandler.getContentRect()); + mLimitLineClippingRect.inset(-l.getLineWidth(), 0.f); + c.clipRect(mLimitLineClippingRect); + + position[0] = l.getLimit(); + position[1] = 0.f; + + mTrans.pointValuesToPixel(position); + + renderLimitLineLine(c, l, position); + renderLimitLineLabel(c, l, position, 2.f + l.getYOffset()); + + c.restoreToCount(clipRestoreCount); + } + } + + float[] mLimitLineSegmentsBuffer = new float[4]; + private Path mLimitLinePath = new Path(); + + public void renderLimitLineLine(Canvas c, LimitLine limitLine, float[] position) { + mLimitLineSegmentsBuffer[0] = position[0]; + mLimitLineSegmentsBuffer[1] = mViewPortHandler.contentTop(); + mLimitLineSegmentsBuffer[2] = position[0]; + mLimitLineSegmentsBuffer[3] = mViewPortHandler.contentBottom(); + + mLimitLinePath.reset(); + mLimitLinePath.moveTo(mLimitLineSegmentsBuffer[0], mLimitLineSegmentsBuffer[1]); + mLimitLinePath.lineTo(mLimitLineSegmentsBuffer[2], mLimitLineSegmentsBuffer[3]); + + mLimitLinePaint.setStyle(Paint.Style.STROKE); + mLimitLinePaint.setColor(limitLine.getLineColor()); + mLimitLinePaint.setStrokeWidth(limitLine.getLineWidth()); + mLimitLinePaint.setPathEffect(limitLine.getDashPathEffect()); + + c.drawPath(mLimitLinePath, mLimitLinePaint); + } + + public void renderLimitLineLabel(Canvas c, LimitLine limitLine, float[] position, float yOffset) { + String label = limitLine.getLabel(); + + // if drawing the limit-value label is enabled + if (label != null && !label.equals("")) { + + mLimitLinePaint.setStyle(limitLine.getTextStyle()); + mLimitLinePaint.setPathEffect(null); + mLimitLinePaint.setColor(limitLine.getTextColor()); + mLimitLinePaint.setStrokeWidth(0.5f); + mLimitLinePaint.setTextSize(limitLine.getTextSize()); + + + float xOffset = limitLine.getLineWidth() + limitLine.getXOffset(); + + final LimitLine.LimitLabelPosition labelPosition = limitLine.getLabelPosition(); + + if (labelPosition == LimitLine.LimitLabelPosition.RIGHT_TOP) { + + final float labelLineHeight = Utils.calcTextHeight(mLimitLinePaint, label); + mLimitLinePaint.setTextAlign(Align.LEFT); + c.drawText(label, position[0] + xOffset, mViewPortHandler.contentTop() + yOffset + labelLineHeight, + mLimitLinePaint); + } else if (labelPosition == LimitLine.LimitLabelPosition.RIGHT_BOTTOM) { + + mLimitLinePaint.setTextAlign(Align.LEFT); + c.drawText(label, position[0] + xOffset, mViewPortHandler.contentBottom() - yOffset, mLimitLinePaint); + } else if (labelPosition == LimitLine.LimitLabelPosition.LEFT_TOP) { + + mLimitLinePaint.setTextAlign(Align.RIGHT); + final float labelLineHeight = Utils.calcTextHeight(mLimitLinePaint, label); + c.drawText(label, position[0] - xOffset, mViewPortHandler.contentTop() + yOffset + labelLineHeight, + mLimitLinePaint); + } else { + + mLimitLinePaint.setTextAlign(Align.RIGHT); + c.drawText(label, position[0] - xOffset, mViewPortHandler.contentBottom() - yOffset, mLimitLinePaint); + } + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/XAxisRendererHorizontalBarChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/XAxisRendererHorizontalBarChart.java new file mode 100644 index 0000000..86047cf --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/XAxisRendererHorizontalBarChart.java @@ -0,0 +1,310 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Path; +import android.graphics.RectF; + +import com.github.mikephil.charting.charts.BarChart; +import com.github.mikephil.charting.components.LimitLine; +import com.github.mikephil.charting.components.XAxis; +import com.github.mikephil.charting.components.XAxis.XAxisPosition; +import com.github.mikephil.charting.utils.FSize; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.MPPointD; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.util.List; + +public class XAxisRendererHorizontalBarChart extends XAxisRenderer { + + protected BarChart mChart; + + public XAxisRendererHorizontalBarChart(ViewPortHandler viewPortHandler, XAxis xAxis, + Transformer trans, BarChart chart) { + super(viewPortHandler, xAxis, trans); + + this.mChart = chart; + } + + @Override + public void computeAxis(float min, float max, boolean inverted) { + + // calculate the starting and entry point of the y-labels (depending on + // zoom / contentrect bounds) + if (mViewPortHandler.contentWidth() > 10 && !mViewPortHandler.isFullyZoomedOutY()) { + + MPPointD p1 = mTrans.getValuesByTouchPoint(mViewPortHandler.contentLeft(), mViewPortHandler.contentBottom()); + MPPointD p2 = mTrans.getValuesByTouchPoint(mViewPortHandler.contentLeft(), mViewPortHandler.contentTop()); + + if (inverted) { + + min = (float) p2.y; + max = (float) p1.y; + } else { + + min = (float) p1.y; + max = (float) p2.y; + } + + MPPointD.recycleInstance(p1); + MPPointD.recycleInstance(p2); + } + + computeAxisValues(min, max); + } + + @Override + protected void computeSize() { + + mAxisLabelPaint.setTypeface(mXAxis.getTypeface()); + mAxisLabelPaint.setTextSize(mXAxis.getTextSize()); + + String longest = mXAxis.getLongestLabel(); + + final FSize labelSize = Utils.calcTextSize(mAxisLabelPaint, longest); + + final float labelWidth = (int)(labelSize.width + mXAxis.getXOffset() * 3.5f); + final float labelHeight = labelSize.height; + + final FSize labelRotatedSize = Utils.getSizeOfRotatedRectangleByDegrees( + labelSize.width, + labelHeight, + mXAxis.getLabelRotationAngle()); + + mXAxis.mLabelWidth = Math.round(labelWidth); + mXAxis.mLabelHeight = Math.round(labelHeight); + mXAxis.mLabelRotatedWidth = (int)(labelRotatedSize.width + mXAxis.getXOffset() * 3.5f); + mXAxis.mLabelRotatedHeight = Math.round(labelRotatedSize.height); + + FSize.recycleInstance(labelRotatedSize); + } + + @Override + public void renderAxisLabels(Canvas c) { + + if (!mXAxis.isEnabled() || !mXAxis.isDrawLabelsEnabled()) + return; + + float xoffset = mXAxis.getXOffset(); + + mAxisLabelPaint.setTypeface(mXAxis.getTypeface()); + mAxisLabelPaint.setTextSize(mXAxis.getTextSize()); + mAxisLabelPaint.setColor(mXAxis.getTextColor()); + + MPPointF pointF = MPPointF.getInstance(0,0); + + if (mXAxis.getPosition() == XAxisPosition.TOP) { + pointF.x = 0.0f; + pointF.y = 0.5f; + drawLabels(c, mViewPortHandler.contentRight() + xoffset, pointF); + + } else if (mXAxis.getPosition() == XAxisPosition.TOP_INSIDE) { + pointF.x = 1.0f; + pointF.y = 0.5f; + drawLabels(c, mViewPortHandler.contentRight() - xoffset, pointF); + + } else if (mXAxis.getPosition() == XAxisPosition.BOTTOM) { + pointF.x = 1.0f; + pointF.y = 0.5f; + drawLabels(c, mViewPortHandler.contentLeft() - xoffset, pointF); + + } else if (mXAxis.getPosition() == XAxisPosition.BOTTOM_INSIDE) { + pointF.x = 1.0f; + pointF.y = 0.5f; + drawLabels(c, mViewPortHandler.contentLeft() + xoffset, pointF); + + } else { // BOTH SIDED + pointF.x = 0.0f; + pointF.y = 0.5f; + drawLabels(c, mViewPortHandler.contentRight() + xoffset, pointF); + pointF.x = 1.0f; + pointF.y = 0.5f; + drawLabels(c, mViewPortHandler.contentLeft() - xoffset, pointF); + } + + MPPointF.recycleInstance(pointF); + } + + @Override + protected void drawLabels(Canvas c, float pos, MPPointF anchor) { + + final float labelRotationAngleDegrees = mXAxis.getLabelRotationAngle(); + boolean centeringEnabled = mXAxis.isCenterAxisLabelsEnabled(); + + float[] positions = new float[mXAxis.mEntryCount * 2]; + + for (int i = 0; i < positions.length; i += 2) { + + // only fill x values + if (centeringEnabled) { + positions[i + 1] = mXAxis.mCenteredEntries[i / 2]; + } else { + positions[i + 1] = mXAxis.mEntries[i / 2]; + } + } + + mTrans.pointValuesToPixel(positions); + + for (int i = 0; i < positions.length; i += 2) { + + float y = positions[i + 1]; + + if (mViewPortHandler.isInBoundsY(y)) { + + String label = mXAxis.getValueFormatter().getFormattedValue(mXAxis.mEntries[i / 2], mXAxis); + drawLabel(c, label, pos, y, anchor, labelRotationAngleDegrees); + } + } + } + + @Override + public RectF getGridClippingRect() { + mGridClippingRect.set(mViewPortHandler.getContentRect()); + mGridClippingRect.inset(0.f, -mAxis.getGridLineWidth()); + return mGridClippingRect; + } + + @Override + protected void drawGridLine(Canvas c, float x, float y, Path gridLinePath) { + + gridLinePath.moveTo(mViewPortHandler.contentRight(), y); + gridLinePath.lineTo(mViewPortHandler.contentLeft(), y); + + // draw a path because lines don't support dashing on lower android versions + c.drawPath(gridLinePath, mGridPaint); + + gridLinePath.reset(); + } + + @Override + public void renderAxisLine(Canvas c) { + + if (!mXAxis.isDrawAxisLineEnabled() || !mXAxis.isEnabled()) + return; + + mAxisLinePaint.setColor(mXAxis.getAxisLineColor()); + mAxisLinePaint.setStrokeWidth(mXAxis.getAxisLineWidth()); + + if (mXAxis.getPosition() == XAxisPosition.TOP + || mXAxis.getPosition() == XAxisPosition.TOP_INSIDE + || mXAxis.getPosition() == XAxisPosition.BOTH_SIDED) { + c.drawLine(mViewPortHandler.contentRight(), + mViewPortHandler.contentTop(), mViewPortHandler.contentRight(), + mViewPortHandler.contentBottom(), mAxisLinePaint); + } + + if (mXAxis.getPosition() == XAxisPosition.BOTTOM + || mXAxis.getPosition() == XAxisPosition.BOTTOM_INSIDE + || mXAxis.getPosition() == XAxisPosition.BOTH_SIDED) { + c.drawLine(mViewPortHandler.contentLeft(), + mViewPortHandler.contentTop(), mViewPortHandler.contentLeft(), + mViewPortHandler.contentBottom(), mAxisLinePaint); + } + } + + protected Path mRenderLimitLinesPathBuffer = new Path(); + /** + * Draws the LimitLines associated with this axis to the screen. + * This is the standard YAxis renderer using the XAxis limit lines. + * + * @param c + */ + @Override + public void renderLimitLines(Canvas c) { + + List limitLines = mXAxis.getLimitLines(); + + if (limitLines == null || limitLines.size() <= 0) + return; + + float[] pts = mRenderLimitLinesBuffer; + pts[0] = 0; + pts[1] = 0; + + Path limitLinePath = mRenderLimitLinesPathBuffer; + limitLinePath.reset(); + + for (int i = 0; i < limitLines.size(); i++) { + + LimitLine l = limitLines.get(i); + + if(!l.isEnabled()) + continue; + + int clipRestoreCount = c.save(); + mLimitLineClippingRect.set(mViewPortHandler.getContentRect()); + mLimitLineClippingRect.inset(0.f, -l.getLineWidth()); + c.clipRect(mLimitLineClippingRect); + + mLimitLinePaint.setStyle(Paint.Style.STROKE); + mLimitLinePaint.setColor(l.getLineColor()); + mLimitLinePaint.setStrokeWidth(l.getLineWidth()); + mLimitLinePaint.setPathEffect(l.getDashPathEffect()); + + pts[1] = l.getLimit(); + + mTrans.pointValuesToPixel(pts); + + limitLinePath.moveTo(mViewPortHandler.contentLeft(), pts[1]); + limitLinePath.lineTo(mViewPortHandler.contentRight(), pts[1]); + + c.drawPath(limitLinePath, mLimitLinePaint); + limitLinePath.reset(); + // c.drawLines(pts, mLimitLinePaint); + + String label = l.getLabel(); + + // if drawing the limit-value label is enabled + if (label != null && !label.equals("")) { + + mLimitLinePaint.setStyle(l.getTextStyle()); + mLimitLinePaint.setPathEffect(null); + mLimitLinePaint.setColor(l.getTextColor()); + mLimitLinePaint.setStrokeWidth(0.5f); + mLimitLinePaint.setTextSize(l.getTextSize()); + + final float labelLineHeight = Utils.calcTextHeight(mLimitLinePaint, label); + float xOffset = Utils.convertDpToPixel(4f) + l.getXOffset(); + float yOffset = l.getLineWidth() + labelLineHeight + l.getYOffset(); + + final LimitLine.LimitLabelPosition position = l.getLabelPosition(); + + if (position == LimitLine.LimitLabelPosition.RIGHT_TOP) { + + mLimitLinePaint.setTextAlign(Align.RIGHT); + c.drawText(label, + mViewPortHandler.contentRight() - xOffset, + pts[1] - yOffset + labelLineHeight, mLimitLinePaint); + + } else if (position == LimitLine.LimitLabelPosition.RIGHT_BOTTOM) { + + mLimitLinePaint.setTextAlign(Align.RIGHT); + c.drawText(label, + mViewPortHandler.contentRight() - xOffset, + pts[1] + yOffset, mLimitLinePaint); + + } else if (position == LimitLine.LimitLabelPosition.LEFT_TOP) { + + mLimitLinePaint.setTextAlign(Align.LEFT); + c.drawText(label, + mViewPortHandler.contentLeft() + xOffset, + pts[1] - yOffset + labelLineHeight, mLimitLinePaint); + + } else { + + mLimitLinePaint.setTextAlign(Align.LEFT); + c.drawText(label, + mViewPortHandler.offsetLeft() + xOffset, + pts[1] + yOffset, mLimitLinePaint); + } + } + + c.restoreToCount(clipRestoreCount); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/XAxisRendererRadarChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/XAxisRendererRadarChart.java new file mode 100644 index 0000000..956e8c7 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/XAxisRendererRadarChart.java @@ -0,0 +1,71 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.PointF; + +import com.github.mikephil.charting.charts.RadarChart; +import com.github.mikephil.charting.components.XAxis; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +public class XAxisRendererRadarChart extends XAxisRenderer { + + private RadarChart mChart; + + public XAxisRendererRadarChart(ViewPortHandler viewPortHandler, XAxis xAxis, RadarChart chart) { + super(viewPortHandler, xAxis, null); + + mChart = chart; + } + + @Override + public void renderAxisLabels(Canvas c) { + + if (!mXAxis.isEnabled() || !mXAxis.isDrawLabelsEnabled()) + return; + + final float labelRotationAngleDegrees = mXAxis.getLabelRotationAngle(); + final MPPointF drawLabelAnchor = MPPointF.getInstance(0.5f, 0.25f); + + mAxisLabelPaint.setTypeface(mXAxis.getTypeface()); + mAxisLabelPaint.setTextSize(mXAxis.getTextSize()); + mAxisLabelPaint.setColor(mXAxis.getTextColor()); + + float sliceangle = mChart.getSliceAngle(); + + // calculate the factor that is needed for transforming the value to + // pixels + float factor = mChart.getFactor(); + + MPPointF center = mChart.getCenterOffsets(); + MPPointF pOut = MPPointF.getInstance(0,0); + for (int i = 0; i < mChart.getData().getMaxEntryCountSet().getEntryCount(); i++) { + + String label = mXAxis.getValueFormatter().getFormattedValue(i, mXAxis); + + float angle = (sliceangle * i + mChart.getRotationAngle()) % 360f; + + Utils.getPosition(center, mChart.getYRange() * factor + + mXAxis.mLabelRotatedWidth / 2f, angle, pOut); + + drawLabel(c, label, pOut.x, pOut.y - mXAxis.mLabelRotatedHeight / 2.f, + drawLabelAnchor, labelRotationAngleDegrees); + } + + MPPointF.recycleInstance(center); + MPPointF.recycleInstance(pOut); + MPPointF.recycleInstance(drawLabelAnchor); + } + + /** + * XAxis LimitLines on RadarChart not yet supported. + * + * @param c + */ + @Override + public void renderLimitLines(Canvas c) { + // this space intentionally left blank + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/YAxisRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/YAxisRenderer.java new file mode 100644 index 0000000..53cca7e --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/YAxisRenderer.java @@ -0,0 +1,352 @@ +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Path; +import android.graphics.RectF; + +import com.github.mikephil.charting.components.LimitLine; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.components.YAxis.AxisDependency; +import com.github.mikephil.charting.components.YAxis.YAxisLabelPosition; +import com.github.mikephil.charting.utils.MPPointD; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.util.List; + +public class YAxisRenderer extends AxisRenderer { + + protected YAxis mYAxis; + + protected Paint mZeroLinePaint; + + public YAxisRenderer(ViewPortHandler viewPortHandler, YAxis yAxis, Transformer trans) { + super(viewPortHandler, trans, yAxis); + + this.mYAxis = yAxis; + + if(mViewPortHandler != null) { + + mAxisLabelPaint.setColor(Color.BLACK); + mAxisLabelPaint.setTextSize(Utils.convertDpToPixel(10f)); + + mZeroLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mZeroLinePaint.setColor(Color.GRAY); + mZeroLinePaint.setStrokeWidth(1f); + mZeroLinePaint.setStyle(Paint.Style.STROKE); + } + } + + /** + * draws the y-axis labels to the screen + */ + @Override + public void renderAxisLabels(Canvas c) { + + if (!mYAxis.isEnabled() || !mYAxis.isDrawLabelsEnabled()) + return; + + float[] positions = getTransformedPositions(); + + mAxisLabelPaint.setTypeface(mYAxis.getTypeface()); + mAxisLabelPaint.setTextSize(mYAxis.getTextSize()); + mAxisLabelPaint.setColor(mYAxis.getTextColor()); + + float xoffset = mYAxis.getXOffset(); + float yoffset = Utils.calcTextHeight(mAxisLabelPaint, "A") / 2.5f + mYAxis.getYOffset(); + + AxisDependency dependency = mYAxis.getAxisDependency(); + YAxisLabelPosition labelPosition = mYAxis.getLabelPosition(); + + float xPos = 0f; + + if (dependency == AxisDependency.LEFT) { + + if (labelPosition == YAxisLabelPosition.OUTSIDE_CHART) { + mAxisLabelPaint.setTextAlign(Align.RIGHT); + xPos = mViewPortHandler.offsetLeft() - xoffset; + } else { + mAxisLabelPaint.setTextAlign(Align.LEFT); + xPos = mViewPortHandler.offsetLeft() + xoffset; + } + + } else { + + if (labelPosition == YAxisLabelPosition.OUTSIDE_CHART) { + mAxisLabelPaint.setTextAlign(Align.LEFT); + xPos = mViewPortHandler.contentRight() + xoffset; + } else { + mAxisLabelPaint.setTextAlign(Align.RIGHT); + xPos = mViewPortHandler.contentRight() - xoffset; + } + } + + drawYLabels(c, xPos, positions, yoffset); + } + + @Override + public void renderAxisLine(Canvas c) { + + if (!mYAxis.isEnabled() || !mYAxis.isDrawAxisLineEnabled()) + return; + + mAxisLinePaint.setColor(mYAxis.getAxisLineColor()); + mAxisLinePaint.setStrokeWidth(mYAxis.getAxisLineWidth()); + + if (mYAxis.getAxisDependency() == AxisDependency.LEFT) { + c.drawLine(mViewPortHandler.contentLeft(), mViewPortHandler.contentTop(), mViewPortHandler.contentLeft(), + mViewPortHandler.contentBottom(), mAxisLinePaint); + } else { + c.drawLine(mViewPortHandler.contentRight(), mViewPortHandler.contentTop(), mViewPortHandler.contentRight(), + mViewPortHandler.contentBottom(), mAxisLinePaint); + } + } + + /** + * draws the y-labels on the specified x-position + * + * @param fixedPosition + * @param positions + */ + protected void drawYLabels(Canvas c, float fixedPosition, float[] positions, float offset) { + + final int from = mYAxis.isDrawBottomYLabelEntryEnabled() ? 0 : 1; + final int to = mYAxis.isDrawTopYLabelEntryEnabled() + ? mYAxis.mEntryCount + : (mYAxis.mEntryCount - 1); + + float xOffset = mYAxis.getLabelXOffset(); + + // draw + for (int i = from; i < to; i++) { + + String text = mYAxis.getFormattedLabel(i); + + c.drawText(text, + fixedPosition + xOffset, + positions[i * 2 + 1] + offset, + mAxisLabelPaint); + } + } + + protected Path mRenderGridLinesPath = new Path(); + @Override + public void renderGridLines(Canvas c) { + + if (!mYAxis.isEnabled()) + return; + + if (mYAxis.isDrawGridLinesEnabled()) { + + int clipRestoreCount = c.save(); + c.clipRect(getGridClippingRect()); + + float[] positions = getTransformedPositions(); + + mGridPaint.setColor(mYAxis.getGridColor()); + mGridPaint.setStrokeWidth(mYAxis.getGridLineWidth()); + mGridPaint.setPathEffect(mYAxis.getGridDashPathEffect()); + + Path gridLinePath = mRenderGridLinesPath; + gridLinePath.reset(); + + // draw the grid + for (int i = 0; i < positions.length; i += 2) { + + // draw a path because lines don't support dashing on lower android versions + c.drawPath(linePath(gridLinePath, i, positions), mGridPaint); + gridLinePath.reset(); + } + + c.restoreToCount(clipRestoreCount); + } + + if (mYAxis.isDrawZeroLineEnabled()) { + drawZeroLine(c); + } + } + + protected RectF mGridClippingRect = new RectF(); + + public RectF getGridClippingRect() { + mGridClippingRect.set(mViewPortHandler.getContentRect()); + mGridClippingRect.inset(0.f, -mAxis.getGridLineWidth()); + return mGridClippingRect; + } + + /** + * Calculates the path for a grid line. + * + * @param p + * @param i + * @param positions + * @return + */ + protected Path linePath(Path p, int i, float[] positions) { + + p.moveTo(mViewPortHandler.offsetLeft(), positions[i + 1]); + p.lineTo(mViewPortHandler.contentRight(), positions[i + 1]); + + return p; + } + + protected float[] mGetTransformedPositionsBuffer = new float[2]; + /** + * Transforms the values contained in the axis entries to screen pixels and returns them in form of a float array + * of x- and y-coordinates. + * + * @return + */ + protected float[] getTransformedPositions() { + + if(mGetTransformedPositionsBuffer.length != mYAxis.mEntryCount * 2){ + mGetTransformedPositionsBuffer = new float[mYAxis.mEntryCount * 2]; + } + float[] positions = mGetTransformedPositionsBuffer; + + for (int i = 0; i < positions.length; i += 2) { + // only fill y values, x values are not needed for y-labels + positions[i + 1] = mYAxis.mEntries[i / 2]; + } + + mTrans.pointValuesToPixel(positions); + return positions; + } + + protected Path mDrawZeroLinePath = new Path(); + protected RectF mZeroLineClippingRect = new RectF(); + + /** + * Draws the zero line. + */ + protected void drawZeroLine(Canvas c) { + + int clipRestoreCount = c.save(); + mZeroLineClippingRect.set(mViewPortHandler.getContentRect()); + mZeroLineClippingRect.inset(0.f, -mYAxis.getZeroLineWidth()); + c.clipRect(mZeroLineClippingRect); + + // draw zero line + MPPointD pos = mTrans.getPixelForValues(0f, 0f); + + mZeroLinePaint.setColor(mYAxis.getZeroLineColor()); + mZeroLinePaint.setStrokeWidth(mYAxis.getZeroLineWidth()); + + Path zeroLinePath = mDrawZeroLinePath; + zeroLinePath.reset(); + + zeroLinePath.moveTo(mViewPortHandler.contentLeft(), (float) pos.y); + zeroLinePath.lineTo(mViewPortHandler.contentRight(), (float) pos.y); + + // draw a path because lines don't support dashing on lower android versions + c.drawPath(zeroLinePath, mZeroLinePaint); + + c.restoreToCount(clipRestoreCount); + } + + protected Path mRenderLimitLines = new Path(); + protected float[] mRenderLimitLinesBuffer = new float[2]; + protected RectF mLimitLineClippingRect = new RectF(); + /** + * Draws the LimitLines associated with this axis to the screen. + * + * @param c + */ + @Override + public void renderLimitLines(Canvas c) { + + List limitLines = mYAxis.getLimitLines(); + + if (limitLines == null || limitLines.size() <= 0) + return; + + float[] pts = mRenderLimitLinesBuffer; + pts[0] = 0; + pts[1] = 0; + Path limitLinePath = mRenderLimitLines; + limitLinePath.reset(); + + for (int i = 0; i < limitLines.size(); i++) { + + LimitLine l = limitLines.get(i); + + if (!l.isEnabled()) + continue; + + int clipRestoreCount = c.save(); + mLimitLineClippingRect.set(mViewPortHandler.getContentRect()); + mLimitLineClippingRect.inset(0.f, -l.getLineWidth()); + c.clipRect(mLimitLineClippingRect); + + mLimitLinePaint.setStyle(Paint.Style.STROKE); + mLimitLinePaint.setColor(l.getLineColor()); + mLimitLinePaint.setStrokeWidth(l.getLineWidth()); + mLimitLinePaint.setPathEffect(l.getDashPathEffect()); + + pts[1] = l.getLimit(); + + mTrans.pointValuesToPixel(pts); + + limitLinePath.moveTo(mViewPortHandler.contentLeft(), pts[1]); + limitLinePath.lineTo(mViewPortHandler.contentRight(), pts[1]); + + c.drawPath(limitLinePath, mLimitLinePaint); + limitLinePath.reset(); + // c.drawLines(pts, mLimitLinePaint); + + String label = l.getLabel(); + + // if drawing the limit-value label is enabled + if (label != null && !label.equals("")) { + + mLimitLinePaint.setStyle(l.getTextStyle()); + mLimitLinePaint.setPathEffect(null); + mLimitLinePaint.setColor(l.getTextColor()); + mLimitLinePaint.setTypeface(l.getTypeface()); + mLimitLinePaint.setStrokeWidth(0.5f); + mLimitLinePaint.setTextSize(l.getTextSize()); + + final float labelLineHeight = Utils.calcTextHeight(mLimitLinePaint, label); + float xOffset = Utils.convertDpToPixel(4f) + l.getXOffset(); + float yOffset = l.getLineWidth() + labelLineHeight + l.getYOffset(); + + final LimitLine.LimitLabelPosition position = l.getLabelPosition(); + + if (position == LimitLine.LimitLabelPosition.RIGHT_TOP) { + + mLimitLinePaint.setTextAlign(Align.RIGHT); + c.drawText(label, + mViewPortHandler.contentRight() - xOffset, + pts[1] - yOffset + labelLineHeight, mLimitLinePaint); + + } else if (position == LimitLine.LimitLabelPosition.RIGHT_BOTTOM) { + + mLimitLinePaint.setTextAlign(Align.RIGHT); + c.drawText(label, + mViewPortHandler.contentRight() - xOffset, + pts[1] + yOffset, mLimitLinePaint); + + } else if (position == LimitLine.LimitLabelPosition.LEFT_TOP) { + + mLimitLinePaint.setTextAlign(Align.LEFT); + c.drawText(label, + mViewPortHandler.contentLeft() + xOffset, + pts[1] - yOffset + labelLineHeight, mLimitLinePaint); + + } else { + + mLimitLinePaint.setTextAlign(Align.LEFT); + c.drawText(label, + mViewPortHandler.offsetLeft() + xOffset, + pts[1] + yOffset, mLimitLinePaint); + } + } + + c.restoreToCount(clipRestoreCount); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/YAxisRendererHorizontalBarChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/YAxisRendererHorizontalBarChart.java new file mode 100644 index 0000000..fedf805 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/YAxisRendererHorizontalBarChart.java @@ -0,0 +1,315 @@ + +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Path; +import android.graphics.RectF; + +import com.github.mikephil.charting.components.LimitLine; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.components.YAxis.AxisDependency; +import com.github.mikephil.charting.components.YAxis.YAxisLabelPosition; +import com.github.mikephil.charting.utils.MPPointD; +import com.github.mikephil.charting.utils.Transformer; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.util.List; + +public class YAxisRendererHorizontalBarChart extends YAxisRenderer { + + public YAxisRendererHorizontalBarChart(ViewPortHandler viewPortHandler, YAxis yAxis, + Transformer trans) { + super(viewPortHandler, yAxis, trans); + + mLimitLinePaint.setTextAlign(Align.LEFT); + } + + /** + * Computes the axis values. + * + * @param yMin - the minimum y-value in the data object for this axis + * @param yMax - the maximum y-value in the data object for this axis + */ + @Override + public void computeAxis(float yMin, float yMax, boolean inverted) { + + // calculate the starting and entry point of the y-labels (depending on + // zoom / contentrect bounds) + if (mViewPortHandler.contentHeight() > 10 && !mViewPortHandler.isFullyZoomedOutX()) { + + MPPointD p1 = mTrans.getValuesByTouchPoint(mViewPortHandler.contentLeft(), + mViewPortHandler.contentTop()); + MPPointD p2 = mTrans.getValuesByTouchPoint(mViewPortHandler.contentRight(), + mViewPortHandler.contentTop()); + + if (!inverted) { + yMin = (float) p1.x; + yMax = (float) p2.x; + } else { + yMin = (float) p2.x; + yMax = (float) p1.x; + } + + MPPointD.recycleInstance(p1); + MPPointD.recycleInstance(p2); + } + + computeAxisValues(yMin, yMax); + } + + /** + * draws the y-axis labels to the screen + */ + @Override + public void renderAxisLabels(Canvas c) { + + if (!mYAxis.isEnabled() || !mYAxis.isDrawLabelsEnabled()) + return; + + float[] positions = getTransformedPositions(); + + mAxisLabelPaint.setTypeface(mYAxis.getTypeface()); + mAxisLabelPaint.setTextSize(mYAxis.getTextSize()); + mAxisLabelPaint.setColor(mYAxis.getTextColor()); + mAxisLabelPaint.setTextAlign(Align.CENTER); + + float baseYOffset = Utils.convertDpToPixel(2.5f); + float textHeight = Utils.calcTextHeight(mAxisLabelPaint, "Q"); + + AxisDependency dependency = mYAxis.getAxisDependency(); + YAxisLabelPosition labelPosition = mYAxis.getLabelPosition(); + + float yPos = 0f; + + if (dependency == AxisDependency.LEFT) { + + if (labelPosition == YAxisLabelPosition.OUTSIDE_CHART) { + yPos = mViewPortHandler.contentTop() - baseYOffset; + } else { + yPos = mViewPortHandler.contentTop() - baseYOffset; + } + + } else { + + if (labelPosition == YAxisLabelPosition.OUTSIDE_CHART) { + yPos = mViewPortHandler.contentBottom() + textHeight + baseYOffset; + } else { + yPos = mViewPortHandler.contentBottom() + textHeight + baseYOffset; + } + } + + drawYLabels(c, yPos, positions, mYAxis.getYOffset()); + } + + @Override + public void renderAxisLine(Canvas c) { + + if (!mYAxis.isEnabled() || !mYAxis.isDrawAxisLineEnabled()) + return; + + mAxisLinePaint.setColor(mYAxis.getAxisLineColor()); + mAxisLinePaint.setStrokeWidth(mYAxis.getAxisLineWidth()); + + if (mYAxis.getAxisDependency() == AxisDependency.LEFT) { + c.drawLine(mViewPortHandler.contentLeft(), + mViewPortHandler.contentTop(), mViewPortHandler.contentRight(), + mViewPortHandler.contentTop(), mAxisLinePaint); + } else { + c.drawLine(mViewPortHandler.contentLeft(), + mViewPortHandler.contentBottom(), mViewPortHandler.contentRight(), + mViewPortHandler.contentBottom(), mAxisLinePaint); + } + } + + /** + * draws the y-labels on the specified x-position + * + * @param fixedPosition + * @param positions + */ + @Override + protected void drawYLabels(Canvas c, float fixedPosition, float[] positions, float offset) { + + mAxisLabelPaint.setTypeface(mYAxis.getTypeface()); + mAxisLabelPaint.setTextSize(mYAxis.getTextSize()); + mAxisLabelPaint.setColor(mYAxis.getTextColor()); + + final int from = mYAxis.isDrawBottomYLabelEntryEnabled() ? 0 : 1; + final int to = mYAxis.isDrawTopYLabelEntryEnabled() + ? mYAxis.mEntryCount + : (mYAxis.mEntryCount - 1); + + float xOffset = mYAxis.getLabelXOffset(); + + for (int i = from; i < to; i++) { + + String text = mYAxis.getFormattedLabel(i); + + c.drawText(text, + positions[i * 2], + fixedPosition - offset + xOffset, + mAxisLabelPaint); + } + } + + @Override + protected float[] getTransformedPositions() { + + if(mGetTransformedPositionsBuffer.length != mYAxis.mEntryCount * 2) { + mGetTransformedPositionsBuffer = new float[mYAxis.mEntryCount * 2]; + } + float[] positions = mGetTransformedPositionsBuffer; + + for (int i = 0; i < positions.length; i += 2) { + // only fill x values, y values are not needed for x-labels + positions[i] = mYAxis.mEntries[i / 2]; + } + + mTrans.pointValuesToPixel(positions); + return positions; + } + + @Override + public RectF getGridClippingRect() { + mGridClippingRect.set(mViewPortHandler.getContentRect()); + mGridClippingRect.inset(-mAxis.getGridLineWidth(), 0.f); + return mGridClippingRect; + } + + @Override + protected Path linePath(Path p, int i, float[] positions) { + + p.moveTo(positions[i], mViewPortHandler.contentTop()); + p.lineTo(positions[i], mViewPortHandler.contentBottom()); + + return p; + } + + protected Path mDrawZeroLinePathBuffer = new Path(); + + @Override + protected void drawZeroLine(Canvas c) { + + int clipRestoreCount = c.save(); + mZeroLineClippingRect.set(mViewPortHandler.getContentRect()); + mZeroLineClippingRect.inset(-mYAxis.getZeroLineWidth(), 0.f); + c.clipRect(mLimitLineClippingRect); + + // draw zero line + MPPointD pos = mTrans.getPixelForValues(0f, 0f); + + mZeroLinePaint.setColor(mYAxis.getZeroLineColor()); + mZeroLinePaint.setStrokeWidth(mYAxis.getZeroLineWidth()); + + Path zeroLinePath = mDrawZeroLinePathBuffer; + zeroLinePath.reset(); + + zeroLinePath.moveTo((float) pos.x - 1, mViewPortHandler.contentTop()); + zeroLinePath.lineTo((float) pos.x - 1, mViewPortHandler.contentBottom()); + + // draw a path because lines don't support dashing on lower android versions + c.drawPath(zeroLinePath, mZeroLinePaint); + + c.restoreToCount(clipRestoreCount); + } + + protected Path mRenderLimitLinesPathBuffer = new Path(); + protected float[] mRenderLimitLinesBuffer = new float[4]; + /** + * Draws the LimitLines associated with this axis to the screen. + * This is the standard XAxis renderer using the YAxis limit lines. + * + * @param c + */ + @Override + public void renderLimitLines(Canvas c) { + + List limitLines = mYAxis.getLimitLines(); + + if (limitLines == null || limitLines.size() <= 0) + return; + + float[] pts = mRenderLimitLinesBuffer; + pts[0] = 0; + pts[1] = 0; + pts[2] = 0; + pts[3] = 0; + Path limitLinePath = mRenderLimitLinesPathBuffer; + limitLinePath.reset(); + + for (int i = 0; i < limitLines.size(); i++) { + + LimitLine l = limitLines.get(i); + + if (!l.isEnabled()) + continue; + + int clipRestoreCount = c.save(); + mLimitLineClippingRect.set(mViewPortHandler.getContentRect()); + mLimitLineClippingRect.inset(-l.getLineWidth(), 0.f); + c.clipRect(mLimitLineClippingRect); + + pts[0] = l.getLimit(); + pts[2] = l.getLimit(); + + mTrans.pointValuesToPixel(pts); + + pts[1] = mViewPortHandler.contentTop(); + pts[3] = mViewPortHandler.contentBottom(); + + limitLinePath.moveTo(pts[0], pts[1]); + limitLinePath.lineTo(pts[2], pts[3]); + + mLimitLinePaint.setStyle(Paint.Style.STROKE); + mLimitLinePaint.setColor(l.getLineColor()); + mLimitLinePaint.setPathEffect(l.getDashPathEffect()); + mLimitLinePaint.setStrokeWidth(l.getLineWidth()); + + c.drawPath(limitLinePath, mLimitLinePaint); + limitLinePath.reset(); + + String label = l.getLabel(); + + // if drawing the limit-value label is enabled + if (label != null && !label.equals("")) { + + mLimitLinePaint.setStyle(l.getTextStyle()); + mLimitLinePaint.setPathEffect(null); + mLimitLinePaint.setColor(l.getTextColor()); + mLimitLinePaint.setTypeface(l.getTypeface()); + mLimitLinePaint.setStrokeWidth(0.5f); + mLimitLinePaint.setTextSize(l.getTextSize()); + + float xOffset = l.getLineWidth() + l.getXOffset(); + float yOffset = Utils.convertDpToPixel(2f) + l.getYOffset(); + + final LimitLine.LimitLabelPosition position = l.getLabelPosition(); + + if (position == LimitLine.LimitLabelPosition.RIGHT_TOP) { + + final float labelLineHeight = Utils.calcTextHeight(mLimitLinePaint, label); + mLimitLinePaint.setTextAlign(Align.LEFT); + c.drawText(label, pts[0] + xOffset, mViewPortHandler.contentTop() + yOffset + labelLineHeight, mLimitLinePaint); + } else if (position == LimitLine.LimitLabelPosition.RIGHT_BOTTOM) { + + mLimitLinePaint.setTextAlign(Align.LEFT); + c.drawText(label, pts[0] + xOffset, mViewPortHandler.contentBottom() - yOffset, mLimitLinePaint); + } else if (position == LimitLine.LimitLabelPosition.LEFT_TOP) { + + mLimitLinePaint.setTextAlign(Align.RIGHT); + final float labelLineHeight = Utils.calcTextHeight(mLimitLinePaint, label); + c.drawText(label, pts[0] - xOffset, mViewPortHandler.contentTop() + yOffset + labelLineHeight, mLimitLinePaint); + } else { + + mLimitLinePaint.setTextAlign(Align.RIGHT); + c.drawText(label, pts[0] - xOffset, mViewPortHandler.contentBottom() - yOffset, mLimitLinePaint); + } + } + + c.restoreToCount(clipRestoreCount); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/YAxisRendererRadarChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/YAxisRendererRadarChart.java new file mode 100644 index 0000000..f7b1ad9 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/YAxisRendererRadarChart.java @@ -0,0 +1,232 @@ +package com.github.mikephil.charting.renderer; + +import android.graphics.Canvas; +import android.graphics.Path; +import android.graphics.PointF; + +import com.github.mikephil.charting.charts.RadarChart; +import com.github.mikephil.charting.components.LimitLine; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.utils.MPPointF; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +import java.util.List; + +public class YAxisRendererRadarChart extends YAxisRenderer { + + private RadarChart mChart; + + public YAxisRendererRadarChart(ViewPortHandler viewPortHandler, YAxis yAxis, RadarChart chart) { + super(viewPortHandler, yAxis, null); + + this.mChart = chart; + } + + @Override + protected void computeAxisValues(float min, float max) { + + float yMin = min; + float yMax = max; + + int labelCount = mAxis.getLabelCount(); + double range = Math.abs(yMax - yMin); + + if (labelCount == 0 || range <= 0 || Double.isInfinite(range)) { + mAxis.mEntries = new float[]{}; + mAxis.mCenteredEntries = new float[]{}; + mAxis.mEntryCount = 0; + return; + } + + // Find out how much spacing (in y value space) between axis values + double rawInterval = range / labelCount; + double interval = Utils.roundToNextSignificant(rawInterval); + + // If granularity is enabled, then do not allow the interval to go below specified granularity. + // This is used to avoid repeated values when rounding values for display. + if (mAxis.isGranularityEnabled()) + interval = interval < mAxis.getGranularity() ? mAxis.getGranularity() : interval; + + // Normalize interval + double intervalMagnitude = Utils.roundToNextSignificant(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 + // if it's 0.0 after floor(), we use the old value + interval = Math.floor(10.0 * intervalMagnitude) == 0.0 + ? interval + : Math.floor(10.0 * intervalMagnitude); + } + + boolean centeringEnabled = mAxis.isCenterAxisLabelsEnabled(); + int n = centeringEnabled ? 1 : 0; + + // force label count + if (mAxis.isForceLabelsEnabled()) { + + float step = (float) range / (float) (labelCount - 1); + mAxis.mEntryCount = labelCount; + + if (mAxis.mEntries.length < labelCount) { + // Ensure stops contains at least numStops elements. + mAxis.mEntries = new float[labelCount]; + } + + float v = min; + + for (int i = 0; i < labelCount; i++) { + mAxis.mEntries[i] = v; + v += step; + } + + n = labelCount; + + // no forced count + } else { + + double first = interval == 0.0 ? 0.0 : Math.ceil(yMin / interval) * interval; + if (centeringEnabled) { + first -= interval; + } + + double last = interval == 0.0 ? 0.0 : Utils.nextUp(Math.floor(yMax / interval) * interval); + + double f; + int i; + + if (interval != 0.0) { + for (f = first; f <= last; f += interval) { + ++n; + } + } + + n++; + + mAxis.mEntryCount = n; + + if (mAxis.mEntries.length < n) { + // Ensure stops contains at least numStops elements. + mAxis.mEntries = new float[n]; + } + + for (f = first, i = 0; i < n; f += interval, ++i) { + + if (f == 0.0) // Fix for negative zero case (Where value == -0.0, and 0.0 == -0.0) + f = 0.0; + + mAxis.mEntries[i] = (float) f; + } + } + + // set decimals + if (interval < 1) { + mAxis.mDecimals = (int) Math.ceil(-Math.log10(interval)); + } else { + mAxis.mDecimals = 0; + } + + if (centeringEnabled) { + + if (mAxis.mCenteredEntries.length < n) { + mAxis.mCenteredEntries = new float[n]; + } + + float offset = (mAxis.mEntries[1] - mAxis.mEntries[0]) / 2f; + + for (int i = 0; i < n; i++) { + mAxis.mCenteredEntries[i] = mAxis.mEntries[i] + offset; + } + } + + mAxis.mAxisMinimum = mAxis.mEntries[0]; + mAxis.mAxisMaximum = mAxis.mEntries[n-1]; + mAxis.mAxisRange = Math.abs(mAxis.mAxisMaximum - mAxis.mAxisMinimum); + } + + @Override + public void renderAxisLabels(Canvas c) { + + if (!mYAxis.isEnabled() || !mYAxis.isDrawLabelsEnabled()) + return; + + mAxisLabelPaint.setTypeface(mYAxis.getTypeface()); + mAxisLabelPaint.setTextSize(mYAxis.getTextSize()); + mAxisLabelPaint.setColor(mYAxis.getTextColor()); + + MPPointF center = mChart.getCenterOffsets(); + MPPointF pOut = MPPointF.getInstance(0,0); + float factor = mChart.getFactor(); + + final int from = mYAxis.isDrawBottomYLabelEntryEnabled() ? 0 : 1; + final int to = mYAxis.isDrawTopYLabelEntryEnabled() + ? mYAxis.mEntryCount + : (mYAxis.mEntryCount - 1); + + float xOffset = mYAxis.getLabelXOffset(); + + for (int j = from; j < to; j++) { + + float r = (mYAxis.mEntries[j] - mYAxis.mAxisMinimum) * factor; + + Utils.getPosition(center, r, mChart.getRotationAngle(), pOut); + + String label = mYAxis.getFormattedLabel(j); + + c.drawText(label, pOut.x + xOffset, pOut.y, mAxisLabelPaint); + } + MPPointF.recycleInstance(center); + MPPointF.recycleInstance(pOut); + } + + private Path mRenderLimitLinesPathBuffer = new Path(); + @Override + public void renderLimitLines(Canvas c) { + + List limitLines = mYAxis.getLimitLines(); + + if (limitLines == null) + return; + + float sliceangle = mChart.getSliceAngle(); + + // calculate the factor that is needed for transforming the value to + // pixels + float factor = mChart.getFactor(); + + MPPointF center = mChart.getCenterOffsets(); + MPPointF pOut = MPPointF.getInstance(0,0); + for (int i = 0; i < limitLines.size(); i++) { + + LimitLine l = limitLines.get(i); + + if (!l.isEnabled()) + continue; + + mLimitLinePaint.setColor(l.getLineColor()); + mLimitLinePaint.setPathEffect(l.getDashPathEffect()); + mLimitLinePaint.setStrokeWidth(l.getLineWidth()); + + float r = (l.getLimit() - mChart.getYChartMin()) * factor; + + Path limitPath = mRenderLimitLinesPathBuffer; + limitPath.reset(); + + + for (int j = 0; j < mChart.getData().getMaxEntryCountSet().getEntryCount(); j++) { + + Utils.getPosition(center, r, sliceangle * j + mChart.getRotationAngle(), pOut); + + if (j == 0) + limitPath.moveTo(pOut.x, pOut.y); + else + limitPath.lineTo(pOut.x, pOut.y); + } + limitPath.close(); + + c.drawPath(limitPath, mLimitLinePaint); + } + MPPointF.recycleInstance(center); + MPPointF.recycleInstance(pOut); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/ChevronDownShapeRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/ChevronDownShapeRenderer.java new file mode 100644 index 0000000..9328b27 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/ChevronDownShapeRenderer.java @@ -0,0 +1,41 @@ +package com.github.mikephil.charting.renderer.scatter; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import com.github.mikephil.charting.interfaces.datasets.IScatterDataSet; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by wajdic on 15/06/2016. + * Created at Time 09:08 + */ +public class ChevronDownShapeRenderer implements IShapeRenderer +{ + + + @Override + public void renderShape(Canvas c, IScatterDataSet dataSet, ViewPortHandler viewPortHandler, + float posX, float posY, Paint renderPaint) { + + final float shapeHalf = dataSet.getScatterShapeSize() / 2f; + + renderPaint.setStyle(Paint.Style.STROKE); + renderPaint.setStrokeWidth(Utils.convertDpToPixel(1f)); + + c.drawLine( + posX, + posY + (2 * shapeHalf), + posX + (2 * shapeHalf), + posY, + renderPaint); + + c.drawLine( + posX, + posY + (2 * shapeHalf), + posX - (2 * shapeHalf), + posY, + renderPaint); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/ChevronUpShapeRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/ChevronUpShapeRenderer.java new file mode 100644 index 0000000..6dea0ab --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/ChevronUpShapeRenderer.java @@ -0,0 +1,42 @@ +package com.github.mikephil.charting.renderer.scatter; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import com.github.mikephil.charting.interfaces.datasets.IScatterDataSet; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by wajdic on 15/06/2016. + * Created at Time 09:08 + */ +public class ChevronUpShapeRenderer implements IShapeRenderer +{ + + + @Override + public void renderShape(Canvas c, IScatterDataSet dataSet, ViewPortHandler viewPortHandler, + float posX, float posY, Paint renderPaint) { + + final float shapeHalf = dataSet.getScatterShapeSize() / 2f; + + renderPaint.setStyle(Paint.Style.STROKE); + renderPaint.setStrokeWidth(Utils.convertDpToPixel(1f)); + + c.drawLine( + posX, + posY - (2 * shapeHalf), + posX + (2 * shapeHalf), + posY, + renderPaint); + + c.drawLine( + posX, + posY - (2 * shapeHalf), + posX - (2 * shapeHalf), + posY, + renderPaint); + + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/CircleShapeRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/CircleShapeRenderer.java new file mode 100644 index 0000000..ac7abb9 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/CircleShapeRenderer.java @@ -0,0 +1,63 @@ +package com.github.mikephil.charting.renderer.scatter; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import com.github.mikephil.charting.interfaces.datasets.IScatterDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by wajdic on 15/06/2016. + * Created at Time 09:08 + */ +public class CircleShapeRenderer implements IShapeRenderer +{ + + @Override + public void renderShape(Canvas c, IScatterDataSet dataSet, ViewPortHandler viewPortHandler, + float posX, float posY, Paint renderPaint) { + + final float shapeSize = dataSet.getScatterShapeSize(); + final float shapeHalf = shapeSize / 2f; + final float shapeHoleSizeHalf = Utils.convertDpToPixel(dataSet.getScatterShapeHoleRadius()); + final float shapeHoleSize = shapeHoleSizeHalf * 2.f; + final float shapeStrokeSize = (shapeSize - shapeHoleSize) / 2.f; + final float shapeStrokeSizeHalf = shapeStrokeSize / 2.f; + + final int shapeHoleColor = dataSet.getScatterShapeHoleColor(); + + if (shapeSize > 0.0) { + renderPaint.setStyle(Paint.Style.STROKE); + renderPaint.setStrokeWidth(shapeStrokeSize); + + c.drawCircle( + posX, + posY, + shapeHoleSizeHalf + shapeStrokeSizeHalf, + renderPaint); + + if (shapeHoleColor != ColorTemplate.COLOR_NONE) { + renderPaint.setStyle(Paint.Style.FILL); + + renderPaint.setColor(shapeHoleColor); + c.drawCircle( + posX, + posY, + shapeHoleSizeHalf, + renderPaint); + } + } else { + renderPaint.setStyle(Paint.Style.FILL); + + c.drawCircle( + posX, + posY, + shapeHalf, + renderPaint); + } + + } + +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/CrossShapeRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/CrossShapeRenderer.java new file mode 100644 index 0000000..202670d --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/CrossShapeRenderer.java @@ -0,0 +1,41 @@ +package com.github.mikephil.charting.renderer.scatter; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import com.github.mikephil.charting.interfaces.datasets.IScatterDataSet; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by wajdic on 15/06/2016. + * Created at Time 09:08 + */ +public class CrossShapeRenderer implements IShapeRenderer +{ + + + @Override + public void renderShape(Canvas c, IScatterDataSet dataSet, ViewPortHandler viewPortHandler, + float posX, float posY, Paint renderPaint) { + + final float shapeHalf = dataSet.getScatterShapeSize() / 2f; + + renderPaint.setStyle(Paint.Style.STROKE); + renderPaint.setStrokeWidth(Utils.convertDpToPixel(1f)); + + c.drawLine( + posX - shapeHalf, + posY, + posX + shapeHalf, + posY, + renderPaint); + c.drawLine( + posX, + posY - shapeHalf, + posX, + posY + shapeHalf, + renderPaint); + + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/IShapeRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/IShapeRenderer.java new file mode 100644 index 0000000..20b57a9 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/IShapeRenderer.java @@ -0,0 +1,28 @@ +package com.github.mikephil.charting.renderer.scatter; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import com.github.mikephil.charting.interfaces.datasets.IScatterDataSet; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by wajdic on 15/06/2016. + * Created at Time 09:07 + */ +public interface IShapeRenderer +{ + + /** + * Renders the provided ScatterDataSet with a shape. + * + * @param c Canvas object for drawing the shape + * @param dataSet The DataSet to be drawn + * @param viewPortHandler Contains information about the current state of the view + * @param posX Position to draw the shape at + * @param posY Position to draw the shape at + * @param renderPaint Paint object used for styling and drawing + */ + void renderShape(Canvas c, IScatterDataSet dataSet, ViewPortHandler viewPortHandler, + float posX, float posY, Paint renderPaint); +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/SquareShapeRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/SquareShapeRenderer.java new file mode 100644 index 0000000..ac98679 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/SquareShapeRenderer.java @@ -0,0 +1,63 @@ +package com.github.mikephil.charting.renderer.scatter; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import com.github.mikephil.charting.interfaces.datasets.IScatterDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by wajdic on 15/06/2016. + * Created at Time 09:08 + */ +public class SquareShapeRenderer implements IShapeRenderer +{ + + + @Override + public void renderShape(Canvas c, IScatterDataSet dataSet, ViewPortHandler viewPortHandler, + float posX, float posY, Paint renderPaint) { + + final float shapeSize = dataSet.getScatterShapeSize(); + final float shapeHalf = shapeSize / 2f; + final float shapeHoleSizeHalf = Utils.convertDpToPixel(dataSet.getScatterShapeHoleRadius()); + final float shapeHoleSize = shapeHoleSizeHalf * 2.f; + final float shapeStrokeSize = (shapeSize - shapeHoleSize) / 2.f; + final float shapeStrokeSizeHalf = shapeStrokeSize / 2.f; + + final int shapeHoleColor = dataSet.getScatterShapeHoleColor(); + + if (shapeSize > 0.0) { + renderPaint.setStyle(Paint.Style.STROKE); + renderPaint.setStrokeWidth(shapeStrokeSize); + + c.drawRect(posX - shapeHoleSizeHalf - shapeStrokeSizeHalf, + posY - shapeHoleSizeHalf - shapeStrokeSizeHalf, + posX + shapeHoleSizeHalf + shapeStrokeSizeHalf, + posY + shapeHoleSizeHalf + shapeStrokeSizeHalf, + renderPaint); + + if (shapeHoleColor != ColorTemplate.COLOR_NONE) { + renderPaint.setStyle(Paint.Style.FILL); + + renderPaint.setColor(shapeHoleColor); + c.drawRect(posX - shapeHoleSizeHalf, + posY - shapeHoleSizeHalf, + posX + shapeHoleSizeHalf, + posY + shapeHoleSizeHalf, + renderPaint); + } + + } else { + renderPaint.setStyle(Paint.Style.FILL); + + c.drawRect(posX - shapeHalf, + posY - shapeHalf, + posX + shapeHalf, + posY + shapeHalf, + renderPaint); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/TriangleShapeRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/TriangleShapeRenderer.java new file mode 100644 index 0000000..5343454 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/TriangleShapeRenderer.java @@ -0,0 +1,80 @@ +package com.github.mikephil.charting.renderer.scatter; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; + +import com.github.mikephil.charting.interfaces.datasets.IScatterDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by wajdic on 15/06/2016. + * Created at Time 09:08 + */ +public class TriangleShapeRenderer implements IShapeRenderer +{ + + protected Path mTrianglePathBuffer = new Path(); + + @Override + public void renderShape(Canvas c, IScatterDataSet dataSet, ViewPortHandler viewPortHandler, + float posX, float posY, Paint renderPaint) { + + final float shapeSize = dataSet.getScatterShapeSize(); + final float shapeHalf = shapeSize / 2f; + final float shapeHoleSizeHalf = Utils.convertDpToPixel(dataSet.getScatterShapeHoleRadius()); + final float shapeHoleSize = shapeHoleSizeHalf * 2.f; + final float shapeStrokeSize = (shapeSize - shapeHoleSize) / 2.f; + + final int shapeHoleColor = dataSet.getScatterShapeHoleColor(); + + renderPaint.setStyle(Paint.Style.FILL); + + // create a triangle path + Path tri = mTrianglePathBuffer; + tri.reset(); + + tri.moveTo(posX, posY - shapeHalf); + tri.lineTo(posX + shapeHalf, posY + shapeHalf); + tri.lineTo(posX - shapeHalf, posY + shapeHalf); + + if (shapeSize > 0.0) { + tri.lineTo(posX, posY - shapeHalf); + + tri.moveTo(posX - shapeHalf + shapeStrokeSize, + posY + shapeHalf - shapeStrokeSize); + tri.lineTo(posX + shapeHalf - shapeStrokeSize, + posY + shapeHalf - shapeStrokeSize); + tri.lineTo(posX, + posY - shapeHalf + shapeStrokeSize); + tri.lineTo(posX - shapeHalf + shapeStrokeSize, + posY + shapeHalf - shapeStrokeSize); + } + + tri.close(); + + c.drawPath(tri, renderPaint); + tri.reset(); + + if (shapeSize > 0.0 && + shapeHoleColor != ColorTemplate.COLOR_NONE) { + + renderPaint.setColor(shapeHoleColor); + + tri.moveTo(posX, + posY - shapeHalf + shapeStrokeSize); + tri.lineTo(posX + shapeHalf - shapeStrokeSize, + posY + shapeHalf - shapeStrokeSize); + tri.lineTo(posX - shapeHalf + shapeStrokeSize, + posY + shapeHalf - shapeStrokeSize); + tri.close(); + + c.drawPath(tri, renderPaint); + tri.reset(); + } + + } + +} \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/XShapeRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/XShapeRenderer.java new file mode 100644 index 0000000..225640e --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/scatter/XShapeRenderer.java @@ -0,0 +1,42 @@ +package com.github.mikephil.charting.renderer.scatter; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import com.github.mikephil.charting.interfaces.datasets.IScatterDataSet; +import com.github.mikephil.charting.utils.Utils; +import com.github.mikephil.charting.utils.ViewPortHandler; + +/** + * Created by wajdic on 15/06/2016. + * Created at Time 09:08 + */ +public class XShapeRenderer implements IShapeRenderer +{ + + + @Override + public void renderShape(Canvas c, IScatterDataSet dataSet, ViewPortHandler viewPortHandler, + float posX, float posY, Paint renderPaint) { + + final float shapeHalf = dataSet.getScatterShapeSize() / 2f; + + renderPaint.setStyle(Paint.Style.STROKE); + renderPaint.setStrokeWidth(Utils.convertDpToPixel(1f)); + + c.drawLine( + posX - shapeHalf, + posY - shapeHalf, + posX + shapeHalf, + posY + shapeHalf, + renderPaint); + c.drawLine( + posX + shapeHalf, + posY - shapeHalf, + posX - shapeHalf, + posY + shapeHalf, + renderPaint); + + } + +} \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/ColorTemplate.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/ColorTemplate.java new file mode 100644 index 0000000..4d9c1de --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/ColorTemplate.java @@ -0,0 +1,128 @@ + +package com.github.mikephil.charting.utils; + +import android.content.res.Resources; +import android.graphics.Color; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class that holds predefined color integer arrays (e.g. + * ColorTemplate.VORDIPLOM_COLORS) and convenience methods for loading colors + * from resources. + * + * @author Philipp Jahoda + */ +public class ColorTemplate { + + /** + * an "invalid" color that indicates that no color is set + */ + public static final int COLOR_NONE = 0x00112233; + + /** + * this "color" is used for the Legend creation and indicates that the next + * form should be skipped + */ + public static final int COLOR_SKIP = 0x00112234; + + /** + * THE COLOR THEMES ARE PREDEFINED (predefined color integer arrays), FEEL + * FREE TO CREATE YOUR OWN WITH AS MANY DIFFERENT COLORS AS YOU WANT + */ + public static final int[] LIBERTY_COLORS = { + Color.rgb(207, 248, 246), Color.rgb(148, 212, 212), Color.rgb(136, 180, 187), + Color.rgb(118, 174, 175), Color.rgb(42, 109, 130) + }; + public static final int[] JOYFUL_COLORS = { + Color.rgb(217, 80, 138), Color.rgb(254, 149, 7), Color.rgb(254, 247, 120), + Color.rgb(106, 167, 134), Color.rgb(53, 194, 209) + }; + public static final int[] PASTEL_COLORS = { + Color.rgb(64, 89, 128), Color.rgb(149, 165, 124), Color.rgb(217, 184, 162), + Color.rgb(191, 134, 134), Color.rgb(179, 48, 80) + }; + public static final int[] COLORFUL_COLORS = { + Color.rgb(193, 37, 82), Color.rgb(255, 102, 0), Color.rgb(245, 199, 0), + Color.rgb(106, 150, 31), Color.rgb(179, 100, 53) + }; + public static final int[] VORDIPLOM_COLORS = { + Color.rgb(192, 255, 140), Color.rgb(255, 247, 140), Color.rgb(255, 208, 140), + Color.rgb(140, 234, 255), Color.rgb(255, 140, 157) + }; + public static final int[] MATERIAL_COLORS = { + rgb("#2ecc71"), rgb("#f1c40f"), rgb("#e74c3c"), rgb("#3498db") + }; + + /** + * Converts the given hex-color-string to rgb. + * + * @param hex + * @return + */ + public static int rgb(String hex) { + int color = (int) Long.parseLong(hex.replace("#", ""), 16); + int r = (color >> 16) & 0xFF; + int g = (color >> 8) & 0xFF; + int b = (color >> 0) & 0xFF; + return Color.rgb(r, g, b); + } + + /** + * Returns the Android ICS holo blue light color. + * + * @return + */ + public static int getHoloBlue() { + return Color.rgb(51, 181, 229); + } + + /** + * Sets the alpha component of the given color. + * + * @param color + * @param alpha 0 - 255 + * @return + */ + public static int colorWithAlpha(int color, int alpha) { + return (color & 0xffffff) | ((alpha & 0xff) << 24); + } + + /** + * turn an array of resource-colors (contains resource-id integers) into an + * array list of actual color integers + * + * @param r + * @param colors an integer array of resource id's of colors + * @return + */ + public static List createColors(Resources r, int[] colors) { + + List result = new ArrayList(); + + for (int i : colors) { + result.add(r.getColor(i)); + } + + return result; + } + + /** + * Turns an array of colors (integer color values) into an ArrayList of + * colors. + * + * @param colors + * @return + */ + public static List createColors(int[] colors) { + + List result = new ArrayList(); + + for (int i : colors) { + result.add(i); + } + + return result; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/EntryXComparator.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/EntryXComparator.java new file mode 100644 index 0000000..8f59c12 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/EntryXComparator.java @@ -0,0 +1,22 @@ +package com.github.mikephil.charting.utils; + +import com.github.mikephil.charting.data.Entry; + +import java.util.Comparator; + +/** + * Comparator for comparing Entry-objects by their x-value. + * Created by philipp on 17/06/15. + */ +public class EntryXComparator implements Comparator { + @Override + public int compare(Entry entry1, Entry entry2) { + float diff = entry1.getX() - entry2.getX(); + + if (diff == 0f) return 0; + else { + if (diff > 0f) return 1; + else return -1; + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/FSize.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/FSize.java new file mode 100644 index 0000000..a12bc45 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/FSize.java @@ -0,0 +1,79 @@ + +package com.github.mikephil.charting.utils; + +import java.util.List; + +/** + * Class for describing width and height dimensions in some arbitrary + * unit. Replacement for the android.Util.SizeF which is available only on API >= 21. + */ +public final class FSize extends ObjectPool.Poolable{ + + // TODO : Encapsulate width & height + + public float width; + public float height; + + private static ObjectPool pool; + + static { + pool = ObjectPool.create(256, new FSize(0,0)); + pool.setReplenishPercentage(0.5f); + } + + + protected ObjectPool.Poolable instantiate(){ + return new FSize(0,0); + } + + public static FSize getInstance(final float width, final float height){ + FSize result = pool.get(); + result.width = width; + result.height = height; + return result; + } + + public static void recycleInstance(FSize instance){ + pool.recycle(instance); + } + + public static void recycleInstances(List instances){ + pool.recycle(instances); + } + + public FSize() { + } + + public FSize(final float width, final float height) { + this.width = width; + this.height = height; + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (obj instanceof FSize) { + final FSize other = (FSize) obj; + return width == other.width && height == other.height; + } + return false; + } + + @Override + public String toString() { + return width + "x" + height; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Float.floatToIntBits(width) ^ Float.floatToIntBits(height); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/FileUtils.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/FileUtils.java new file mode 100644 index 0000000..5aff51f --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/FileUtils.java @@ -0,0 +1,301 @@ + +package com.github.mikephil.charting.utils; + +import android.content.res.AssetManager; +import android.os.Environment; +import android.util.Log; + +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.data.Entry; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +/** + * Utilities class for interacting with the assets and the devices storage to + * load and save DataSet objects from and to .txt files. + * + * @author Philipp Jahoda + */ +public class FileUtils { + + private static final String LOG = "MPChart-FileUtils"; + + /** + * Loads a an Array of Entries from a textfile from the sd-card. + * + * @param path the name of the file on the sd-card (+ path if needed) + * @return + */ + public static List loadEntriesFromFile(String path) { + + File sdcard = Environment.getExternalStorageDirectory(); + + // Get the text file + File file = new File(sdcard, path); + + List entries = new ArrayList(); + + try { + @SuppressWarnings("resource") + BufferedReader br = new BufferedReader(new FileReader(file)); + String line; + + while ((line = br.readLine()) != null) { + String[] split = line.split("#"); + + if (split.length <= 2) { + entries.add(new Entry(Float.parseFloat(split[0]), Integer.parseInt(split[1]))); + } else { + + float[] vals = new float[split.length - 1]; + + for (int i = 0; i < vals.length; i++) { + vals[i] = Float.parseFloat(split[i]); + } + + entries.add(new BarEntry(Integer.parseInt(split[split.length - 1]), vals)); + } + } + } catch (IOException e) { + Log.e(LOG, e.toString()); + } + + return entries; + + // File sdcard = Environment.getExternalStorageDirectory(); + // + // // Get the text file + // File file = new File(sdcard, path); + // + // List entries = new ArrayList(); + // String label = ""; + // + // try { + // @SuppressWarnings("resource") + // BufferedReader br = new BufferedReader(new FileReader(file)); + // String line = br.readLine(); + // + // // firstline is the label + // label = line; + // + // while ((line = br.readLine()) != null) { + // String[] split = line.split("#"); + // entries.add(new Entry(Float.parseFloat(split[0]), + // Integer.parseInt(split[1]))); + // } + // } catch (IOException e) { + // Log.e(LOG, e.toString()); + // } + // + // DataSet ds = new DataSet(entries, label); + // return ds; + } + + /** + * Loads an array of Entries from a textfile from the assets folder. + * + * @param am + * @param path the name of the file in the assets folder (+ path if needed) + * @return + */ + public static List loadEntriesFromAssets(AssetManager am, String path) { + + List entries = new ArrayList(); + + BufferedReader reader = null; + try { + reader = new BufferedReader( + new InputStreamReader(am.open(path), "UTF-8")); + + String line = reader.readLine(); + + while (line != null) { + // process line + String[] split = line.split("#"); + + if (split.length <= 2) { + entries.add(new Entry(Float.parseFloat(split[1]), Float.parseFloat(split[0]))); + } else { + + float[] vals = new float[split.length - 1]; + + for (int i = 0; i < vals.length; i++) { + vals[i] = Float.parseFloat(split[i]); + } + + entries.add(new BarEntry(Integer.parseInt(split[split.length - 1]), vals)); + } + line = reader.readLine(); + } + } catch (IOException e) { + Log.e(LOG, e.toString()); + + } finally { + + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + Log.e(LOG, e.toString()); + } + } + } + + return entries; + + // String label = null; + // List entries = new ArrayList(); + // + // BufferedReader reader = null; + // try { + // reader = new BufferedReader( + // new InputStreamReader(am.open(path), "UTF-8")); + // + // // do reading, usually loop until end of file reading + // label = reader.readLine(); + // String line = reader.readLine(); + // + // while (line != null) { + // // process line + // String[] split = line.split("#"); + // entries.add(new Entry(Float.parseFloat(split[0]), + // Integer.parseInt(split[1]))); + // line = reader.readLine(); + // } + // } catch (IOException e) { + // Log.e(LOG, e.toString()); + // + // } finally { + // + // if (reader != null) { + // try { + // reader.close(); + // } catch (IOException e) { + // Log.e(LOG, e.toString()); + // } + // } + // } + // + // DataSet ds = new DataSet(entries, label); + // return ds; + } + + /** + * Saves an Array of Entries to the specified location on the sdcard + * + * @param entries + * @param path + */ + public static void saveToSdCard(List entries, String path) { + + File sdcard = Environment.getExternalStorageDirectory(); + + File saved = new File(sdcard, path); + if (!saved.exists()) + { + try + { + saved.createNewFile(); + } catch (IOException e) + { + Log.e(LOG, e.toString()); + } + } + try + { + // BufferedWriter for performance, true to set append to file flag + BufferedWriter buf = new BufferedWriter(new FileWriter(saved, true)); + + for (Entry e : entries) { + + buf.append(e.getY() + "#" + e.getX()); + buf.newLine(); + } + + buf.close(); + } catch (IOException e) + { + Log.e(LOG, e.toString()); + } + } + + public static List loadBarEntriesFromAssets(AssetManager am, String path) { + + List entries = new ArrayList(); + + BufferedReader reader = null; + try { + reader = new BufferedReader( + new InputStreamReader(am.open(path), "UTF-8")); + + String line = reader.readLine(); + + while (line != null) { + // process line + String[] split = line.split("#"); + + entries.add(new BarEntry(Float.parseFloat(split[1]), Float.parseFloat(split[0]))); + + line = reader.readLine(); + } + } catch (IOException e) { + Log.e(LOG, e.toString()); + + } finally { + + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + Log.e(LOG, e.toString()); + } + } + } + + return entries; + + // String label = null; + // ArrayList entries = new ArrayList(); + // + // BufferedReader reader = null; + // try { + // reader = new BufferedReader( + // new InputStreamReader(am.open(path), "UTF-8")); + // + // // do reading, usually loop until end of file reading + // label = reader.readLine(); + // String line = reader.readLine(); + // + // while (line != null) { + // // process line + // String[] split = line.split("#"); + // entries.add(new Entry(Float.parseFloat(split[0]), + // Integer.parseInt(split[1]))); + // line = reader.readLine(); + // } + // } catch (IOException e) { + // Log.e(LOG, e.toString()); + // + // } finally { + // + // if (reader != null) { + // try { + // reader.close(); + // } catch (IOException e) { + // Log.e(LOG, e.toString()); + // } + // } + // } + // + // DataSet ds = new DataSet(entries, label); + // return ds; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/Fill.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/Fill.java new file mode 100644 index 0000000..d12e1fb --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/Fill.java @@ -0,0 +1,342 @@ +package com.github.mikephil.charting.utils; + +import android.graphics.Canvas; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class Fill +{ + public enum Type + { + EMPTY, COLOR, LINEAR_GRADIENT, DRAWABLE + } + + public enum Direction + { + DOWN, UP, RIGHT, LEFT + } + + /** + * the type of fill + */ + private Type mType = Type.EMPTY; + + /** + * the color that is used for filling + */ + @Nullable + private Integer mColor = null; + + private Integer mFinalColor = null; + + /** + * the drawable to be used for filling + */ + @Nullable + protected Drawable mDrawable; + + @Nullable + private int[] mGradientColors; + + @Nullable + private float[] mGradientPositions; + + /** + * transparency used for filling + */ + private int mAlpha = 255; + + public Fill() + { + } + + public Fill(int color) + { + this.mType = Type.COLOR; + this.mColor = color; + calculateFinalColor(); + } + + public Fill(int startColor, int endColor) + { + this.mType = Type.LINEAR_GRADIENT; + this.mGradientColors = new int[]{startColor, endColor}; + } + + public Fill(@NonNull int[] gradientColors) + { + this.mType = Type.LINEAR_GRADIENT; + this.mGradientColors = gradientColors; + } + + public Fill(@NonNull int[] gradientColors, @NonNull float[] gradientPositions) + { + this.mType = Type.LINEAR_GRADIENT; + this.mGradientColors = gradientColors; + this.mGradientPositions = gradientPositions; + } + + public Fill(@NonNull Drawable drawable) + { + this.mType = Type.DRAWABLE; + this.mDrawable = drawable; + } + + public Type getType() + { + return mType; + } + + public void setType(Type type) + { + this.mType = type; + } + + @Nullable + public Integer getColor() + { + return mColor; + } + + public void setColor(int color) + { + this.mColor = color; + calculateFinalColor(); + } + + public int[] getGradientColors() + { + return mGradientColors; + } + + public void setGradientColors(int[] colors) + { + this.mGradientColors = colors; + } + + public float[] getGradientPositions() + { + return mGradientPositions; + } + + public void setGradientPositions(float[] positions) + { + this.mGradientPositions = positions; + } + + public void setGradientColors(int startColor, int endColor) + { + this.mGradientColors = new int[]{startColor, endColor}; + } + + public int getAlpha() + { + return mAlpha; + } + + public void setAlpha(int alpha) + { + this.mAlpha = alpha; + calculateFinalColor(); + } + + private void calculateFinalColor() + { + if (mColor == null) + { + mFinalColor = null; + } else + { + int alpha = (int) Math.floor(((mColor >> 24) / 255.0) * (mAlpha / 255.0) * 255.0); + mFinalColor = (alpha << 24) | (mColor & 0xffffff); + } + } + + public void fillRect(Canvas c, Paint paint, + float left, float top, float right, float bottom, + Direction gradientDirection) + { + switch (mType) + { + case EMPTY: + return; + + case COLOR: + { + if (mFinalColor == null) return; + + if (isClipPathSupported()) + { + int save = c.save(); + + c.clipRect(left, top, right, bottom); + c.drawColor(mFinalColor); + + c.restoreToCount(save); + } + else + { + // save + Paint.Style previous = paint.getStyle(); + int previousColor = paint.getColor(); + + // set + paint.setStyle(Paint.Style.FILL); + paint.setColor(mFinalColor); + + c.drawRect(left, top, right, bottom, paint); + + // restore + paint.setColor(previousColor); + paint.setStyle(previous); + } + } + break; + + case LINEAR_GRADIENT: + { + if (mGradientColors == null) return; + + LinearGradient gradient = new LinearGradient( + (int) (gradientDirection == Direction.RIGHT + ? right + : gradientDirection == Direction.LEFT + ? left + : left), + (int) (gradientDirection == Direction.UP + ? bottom + : gradientDirection == Direction.DOWN + ? top + : top), + (int) (gradientDirection == Direction.RIGHT + ? left + : gradientDirection == Direction.LEFT + ? right + : left), + (int) (gradientDirection == Direction.UP + ? top + : gradientDirection == Direction.DOWN + ? bottom + : top), + mGradientColors, + mGradientPositions, + android.graphics.Shader.TileMode.MIRROR); + + paint.setShader(gradient); + + c.drawRect(left, top, right, bottom, paint); + } + break; + + case DRAWABLE: + { + if (mDrawable == null) return; + + mDrawable.setBounds((int) left, (int) top, (int) right, (int) bottom); + mDrawable.draw(c); + } + break; + } + } + + public void fillPath(Canvas c, Path path, Paint paint, + @Nullable RectF clipRect) + { + switch (mType) + { + case EMPTY: + return; + + case COLOR: + { + if (mFinalColor == null) return; + + if (clipRect != null && isClipPathSupported()) + { + int save = c.save(); + + c.clipPath(path); + c.drawColor(mFinalColor); + + c.restoreToCount(save); + } + else + { + // save + Paint.Style previous = paint.getStyle(); + int previousColor = paint.getColor(); + + // set + paint.setStyle(Paint.Style.FILL); + paint.setColor(mFinalColor); + + c.drawPath(path, paint); + + // restore + paint.setColor(previousColor); + paint.setStyle(previous); + } + } + break; + + case LINEAR_GRADIENT: + { + if (mGradientColors == null) return; + + LinearGradient gradient = new LinearGradient( + 0, + 0, + c.getWidth(), + c.getHeight(), + mGradientColors, + mGradientPositions, + android.graphics.Shader.TileMode.MIRROR); + + paint.setShader(gradient); + + c.drawPath(path, paint); + } + break; + + case DRAWABLE: + { + if (mDrawable == null) return; + + ensureClipPathSupported(); + + int save = c.save(); + c.clipPath(path); + + mDrawable.setBounds( + clipRect == null ? 0 : (int) clipRect.left, + clipRect == null ? 0 : (int) clipRect.top, + clipRect == null ? c.getWidth() : (int) clipRect.right, + clipRect == null ? c.getHeight() : (int) clipRect.bottom); + mDrawable.draw(c); + + c.restoreToCount(save); + } + break; + } + } + + private boolean isClipPathSupported() + { + return Utils.getSDKInt() >= 18; + } + + private void ensureClipPathSupported() + { + if (Utils.getSDKInt() < 18) + { + throw new RuntimeException("Fill-drawables not (yet) supported below API level 18, " + + "this code was run on API level " + Utils.getSDKInt() + "."); + } + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/HorizontalViewPortHandler.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/HorizontalViewPortHandler.java new file mode 100644 index 0000000..5a415b0 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/HorizontalViewPortHandler.java @@ -0,0 +1,29 @@ + +package com.github.mikephil.charting.utils; + +/** + * ViewPortHandler for HorizontalBarChart. + */ +public class HorizontalViewPortHandler extends ViewPortHandler { + + +// @Override +// public void setMinimumScaleX(float xScale) { +// setMinimumScaleY(xScale); +// } +// +// @Override +// public void setMinimumScaleY(float yScale) { +// setMinimumScaleX(yScale); +// } +// +// @Override +// public void setMinMaxScaleX(float minScaleX, float maxScaleX) { +// setMinMaxScaleY(minScaleX, maxScaleX); +// } +// +// @Override +// public void setMinMaxScaleY(float minScaleY, float maxScaleY) { +// setMinMaxScaleX(minScaleY, maxScaleY); +// } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/MPPointD.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/MPPointD.java new file mode 100644 index 0000000..f6220a7 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/MPPointD.java @@ -0,0 +1,53 @@ + +package com.github.mikephil.charting.utils; + +import java.util.List; + +/** + * Point encapsulating two double values. + * + * @author Philipp Jahoda + */ +public class MPPointD extends ObjectPool.Poolable { + + private static ObjectPool pool; + + static { + pool = ObjectPool.create(64, new MPPointD(0,0)); + pool.setReplenishPercentage(0.5f); + } + + public static MPPointD getInstance(double x, double y){ + MPPointD result = pool.get(); + result.x = x; + result.y = y; + return result; + } + + public static void recycleInstance(MPPointD instance){ + pool.recycle(instance); + } + + public static void recycleInstances(List instances){ + pool.recycle(instances); + } + + public double x; + public double y; + + protected ObjectPool.Poolable instantiate(){ + return new MPPointD(0,0); + } + + private MPPointD(double x, double y) { + this.x = x; + this.y = y; + } + + /** + * returns a string representation of the object + */ + public String toString() { + return "MPPointD, x: " + x + ", y: " + y; + } +} \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/MPPointF.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/MPPointF.java new file mode 100644 index 0000000..fb5a00f --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/MPPointF.java @@ -0,0 +1,99 @@ +package com.github.mikephil.charting.utils; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.List; + +/** + * Created by Tony Patino on 6/24/16. + */ +public class MPPointF extends ObjectPool.Poolable { + + private static ObjectPool pool; + + public float x; + public float y; + + static { + pool = ObjectPool.create(32, new MPPointF(0,0)); + pool.setReplenishPercentage(0.5f); + } + + public MPPointF() { + } + + public MPPointF(float x, float y) { + this.x = x; + this.y = y; + } + + public static MPPointF getInstance(float x, float y) { + MPPointF result = pool.get(); + result.x = x; + result.y = y; + return result; + } + + public static MPPointF getInstance() { + return pool.get(); + } + + public static MPPointF getInstance(MPPointF copy) { + MPPointF result = pool.get(); + result.x = copy.x; + result.y = copy.y; + return result; + } + + public static void recycleInstance(MPPointF instance){ + pool.recycle(instance); + } + + public static void recycleInstances(List instances){ + pool.recycle(instances); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + /** + * Return a new point from the data in the specified parcel. + */ + public MPPointF createFromParcel(Parcel in) { + MPPointF r = new MPPointF(0,0); + r.my_readFromParcel(in); + return r; + } + + /** + * Return an array of rectangles of the specified size. + */ + public MPPointF[] newArray(int size) { + return new MPPointF[size]; + } + }; + + /** + * Set the point's coordinates from the data stored in the specified + * parcel. To write a point to a parcel, call writeToParcel(). + * Provided to support older Android devices. + * + * @param in The parcel to read the point's coordinates from + */ + public void my_readFromParcel(Parcel in) { + x = in.readFloat(); + y = in.readFloat(); + } + + public float getX(){ + return this.x; + } + + public float getY(){ + return this.y; + } + + @Override + protected ObjectPool.Poolable instantiate() { + return new MPPointF(0,0); + } +} \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/ObjectPool.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/ObjectPool.java new file mode 100644 index 0000000..d1d5437 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/ObjectPool.java @@ -0,0 +1,218 @@ +package com.github.mikephil.charting.utils; + +import java.util.List; + +/** + * An object pool for recycling of object instances extending Poolable. + * + * + * Cost/Benefit : + * Cost - The pool can only contain objects extending Poolable. + * Benefit - The pool can very quickly determine if an object is elligable for storage without iteration. + * Benefit - The pool can also know if an instance of Poolable is already stored in a different pool instance. + * Benefit - The pool can grow as needed, if it is empty + * Cost - However, refilling the pool when it is empty might incur a time cost with sufficiently large capacity. Set the replenishPercentage to a lower number if this is a concern. + * + * Created by Tony Patino on 6/20/16. + */ +public class ObjectPool { + + private static int ids = 0; + + private int poolId; + private int desiredCapacity; + private Object[] objects; + private int objectsPointer; + private T modelObject; + private float replenishPercentage; + + + /** + * Returns the id of the given pool instance. + * + * @return an integer ID belonging to this pool instance. + */ + public int getPoolId(){ + return poolId; + } + + /** + * Returns an ObjectPool instance, of a given starting capacity, that recycles instances of a given Poolable object. + * + * @param withCapacity A positive integer value. + * @param object An instance of the object that the pool should recycle. + * @return + */ + public static synchronized ObjectPool create(int withCapacity, Poolable object){ + ObjectPool result = new ObjectPool(withCapacity, object); + result.poolId = ids; + ids++; + + return result; + } + + private ObjectPool(int withCapacity, T object){ + if(withCapacity <= 0){ + throw new IllegalArgumentException("Object Pool must be instantiated with a capacity greater than 0!"); + } + this.desiredCapacity = withCapacity; + this.objects = new Object[this.desiredCapacity]; + this.objectsPointer = 0; + this.modelObject = object; + this.replenishPercentage = 1.0f; + this.refillPool(); + } + + /** + * Set the percentage of the pool to replenish on empty. Valid values are between + * 0.00f and 1.00f + * + * @param percentage a value between 0 and 1, representing the percentage of the pool to replenish. + */ + public void setReplenishPercentage(float percentage){ + float p = percentage; + if(p > 1){ + p = 1; + } + else if(p < 0f){ + p = 0f; + } + this.replenishPercentage = p; + } + + public float getReplenishPercentage(){ + return replenishPercentage; + } + + private void refillPool(){ + this.refillPool(this.replenishPercentage); + } + + private void refillPool(float percentage){ + int portionOfCapacity = (int) (desiredCapacity * percentage); + + if(portionOfCapacity < 1){ + portionOfCapacity = 1; + }else if(portionOfCapacity > desiredCapacity){ + portionOfCapacity = desiredCapacity; + } + + for(int i = 0 ; i < portionOfCapacity ; i++){ + this.objects[i] = modelObject.instantiate(); + } + objectsPointer = portionOfCapacity - 1; + } + + /** + * Returns an instance of Poolable. If get() is called with an empty pool, the pool will be + * replenished. If the pool capacity is sufficiently large, this could come at a performance + * cost. + * + * @return An instance of Poolable object T + */ + public synchronized T get(){ + + if(this.objectsPointer == -1 && this.replenishPercentage > 0.0f){ + this.refillPool(); + } + + T result = (T)objects[this.objectsPointer]; + result.currentOwnerId = Poolable.NO_OWNER; + this.objectsPointer--; + + return result; + } + + /** + * Recycle an instance of Poolable that this pool is capable of generating. + * The T instance passed must not already exist inside this or any other ObjectPool instance. + * + * @param object An object of type T to recycle + */ + public synchronized void recycle(T object){ + if(object.currentOwnerId != Poolable.NO_OWNER){ + if(object.currentOwnerId == this.poolId){ + throw new IllegalArgumentException("The object passed is already stored in this pool!"); + }else { + throw new IllegalArgumentException("The object to recycle already belongs to poolId " + object.currentOwnerId + ". Object cannot belong to two different pool instances simultaneously!"); + } + } + + this.objectsPointer++; + if(this.objectsPointer >= objects.length){ + this.resizePool(); + } + + object.currentOwnerId = this.poolId; + objects[this.objectsPointer] = object; + + } + + /** + * Recycle a List of Poolables that this pool is capable of generating. + * The T instances passed must not already exist inside this or any other ObjectPool instance. + * + * @param objects A list of objects of type T to recycle + */ + public synchronized void recycle(List objects){ + while(objects.size() + this.objectsPointer + 1 > this.desiredCapacity){ + this.resizePool(); + } + final int objectsListSize = objects.size(); + + // Not relying on recycle(T object) because this is more performant. + for(int i = 0 ; i < objectsListSize ; i++){ + T object = objects.get(i); + if(object.currentOwnerId != Poolable.NO_OWNER){ + if(object.currentOwnerId == this.poolId){ + throw new IllegalArgumentException("The object passed is already stored in this pool!"); + }else { + throw new IllegalArgumentException("The object to recycle already belongs to poolId " + object.currentOwnerId + ". Object cannot belong to two different pool instances simultaneously!"); + } + } + object.currentOwnerId = this.poolId; + this.objects[this.objectsPointer + 1 + i] = object; + } + this.objectsPointer += objectsListSize; + } + + private void resizePool() { + final int oldCapacity = this.desiredCapacity; + this.desiredCapacity *= 2; + Object[] temp = new Object[this.desiredCapacity]; + for(int i = 0 ; i < oldCapacity ; i++){ + temp[i] = this.objects[i]; + } + this.objects = temp; + } + + /** + * Returns the capacity of this object pool. Note : The pool will automatically resize + * to contain additional objects if the user tries to add more objects than the pool's + * capacity allows, but this comes at a performance cost. + * + * @return The capacity of the pool. + */ + public int getPoolCapacity(){ + return this.objects.length; + } + + /** + * Returns the number of objects remaining in the pool, for diagnostic purposes. + * + * @return The number of objects remaining in the pool. + */ + public int getPoolCount(){ + return this.objectsPointer + 1; + } + + + public static abstract class Poolable{ + + public static int NO_OWNER = -1; + int currentOwnerId = NO_OWNER; + + protected abstract Poolable instantiate(); + + } +} \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/Transformer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/Transformer.java new file mode 100644 index 0000000..0496bd0 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/Transformer.java @@ -0,0 +1,459 @@ + +package com.github.mikephil.charting.utils; + +import android.graphics.Matrix; +import android.graphics.Path; +import android.graphics.RectF; + +import com.github.mikephil.charting.data.CandleEntry; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.interfaces.datasets.IBubbleDataSet; +import com.github.mikephil.charting.interfaces.datasets.ICandleDataSet; +import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; +import com.github.mikephil.charting.interfaces.datasets.IScatterDataSet; + +import java.util.List; + +/** + * Transformer class that contains all matrices and is responsible for + * transforming values into pixels on the screen and backwards. + * + * @author Philipp Jahoda + */ +public class Transformer { + + /** + * matrix to map the values to the screen pixels + */ + protected Matrix mMatrixValueToPx = new Matrix(); + + /** + * matrix for handling the different offsets of the chart + */ + protected Matrix mMatrixOffset = new Matrix(); + + protected ViewPortHandler mViewPortHandler; + + public Transformer(ViewPortHandler viewPortHandler) { + this.mViewPortHandler = viewPortHandler; + } + + /** + * Prepares the matrix that transforms values to pixels. Calculates the + * scale factors from the charts size and offsets. + * + * @param xChartMin + * @param deltaX + * @param deltaY + * @param yChartMin + */ + public void prepareMatrixValuePx(float xChartMin, float deltaX, float deltaY, float yChartMin) { + + float scaleX = (float) ((mViewPortHandler.contentWidth()) / deltaX); + float scaleY = (float) ((mViewPortHandler.contentHeight()) / deltaY); + + if (Float.isInfinite(scaleX)) { + scaleX = 0; + } + if (Float.isInfinite(scaleY)) { + scaleY = 0; + } + + // setup all matrices + mMatrixValueToPx.reset(); + mMatrixValueToPx.postTranslate(-xChartMin, -yChartMin); + mMatrixValueToPx.postScale(scaleX, -scaleY); + } + + /** + * Prepares the matrix that contains all offsets. + * + * @param inverted + */ + public void prepareMatrixOffset(boolean inverted) { + + mMatrixOffset.reset(); + + // offset.postTranslate(mOffsetLeft, getHeight() - mOffsetBottom); + + if (!inverted) + mMatrixOffset.postTranslate(mViewPortHandler.offsetLeft(), + mViewPortHandler.getChartHeight() - mViewPortHandler.offsetBottom()); + else { + mMatrixOffset + .setTranslate(mViewPortHandler.offsetLeft(), -mViewPortHandler.offsetTop()); + mMatrixOffset.postScale(1.0f, -1.0f); + } + } + + protected float[] valuePointsForGenerateTransformedValuesScatter = new float[1]; + + /** + * Transforms an List of Entry into a float array containing the x and + * y values transformed with all matrices for the SCATTERCHART. + * + * @param data + * @return + */ + public float[] generateTransformedValuesScatter(IScatterDataSet data, float phaseX, + float phaseY, int from, int to) { + + final int count = (int) ((to - from) * phaseX + 1) * 2; + + if (valuePointsForGenerateTransformedValuesScatter.length != count) { + valuePointsForGenerateTransformedValuesScatter = new float[count]; + } + float[] valuePoints = valuePointsForGenerateTransformedValuesScatter; + + for (int j = 0; j < count; j += 2) { + + Entry e = data.getEntryForIndex(j / 2 + from); + + if (e != null) { + valuePoints[j] = e.getX(); + valuePoints[j + 1] = e.getY() * phaseY; + } else { + valuePoints[j] = 0; + valuePoints[j + 1] = 0; + } + } + + getValueToPixelMatrix().mapPoints(valuePoints); + + return valuePoints; + } + + protected float[] valuePointsForGenerateTransformedValuesBubble = new float[1]; + + /** + * Transforms an List of Entry into a float array containing the x and + * y values transformed with all matrices for the BUBBLECHART. + * + * @param data + * @return + */ + public float[] generateTransformedValuesBubble(IBubbleDataSet data, float phaseY, int from, int to) { + + final int count = (to - from + 1) * 2; // (int) Math.ceil((to - from) * phaseX) * 2; + + if (valuePointsForGenerateTransformedValuesBubble.length != count) { + valuePointsForGenerateTransformedValuesBubble = new float[count]; + } + float[] valuePoints = valuePointsForGenerateTransformedValuesBubble; + + for (int j = 0; j < count; j += 2) { + + Entry e = data.getEntryForIndex(j / 2 + from); + + if (e != null) { + valuePoints[j] = e.getX(); + valuePoints[j + 1] = e.getY() * phaseY; + } else { + valuePoints[j] = 0; + valuePoints[j + 1] = 0; + } + } + + getValueToPixelMatrix().mapPoints(valuePoints); + + return valuePoints; + } + + protected float[] valuePointsForGenerateTransformedValuesLine = new float[1]; + + /** + * Transforms an List of Entry into a float array containing the x and + * y values transformed with all matrices for the LINECHART. + * + * @param data + * @return + */ + public float[] generateTransformedValuesLine(ILineDataSet data, + float phaseX, float phaseY, + int min, int max) { + + final int count = ((int) ((max - min) * phaseX) + 1) * 2; + + if (valuePointsForGenerateTransformedValuesLine.length != count) { + valuePointsForGenerateTransformedValuesLine = new float[count]; + } + float[] valuePoints = valuePointsForGenerateTransformedValuesLine; + + for (int j = 0; j < count; j += 2) { + + Entry e = data.getEntryForIndex(j / 2 + min); + + if (e != null) { + valuePoints[j] = e.getX(); + valuePoints[j + 1] = e.getY() * phaseY; + } else { + valuePoints[j] = 0; + valuePoints[j + 1] = 0; + } + } + + getValueToPixelMatrix().mapPoints(valuePoints); + + return valuePoints; + } + + protected float[] valuePointsForGenerateTransformedValuesCandle = new float[1]; + + /** + * Transforms an List of Entry into a float array containing the x and + * y values transformed with all matrices for the CANDLESTICKCHART. + * + * @param data + * @return + */ + public float[] generateTransformedValuesCandle(ICandleDataSet data, + float phaseX, float phaseY, int from, int to) { + + final int count = (int) ((to - from) * phaseX + 1) * 2; + + if (valuePointsForGenerateTransformedValuesCandle.length != count) { + valuePointsForGenerateTransformedValuesCandle = new float[count]; + } + float[] valuePoints = valuePointsForGenerateTransformedValuesCandle; + + for (int j = 0; j < count; j += 2) { + + CandleEntry e = data.getEntryForIndex(j / 2 + from); + + if (e != null) { + valuePoints[j] = e.getX(); + valuePoints[j + 1] = e.getHigh() * phaseY; + } else { + valuePoints[j] = 0; + valuePoints[j + 1] = 0; + } + } + + getValueToPixelMatrix().mapPoints(valuePoints); + + return valuePoints; + } + + /** + * transform a path with all the given matrices VERY IMPORTANT: keep order + * to value-touch-offset + * + * @param path + */ + public void pathValueToPixel(Path path) { + + path.transform(mMatrixValueToPx); + path.transform(mViewPortHandler.getMatrixTouch()); + path.transform(mMatrixOffset); + } + + /** + * Transforms multiple paths will all matrices. + * + * @param paths + */ + public void pathValuesToPixel(List paths) { + + for (int i = 0; i < paths.size(); i++) { + pathValueToPixel(paths.get(i)); + } + } + + /** + * Transform an array of points with all matrices. VERY IMPORTANT: Keep + * matrix order "value-touch-offset" when transforming. + * + * @param pts + */ + public void pointValuesToPixel(float[] pts) { + + mMatrixValueToPx.mapPoints(pts); + mViewPortHandler.getMatrixTouch().mapPoints(pts); + mMatrixOffset.mapPoints(pts); + } + + /** + * Transform a rectangle with all matrices. + * + * @param r + */ + public void rectValueToPixel(RectF r) { + + mMatrixValueToPx.mapRect(r); + mViewPortHandler.getMatrixTouch().mapRect(r); + mMatrixOffset.mapRect(r); + } + + /** + * Transform a rectangle with all matrices with potential animation phases. + * + * @param r + * @param phaseY + */ + public void rectToPixelPhase(RectF r, float phaseY) { + + // multiply the height of the rect with the phase + r.top *= phaseY; + r.bottom *= phaseY; + + mMatrixValueToPx.mapRect(r); + mViewPortHandler.getMatrixTouch().mapRect(r); + mMatrixOffset.mapRect(r); + } + + public void rectToPixelPhaseHorizontal(RectF r, float phaseY) { + + // multiply the height of the rect with the phase + r.left *= phaseY; + r.right *= phaseY; + + mMatrixValueToPx.mapRect(r); + mViewPortHandler.getMatrixTouch().mapRect(r); + mMatrixOffset.mapRect(r); + } + + /** + * Transform a rectangle with all matrices with potential animation phases. + * + * @param r + */ + public void rectValueToPixelHorizontal(RectF r) { + + mMatrixValueToPx.mapRect(r); + mViewPortHandler.getMatrixTouch().mapRect(r); + mMatrixOffset.mapRect(r); + } + + /** + * Transform a rectangle with all matrices with potential animation phases. + * + * @param r + * @param phaseY + */ + public void rectValueToPixelHorizontal(RectF r, float phaseY) { + + // multiply the height of the rect with the phase + r.left *= phaseY; + r.right *= phaseY; + + mMatrixValueToPx.mapRect(r); + mViewPortHandler.getMatrixTouch().mapRect(r); + mMatrixOffset.mapRect(r); + } + + /** + * transforms multiple rects with all matrices + * + * @param rects + */ + public void rectValuesToPixel(List rects) { + + Matrix m = getValueToPixelMatrix(); + + for (int i = 0; i < rects.size(); i++) + m.mapRect(rects.get(i)); + } + + protected Matrix mPixelToValueMatrixBuffer = new Matrix(); + + /** + * Transforms the given array of touch positions (pixels) (x, y, x, y, ...) + * into values on the chart. + * + * @param pixels + */ + public void pixelsToValue(float[] pixels) { + + Matrix tmp = mPixelToValueMatrixBuffer; + tmp.reset(); + + // invert all matrixes to convert back to the original value + mMatrixOffset.invert(tmp); + tmp.mapPoints(pixels); + + mViewPortHandler.getMatrixTouch().invert(tmp); + tmp.mapPoints(pixels); + + mMatrixValueToPx.invert(tmp); + tmp.mapPoints(pixels); + } + + /** + * buffer for performance + */ + float[] ptsBuffer = new float[2]; + + /** + * Returns a recyclable MPPointD instance. + * returns the x and y values in the chart at the given touch point + * (encapsulated in a MPPointD). This method transforms pixel coordinates to + * coordinates / values in the chart. This is the opposite method to + * getPixelForValues(...). + * + * @param x + * @param y + * @return + */ + public MPPointD getValuesByTouchPoint(float x, float y) { + + MPPointD result = MPPointD.getInstance(0, 0); + getValuesByTouchPoint(x, y, result); + return result; + } + + public void getValuesByTouchPoint(float x, float y, MPPointD outputPoint) { + + ptsBuffer[0] = x; + ptsBuffer[1] = y; + + pixelsToValue(ptsBuffer); + + outputPoint.x = ptsBuffer[0]; + outputPoint.y = ptsBuffer[1]; + } + + /** + * Returns a recyclable MPPointD instance. + * Returns the x and y coordinates (pixels) for a given x and y value in the chart. + * + * @param x + * @param y + * @return + */ + public MPPointD getPixelForValues(float x, float y) { + + ptsBuffer[0] = x; + ptsBuffer[1] = y; + + pointValuesToPixel(ptsBuffer); + + double xPx = ptsBuffer[0]; + double yPx = ptsBuffer[1]; + + return MPPointD.getInstance(xPx, yPx); + } + + public Matrix getValueMatrix() { + return mMatrixValueToPx; + } + + public Matrix getOffsetMatrix() { + return mMatrixOffset; + } + + private Matrix mMBuffer1 = new Matrix(); + + public Matrix getValueToPixelMatrix() { + mMBuffer1.set(mMatrixValueToPx); + mMBuffer1.postConcat(mViewPortHandler.mMatrixTouch); + mMBuffer1.postConcat(mMatrixOffset); + return mMBuffer1; + } + + private Matrix mMBuffer2 = new Matrix(); + + public Matrix getPixelToValueMatrix() { + getValueToPixelMatrix().invert(mMBuffer2); + return mMBuffer2; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/TransformerHorizontalBarChart.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/TransformerHorizontalBarChart.java new file mode 100644 index 0000000..05fa82a --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/TransformerHorizontalBarChart.java @@ -0,0 +1,44 @@ + +package com.github.mikephil.charting.utils; + +/** + * Transformer class for the HorizontalBarChart. + * + * @author Philipp Jahoda + */ +public class TransformerHorizontalBarChart extends Transformer { + + public TransformerHorizontalBarChart(ViewPortHandler viewPortHandler) { + super(viewPortHandler); + } + + /** + * Prepares the matrix that contains all offsets. + * + * @param inverted + */ + public void prepareMatrixOffset(boolean inverted) { + + mMatrixOffset.reset(); + + // offset.postTranslate(mOffsetLeft, getHeight() - mOffsetBottom); + + if (!inverted) + mMatrixOffset.postTranslate(mViewPortHandler.offsetLeft(), + mViewPortHandler.getChartHeight() - mViewPortHandler.offsetBottom()); + else { + mMatrixOffset + .setTranslate( + -(mViewPortHandler.getChartWidth() - mViewPortHandler.offsetRight()), + mViewPortHandler.getChartHeight() - mViewPortHandler.offsetBottom()); + mMatrixOffset.postScale(-1.0f, 1.0f); + } + + // mMatrixOffset.set(offset); + + // mMatrixOffset.reset(); + // + // mMatrixOffset.postTranslate(mOffsetLeft, getHeight() - + // mOffsetBottom); + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/Utils.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/Utils.java new file mode 100644 index 0000000..c302673 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/Utils.java @@ -0,0 +1,779 @@ + +package com.github.mikephil.charting.utils; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.SizeF; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; + +import com.github.mikephil.charting.formatter.DefaultValueFormatter; +import com.github.mikephil.charting.formatter.IValueFormatter; + +import java.util.List; + +/** + * Utilities class that has some helper methods. Needs to be initialized by + * calling Utils.init(...) before usage. Inside the Chart.init() method, this is + * done, if the Utils are used before that, Utils.init(...) needs to be called + * manually. + * + * @author Philipp Jahoda + */ +public abstract class Utils { + + private static DisplayMetrics mMetrics; + private static int mMinimumFlingVelocity = 50; + private static int mMaximumFlingVelocity = 8000; + public final static double DEG2RAD = (Math.PI / 180.0); + public final static float FDEG2RAD = ((float) Math.PI / 180.f); + + @SuppressWarnings("unused") + public final static double DOUBLE_EPSILON = Double.longBitsToDouble(1); + + @SuppressWarnings("unused") + public final static float FLOAT_EPSILON = Float.intBitsToFloat(1); + + /** + * initialize method, called inside the Chart.init() method. + * + * @param context + */ + @SuppressWarnings("deprecation") + public static void init(Context context) { + + if (context == null) { + // noinspection deprecation + mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity(); + // noinspection deprecation + mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity(); + + Log.e("MPChartLib-Utils" + , "Utils.init(...) PROVIDED CONTEXT OBJECT IS NULL"); + + } else { + ViewConfiguration viewConfiguration = ViewConfiguration.get(context); + mMinimumFlingVelocity = viewConfiguration.getScaledMinimumFlingVelocity(); + mMaximumFlingVelocity = viewConfiguration.getScaledMaximumFlingVelocity(); + + Resources res = context.getResources(); + mMetrics = res.getDisplayMetrics(); + } + } + + /** + * initialize method, called inside the Chart.init() method. backwards + * compatibility - to not break existing code + * + * @param res + */ + @Deprecated + public static void init(Resources res) { + + mMetrics = res.getDisplayMetrics(); + + // noinspection deprecation + mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity(); + // noinspection deprecation + mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity(); + } + + /** + * This method converts dp unit to equivalent pixels, depending on device + * density. NEEDS UTILS TO BE INITIALIZED BEFORE USAGE. + * + * @param dp A value in dp (density independent pixels) unit. Which we need + * to convert into pixels + * @return A float value to represent px equivalent to dp depending on + * device density + */ + public static float convertDpToPixel(float dp) { + + if (mMetrics == null) { + + Log.e("MPChartLib-Utils", + "Utils NOT INITIALIZED. You need to call Utils.init(...) at least once before" + + " calling Utils.convertDpToPixel(...). Otherwise conversion does not " + + "take place."); + return dp; + } + + return dp * mMetrics.density; + } + + /** + * This method converts device specific pixels to density independent + * pixels. NEEDS UTILS TO BE INITIALIZED BEFORE USAGE. + * + * @param px A value in px (pixels) unit. Which we need to convert into db + * @return A float value to represent dp equivalent to px value + */ + public static float convertPixelsToDp(float px) { + + if (mMetrics == null) { + + Log.e("MPChartLib-Utils", + "Utils NOT INITIALIZED. You need to call Utils.init(...) at least once before" + + " calling Utils.convertPixelsToDp(...). Otherwise conversion does not" + + " take place."); + return px; + } + + return px / mMetrics.density; + } + + /** + * calculates the approximate width of a text, depending on a demo text + * avoid repeated calls (e.g. inside drawing methods) + * + * @param paint + * @param demoText + * @return + */ + public static int calcTextWidth(Paint paint, String demoText) { + return (int) paint.measureText(demoText); + } + + private static Rect mCalcTextHeightRect = new Rect(); + /** + * calculates the approximate height of a text, depending on a demo text + * avoid repeated calls (e.g. inside drawing methods) + * + * @param paint + * @param demoText + * @return + */ + public static int calcTextHeight(Paint paint, String demoText) { + + Rect r = mCalcTextHeightRect; + r.set(0,0,0,0); + paint.getTextBounds(demoText, 0, demoText.length(), r); + return r.height(); + } + + private static Paint.FontMetrics mFontMetrics = new Paint.FontMetrics(); + + public static float getLineHeight(Paint paint) { + return getLineHeight(paint, mFontMetrics); + } + + public static float getLineHeight(Paint paint, Paint.FontMetrics fontMetrics){ + paint.getFontMetrics(fontMetrics); + return fontMetrics.descent - fontMetrics.ascent; + } + + public static float getLineSpacing(Paint paint) { + return getLineSpacing(paint, mFontMetrics); + } + + public static float getLineSpacing(Paint paint, Paint.FontMetrics fontMetrics){ + paint.getFontMetrics(fontMetrics); + return fontMetrics.ascent - fontMetrics.top + fontMetrics.bottom; + } + + /** + * Returns a recyclable FSize instance. + * calculates the approximate size of a text, depending on a demo text + * avoid repeated calls (e.g. inside drawing methods) + * + * @param paint + * @param demoText + * @return A Recyclable FSize instance + */ + public static FSize calcTextSize(Paint paint, String demoText) { + + FSize result = FSize.getInstance(0,0); + calcTextSize(paint, demoText, result); + return result; + } + + private static Rect mCalcTextSizeRect = new Rect(); + /** + * calculates the approximate size of a text, depending on a demo text + * avoid repeated calls (e.g. inside drawing methods) + * + * @param paint + * @param demoText + * @param outputFSize An output variable, modified by the function. + */ + public static void calcTextSize(Paint paint, String demoText, FSize outputFSize) { + + Rect r = mCalcTextSizeRect; + r.set(0,0,0,0); + paint.getTextBounds(demoText, 0, demoText.length(), r); + outputFSize.width = r.width(); + outputFSize.height = r.height(); + + } + + + /** + * Math.pow(...) is very expensive, so avoid calling it and create it + * yourself. + */ + private static final int POW_10[] = { + 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000 + }; + + private static IValueFormatter mDefaultValueFormatter = generateDefaultValueFormatter(); + + private static IValueFormatter generateDefaultValueFormatter() { + final DefaultValueFormatter formatter = new DefaultValueFormatter(1); + return formatter; + } + + /// - returns: The default value formatter used for all chart components that needs a default + public static IValueFormatter getDefaultValueFormatter() + { + return mDefaultValueFormatter; + } + + /** + * Formats the given number to the given number of decimals, and returns the + * number as a string, maximum 35 characters. If thousands are separated, the separating + * character is a dot ("."). + * + * @param number + * @param digitCount + * @param separateThousands set this to true to separate thousands values + * @return + */ + public static String formatNumber(float number, int digitCount, boolean separateThousands) { + return formatNumber(number, digitCount, separateThousands, '.'); + } + + /** + * Formats the given number to the given number of decimals, and returns the + * number as a string, maximum 35 characters. + * + * @param number + * @param digitCount + * @param separateThousands set this to true to separate thousands values + * @param separateChar a caracter to be paced between the "thousands" + * @return + */ + public static String formatNumber(float number, int digitCount, boolean separateThousands, + char separateChar) { + + char[] out = new char[35]; + + boolean neg = false; + if (number == 0) { + return "0"; + } + + boolean zero = false; + if (number < 1 && number > -1) { + zero = true; + } + + if (number < 0) { + neg = true; + number = -number; + } + + if (digitCount > POW_10.length) { + digitCount = POW_10.length - 1; + } + + number *= POW_10[digitCount]; + long lval = Math.round(number); + int ind = out.length - 1; + int charCount = 0; + boolean decimalPointAdded = false; + + while (lval != 0 || charCount < (digitCount + 1)) { + int digit = (int) (lval % 10); + lval = lval / 10; + out[ind--] = (char) (digit + '0'); + charCount++; + + // add decimal point + if (charCount == digitCount) { + out[ind--] = ','; + charCount++; + decimalPointAdded = true; + + // add thousand separators + } else if (separateThousands && lval != 0 && charCount > digitCount) { + + if (decimalPointAdded) { + + if ((charCount - digitCount) % 4 == 0) { + out[ind--] = separateChar; + charCount++; + } + + } else { + + if ((charCount - digitCount) % 4 == 3) { + out[ind--] = separateChar; + charCount++; + } + } + } + } + + // if number around zero (between 1 and -1) + if (zero) { + out[ind--] = '0'; + charCount += 1; + } + + // if the number is negative + if (neg) { + out[ind--] = '-'; + charCount += 1; + } + + int start = out.length - charCount; + + // use this instead of "new String(...)" because of issue < Android 4.0 + return String.valueOf(out, start, out.length - start); + } + + /** + * rounds the given number to the next significant number + * + * @param number + * @return + */ + public static float roundToNextSignificant(double number) { + if (Double.isInfinite(number) || + Double.isNaN(number) || + number == 0.0) + return 0; + + final float d = (float) Math.ceil((float) Math.log10(number < 0 ? -number : number)); + final int pw = 1 - (int) d; + final float magnitude = (float) Math.pow(10, pw); + final long shifted = Math.round(number * magnitude); + return shifted / magnitude; + } + + /** + * Returns the appropriate number of decimals to be used for the provided + * number. + * + * @param number + * @return + */ + public static int getDecimals(float number) { + + float i = roundToNextSignificant(number); + + if (Float.isInfinite(i)) + return 0; + + return (int) Math.ceil(-Math.log10(i)) + 2; + } + + /** + * Converts the provided Integer List to an int array. + * + * @param integers + * @return + */ + public static int[] convertIntegers(List integers) { + + int[] ret = new int[integers.size()]; + + copyIntegers(integers, ret); + + return ret; + } + + public static void copyIntegers(List from, int[] to){ + int count = to.length < from.size() ? to.length : from.size(); + for(int i = 0 ; i < count ; i++){ + to[i] = from.get(i); + } + } + + /** + * Converts the provided String List to a String array. + * + * @param strings + * @return + */ + public static String[] convertStrings(List strings) { + + String[] ret = new String[strings.size()]; + + for (int i = 0; i < ret.length; i++) { + ret[i] = strings.get(i); + } + + return ret; + } + + public static void copyStrings(List from, String[] to){ + int count = to.length < from.size() ? to.length : from.size(); + for(int i = 0 ; i < count ; i++){ + to[i] = from.get(i); + } + } + + /** + * Replacement for the Math.nextUp(...) method that is only available in + * HONEYCOMB and higher. Dat's some seeeeek sheeet. + * + * @param d + * @return + */ + public static double nextUp(double d) { + if (d == Double.POSITIVE_INFINITY) + return d; + else { + d += 0.0d; + return Double.longBitsToDouble(Double.doubleToRawLongBits(d) + + ((d >= 0.0d) ? +1L : -1L)); + } + } + + /** + * Returns a recyclable MPPointF instance. + * Calculates the position around a center point, depending on the distance + * from the center, and the angle of the position around the center. + * + * @param center + * @param dist + * @param angle in degrees, converted to radians internally + * @return + */ + public static MPPointF getPosition(MPPointF center, float dist, float angle) { + + MPPointF p = MPPointF.getInstance(0,0); + getPosition(center, dist, angle, p); + return p; + } + + public static void getPosition(MPPointF center, float dist, float angle, MPPointF outputPoint){ + outputPoint.x = (float) (center.x + dist * Math.cos(Math.toRadians(angle))); + outputPoint.y = (float) (center.y + dist * Math.sin(Math.toRadians(angle))); + } + + public static void velocityTrackerPointerUpCleanUpIfNecessary(MotionEvent ev, + VelocityTracker tracker) { + + // Check the dot product of current velocities. + // If the pointer that left was opposing another velocity vector, clear. + tracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); + final int upIndex = ev.getActionIndex(); + final int id1 = ev.getPointerId(upIndex); + final float x1 = tracker.getXVelocity(id1); + final float y1 = tracker.getYVelocity(id1); + for (int i = 0, count = ev.getPointerCount(); i < count; i++) { + if (i == upIndex) + continue; + + final int id2 = ev.getPointerId(i); + final float x = x1 * tracker.getXVelocity(id2); + final float y = y1 * tracker.getYVelocity(id2); + + final float dot = x + y; + if (dot < 0) { + tracker.clear(); + break; + } + } + } + + /** + * Original method view.postInvalidateOnAnimation() only supportd in API >= + * 16, This is a replica of the code from ViewCompat. + * + * @param view + */ + @SuppressLint("NewApi") + public static void postInvalidateOnAnimation(View view) { + if (Build.VERSION.SDK_INT >= 16) + view.postInvalidateOnAnimation(); + else + view.postInvalidateDelayed(10); + } + + public static int getMinimumFlingVelocity() { + return mMinimumFlingVelocity; + } + + public static int getMaximumFlingVelocity() { + return mMaximumFlingVelocity; + } + + /** + * returns an angle between 0.f < 360.f (not less than zero, less than 360) + */ + public static float getNormalizedAngle(float angle) { + while (angle < 0.f) + angle += 360.f; + + return angle % 360.f; + } + + private static Rect mDrawableBoundsCache = new Rect(); + + public static void drawImage(Canvas canvas, + Drawable drawable, + int x, int y, + int width, int height) { + + MPPointF drawOffset = MPPointF.getInstance(); + drawOffset.x = x - (width / 2); + drawOffset.y = y - (height / 2); + + drawable.copyBounds(mDrawableBoundsCache); + drawable.setBounds( + mDrawableBoundsCache.left, + mDrawableBoundsCache.top, + mDrawableBoundsCache.left + width, + mDrawableBoundsCache.top + width); + + int saveId = canvas.save(); + // translate to the correct position and draw + canvas.translate(drawOffset.x, drawOffset.y); + drawable.draw(canvas); + canvas.restoreToCount(saveId); + } + + private static Rect mDrawTextRectBuffer = new Rect(); + private static Paint.FontMetrics mFontMetricsBuffer = new Paint.FontMetrics(); + + public static void drawXAxisValue(Canvas c, String text, float x, float y, + Paint paint, + MPPointF anchor, float angleDegrees) { + + float drawOffsetX = 0.f; + float drawOffsetY = 0.f; + + final float lineHeight = paint.getFontMetrics(mFontMetricsBuffer); + paint.getTextBounds(text, 0, text.length(), mDrawTextRectBuffer); + + // Android sometimes has pre-padding + drawOffsetX -= mDrawTextRectBuffer.left; + + // Android does not snap the bounds to line boundaries, + // and draws from bottom to top. + // And we want to normalize it. + drawOffsetY += -mFontMetricsBuffer.ascent; + + // To have a consistent point of reference, we always draw left-aligned + Paint.Align originalTextAlign = paint.getTextAlign(); + paint.setTextAlign(Paint.Align.LEFT); + + if (angleDegrees != 0.f) { + + // Move the text drawing rect in a way that it always rotates around its center + drawOffsetX -= mDrawTextRectBuffer.width() * 0.5f; + drawOffsetY -= lineHeight * 0.5f; + + float translateX = x; + float translateY = y; + + // Move the "outer" rect relative to the anchor, assuming its centered + if (anchor.x != 0.5f || anchor.y != 0.5f) { + final FSize rotatedSize = getSizeOfRotatedRectangleByDegrees( + mDrawTextRectBuffer.width(), + lineHeight, + angleDegrees); + + translateX -= rotatedSize.width * (anchor.x - 0.5f); + translateY -= rotatedSize.height * (anchor.y - 0.5f); + FSize.recycleInstance(rotatedSize); + } + + c.save(); + c.translate(translateX, translateY); + c.rotate(angleDegrees); + + c.drawText(text, drawOffsetX, drawOffsetY, paint); + + c.restore(); + } else { + if (anchor.x != 0.f || anchor.y != 0.f) { + + drawOffsetX -= mDrawTextRectBuffer.width() * anchor.x; + drawOffsetY -= lineHeight * anchor.y; + } + + drawOffsetX += x; + drawOffsetY += y; + + c.drawText(text, drawOffsetX, drawOffsetY, paint); + } + + paint.setTextAlign(originalTextAlign); + } + + public static void drawMultilineText(Canvas c, StaticLayout textLayout, + float x, float y, + TextPaint paint, + MPPointF anchor, float angleDegrees) { + + float drawOffsetX = 0.f; + float drawOffsetY = 0.f; + float drawWidth; + float drawHeight; + + final float lineHeight = paint.getFontMetrics(mFontMetricsBuffer); + + drawWidth = textLayout.getWidth(); + drawHeight = textLayout.getLineCount() * lineHeight; + + // Android sometimes has pre-padding + drawOffsetX -= mDrawTextRectBuffer.left; + + // Android does not snap the bounds to line boundaries, + // and draws from bottom to top. + // And we want to normalize it. + drawOffsetY += drawHeight; + + // To have a consistent point of reference, we always draw left-aligned + Paint.Align originalTextAlign = paint.getTextAlign(); + paint.setTextAlign(Paint.Align.LEFT); + + if (angleDegrees != 0.f) { + + // Move the text drawing rect in a way that it always rotates around its center + drawOffsetX -= drawWidth * 0.5f; + drawOffsetY -= drawHeight * 0.5f; + + float translateX = x; + float translateY = y; + + // Move the "outer" rect relative to the anchor, assuming its centered + if (anchor.x != 0.5f || anchor.y != 0.5f) { + final FSize rotatedSize = getSizeOfRotatedRectangleByDegrees( + drawWidth, + drawHeight, + angleDegrees); + + translateX -= rotatedSize.width * (anchor.x - 0.5f); + translateY -= rotatedSize.height * (anchor.y - 0.5f); + FSize.recycleInstance(rotatedSize); + } + + c.save(); + c.translate(translateX, translateY); + c.rotate(angleDegrees); + + c.translate(drawOffsetX, drawOffsetY); + textLayout.draw(c); + + c.restore(); + } else { + if (anchor.x != 0.f || anchor.y != 0.f) { + + drawOffsetX -= drawWidth * anchor.x; + drawOffsetY -= drawHeight * anchor.y; + } + + drawOffsetX += x; + drawOffsetY += y; + + c.save(); + + c.translate(drawOffsetX, drawOffsetY); + textLayout.draw(c); + + c.restore(); + } + + paint.setTextAlign(originalTextAlign); + } + + public static void drawMultilineText(Canvas c, String text, + float x, float y, + TextPaint paint, + FSize constrainedToSize, + MPPointF anchor, float angleDegrees) { + + StaticLayout textLayout = new StaticLayout( + text, 0, text.length(), + paint, + (int) Math.max(Math.ceil(constrainedToSize.width), 1.f), + Layout.Alignment.ALIGN_NORMAL, 1.f, 0.f, false); + + + drawMultilineText(c, textLayout, x, y, paint, anchor, angleDegrees); + } + + /** + * Returns a recyclable FSize instance. + * Represents size of a rotated rectangle by degrees. + * + * @param rectangleSize + * @param degrees + * @return A Recyclable FSize instance + */ + public static FSize getSizeOfRotatedRectangleByDegrees(FSize rectangleSize, float degrees) { + final float radians = degrees * FDEG2RAD; + return getSizeOfRotatedRectangleByRadians(rectangleSize.width, rectangleSize.height, + radians); + } + + /** + * Returns a recyclable FSize instance. + * Represents size of a rotated rectangle by radians. + * + * @param rectangleSize + * @param radians + * @return A Recyclable FSize instance + */ + public static FSize getSizeOfRotatedRectangleByRadians(FSize rectangleSize, float radians) { + return getSizeOfRotatedRectangleByRadians(rectangleSize.width, rectangleSize.height, + radians); + } + + /** + * Returns a recyclable FSize instance. + * Represents size of a rotated rectangle by degrees. + * + * @param rectangleWidth + * @param rectangleHeight + * @param degrees + * @return A Recyclable FSize instance + */ + public static FSize getSizeOfRotatedRectangleByDegrees(float rectangleWidth, float + rectangleHeight, float degrees) { + final float radians = degrees * FDEG2RAD; + return getSizeOfRotatedRectangleByRadians(rectangleWidth, rectangleHeight, radians); + } + + /** + * Returns a recyclable FSize instance. + * Represents size of a rotated rectangle by radians. + * + * @param rectangleWidth + * @param rectangleHeight + * @param radians + * @return A Recyclable FSize instance + */ + public static FSize getSizeOfRotatedRectangleByRadians(float rectangleWidth, float + rectangleHeight, float radians) { + return FSize.getInstance( + Math.abs(rectangleWidth * (float) Math.cos(radians)) + Math.abs(rectangleHeight * + (float) Math.sin(radians)), + Math.abs(rectangleWidth * (float) Math.sin(radians)) + Math.abs(rectangleHeight * + (float) Math.cos(radians)) + ); + } + + public static int getSDKInt() { + return android.os.Build.VERSION.SDK_INT; + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/utils/ViewPortHandler.java b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/ViewPortHandler.java new file mode 100644 index 0000000..71b4b9b --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/utils/ViewPortHandler.java @@ -0,0 +1,759 @@ + +package com.github.mikephil.charting.utils; + +import android.graphics.Matrix; +import android.graphics.RectF; +import android.view.View; + +/** + * Class that contains information about the charts current viewport settings, including offsets, scale & translation + * levels, ... + * + * @author Philipp Jahoda + */ +public class ViewPortHandler { + + /** + * matrix used for touch events + */ + protected final Matrix mMatrixTouch = new Matrix(); + + /** + * this rectangle defines the area in which graph values can be drawn + */ + protected RectF mContentRect = new RectF(); + + protected float mChartWidth = 0f; + protected float mChartHeight = 0f; + + /** + * minimum scale value on the y-axis + */ + private float mMinScaleY = 1f; + + /** + * maximum scale value on the y-axis + */ + private float mMaxScaleY = Float.MAX_VALUE; + + /** + * minimum scale value on the x-axis + */ + private float mMinScaleX = 1f; + + /** + * maximum scale value on the x-axis + */ + private float mMaxScaleX = Float.MAX_VALUE; + + /** + * contains the current scale factor of the x-axis + */ + private float mScaleX = 1f; + + /** + * contains the current scale factor of the y-axis + */ + private float mScaleY = 1f; + + /** + * current translation (drag distance) on the x-axis + */ + private float mTransX = 0f; + + /** + * current translation (drag distance) on the y-axis + */ + private float mTransY = 0f; + + /** + * offset that allows the chart to be dragged over its bounds on the x-axis + */ + private float mTransOffsetX = 0f; + + /** + * offset that allows the chart to be dragged over its bounds on the x-axis + */ + private float mTransOffsetY = 0f; + + /** + * Constructor - don't forget calling setChartDimens(...) + */ + public ViewPortHandler() { + + } + + /** + * Sets the width and height of the chart. + * + * @param width + * @param height + */ + + public void setChartDimens(float width, float height) { + + float offsetLeft = this.offsetLeft(); + float offsetTop = this.offsetTop(); + float offsetRight = this.offsetRight(); + float offsetBottom = this.offsetBottom(); + + mChartHeight = height; + mChartWidth = width; + + restrainViewPort(offsetLeft, offsetTop, offsetRight, offsetBottom); + } + + public boolean hasChartDimens() { + if (mChartHeight > 0 && mChartWidth > 0) + return true; + else + return false; + } + + public void restrainViewPort(float offsetLeft, float offsetTop, float offsetRight, + float offsetBottom) { + mContentRect.set(offsetLeft, offsetTop, mChartWidth - offsetRight, mChartHeight + - offsetBottom); + } + + public float offsetLeft() { + return mContentRect.left; + } + + public float offsetRight() { + return mChartWidth - mContentRect.right; + } + + public float offsetTop() { + return mContentRect.top; + } + + public float offsetBottom() { + return mChartHeight - mContentRect.bottom; + } + + public float contentTop() { + return mContentRect.top; + } + + public float contentLeft() { + return mContentRect.left; + } + + public float contentRight() { + return mContentRect.right; + } + + public float contentBottom() { + return mContentRect.bottom; + } + + public float contentWidth() { + return mContentRect.width(); + } + + public float contentHeight() { + return mContentRect.height(); + } + + public RectF getContentRect() { + return mContentRect; + } + + public MPPointF getContentCenter() { + return MPPointF.getInstance(mContentRect.centerX(), mContentRect.centerY()); + } + + public float getChartHeight() { + return mChartHeight; + } + + public float getChartWidth() { + return mChartWidth; + } + + /** + * Returns the smallest extension of the content rect (width or height). + * + * @return + */ + public float getSmallestContentExtension() { + return Math.min(mContentRect.width(), mContentRect.height()); + } + + /** + * ################ ################ ################ ################ + */ + /** CODE BELOW THIS RELATED TO SCALING AND GESTURES */ + + /** + * Zooms in by 1.4f, x and y are the coordinates (in pixels) of the zoom + * center. + * + * @param x + * @param y + */ + public Matrix zoomIn(float x, float y) { + + Matrix save = new Matrix(); + zoomIn(x, y, save); + return save; + } + + public void zoomIn(float x, float y, Matrix outputMatrix) { + outputMatrix.reset(); + outputMatrix.set(mMatrixTouch); + outputMatrix.postScale(1.4f, 1.4f, x, y); + } + + /** + * Zooms out by 0.7f, x and y are the coordinates (in pixels) of the zoom + * center. + */ + public Matrix zoomOut(float x, float y) { + + Matrix save = new Matrix(); + zoomOut(x, y, save); + return save; + } + + public void zoomOut(float x, float y, Matrix outputMatrix) { + outputMatrix.reset(); + outputMatrix.set(mMatrixTouch); + outputMatrix.postScale(0.7f, 0.7f, x, y); + } + + /** + * Zooms out to original size. + * @param outputMatrix + */ + public void resetZoom(Matrix outputMatrix) { + outputMatrix.reset(); + outputMatrix.set(mMatrixTouch); + outputMatrix.postScale(1.0f, 1.0f, 0.0f, 0.0f); + } + + /** + * Post-scales by the specified scale factors. + * + * @param scaleX + * @param scaleY + * @return + */ + public Matrix zoom(float scaleX, float scaleY) { + + Matrix save = new Matrix(); + zoom(scaleX, scaleY, save); + return save; + } + + public void zoom(float scaleX, float scaleY, Matrix outputMatrix) { + outputMatrix.reset(); + outputMatrix.set(mMatrixTouch); + outputMatrix.postScale(scaleX, scaleY); + } + + /** + * Post-scales by the specified scale factors. x and y is pivot. + * + * @param scaleX + * @param scaleY + * @param x + * @param y + * @return + */ + public Matrix zoom(float scaleX, float scaleY, float x, float y) { + + Matrix save = new Matrix(); + zoom(scaleX, scaleY, x, y, save); + return save; + } + + public void zoom(float scaleX, float scaleY, float x, float y, Matrix outputMatrix) { + outputMatrix.reset(); + outputMatrix.set(mMatrixTouch); + outputMatrix.postScale(scaleX, scaleY, x, y); + } + + /** + * Sets the scale factor to the specified values. + * + * @param scaleX + * @param scaleY + * @return + */ + public Matrix setZoom(float scaleX, float scaleY) { + + Matrix save = new Matrix(); + setZoom(scaleX, scaleY, save); + return save; + } + + public void setZoom(float scaleX, float scaleY, Matrix outputMatrix) { + outputMatrix.reset(); + outputMatrix.set(mMatrixTouch); + outputMatrix.setScale(scaleX, scaleY); + } + + /** + * Sets the scale factor to the specified values. x and y is pivot. + * + * @param scaleX + * @param scaleY + * @param x + * @param y + * @return + */ + public Matrix setZoom(float scaleX, float scaleY, float x, float y) { + + Matrix save = new Matrix(); + save.set(mMatrixTouch); + + save.setScale(scaleX, scaleY, x, y); + + return save; + } + + protected float[] valsBufferForFitScreen = new float[9]; + + /** + * Resets all zooming and dragging and makes the chart fit exactly it's + * bounds. + */ + public Matrix fitScreen() { + + Matrix save = new Matrix(); + fitScreen(save); + return save; + } + + /** + * Resets all zooming and dragging and makes the chart fit exactly it's + * bounds. Output Matrix is available for those who wish to cache the object. + */ + public void fitScreen(Matrix outputMatrix) { + mMinScaleX = 1f; + mMinScaleY = 1f; + + outputMatrix.set(mMatrixTouch); + + float[] vals = valsBufferForFitScreen; + for (int i = 0; i < 9; i++) { + vals[i] = 0; + } + + outputMatrix.getValues(vals); + + // reset all translations and scaling + vals[Matrix.MTRANS_X] = 0f; + vals[Matrix.MTRANS_Y] = 0f; + vals[Matrix.MSCALE_X] = 1f; + vals[Matrix.MSCALE_Y] = 1f; + + outputMatrix.setValues(vals); + } + + /** + * Post-translates to the specified points. Less Performant. + * + * @param transformedPts + * @return + */ + public Matrix translate(final float[] transformedPts) { + + Matrix save = new Matrix(); + translate(transformedPts, save); + return save; + } + + /** + * Post-translates to the specified points. Output matrix allows for caching objects. + * + * @param transformedPts + * @return + */ + public void translate(final float[] transformedPts, Matrix outputMatrix) { + outputMatrix.reset(); + outputMatrix.set(mMatrixTouch); + final float x = transformedPts[0] - offsetLeft(); + final float y = transformedPts[1] - offsetTop(); + outputMatrix.postTranslate(-x, -y); + } + + protected Matrix mCenterViewPortMatrixBuffer = new Matrix(); + + /** + * Centers the viewport around the specified position (x-index and y-value) + * in the chart. Centering the viewport outside the bounds of the chart is + * not possible. Makes most sense in combination with the + * setScaleMinima(...) method. + * + * @param transformedPts the position to center view viewport to + * @param view + * @return save + */ + public void centerViewPort(final float[] transformedPts, final View view) { + + Matrix save = mCenterViewPortMatrixBuffer; + save.reset(); + save.set(mMatrixTouch); + + final float x = transformedPts[0] - offsetLeft(); + final float y = transformedPts[1] - offsetTop(); + + save.postTranslate(-x, -y); + + refresh(save, view, true); + } + + /** + * buffer for storing the 9 matrix values of a 3x3 matrix + */ + protected final float[] matrixBuffer = new float[9]; + + /** + * call this method to refresh the graph with a given matrix + * + * @param newMatrix + * @return + */ + public Matrix refresh(Matrix newMatrix, View chart, boolean invalidate) { + + mMatrixTouch.set(newMatrix); + + // make sure scale and translation are within their bounds + limitTransAndScale(mMatrixTouch, mContentRect); + + if (invalidate) + chart.invalidate(); + + newMatrix.set(mMatrixTouch); + return newMatrix; + } + + /** + * limits the maximum scale and X translation of the given matrix + * + * @param matrix + */ + public void limitTransAndScale(Matrix matrix, RectF content) { + + matrix.getValues(matrixBuffer); + + float curTransX = matrixBuffer[Matrix.MTRANS_X]; + float curScaleX = matrixBuffer[Matrix.MSCALE_X]; + + float curTransY = matrixBuffer[Matrix.MTRANS_Y]; + float curScaleY = matrixBuffer[Matrix.MSCALE_Y]; + + // min scale-x is 1f + mScaleX = Math.min(Math.max(mMinScaleX, curScaleX), mMaxScaleX); + + // min scale-y is 1f + mScaleY = Math.min(Math.max(mMinScaleY, curScaleY), mMaxScaleY); + + float width = 0f; + float height = 0f; + + if (content != null) { + width = content.width(); + height = content.height(); + } + + float maxTransX = -width * (mScaleX - 1f); + mTransX = Math.min(Math.max(curTransX, maxTransX - mTransOffsetX), mTransOffsetX); + + float maxTransY = height * (mScaleY - 1f); + mTransY = Math.max(Math.min(curTransY, maxTransY + mTransOffsetY), -mTransOffsetY); + + matrixBuffer[Matrix.MTRANS_X] = mTransX; + matrixBuffer[Matrix.MSCALE_X] = mScaleX; + + matrixBuffer[Matrix.MTRANS_Y] = mTransY; + matrixBuffer[Matrix.MSCALE_Y] = mScaleY; + + matrix.setValues(matrixBuffer); + } + + /** + * Sets the minimum scale factor for the x-axis + * + * @param xScale + */ + public void setMinimumScaleX(float xScale) { + + if (xScale < 1f) + xScale = 1f; + + mMinScaleX = xScale; + + limitTransAndScale(mMatrixTouch, mContentRect); + } + + /** + * Sets the maximum scale factor for the x-axis + * + * @param xScale + */ + public void setMaximumScaleX(float xScale) { + + if (xScale == 0.f) + xScale = Float.MAX_VALUE; + + mMaxScaleX = xScale; + + limitTransAndScale(mMatrixTouch, mContentRect); + } + + /** + * Sets the minimum and maximum scale factors for the x-axis + * + * @param minScaleX + * @param maxScaleX + */ + public void setMinMaxScaleX(float minScaleX, float maxScaleX) { + + if (minScaleX < 1f) + minScaleX = 1f; + + if (maxScaleX == 0.f) + maxScaleX = Float.MAX_VALUE; + + mMinScaleX = minScaleX; + mMaxScaleX = maxScaleX; + + limitTransAndScale(mMatrixTouch, mContentRect); + } + + /** + * Sets the minimum scale factor for the y-axis + * + * @param yScale + */ + public void setMinimumScaleY(float yScale) { + + if (yScale < 1f) + yScale = 1f; + + mMinScaleY = yScale; + + limitTransAndScale(mMatrixTouch, mContentRect); + } + + /** + * Sets the maximum scale factor for the y-axis + * + * @param yScale + */ + public void setMaximumScaleY(float yScale) { + + if (yScale == 0.f) + yScale = Float.MAX_VALUE; + + mMaxScaleY = yScale; + + limitTransAndScale(mMatrixTouch, mContentRect); + } + + public void setMinMaxScaleY(float minScaleY, float maxScaleY) { + + if (minScaleY < 1f) + minScaleY = 1f; + + if (maxScaleY == 0.f) + maxScaleY = Float.MAX_VALUE; + + mMinScaleY = minScaleY; + mMaxScaleY = maxScaleY; + + limitTransAndScale(mMatrixTouch, mContentRect); + } + + /** + * Returns the charts-touch matrix used for translation and scale on touch. + * + * @return + */ + public Matrix getMatrixTouch() { + return mMatrixTouch; + } + + /** + * ################ ################ ################ ################ + */ + /** + * BELOW METHODS FOR BOUNDS CHECK + */ + + public boolean isInBoundsX(float x) { + return isInBoundsLeft(x) && isInBoundsRight(x); + } + + public boolean isInBoundsY(float y) { + return isInBoundsTop(y) && isInBoundsBottom(y); + } + + public boolean isInBounds(float x, float y) { + return isInBoundsX(x) && isInBoundsY(y); + } + + public boolean isInBoundsLeft(float x) { + return mContentRect.left <= x + 1; + } + + public boolean isInBoundsRight(float x) { + x = (float) ((int) (x * 100.f)) / 100.f; + return mContentRect.right >= x - 1; + } + + public boolean isInBoundsTop(float y) { + return mContentRect.top <= y; + } + + public boolean isInBoundsBottom(float y) { + y = (float) ((int) (y * 100.f)) / 100.f; + return mContentRect.bottom >= y; + } + + /** + * returns the current x-scale factor + */ + public float getScaleX() { + return mScaleX; + } + + /** + * returns the current y-scale factor + */ + public float getScaleY() { + return mScaleY; + } + + public float getMinScaleX() { + return mMinScaleX; + } + + public float getMaxScaleX() { + return mMaxScaleX; + } + + public float getMinScaleY() { + return mMinScaleY; + } + + public float getMaxScaleY() { + return mMaxScaleY; + } + + /** + * Returns the translation (drag / pan) distance on the x-axis + * + * @return + */ + public float getTransX() { + return mTransX; + } + + /** + * Returns the translation (drag / pan) distance on the y-axis + * + * @return + */ + public float getTransY() { + return mTransY; + } + + /** + * if the chart is fully zoomed out, return true + * + * @return + */ + public boolean isFullyZoomedOut() { + + return isFullyZoomedOutX() && isFullyZoomedOutY(); + } + + /** + * Returns true if the chart is fully zoomed out on it's y-axis (vertical). + * + * @return + */ + public boolean isFullyZoomedOutY() { + return !(mScaleY > mMinScaleY || mMinScaleY > 1f); + } + + /** + * Returns true if the chart is fully zoomed out on it's x-axis + * (horizontal). + * + * @return + */ + public boolean isFullyZoomedOutX() { + return !(mScaleX > mMinScaleX || mMinScaleX > 1f); + } + + /** + * Set an offset in dp that allows the user to drag the chart over it's + * bounds on the x-axis. + * + * @param offset + */ + public void setDragOffsetX(float offset) { + mTransOffsetX = Utils.convertDpToPixel(offset); + } + + /** + * Set an offset in dp that allows the user to drag the chart over it's + * bounds on the y-axis. + * + * @param offset + */ + public void setDragOffsetY(float offset) { + mTransOffsetY = Utils.convertDpToPixel(offset); + } + + /** + * Returns true if both drag offsets (x and y) are zero or smaller. + * + * @return + */ + public boolean hasNoDragOffset() { + return mTransOffsetX <= 0 && mTransOffsetY <= 0; + } + + /** + * Returns true if the chart is not yet fully zoomed out on the x-axis + * + * @return + */ + public boolean canZoomOutMoreX() { + return mScaleX > mMinScaleX; + } + + /** + * Returns true if the chart is not yet fully zoomed in on the x-axis + * + * @return + */ + public boolean canZoomInMoreX() { + return mScaleX < mMaxScaleX; + } + + /** + * Returns true if the chart is not yet fully zoomed out on the y-axis + * + * @return + */ + public boolean canZoomOutMoreY() { + return mScaleY > mMinScaleY; + } + + /** + * Returns true if the chart is not yet fully zoomed in on the y-axis + * + * @return + */ + public boolean canZoomInMoreY() { + return mScaleY < mMaxScaleY; + } +} diff --git a/MPChartLib/src/test/java/com/github/mikephil/charting/test/ApproximatorTest.java b/MPChartLib/src/test/java/com/github/mikephil/charting/test/ApproximatorTest.java new file mode 100644 index 0000000..784c290 --- /dev/null +++ b/MPChartLib/src/test/java/com/github/mikephil/charting/test/ApproximatorTest.java @@ -0,0 +1,43 @@ +package com.github.mikephil.charting.test; + +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.filter.Approximator; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static junit.framework.Assert.assertEquals; + +/** + * Created by philipp on 07/06/16. + */ +public class ApproximatorTest { + + @Test + public void testApproximation() { + + float[] points = new float[]{ + 10, 20, + 20, 30, + 25, 25, + 30, 28, + 31, 31, + 33, 33, + 40, 40, + 44, 40, + 48, 23, + 50, 20, + 55, 20, + 60, 25}; + + assertEquals(24, points.length); + + Approximator a = new Approximator(); + + float[] reduced = a.reduceWithDouglasPeucker(points, 2); + + assertEquals(18, reduced.length); + } +} diff --git a/MPChartLib/src/test/java/com/github/mikephil/charting/test/AxisRendererTest.java b/MPChartLib/src/test/java/com/github/mikephil/charting/test/AxisRendererTest.java new file mode 100644 index 0000000..05cb2f3 --- /dev/null +++ b/MPChartLib/src/test/java/com/github/mikephil/charting/test/AxisRendererTest.java @@ -0,0 +1,105 @@ +package com.github.mikephil.charting.test; + +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.renderer.AxisRenderer; +import com.github.mikephil.charting.renderer.YAxisRenderer; + +import org.junit.Test; + +import static junit.framework.Assert.assertEquals; + +/** + * Created by philipp on 31/05/16. + */ +public class AxisRendererTest { + + + @Test + public void testComputeAxisValues() { + + YAxis yAxis = new YAxis(); + yAxis.setLabelCount(6); + AxisRenderer renderer = new YAxisRenderer(null, yAxis, null); + + renderer.computeAxis(0, 100, false); + float[] entries = yAxis.mEntries; + + assertEquals(6, entries.length); + assertEquals(20, entries[1] - entries[0], 0.01); // interval 20 + assertEquals(0, entries[0], 0.01); + assertEquals(100, entries[entries.length - 1], 0.01); + + yAxis = new YAxis(); + yAxis.setLabelCount(6); + yAxis.setGranularity(50f); + renderer = new YAxisRenderer(null, yAxis, null); + + renderer.computeAxis(0, 100, false); + entries = yAxis.mEntries; + + assertEquals(3, entries.length); + assertEquals(50, entries[1] - entries[0], 0.01); // interval 50 + assertEquals(0, entries[0], 0.01); + assertEquals(100, entries[entries.length - 1], 0.01); + + yAxis = new YAxis(); + yAxis.setLabelCount(5, true); + renderer = new YAxisRenderer(null, yAxis, null); + + renderer.computeAxis(0, 100, false); + entries = yAxis.mEntries; + + assertEquals(5, entries.length); + assertEquals(25, entries[1] - entries[0], 0.01); // interval 25 + assertEquals(0, entries[0], 0.01); + assertEquals(100, entries[entries.length - 1], 0.01); + + yAxis = new YAxis(); + yAxis.setLabelCount(5, true); + renderer = new YAxisRenderer(null, yAxis, null); + + renderer.computeAxis(0, 0.01f, false); + entries = yAxis.mEntries; + + assertEquals(5, entries.length); + assertEquals(0.0025, entries[1] - entries[0], 0.0001); + assertEquals(0, entries[0], 0.0001); + assertEquals(0.01, entries[entries.length - 1], 0.0001); + + yAxis = new YAxis(); + yAxis.setLabelCount(5, false); + renderer = new YAxisRenderer(null, yAxis, null); + + renderer.computeAxis(0, 0.01f, false); + entries = yAxis.mEntries; + + assertEquals(5, entries.length); + assertEquals(0.0020, entries[1] - entries[0], 0.0001); + assertEquals(0, entries[0], 0.0001); + assertEquals(0.0080, entries[entries.length - 1], 0.0001); + + yAxis = new YAxis(); + yAxis.setLabelCount(6); + renderer = new YAxisRenderer(null, yAxis, null); + + renderer.computeAxis(-50, 50, false); + entries = yAxis.mEntries; + + assertEquals(5, entries.length); + assertEquals(-40, entries[0], 0.0001); + assertEquals(0, entries[2], 0.0001); + assertEquals(40, entries[entries.length - 1], 0.0001); + + yAxis = new YAxis(); + yAxis.setLabelCount(6); + renderer = new YAxisRenderer(null, yAxis, null); + + renderer.computeAxis(-50, 100, false); + entries = yAxis.mEntries; + + assertEquals(5, entries.length); + assertEquals(-30, entries[0], 0.0001); + assertEquals(30, entries[2], 0.0001); + assertEquals(90, entries[entries.length - 1], 0.0001); + } +} diff --git a/MPChartLib/src/test/java/com/github/mikephil/charting/test/BarDataTest.java b/MPChartLib/src/test/java/com/github/mikephil/charting/test/BarDataTest.java new file mode 100644 index 0000000..99954bc --- /dev/null +++ b/MPChartLib/src/test/java/com/github/mikephil/charting/test/BarDataTest.java @@ -0,0 +1,72 @@ +package com.github.mikephil.charting.test; + +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.BarDataSet; +import com.github.mikephil.charting.data.BarEntry; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static junit.framework.Assert.assertEquals; + +/** + * Created by philipp on 06/06/16. + */ +public class BarDataTest { + + @Test + public void testGroupBars() { + + float groupSpace = 5f; + float barSpace = 1f; + + List values1 = new ArrayList<>(); + List values2 = new ArrayList<>(); + + for(int i = 0; i < 5; i++) { + values1.add(new BarEntry(i, 50)); + values2.add(new BarEntry(i, 60)); + } + + BarDataSet barDataSet1 = new BarDataSet(values1, "Set1"); + BarDataSet barDataSet2 = new BarDataSet(values2, "Set2"); + + BarData data = new BarData(barDataSet1, barDataSet2); + data.setBarWidth(10f); + + float groupWidth = data.getGroupWidth(groupSpace, barSpace); + assertEquals(27f, groupWidth, 0.01f); + + assertEquals(0f, values1.get(0).getX(), 0.01f); + assertEquals(1f, values1.get(1).getX(), 0.01f); + + data.groupBars(1000, groupSpace, barSpace); + + // 1000 + 2.5 + 0.5 + 5 + assertEquals(1008f, values1.get(0).getX(), 0.01f); + assertEquals(1019f, values2.get(0).getX(), 0.01f); + assertEquals(1035f, values1.get(1).getX(), 0.01f); + assertEquals(1046f, values2.get(1).getX(), 0.01f); + + data.groupBars(-1000, groupSpace, barSpace); + + assertEquals(-992f, values1.get(0).getX(), 0.01f); + assertEquals(-981f, values2.get(0).getX(), 0.01f); + assertEquals(-965f, values1.get(1).getX(), 0.01f); + assertEquals(-954f, values2.get(1).getX(), 0.01f); + + data.setBarWidth(20f); + groupWidth = data.getGroupWidth(groupSpace, barSpace); + assertEquals(47f, groupWidth, 0.01f); + + data.setBarWidth(10f); + data.groupBars(-20, groupSpace, barSpace); + + assertEquals(-12f, values1.get(0).getX(), 0.01f); + assertEquals(-1f, values2.get(0).getX(), 0.01f); + assertEquals(15f, values1.get(1).getX(), 0.01f); + assertEquals(26f, values2.get(1).getX(), 0.01f); + } +} diff --git a/MPChartLib/src/test/java/com/github/mikephil/charting/test/ChartDataTest.java b/MPChartLib/src/test/java/com/github/mikephil/charting/test/ChartDataTest.java new file mode 100644 index 0000000..ae464ee --- /dev/null +++ b/MPChartLib/src/test/java/com/github/mikephil/charting/test/ChartDataTest.java @@ -0,0 +1,211 @@ +package com.github.mikephil.charting.test; + +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.LineData; +import com.github.mikephil.charting.data.LineDataSet; +import com.github.mikephil.charting.data.ScatterData; +import com.github.mikephil.charting.data.ScatterDataSet; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +/** + * Created by philipp on 06/06/16. + */ +public class ChartDataTest { + + @Test + public void testDynamicChartData() { + + List entries1 = new ArrayList(); + entries1.add(new Entry(10, 10)); + entries1.add(new Entry(15, -2)); + entries1.add(new Entry(21, 50)); + + ScatterDataSet set1 = new ScatterDataSet(entries1, ""); + + List entries2 = new ArrayList(); + entries2.add(new Entry(-1, 10)); + entries2.add(new Entry(10, 2)); + entries2.add(new Entry(20, 5)); + + ScatterDataSet set2 = new ScatterDataSet(entries2, ""); + + ScatterData data = new ScatterData(set1, set2); + + assertEquals(-2, data.getYMin(YAxis.AxisDependency.LEFT), 0.01f); + assertEquals(50f, data.getYMax(YAxis.AxisDependency.LEFT), 0.01f); + + assertEquals(6, data.getEntryCount()); + + assertEquals(-1f, data.getXMin(), 0.01f); + assertEquals(21f, data.getXMax(), 0.01f); + + assertEquals(-2f, data.getYMin(), 0.01f); + assertEquals(50f, data.getYMax(), 0.01f); + + assertEquals(3, data.getMaxEntryCountSet().getEntryCount()); + + // now add and remove values + data.addEntry(new Entry(-10, -10), 0); + + assertEquals(set1, data.getMaxEntryCountSet()); + assertEquals(4, data.getMaxEntryCountSet().getEntryCount()); + + assertEquals(-10f, data.getYMin(YAxis.AxisDependency.LEFT), 0.01f); + assertEquals(50f, data.getYMax(YAxis.AxisDependency.LEFT), 0.01f); + + assertEquals(-10f, data.getXMin(), 0.01f); + assertEquals(21f, data.getXMax(), 0.01f); + + assertEquals(-10f, data.getYMin(), 0.01f); + assertEquals(50f, data.getYMax(), 0.01f); + + data.addEntry(new Entry(-100, 100), 0); + data.addEntry(new Entry(0, -100), 0); + + assertEquals(-100f, data.getYMin(YAxis.AxisDependency.LEFT), 0.01f); + assertEquals(100f, data.getYMax(YAxis.AxisDependency.LEFT), 0.01f); + + // right axis will adapt left axis values if no right axis values are present + assertEquals(-100, data.getYMin(YAxis.AxisDependency.RIGHT), 0.01f); + assertEquals(100f, data.getYMax(YAxis.AxisDependency.RIGHT), 0.01f); + + List entries3 = new ArrayList(); + entries3.add(new Entry(0, 200)); + entries3.add(new Entry(0, -50)); + + ScatterDataSet set3 = new ScatterDataSet(entries3, ""); + set3.setAxisDependency(YAxis.AxisDependency.RIGHT); + + data.addDataSet(set3); + + assertEquals(3, data.getDataSetCount()); + + assertEquals(-100f, data.getYMin(YAxis.AxisDependency.LEFT), 0.01f); + assertEquals(100f, data.getYMax(YAxis.AxisDependency.LEFT), 0.01f); + + assertEquals(-50f, data.getYMin(YAxis.AxisDependency.RIGHT), 0.01f); + assertEquals(200f, data.getYMax(YAxis.AxisDependency.RIGHT), 0.01f); + + LineData lineData = new LineData(); + + assertEquals(Float.MAX_VALUE, lineData.getYMin(), 0.01f); + assertEquals(-Float.MAX_VALUE, lineData.getYMax(), 0.01f); + + assertEquals(Float.MAX_VALUE, lineData.getYMin(YAxis.AxisDependency.LEFT), 0.01f); + assertEquals(-Float.MAX_VALUE, lineData.getYMax(YAxis.AxisDependency.LEFT), 0.01f); + + assertEquals(Float.MAX_VALUE, lineData.getYMin(YAxis.AxisDependency.RIGHT), 0.01f); + assertEquals(-Float.MAX_VALUE, lineData.getYMax(YAxis.AxisDependency.RIGHT), 0.01f); + + assertEquals(0, lineData.getDataSetCount()); + + List lineEntries1 = new ArrayList(); + lineEntries1.add(new Entry(10, 90)); + lineEntries1.add(new Entry(1000, 1000)); + + LineDataSet lineSet1 = new LineDataSet(lineEntries1, ""); + + lineData.addDataSet(lineSet1); + + assertEquals(1, lineData.getDataSetCount()); + assertEquals(2, lineSet1.getEntryCount()); + assertEquals(2, lineData.getEntryCount()); + + assertEquals(10, lineData.getXMin(), 0.01f); + assertEquals(1000f, lineData.getXMax(), 0.01f); + + assertEquals(90, lineData.getYMin(), 0.01f); + assertEquals(1000, lineData.getYMax(), 0.01f); + + assertEquals(90, lineData.getYMin(YAxis.AxisDependency.LEFT), 0.01f); + assertEquals(1000f, lineData.getYMax(YAxis.AxisDependency.LEFT), 0.01f); + + assertEquals(90, lineData.getYMin(YAxis.AxisDependency.RIGHT), 0.01f); + assertEquals(1000, lineData.getYMax(YAxis.AxisDependency.RIGHT), 0.01f); + + List lineEntries2 = new ArrayList(); + lineEntries2.add(new Entry(-1000, 2000)); + lineEntries2.add(new Entry(2000, -3000)); + + Entry e = new Entry(-1000, 2500); + lineEntries2.add(e); + + LineDataSet lineSet2 = new LineDataSet(lineEntries2, ""); + lineSet2.setAxisDependency(YAxis.AxisDependency.RIGHT); + + lineData.addDataSet(lineSet2); + + assertEquals(2, lineData.getDataSetCount()); + assertEquals(3, lineSet2.getEntryCount()); + assertEquals(5, lineData.getEntryCount()); + + assertEquals(-1000, lineData.getXMin(), 0.01f); + assertEquals(2000, lineData.getXMax(), 0.01f); + + assertEquals(-3000, lineData.getYMin(), 0.01f); + assertEquals(2500, lineData.getYMax(), 0.01f); + + assertEquals(90, lineData.getYMin(YAxis.AxisDependency.LEFT), 0.01f); + assertEquals(1000f, lineData.getYMax(YAxis.AxisDependency.LEFT), 0.01f); + + assertEquals(-3000, lineData.getYMin(YAxis.AxisDependency.RIGHT), 0.01f); + assertEquals(2500, lineData.getYMax(YAxis.AxisDependency.RIGHT), 0.01f); + + assertTrue(lineData.removeEntry(e, 1)); + + assertEquals(-1000, lineData.getXMin(), 0.01f); + assertEquals(2000, lineData.getXMax(), 0.01f); + + assertEquals(-3000, lineData.getYMin(), 0.01f); + assertEquals(2000, lineData.getYMax(), 0.01f); + + assertEquals(90, lineData.getYMin(YAxis.AxisDependency.LEFT), 0.01f); + assertEquals(1000f, lineData.getYMax(YAxis.AxisDependency.LEFT), 0.01f); + + assertEquals(-3000, lineData.getYMin(YAxis.AxisDependency.RIGHT), 0.01f); + assertEquals(2000, lineData.getYMax(YAxis.AxisDependency.RIGHT), 0.01f); + + assertEquals(2, lineData.getDataSetCount()); + assertTrue(lineData.removeDataSet(lineSet2)); + assertEquals(1, lineData.getDataSetCount()); + + assertEquals(10, lineData.getXMin(), 0.01f); + assertEquals(1000, lineData.getXMax(), 0.01f); + + assertEquals(90, lineData.getYMin(), 0.01f); + assertEquals(1000, lineData.getYMax(), 0.01f); + + assertEquals(90, lineData.getYMin(YAxis.AxisDependency.LEFT), 0.01f); + assertEquals(1000f, lineData.getYMax(YAxis.AxisDependency.LEFT), 0.01f); + + assertEquals(90, lineData.getYMin(YAxis.AxisDependency.RIGHT), 0.01f); + assertEquals(1000, lineData.getYMax(YAxis.AxisDependency.RIGHT), 0.01f); + + assertTrue(lineData.removeDataSet(lineSet1)); + assertEquals(0, lineData.getDataSetCount()); + + assertEquals(Float.MAX_VALUE, lineData.getXMin(), 0.01f); + assertEquals(-Float.MAX_VALUE, lineData.getXMax(), 0.01f); + + assertEquals(Float.MAX_VALUE, lineData.getYMin(), 0.01f); + assertEquals(-Float.MAX_VALUE, lineData.getYMax(), 0.01f); + + assertEquals(Float.MAX_VALUE, lineData.getYMin(YAxis.AxisDependency.LEFT), 0.01f); + assertEquals(-Float.MAX_VALUE, lineData.getYMax(YAxis.AxisDependency.LEFT), 0.01f); + + assertEquals(Float.MAX_VALUE, lineData.getYMin(YAxis.AxisDependency.RIGHT), 0.01f); + assertEquals(-Float.MAX_VALUE, lineData.getYMax(YAxis.AxisDependency.RIGHT), 0.01f); + + assertFalse(lineData.removeDataSet(lineSet1)); + assertFalse(lineData.removeDataSet(lineSet2)); + } +} diff --git a/MPChartLib/src/test/java/com/github/mikephil/charting/test/DataSetTest.java b/MPChartLib/src/test/java/com/github/mikephil/charting/test/DataSetTest.java new file mode 100644 index 0000000..3be28d2 --- /dev/null +++ b/MPChartLib/src/test/java/com/github/mikephil/charting/test/DataSetTest.java @@ -0,0 +1,238 @@ +package com.github.mikephil.charting.test; + +import com.github.mikephil.charting.data.DataSet; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.ScatterDataSet; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +/** + * Created by philipp on 31/05/16. + */ +public class DataSetTest { + + @Test + public void testCalcMinMax() { + + List entries = new ArrayList(); + entries.add(new Entry(10, 10)); + entries.add(new Entry(15, 2)); + entries.add(new Entry(21, 5)); + + ScatterDataSet set = new ScatterDataSet(entries, ""); + + assertEquals(10f, set.getXMin(), 0.01f); + assertEquals(21f, set.getXMax(), 0.01f); + + assertEquals(2f, set.getYMin(), 0.01f); + assertEquals(10f, set.getYMax(), 0.01f); + + assertEquals(3, set.getEntryCount()); + + set.addEntry(new Entry(25, 1)); + + assertEquals(10f, set.getXMin(), 0.01f); + assertEquals(25f, set.getXMax(), 0.01f); + + assertEquals(1f, set.getYMin(), 0.01f); + assertEquals(10f, set.getYMax(), 0.01f); + + assertEquals(4, set.getEntryCount()); + + set.removeEntry(3); + + assertEquals(10f, set.getXMin(), 0.01f); + assertEquals(21, set.getXMax(), 0.01f); + + assertEquals(2f, set.getYMin(), 0.01f); + assertEquals(10f, set.getYMax(), 0.01f); + } + + @Test + public void testAddRemoveEntry() { + + List entries = new ArrayList(); + entries.add(new Entry(10, 10)); + entries.add(new Entry(15, 2)); + entries.add(new Entry(21, 5)); + + ScatterDataSet set = new ScatterDataSet(entries, ""); + + assertEquals(3, set.getEntryCount()); + + set.addEntryOrdered(new Entry(5, 1)); + + assertEquals(4, set.getEntryCount()); + + assertEquals(5, set.getXMin(), 0.01f); + assertEquals(21, set.getXMax(), 0.01f); + + assertEquals(1f, set.getYMin(), 0.01f); + assertEquals(10f, set.getYMax(), 0.01f); + + assertEquals(5, set.getEntryForIndex(0).getX(), 0.01f); + assertEquals(1, set.getEntryForIndex(0).getY(), 0.01f); + + set.addEntryOrdered(new Entry(20, 50)); + + assertEquals(5, set.getEntryCount()); + + assertEquals(20, set.getEntryForIndex(3).getX(), 0.01f); + assertEquals(50, set.getEntryForIndex(3).getY(), 0.01f); + + assertTrue(set.removeEntry(3)); + + assertEquals(4, set.getEntryCount()); + + assertEquals(21, set.getEntryForIndex(3).getX(), 0.01f); + assertEquals(5, set.getEntryForIndex(3).getY(), 0.01f); + + assertEquals(5, set.getEntryForIndex(0).getX(), 0.01f); + assertEquals(1, set.getEntryForIndex(0).getY(), 0.01f); + + assertTrue(set.removeFirst()); + + assertEquals(3, set.getEntryCount()); + + assertEquals(10, set.getEntryForIndex(0).getX(), 0.01f); + assertEquals(10, set.getEntryForIndex(0).getY(), 0.01f); + + set.addEntryOrdered(new Entry(15, 3)); + + assertEquals(4, set.getEntryCount()); + + assertEquals(15, set.getEntryForIndex(1).getX(), 0.01f); + assertEquals(3, set.getEntryForIndex(1).getY(), 0.01f); + + assertEquals(21, set.getEntryForIndex(3).getX(), 0.01f); + assertEquals(5, set.getEntryForIndex(3).getY(), 0.01f); + + assertTrue(set.removeLast()); + + assertEquals(3, set.getEntryCount()); + + assertEquals(15, set.getEntryForIndex(2).getX(), 0.01f); + assertEquals(2, set.getEntryForIndex(2).getY(), 0.01f); + + assertTrue(set.removeLast()); + + assertEquals(2, set.getEntryCount()); + + assertTrue(set.removeLast()); + + assertEquals(1, set.getEntryCount()); + + assertEquals(10, set.getEntryForIndex(0).getX(), 0.01f); + assertEquals(10, set.getEntryForIndex(0).getY(), 0.01f); + + assertTrue(set.removeLast()); + + assertEquals(0, set.getEntryCount()); + + assertFalse(set.removeLast()); + assertFalse(set.removeFirst()); + } + + @Test + public void testGetEntryForXValue() { + + List entries = new ArrayList(); + entries.add(new Entry(10, 10)); + entries.add(new Entry(15, 5)); + entries.add(new Entry(21, 5)); + + ScatterDataSet set = new ScatterDataSet(entries, ""); + + Entry closest = set.getEntryForXValue(17, Float.NaN, DataSet.Rounding.CLOSEST); + assertEquals(15, closest.getX(), 0.01f); + assertEquals(5, closest.getY(), 0.01f); + + closest = set.getEntryForXValue(17, Float.NaN, DataSet.Rounding.DOWN); + assertEquals(15, closest.getX(), 0.01f); + assertEquals(5, closest.getY(), 0.01f); + + closest = set.getEntryForXValue(15, Float.NaN, DataSet.Rounding.DOWN); + assertEquals(15, closest.getX(), 0.01f); + assertEquals(5, closest.getY(), 0.01f); + + closest = set.getEntryForXValue(14, Float.NaN, DataSet.Rounding.DOWN); + assertEquals(10, closest.getX(), 0.01f); + assertEquals(10, closest.getY(), 0.01f); + + closest = set.getEntryForXValue(17, Float.NaN, DataSet.Rounding.UP); + assertEquals(21, closest.getX(), 0.01f); + assertEquals(5, closest.getY(), 0.01f); + + closest = set.getEntryForXValue(21, Float.NaN, DataSet.Rounding.UP); + assertEquals(21, closest.getX(), 0.01f); + assertEquals(5, closest.getY(), 0.01f); + + closest = set.getEntryForXValue(21, Float.NaN, DataSet.Rounding.CLOSEST); + assertEquals(21, closest.getX(), 0.01f); + assertEquals(5, closest.getY(), 0.01f); + } + + @Test + public void testGetEntryForXValueWithDuplicates() { + + // sorted list of values (by x position) + List values = new ArrayList(); + values.add(new Entry(0, 10)); + values.add(new Entry(1, 20)); + values.add(new Entry(2, 30)); + values.add(new Entry(3, 40)); + values.add(new Entry(3, 50)); // duplicate + values.add(new Entry(4, 60)); + values.add(new Entry(4, 70)); // duplicate + values.add(new Entry(5, 80)); + values.add(new Entry(6, 90)); + values.add(new Entry(7, 100)); + values.add(new Entry(8, 110)); + values.add(new Entry(8, 120)); // duplicate + + ScatterDataSet set = new ScatterDataSet(values, ""); + + Entry closest = set.getEntryForXValue(0, Float.NaN, DataSet.Rounding.CLOSEST); + assertEquals(0, closest.getX(), 0.01f); + assertEquals(10, closest.getY(), 0.01f); + + closest = set.getEntryForXValue(5, Float.NaN, DataSet.Rounding.CLOSEST); + assertEquals(5, closest.getX(), 0.01f); + assertEquals(80, closest.getY(), 0.01f); + + closest = set.getEntryForXValue(5.4f, Float.NaN, DataSet.Rounding.CLOSEST); + assertEquals(5, closest.getX(), 0.01f); + assertEquals(80, closest.getY(), 0.01f); + + closest = set.getEntryForXValue(4.6f, Float.NaN, DataSet.Rounding.CLOSEST); + assertEquals(5, closest.getX(), 0.01f); + assertEquals(80, closest.getY(), 0.01f); + + closest = set.getEntryForXValue(7, Float.NaN, DataSet.Rounding.CLOSEST); + assertEquals(7, closest.getX(), 0.01f); + assertEquals(100, closest.getY(), 0.01f); + + closest = set.getEntryForXValue(4f, Float.NaN, DataSet.Rounding.CLOSEST); + assertEquals(4, closest.getX(), 0.01f); + assertEquals(60, closest.getY(), 0.01f); + + List entries = set.getEntriesForXValue(4f); + assertEquals(2, entries.size()); + assertEquals(60, entries.get(0).getY(), 0.01f); + assertEquals(70, entries.get(1).getY(), 0.01f); + + entries = set.getEntriesForXValue(3.5f); + assertEquals(0, entries.size()); + + entries = set.getEntriesForXValue(2f); + assertEquals(1, entries.size()); + assertEquals(30, entries.get(0).getY(), 0.01f); + } +} diff --git a/MPChartLib/src/test/java/com/github/mikephil/charting/test/LargeValueFormatterTest.java b/MPChartLib/src/test/java/com/github/mikephil/charting/test/LargeValueFormatterTest.java new file mode 100644 index 0000000..f1e1e02 --- /dev/null +++ b/MPChartLib/src/test/java/com/github/mikephil/charting/test/LargeValueFormatterTest.java @@ -0,0 +1,95 @@ +package com.github.mikephil.charting.test; + +import com.github.mikephil.charting.formatter.LargeValueFormatter; + +import org.junit.Test; + +import static junit.framework.Assert.assertEquals; + +/** + * Created by philipp on 06/06/16. + */ +public class LargeValueFormatterTest { + + @Test + public void test() { + + LargeValueFormatter formatter = new LargeValueFormatter(); + + String result = formatter.getFormattedValue(5f, null); + assertEquals("5", result); + + result = formatter.getFormattedValue(5.5f, null); + assertEquals("5.5", result); + + result = formatter.getFormattedValue(50f, null); + assertEquals("50", result); + + result = formatter.getFormattedValue(50.5f, null); + assertEquals("50.5", result); + + result = formatter.getFormattedValue(500f, null); + assertEquals("500", result); + + result = formatter.getFormattedValue(1100f, null); + assertEquals("1.1k", result); + + result = formatter.getFormattedValue(10000f, null); + assertEquals("10k", result); + + result = formatter.getFormattedValue(10500f, null); + assertEquals("10.5k", result); + + result = formatter.getFormattedValue(100000f, null); + assertEquals("100k", result); + + result = formatter.getFormattedValue(1000000f, null); + assertEquals("1m", result); + + result = formatter.getFormattedValue(1500000f, null); + assertEquals("1.5m", result); + + result = formatter.getFormattedValue(9500000f, null); + assertEquals("9.5m", result); + + result = formatter.getFormattedValue(22200000f, null); + assertEquals("22.2m", result); + + result = formatter.getFormattedValue(222000000f, null); + assertEquals("222m", result); + + result = formatter.getFormattedValue(1000000000f, null); + assertEquals("1b", result); + + result = formatter.getFormattedValue(9900000000f, null); + assertEquals("9.9b", result); + + result = formatter.getFormattedValue(99000000000f, null); + assertEquals("99b", result); + + result = formatter.getFormattedValue(99500000000f, null); + assertEquals("99.5b", result); + + result = formatter.getFormattedValue(999000000000f, null); + assertEquals("999b", result); + + result = formatter.getFormattedValue(1000000000000f, null); + assertEquals("1t", result); + + formatter.setSuffix(new String[]{"", "k", "m", "b", "t", "q"}); // quadrillion support + result = formatter.getFormattedValue(1000000000000000f, null); + assertEquals("1q", result); + + result = formatter.getFormattedValue(1100000000000000f, null); + assertEquals("1.1q", result); + + result = formatter.getFormattedValue(10000000000000000f, null); + assertEquals("10q", result); + + result = formatter.getFormattedValue(13300000000000000f, null); + assertEquals("13.3q", result); + + result = formatter.getFormattedValue(100000000000000000f, null); + assertEquals("100q", result); + } +} diff --git a/MPChartLib/src/test/java/com/github/mikephil/charting/test/ObjectPoolTest.java b/MPChartLib/src/test/java/com/github/mikephil/charting/test/ObjectPoolTest.java new file mode 100644 index 0000000..e1dbe81 --- /dev/null +++ b/MPChartLib/src/test/java/com/github/mikephil/charting/test/ObjectPoolTest.java @@ -0,0 +1,240 @@ +package com.github.mikephil.charting.test; + +import com.github.mikephil.charting.utils.ObjectPool; + +import junit.framework.Assert; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by otheruser on 6/28/16. + */ +public class ObjectPoolTest { + + static class TestPoolable extends ObjectPool.Poolable{ + + private static ObjectPool pool; + + static { + pool = ObjectPool.create(4, new TestPoolable(0,0)); + } + + public int foo = 0; + public int bar = 0; + + protected ObjectPool.Poolable instantiate(){ + return new TestPoolable(0,0); + } + + private TestPoolable(int foo, int bar){ + this.foo = foo; + this.bar = bar; + } + + public static TestPoolable getInstance(int foo, int bar){ + TestPoolable result = pool.get(); + result.foo = foo; + result.bar = bar; + return result; + } + + public static void recycleInstance(TestPoolable instance){ + pool.recycle(instance); + } + + public static void recycleInstances(List instances){ + pool.recycle(instances); + } + + public static ObjectPool getPool(){ + return pool; + } + + } + + @Test + public void testObjectPool(){ + + int poolCapacity = TestPoolable.getPool().getPoolCapacity(); + int poolCount = TestPoolable.getPool().getPoolCount(); + TestPoolable testPoolable; + ArrayList testPoolables = new ArrayList<>(); + + Assert.assertEquals(4, poolCapacity); + Assert.assertEquals(4, poolCount); + + testPoolable = TestPoolable.getInstance(6,7); + Assert.assertEquals(6, testPoolable.foo); + Assert.assertEquals(7, testPoolable.bar); + + poolCapacity = TestPoolable.getPool().getPoolCapacity(); + poolCount = TestPoolable.getPool().getPoolCount(); + + Assert.assertEquals(4, poolCapacity); + Assert.assertEquals(3, poolCount); + + TestPoolable.recycleInstance(testPoolable); + + poolCapacity = TestPoolable.getPool().getPoolCapacity(); + poolCount = TestPoolable.getPool().getPoolCount(); + Assert.assertEquals(4, poolCapacity); + Assert.assertEquals(4, poolCount); + + + testPoolable = TestPoolable.getInstance(20,30); + Assert.assertEquals(20, testPoolable.foo); + Assert.assertEquals(30, testPoolable.bar); + + TestPoolable.recycleInstance(testPoolable); + + poolCapacity = TestPoolable.getPool().getPoolCapacity(); + poolCount = TestPoolable.getPool().getPoolCount(); + Assert.assertEquals(4, poolCapacity); + Assert.assertEquals(4, poolCount); + + testPoolables.add(TestPoolable.getInstance(12,24)); + testPoolables.add(TestPoolable.getInstance(1,2)); + testPoolables.add(TestPoolable.getInstance(3,5)); + testPoolables.add(TestPoolable.getInstance(6,8)); + + poolCapacity = TestPoolable.getPool().getPoolCapacity(); + poolCount = TestPoolable.getPool().getPoolCount(); + Assert.assertEquals(4, poolCapacity); + Assert.assertEquals(0, poolCount); + + + TestPoolable.recycleInstances(testPoolables); + poolCapacity = TestPoolable.getPool().getPoolCapacity(); + poolCount = TestPoolable.getPool().getPoolCount(); + Assert.assertEquals(4, poolCapacity); + Assert.assertEquals(4, poolCount); + + testPoolables.clear(); + + + testPoolables.add(TestPoolable.getInstance(12,24)); + testPoolables.add(TestPoolable.getInstance(1,2)); + testPoolables.add(TestPoolable.getInstance(3,5)); + testPoolables.add(TestPoolable.getInstance(6,8)); + testPoolables.add(TestPoolable.getInstance(8,9)); + Assert.assertEquals(12, testPoolables.get(0).foo); + Assert.assertEquals(24, testPoolables.get(0).bar); + Assert.assertEquals(1, testPoolables.get(1).foo); + Assert.assertEquals(2, testPoolables.get(1).bar); + Assert.assertEquals(3, testPoolables.get(2).foo); + Assert.assertEquals(5, testPoolables.get(2).bar); + Assert.assertEquals(6, testPoolables.get(3).foo); + Assert.assertEquals(8, testPoolables.get(3).bar); + Assert.assertEquals(8, testPoolables.get(4).foo); + Assert.assertEquals(9, testPoolables.get(4).bar); + + + poolCapacity = TestPoolable.getPool().getPoolCapacity(); + poolCount = TestPoolable.getPool().getPoolCount(); + Assert.assertEquals(4, poolCapacity); + Assert.assertEquals(3, poolCount); + + TestPoolable.recycleInstances(testPoolables); + poolCapacity = TestPoolable.getPool().getPoolCapacity(); + poolCount = TestPoolable.getPool().getPoolCount(); + Assert.assertEquals(8, poolCapacity); + Assert.assertEquals(8, poolCount); + + testPoolables.clear(); + + + testPoolables.add(TestPoolable.getInstance(0,0)); + testPoolables.add(TestPoolable.getInstance(6,8)); + testPoolables.add(TestPoolable.getInstance(1,2)); + testPoolables.add(TestPoolable.getInstance(3,5)); + testPoolables.add(TestPoolable.getInstance(8,9)); + testPoolables.add(TestPoolable.getInstance(12,24)); + testPoolables.add(TestPoolable.getInstance(12,24)); + testPoolables.add(TestPoolable.getInstance(12,24)); + testPoolables.add(TestPoolable.getInstance(6,8)); + testPoolables.add(TestPoolable.getInstance(6,8)); + Assert.assertEquals(0, testPoolables.get(0).foo); + Assert.assertEquals(0, testPoolables.get(0).bar); + Assert.assertEquals(6, testPoolables.get(1).foo); + Assert.assertEquals(8, testPoolables.get(1).bar); + Assert.assertEquals(1, testPoolables.get(2).foo); + Assert.assertEquals(2, testPoolables.get(2).bar); + Assert.assertEquals(3, testPoolables.get(3).foo); + Assert.assertEquals(5, testPoolables.get(3).bar); + Assert.assertEquals(8, testPoolables.get(4).foo); + Assert.assertEquals(9, testPoolables.get(4).bar); + Assert.assertEquals(12, testPoolables.get(5).foo); + Assert.assertEquals(24, testPoolables.get(5).bar); + Assert.assertEquals(12, testPoolables.get(6).foo); + Assert.assertEquals(24, testPoolables.get(6).bar); + Assert.assertEquals(12, testPoolables.get(7).foo); + Assert.assertEquals(24, testPoolables.get(7).bar); + Assert.assertEquals(6, testPoolables.get(8).foo); + Assert.assertEquals(8, testPoolables.get(8).bar); + Assert.assertEquals(6, testPoolables.get(9).foo); + Assert.assertEquals(8, testPoolables.get(9).bar); + + for(TestPoolable p : testPoolables){ + TestPoolable.recycleInstance(p); + } + + poolCapacity = TestPoolable.getPool().getPoolCapacity(); + poolCount = TestPoolable.getPool().getPoolCount(); + Assert.assertEquals(16, poolCapacity); + Assert.assertEquals(16, poolCount); + + testPoolable = TestPoolable.getInstance(9001,9001); + Assert.assertEquals(9001, testPoolable.foo); + Assert.assertEquals(9001, testPoolable.bar); + + poolCapacity = TestPoolable.getPool().getPoolCapacity(); + poolCount = TestPoolable.getPool().getPoolCount(); + Assert.assertEquals(16, poolCapacity); + Assert.assertEquals(15, poolCount); + + + TestPoolable.recycleInstance(testPoolable); + + poolCapacity = TestPoolable.getPool().getPoolCapacity(); + poolCount = TestPoolable.getPool().getPoolCount(); + Assert.assertEquals(16, poolCapacity); + Assert.assertEquals(16, poolCount); + + Exception e = null; + try{ + // expect an exception. + TestPoolable.recycleInstance(testPoolable); + }catch (Exception ex){ + e = ex; + }finally{ + Assert.assertEquals(e.getMessage(), true, e != null); + } + + testPoolables.clear(); + + TestPoolable.getPool().setReplenishPercentage(0.5f); + int i = 16; + while(i > 0){ + testPoolables.add(TestPoolable.getInstance(0,0)); + i--; + } + + poolCapacity = TestPoolable.getPool().getPoolCapacity(); + poolCount = TestPoolable.getPool().getPoolCount(); + Assert.assertEquals(16, poolCapacity); + Assert.assertEquals(0, poolCount); + + testPoolables.add(TestPoolable.getInstance(0,0)); + + poolCapacity = TestPoolable.getPool().getPoolCapacity(); + poolCount = TestPoolable.getPool().getPoolCount(); + Assert.assertEquals(16, poolCapacity); + Assert.assertEquals(7, poolCount); + + + } + +} From 3431c036baab95ee8dcd2a9f74f3fa6f75384097 Mon Sep 17 00:00:00 2001 From: Gnanendra18 Date: Thu, 5 May 2022 21:49:37 +0530 Subject: [PATCH 4/5] Changed build tools to 30.0.2 --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index da872c6..4ba3ae1 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,7 +4,7 @@ apply plugin: 'com.google.gms.google-services' android { compileSdkVersion 30 // buildToolsVersion "26.0.1" - buildToolsVersion "29.0.2" + buildToolsVersion "30.0.2" // new line added fro error Execution failed for task ':app:lint'. lintOptions { abortOnError false From 9efe6d5089f63637057c4b43486657d2945d47ee Mon Sep 17 00:00:00 2001 From: Gnanendra18 Date: Thu, 5 May 2022 23:08:47 +0530 Subject: [PATCH 5/5] Changed build tools to 30.0.2 --- MPChartLib/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MPChartLib/build.gradle b/MPChartLib/build.gradle index 13283db..636c608 100644 --- a/MPChartLib/build.gradle +++ b/MPChartLib/build.gradle @@ -3,11 +3,11 @@ apply plugin: 'com.android.library' group='com.github.philjay' android { - compileSdkVersion 28 - buildToolsVersion '28.0.3' + compileSdkVersion 30 + buildToolsVersion '30.0.3' defaultConfig { minSdkVersion 14 - targetSdkVersion 28 + targetSdkVersion 30 versionCode 3 versionName '3.1.0' }