From dfe153139b917cc91dddc1efa0298d4204616462 Mon Sep 17 00:00:00 2001 From: kruland2607 Date: Thu, 28 Jun 2012 13:32:57 +0000 Subject: [PATCH] Added source code for achartengine 1.0.0 in order to apply patches to make it work better. git-svn-id: https://openrocket.svn.sourceforge.net/svnroot/openrocket/trunk@818 180e2498-e6e9-4542-8430-84ac67f01cd8 --- android-libraries/achartengine/.classpath | 8 + android-libraries/achartengine/.project | 33 + .../achartengine/AndroidManifest.xml | 8 + .../achartengine/extra/LICENSE-2.0.txt | 202 +++ .../achartengine/project.properties | 12 + .../src/org/achartengine/ChartFactory.java | 708 +++++++++++ .../org/achartengine/GraphicalActivity.java | 48 + .../src/org/achartengine/GraphicalView.java | 337 +++++ .../src/org/achartengine/ITouchHandler.java | 63 + .../src/org/achartengine/TouchHandler.java | 204 +++ .../src/org/achartengine/TouchHandlerOld.java | 137 ++ .../org/achartengine/chart/AbstractChart.java | 475 +++++++ .../src/org/achartengine/chart/BarChart.java | 329 +++++ .../org/achartengine/chart/BubbleChart.java | 146 +++ .../org/achartengine/chart/ClickableArea.java | 44 + .../achartengine/chart/CombinedXYChart.java | 177 +++ .../achartengine/chart/CubicLineChart.java | 120 ++ .../src/org/achartengine/chart/DialChart.java | 236 ++++ .../org/achartengine/chart/DoughnutChart.java | 162 +++ .../src/org/achartengine/chart/LineChart.java | 175 +++ .../src/org/achartengine/chart/PieChart.java | 136 ++ .../src/org/achartengine/chart/PieMapper.java | 139 +++ .../org/achartengine/chart/PieSegment.java | 70 ++ .../org/achartengine/chart/PointStyle.java | 90 ++ .../org/achartengine/chart/RangeBarChart.java | 145 +++ .../chart/RangeStackedBarChart.java | 29 + .../org/achartengine/chart/RoundChart.java | 143 +++ .../org/achartengine/chart/ScatterChart.java | 266 ++++ .../src/org/achartengine/chart/TimeChart.java | 226 ++++ .../src/org/achartengine/chart/XYChart.java | 905 ++++++++++++++ .../src/org/achartengine/chart/package.html | 6 + .../src/org/achartengine/image/zoom-1.png | Bin 0 -> 1139 bytes .../src/org/achartengine/image/zoom_in.png | Bin 0 -> 1099 bytes .../src/org/achartengine/image/zoom_out.png | Bin 0 -> 1074 bytes .../achartengine/model/CategorySeries.java | 143 +++ .../model/MultipleCategorySeries.java | 145 +++ .../src/org/achartengine/model/Point.java | 52 + .../model/RangeCategorySeries.java | 111 ++ .../achartengine/model/SeriesSelection.java | 49 + .../org/achartengine/model/TimeSeries.java | 43 + .../model/XYMultipleSeriesDataset.java | 94 ++ .../src/org/achartengine/model/XYSeries.java | 255 ++++ .../org/achartengine/model/XYValueSeries.java | 139 +++ .../src/org/achartengine/model/package.html | 6 + .../src/org/achartengine/package.html | 6 + .../achartengine/renderer/BasicStroke.java | 107 ++ .../renderer/DefaultRenderer.java | 750 +++++++++++ .../achartengine/renderer/DialRenderer.java | 196 +++ .../renderer/SimpleSeriesRenderer.java | 235 ++++ .../renderer/XYMultipleSeriesRenderer.java | 1101 +++++++++++++++++ .../renderer/XYSeriesRenderer.java | 128 ++ .../org/achartengine/renderer/package.html | 6 + .../org/achartengine/tools/AbstractTool.java | 111 ++ .../src/org/achartengine/tools/FitZoom.java | 78 ++ .../src/org/achartengine/tools/Pan.java | 163 +++ .../org/achartengine/tools/PanListener.java | 28 + .../src/org/achartengine/tools/Zoom.java | 189 +++ .../src/org/achartengine/tools/ZoomEvent.java | 56 + .../org/achartengine/tools/ZoomListener.java | 33 + .../src/org/achartengine/util/IndexXYMap.java | 108 ++ .../src/org/achartengine/util/MathHelper.java | 174 +++ .../src/org/achartengine/util/XYEntry.java | 45 + .../src/org/achartengine/util/package.html | 6 + android/.classpath | 1 - android/libs/achartengine-1.0.0.jar | Bin 109717 -> 0 bytes android/project.properties | 1 + 66 files changed, 10337 insertions(+), 1 deletion(-) create mode 100644 android-libraries/achartengine/.classpath create mode 100644 android-libraries/achartengine/.project create mode 100644 android-libraries/achartengine/AndroidManifest.xml create mode 100644 android-libraries/achartengine/extra/LICENSE-2.0.txt create mode 100644 android-libraries/achartengine/project.properties create mode 100644 android-libraries/achartengine/src/org/achartengine/ChartFactory.java create mode 100644 android-libraries/achartengine/src/org/achartengine/GraphicalActivity.java create mode 100644 android-libraries/achartengine/src/org/achartengine/GraphicalView.java create mode 100644 android-libraries/achartengine/src/org/achartengine/ITouchHandler.java create mode 100644 android-libraries/achartengine/src/org/achartengine/TouchHandler.java create mode 100644 android-libraries/achartengine/src/org/achartengine/TouchHandlerOld.java create mode 100644 android-libraries/achartengine/src/org/achartengine/chart/AbstractChart.java create mode 100644 android-libraries/achartengine/src/org/achartengine/chart/BarChart.java create mode 100644 android-libraries/achartengine/src/org/achartengine/chart/BubbleChart.java create mode 100644 android-libraries/achartengine/src/org/achartengine/chart/ClickableArea.java create mode 100644 android-libraries/achartengine/src/org/achartengine/chart/CombinedXYChart.java create mode 100644 android-libraries/achartengine/src/org/achartengine/chart/CubicLineChart.java create mode 100644 android-libraries/achartengine/src/org/achartengine/chart/DialChart.java create mode 100644 android-libraries/achartengine/src/org/achartengine/chart/DoughnutChart.java create mode 100644 android-libraries/achartengine/src/org/achartengine/chart/LineChart.java create mode 100644 android-libraries/achartengine/src/org/achartengine/chart/PieChart.java create mode 100644 android-libraries/achartengine/src/org/achartengine/chart/PieMapper.java create mode 100644 android-libraries/achartengine/src/org/achartengine/chart/PieSegment.java create mode 100644 android-libraries/achartengine/src/org/achartengine/chart/PointStyle.java create mode 100644 android-libraries/achartengine/src/org/achartengine/chart/RangeBarChart.java create mode 100644 android-libraries/achartengine/src/org/achartengine/chart/RangeStackedBarChart.java create mode 100644 android-libraries/achartengine/src/org/achartengine/chart/RoundChart.java create mode 100644 android-libraries/achartengine/src/org/achartengine/chart/ScatterChart.java create mode 100644 android-libraries/achartengine/src/org/achartengine/chart/TimeChart.java create mode 100644 android-libraries/achartengine/src/org/achartengine/chart/XYChart.java create mode 100644 android-libraries/achartengine/src/org/achartengine/chart/package.html create mode 100644 android-libraries/achartengine/src/org/achartengine/image/zoom-1.png create mode 100644 android-libraries/achartengine/src/org/achartengine/image/zoom_in.png create mode 100644 android-libraries/achartengine/src/org/achartengine/image/zoom_out.png create mode 100644 android-libraries/achartengine/src/org/achartengine/model/CategorySeries.java create mode 100644 android-libraries/achartengine/src/org/achartengine/model/MultipleCategorySeries.java create mode 100644 android-libraries/achartengine/src/org/achartengine/model/Point.java create mode 100644 android-libraries/achartengine/src/org/achartengine/model/RangeCategorySeries.java create mode 100644 android-libraries/achartengine/src/org/achartengine/model/SeriesSelection.java create mode 100644 android-libraries/achartengine/src/org/achartengine/model/TimeSeries.java create mode 100644 android-libraries/achartengine/src/org/achartengine/model/XYMultipleSeriesDataset.java create mode 100644 android-libraries/achartengine/src/org/achartengine/model/XYSeries.java create mode 100644 android-libraries/achartengine/src/org/achartengine/model/XYValueSeries.java create mode 100644 android-libraries/achartengine/src/org/achartengine/model/package.html create mode 100644 android-libraries/achartengine/src/org/achartengine/package.html create mode 100644 android-libraries/achartengine/src/org/achartengine/renderer/BasicStroke.java create mode 100644 android-libraries/achartengine/src/org/achartengine/renderer/DefaultRenderer.java create mode 100644 android-libraries/achartengine/src/org/achartengine/renderer/DialRenderer.java create mode 100644 android-libraries/achartengine/src/org/achartengine/renderer/SimpleSeriesRenderer.java create mode 100644 android-libraries/achartengine/src/org/achartengine/renderer/XYMultipleSeriesRenderer.java create mode 100644 android-libraries/achartengine/src/org/achartengine/renderer/XYSeriesRenderer.java create mode 100644 android-libraries/achartengine/src/org/achartengine/renderer/package.html create mode 100644 android-libraries/achartengine/src/org/achartengine/tools/AbstractTool.java create mode 100644 android-libraries/achartengine/src/org/achartengine/tools/FitZoom.java create mode 100644 android-libraries/achartengine/src/org/achartengine/tools/Pan.java create mode 100644 android-libraries/achartengine/src/org/achartengine/tools/PanListener.java create mode 100644 android-libraries/achartengine/src/org/achartengine/tools/Zoom.java create mode 100644 android-libraries/achartengine/src/org/achartengine/tools/ZoomEvent.java create mode 100644 android-libraries/achartengine/src/org/achartengine/tools/ZoomListener.java create mode 100644 android-libraries/achartengine/src/org/achartengine/util/IndexXYMap.java create mode 100644 android-libraries/achartengine/src/org/achartengine/util/MathHelper.java create mode 100644 android-libraries/achartengine/src/org/achartengine/util/XYEntry.java create mode 100644 android-libraries/achartengine/src/org/achartengine/util/package.html delete mode 100644 android/libs/achartengine-1.0.0.jar diff --git a/android-libraries/achartengine/.classpath b/android-libraries/achartengine/.classpath new file mode 100644 index 00000000..a4f1e405 --- /dev/null +++ b/android-libraries/achartengine/.classpath @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/android-libraries/achartengine/.project b/android-libraries/achartengine/.project new file mode 100644 index 00000000..fb1c7567 --- /dev/null +++ b/android-libraries/achartengine/.project @@ -0,0 +1,33 @@ + + + achartengine + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/android-libraries/achartengine/AndroidManifest.xml b/android-libraries/achartengine/AndroidManifest.xml new file mode 100644 index 00000000..083e05fc --- /dev/null +++ b/android-libraries/achartengine/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/android-libraries/achartengine/extra/LICENSE-2.0.txt b/android-libraries/achartengine/extra/LICENSE-2.0.txt new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/android-libraries/achartengine/extra/LICENSE-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/android-libraries/achartengine/project.properties b/android-libraries/achartengine/project.properties new file mode 100644 index 00000000..337e8f37 --- /dev/null +++ b/android-libraries/achartengine/project.properties @@ -0,0 +1,12 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system use, +# "ant.properties", and override values to adapt the script to your +# project structure. + +# Project target. +target=android-7 +android.library=true diff --git a/android-libraries/achartengine/src/org/achartengine/ChartFactory.java b/android-libraries/achartengine/src/org/achartengine/ChartFactory.java new file mode 100644 index 00000000..301f1a8f --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/ChartFactory.java @@ -0,0 +1,708 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine; + +import org.achartengine.chart.BarChart; +import org.achartengine.chart.BarChart.Type; +import org.achartengine.chart.BubbleChart; +import org.achartengine.chart.CombinedXYChart; +import org.achartengine.chart.CubicLineChart; +import org.achartengine.chart.DialChart; +import org.achartengine.chart.DoughnutChart; +import org.achartengine.chart.LineChart; +import org.achartengine.chart.PieChart; +import org.achartengine.chart.RangeBarChart; +import org.achartengine.chart.ScatterChart; +import org.achartengine.chart.TimeChart; +import org.achartengine.chart.XYChart; +import org.achartengine.model.CategorySeries; +import org.achartengine.model.MultipleCategorySeries; +import org.achartengine.model.XYMultipleSeriesDataset; +import org.achartengine.renderer.DefaultRenderer; +import org.achartengine.renderer.DialRenderer; +import org.achartengine.renderer.XYMultipleSeriesRenderer; + +import android.content.Context; +import android.content.Intent; + +/** + * Utility methods for creating chart views or intents. + */ +public class ChartFactory { + /** The key for the chart data. */ + public static final String CHART = "chart"; + + /** The key for the chart graphical activity title. */ + public static final String TITLE = "title"; + + private ChartFactory() { + // empty for now + } + + /** + * Creates a line chart view. + * + * @param context the context + * @param dataset the multiple series dataset (cannot be null) + * @param renderer the multiple series renderer (cannot be null) + * @return a line chart graphical view + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset and the renderer don't include the same number of + * series + */ + public static final GraphicalView getLineChartView(Context context, + XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer) { + checkParameters(dataset, renderer); + XYChart chart = new LineChart(dataset, renderer); + return new GraphicalView(context, chart); + } + + /** + * Creates a cubic line chart view. + * + * @param context the context + * @param dataset the multiple series dataset (cannot be null) + * @param renderer the multiple series renderer (cannot be null) + * @return a line chart graphical view + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset and the renderer don't include the same number of + * series + */ + public static final GraphicalView getCubeLineChartView(Context context, + XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer, float smoothness) { + checkParameters(dataset, renderer); + XYChart chart = new CubicLineChart(dataset, renderer, smoothness); + return new GraphicalView(context, chart); + } + + /** + * Creates a scatter chart view. + * + * @param context the context + * @param dataset the multiple series dataset (cannot be null) + * @param renderer the multiple series renderer (cannot be null) + * @return a scatter chart graphical view + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset and the renderer don't include the same number of + * series + */ + public static final GraphicalView getScatterChartView(Context context, + XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer) { + checkParameters(dataset, renderer); + XYChart chart = new ScatterChart(dataset, renderer); + return new GraphicalView(context, chart); + } + + /** + * Creates a bubble chart view. + * + * @param context the context + * @param dataset the multiple series dataset (cannot be null) + * @param renderer the multiple series renderer (cannot be null) + * @return a scatter chart graphical view + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset and the renderer don't include the same number of + * series + */ + public static final GraphicalView getBubbleChartView(Context context, + XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer) { + checkParameters(dataset, renderer); + XYChart chart = new BubbleChart(dataset, renderer); + return new GraphicalView(context, chart); + } + + /** + * Creates a time chart view. + * + * @param context the context + * @param dataset the multiple series dataset (cannot be null) + * @param renderer the multiple series renderer (cannot be null) + * @param format the date format pattern to be used for displaying the X axis + * date labels. If null, a default appropriate format will be used. + * @return a time chart graphical view + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset and the renderer don't include the same number of + * series + */ + public static final GraphicalView getTimeChartView(Context context, + XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer, String format) { + checkParameters(dataset, renderer); + TimeChart chart = new TimeChart(dataset, renderer); + chart.setDateFormat(format); + return new GraphicalView(context, chart); + } + + /** + * Creates a bar chart view. + * + * @param context the context + * @param dataset the multiple series dataset (cannot be null) + * @param renderer the multiple series renderer (cannot be null) + * @param type the bar chart type + * @return a bar chart graphical view + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset and the renderer don't include the same number of + * series + */ + public static final GraphicalView getBarChartView(Context context, + XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer, Type type) { + checkParameters(dataset, renderer); + XYChart chart = new BarChart(dataset, renderer, type); + return new GraphicalView(context, chart); + } + + /** + * Creates a range bar chart view. + * + * @param context the context + * @param dataset the multiple series dataset (cannot be null) + * @param renderer the multiple series renderer (cannot be null) + * @param type the range bar chart type + * @return a bar chart graphical view + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset and the renderer don't include the same number of + * series + */ + public static final GraphicalView getRangeBarChartView(Context context, + XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer, Type type) { + checkParameters(dataset, renderer); + XYChart chart = new RangeBarChart(dataset, renderer, type); + return new GraphicalView(context, chart); + } + + /** + * Creates a combined XY chart view. + * + * @param context the context + * @param dataset the multiple series dataset (cannot be null) + * @param renderer the multiple series renderer (cannot be null) + * @param types the chart types (cannot be null) + * @return a combined XY chart graphical view + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if a dataset number of items is different than the number of + * series renderers or number of chart types + */ + public static final GraphicalView getCombinedXYChartView(Context context, + XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer, String[] types) { + if (dataset == null || renderer == null || types == null + || dataset.getSeriesCount() != types.length) { + throw new IllegalArgumentException( + "Dataset, renderer and types should be not null and the datasets series count should be equal to the types length"); + } + checkParameters(dataset, renderer); + CombinedXYChart chart = new CombinedXYChart(dataset, renderer, types); + return new GraphicalView(context, chart); + } + + /** + * Creates a pie chart intent that can be used to start the graphical view + * activity. + * + * @param context the context + * @param dataset the category series dataset (cannot be null) + * @param renderer the series renderer (cannot be null) + * @return a pie chart view + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset number of items is different than the number of + * series renderers + */ + public static final GraphicalView getPieChartView(Context context, CategorySeries dataset, + DefaultRenderer renderer) { + checkParameters(dataset, renderer); + PieChart chart = new PieChart(dataset, renderer); + return new GraphicalView(context, chart); + } + + /** + * Creates a dial chart intent that can be used to start the graphical view + * activity. + * + * @param context the context + * @param dataset the category series dataset (cannot be null) + * @param renderer the dial renderer (cannot be null) + * @return a pie chart view + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset number of items is different than the number of + * series renderers + */ + public static final GraphicalView getDialChartView(Context context, CategorySeries dataset, + DialRenderer renderer) { + checkParameters(dataset, renderer); + DialChart chart = new DialChart(dataset, renderer); + return new GraphicalView(context, chart); + } + + /** + * Creates a doughnut chart intent that can be used to start the graphical + * view activity. + * + * @param context the context + * @param dataset the multiple category series dataset (cannot be null) + * @param renderer the series renderer (cannot be null) + * @return a pie chart view + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset number of items is different than the number of + * series renderers + */ + public static final GraphicalView getDoughnutChartView(Context context, + MultipleCategorySeries dataset, DefaultRenderer renderer) { + checkParameters(dataset, renderer); + DoughnutChart chart = new DoughnutChart(dataset, renderer); + return new GraphicalView(context, chart); + } + + /** + * + * Creates a line chart intent that can be used to start the graphical view + * activity. + * + * @param context the context + * @param dataset the multiple series dataset (cannot be null) + * @param renderer the multiple series renderer (cannot be null) + * @return a line chart intent + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset and the renderer don't include the same number of + * series + */ + public static final Intent getLineChartIntent(Context context, XYMultipleSeriesDataset dataset, + XYMultipleSeriesRenderer renderer) { + return getLineChartIntent(context, dataset, renderer, ""); + } + + /** + * + * Creates a cubic line chart intent that can be used to start the graphical + * view activity. + * + * @param context the context + * @param dataset the multiple series dataset (cannot be null) + * @param renderer the multiple series renderer (cannot be null) + * @return a line chart intent + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset and the renderer don't include the same number of + * series + */ + public static final Intent getCubicLineChartIntent(Context context, + XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer, float smoothness) { + return getCubicLineChartIntent(context, dataset, renderer, smoothness, ""); + } + + /** + * Creates a scatter chart intent that can be used to start the graphical view + * activity. + * + * @param context the context + * @param dataset the multiple series dataset (cannot be null) + * @param renderer the multiple series renderer (cannot be null) + * @return a scatter chart intent + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset and the renderer don't include the same number of + * series + */ + public static final Intent getScatterChartIntent(Context context, + XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer) { + return getScatterChartIntent(context, dataset, renderer, ""); + } + + /** + * Creates a bubble chart intent that can be used to start the graphical view + * activity. + * + * @param context the context + * @param dataset the multiple series dataset (cannot be null) + * @param renderer the multiple series renderer (cannot be null) + * @return a scatter chart intent + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset and the renderer don't include the same number of + * series + */ + public static final Intent getBubbleChartIntent(Context context, XYMultipleSeriesDataset dataset, + XYMultipleSeriesRenderer renderer) { + return getBubbleChartIntent(context, dataset, renderer, ""); + } + + /** + * Creates a time chart intent that can be used to start the graphical view + * activity. + * + * @param context the context + * @param dataset the multiple series dataset (cannot be null) + * @param renderer the multiple series renderer (cannot be null) + * @param format the date format pattern to be used for displaying the X axis + * date labels. If null, a default appropriate format will be used. + * @return a time chart intent + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset and the renderer don't include the same number of + * series + */ + public static final Intent getTimeChartIntent(Context context, XYMultipleSeriesDataset dataset, + XYMultipleSeriesRenderer renderer, String format) { + return getTimeChartIntent(context, dataset, renderer, format, ""); + } + + /** + * Creates a bar chart intent that can be used to start the graphical view + * activity. + * + * @param context the context + * @param dataset the multiple series dataset (cannot be null) + * @param renderer the multiple series renderer (cannot be null) + * @param type the bar chart type + * @return a bar chart intent + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset and the renderer don't include the same number of + * series + */ + public static final Intent getBarChartIntent(Context context, XYMultipleSeriesDataset dataset, + XYMultipleSeriesRenderer renderer, Type type) { + return getBarChartIntent(context, dataset, renderer, type, ""); + } + + /** + * Creates a line chart intent that can be used to start the graphical view + * activity. + * + * @param context the context + * @param dataset the multiple series dataset (cannot be null) + * @param renderer the multiple series renderer (cannot be null) + * @param activityTitle the graphical chart activity title. If this is null, + * then the title bar will be hidden. If a blank title is passed in, + * then the title bar will be the default. Pass in any other string + * to set a custom title. + * @return a line chart intent + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset and the renderer don't include the same number of + * series + */ + public static final Intent getLineChartIntent(Context context, XYMultipleSeriesDataset dataset, + XYMultipleSeriesRenderer renderer, String activityTitle) { + checkParameters(dataset, renderer); + Intent intent = new Intent(context, GraphicalActivity.class); + XYChart chart = new LineChart(dataset, renderer); + intent.putExtra(CHART, chart); + intent.putExtra(TITLE, activityTitle); + return intent; + } + + /** + * Creates a line chart intent that can be used to start the graphical view + * activity. + * + * @param context the context + * @param dataset the multiple series dataset (cannot be null) + * @param renderer the multiple series renderer (cannot be null) + * @param activityTitle the graphical chart activity title. If this is null, + * then the title bar will be hidden. If a blank title is passed in, + * then the title bar will be the default. Pass in any other string + * to set a custom title. + * @return a line chart intent + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset and the renderer don't include the same number of + * series + */ + public static final Intent getCubicLineChartIntent(Context context, + XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer, float smoothness, + String activityTitle) { + checkParameters(dataset, renderer); + Intent intent = new Intent(context, GraphicalActivity.class); + XYChart chart = new CubicLineChart(dataset, renderer, smoothness); + intent.putExtra(CHART, chart); + intent.putExtra(TITLE, activityTitle); + return intent; + } + + /** + * Creates a scatter chart intent that can be used to start the graphical view + * activity. + * + * @param context the context + * @param dataset the multiple series dataset (cannot be null) + * @param renderer the multiple series renderer (cannot be null) + * @param activityTitle the graphical chart activity title + * @return a scatter chart intent + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset and the renderer don't include the same number of + * series + */ + public static final Intent getScatterChartIntent(Context context, + XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer, String activityTitle) { + checkParameters(dataset, renderer); + Intent intent = new Intent(context, GraphicalActivity.class); + XYChart chart = new ScatterChart(dataset, renderer); + intent.putExtra(CHART, chart); + intent.putExtra(TITLE, activityTitle); + return intent; + } + + /** + * Creates a bubble chart intent that can be used to start the graphical view + * activity. + * + * @param context the context + * @param dataset the multiple series dataset (cannot be null) + * @param renderer the multiple series renderer (cannot be null) + * @param activityTitle the graphical chart activity title + * @return a scatter chart intent + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset and the renderer don't include the same number of + * series + */ + public static final Intent getBubbleChartIntent(Context context, XYMultipleSeriesDataset dataset, + XYMultipleSeriesRenderer renderer, String activityTitle) { + checkParameters(dataset, renderer); + Intent intent = new Intent(context, GraphicalActivity.class); + XYChart chart = new BubbleChart(dataset, renderer); + intent.putExtra(CHART, chart); + intent.putExtra(TITLE, activityTitle); + return intent; + } + + /** + * Creates a time chart intent that can be used to start the graphical view + * activity. + * + * @param context the context + * @param dataset the multiple series dataset (cannot be null) + * @param renderer the multiple series renderer (cannot be null) + * @param format the date format pattern to be used for displaying the X axis + * date labels. If null, a default appropriate format will be used + * @param activityTitle the graphical chart activity title + * @return a time chart intent + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset and the renderer don't include the same number of + * series + */ + public static final Intent getTimeChartIntent(Context context, XYMultipleSeriesDataset dataset, + XYMultipleSeriesRenderer renderer, String format, String activityTitle) { + checkParameters(dataset, renderer); + Intent intent = new Intent(context, GraphicalActivity.class); + TimeChart chart = new TimeChart(dataset, renderer); + chart.setDateFormat(format); + intent.putExtra(CHART, chart); + intent.putExtra(TITLE, activityTitle); + return intent; + } + + /** + * Creates a bar chart intent that can be used to start the graphical view + * activity. + * + * @param context the context + * @param dataset the multiple series dataset (cannot be null) + * @param renderer the multiple series renderer (cannot be null) + * @param type the bar chart type + * @param activityTitle the graphical chart activity title + * @return a bar chart intent + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset and the renderer don't include the same number of + * series + */ + public static final Intent getBarChartIntent(Context context, XYMultipleSeriesDataset dataset, + XYMultipleSeriesRenderer renderer, Type type, String activityTitle) { + checkParameters(dataset, renderer); + Intent intent = new Intent(context, GraphicalActivity.class); + BarChart chart = new BarChart(dataset, renderer, type); + intent.putExtra(CHART, chart); + intent.putExtra(TITLE, activityTitle); + return intent; + } + + /** + * Creates a range bar chart intent that can be used to start the graphical + * view activity. + * + * @param context the context + * @param dataset the multiple series dataset (cannot be null) + * @param renderer the multiple series renderer (cannot be null) + * @param type the range bar chart type + * @param activityTitle the graphical chart activity title + * @return a range bar chart intent + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset and the renderer don't include the same number of + * series + */ + public static final Intent getRangeBarChartIntent(Context context, + XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer, Type type, + String activityTitle) { + checkParameters(dataset, renderer); + Intent intent = new Intent(context, GraphicalActivity.class); + RangeBarChart chart = new RangeBarChart(dataset, renderer, type); + intent.putExtra(CHART, chart); + intent.putExtra(TITLE, activityTitle); + return intent; + } + + /** + * Creates a combined XY chart intent that can be used to start the graphical + * view activity. + * + * @param context the context + * @param dataset the multiple series dataset (cannot be null) + * @param renderer the multiple series renderer (cannot be null) + * @param types the chart types (cannot be null) + * @param activityTitle the graphical chart activity title + * @return a combined XY chart intent + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if a dataset number of items is different than the number of + * series renderers or number of chart types + */ + public static final Intent getCombinedXYChartIntent(Context context, + XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer, String[] types, + String activityTitle) { + if (dataset == null || renderer == null || types == null + || dataset.getSeriesCount() != types.length) { + throw new IllegalArgumentException( + "Datasets, renderers and types should be not null and the datasets series count should be equal to the types length"); + } + checkParameters(dataset, renderer); + Intent intent = new Intent(context, GraphicalActivity.class); + CombinedXYChart chart = new CombinedXYChart(dataset, renderer, types); + intent.putExtra(CHART, chart); + intent.putExtra(TITLE, activityTitle); + return intent; + } + + /** + * Creates a pie chart intent that can be used to start the graphical view + * activity. + * + * @param context the context + * @param dataset the category series dataset (cannot be null) + * @param renderer the series renderer (cannot be null) + * @param activityTitle the graphical chart activity title + * @return a pie chart intent + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset number of items is different than the number of + * series renderers + */ + public static final Intent getPieChartIntent(Context context, CategorySeries dataset, + DefaultRenderer renderer, String activityTitle) { + checkParameters(dataset, renderer); + Intent intent = new Intent(context, GraphicalActivity.class); + PieChart chart = new PieChart(dataset, renderer); + intent.putExtra(CHART, chart); + intent.putExtra(TITLE, activityTitle); + return intent; + } + + /** + * Creates a doughnut chart intent that can be used to start the graphical + * view activity. + * + * @param context the context + * @param dataset the multiple category series dataset (cannot be null) + * @param renderer the series renderer (cannot be null) + * @param activityTitle the graphical chart activity title + * @return a pie chart intent + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset number of items is different than the number of + * series renderers + */ + public static final Intent getDoughnutChartIntent(Context context, + MultipleCategorySeries dataset, DefaultRenderer renderer, String activityTitle) { + checkParameters(dataset, renderer); + Intent intent = new Intent(context, GraphicalActivity.class); + DoughnutChart chart = new DoughnutChart(dataset, renderer); + intent.putExtra(CHART, chart); + intent.putExtra(TITLE, activityTitle); + return intent; + } + + /** + * Creates a dial chart intent that can be used to start the graphical view + * activity. + * + * @param context the context + * @param dataset the category series dataset (cannot be null) + * @param renderer the dial renderer (cannot be null) + * @param activityTitle the graphical chart activity title + * @return a dial chart intent + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset number of items is different than the number of + * series renderers + */ + public static final Intent getDialChartIntent(Context context, CategorySeries dataset, + DialRenderer renderer, String activityTitle) { + checkParameters(dataset, renderer); + Intent intent = new Intent(context, GraphicalActivity.class); + DialChart chart = new DialChart(dataset, renderer); + intent.putExtra(CHART, chart); + intent.putExtra(TITLE, activityTitle); + return intent; + } + + /** + * Checks the validity of the dataset and renderer parameters. + * + * @param dataset the multiple series dataset (cannot be null) + * @param renderer the multiple series renderer (cannot be null) + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset and the renderer don't include the same number of + * series + */ + private static void checkParameters(XYMultipleSeriesDataset dataset, + XYMultipleSeriesRenderer renderer) { + if (dataset == null || renderer == null + || dataset.getSeriesCount() != renderer.getSeriesRendererCount()) { + throw new IllegalArgumentException( + "Dataset and renderer should be not null and should have the same number of series"); + } + } + + /** + * Checks the validity of the dataset and renderer parameters. + * + * @param dataset the category series dataset (cannot be null) + * @param renderer the series renderer (cannot be null) + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset number of items is different than the number of + * series renderers + */ + private static void checkParameters(CategorySeries dataset, DefaultRenderer renderer) { + if (dataset == null || renderer == null + || dataset.getItemCount() != renderer.getSeriesRendererCount()) { + throw new IllegalArgumentException( + "Dataset and renderer should be not null and the dataset number of items should be equal to the number of series renderers"); + } + } + + /** + * Checks the validity of the dataset and renderer parameters. + * + * @param dataset the category series dataset (cannot be null) + * @param renderer the series renderer (cannot be null) + * @throws IllegalArgumentException if dataset is null or renderer is null or + * if the dataset number of items is different than the number of + * series renderers + */ + private static void checkParameters(MultipleCategorySeries dataset, DefaultRenderer renderer) { + if (dataset == null || renderer == null + || !checkMultipleSeriesItems(dataset, renderer.getSeriesRendererCount())) { + throw new IllegalArgumentException( + "Titles and values should be not null and the dataset number of items should be equal to the number of series renderers"); + } + } + + private static boolean checkMultipleSeriesItems(MultipleCategorySeries dataset, int value) { + int count = dataset.getCategoriesCount(); + boolean equal = true; + for (int k = 0; k < count && equal; k++) { + equal = dataset.getValues(k).length == dataset.getTitles(k).length; + } + return equal; + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/GraphicalActivity.java b/android-libraries/achartengine/src/org/achartengine/GraphicalActivity.java new file mode 100644 index 00000000..56c190ae --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/GraphicalActivity.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine; + +import org.achartengine.chart.AbstractChart; + +import android.app.Activity; +import android.os.Bundle; +import android.view.Window; + +/** + * An activity that encapsulates a graphical view of the chart. + */ +public class GraphicalActivity extends Activity { + /** The encapsulated graphical view. */ + private GraphicalView mView; + /** The chart to be drawn. */ + private AbstractChart mChart; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Bundle extras = getIntent().getExtras(); + mChart = (AbstractChart) extras.getSerializable(ChartFactory.CHART); + mView = new GraphicalView(this, mChart); + String title = extras.getString(ChartFactory.TITLE); + if (title == null) { + requestWindowFeature(Window.FEATURE_NO_TITLE); + } else if (title.length() > 0) { + setTitle(title); + } + setContentView(mView); + } + +} \ No newline at end of file diff --git a/android-libraries/achartengine/src/org/achartengine/GraphicalView.java b/android-libraries/achartengine/src/org/achartengine/GraphicalView.java new file mode 100644 index 00000000..e9aebff5 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/GraphicalView.java @@ -0,0 +1,337 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine; + +import org.achartengine.chart.AbstractChart; +import org.achartengine.chart.RoundChart; +import org.achartengine.chart.XYChart; +import org.achartengine.model.Point; +import org.achartengine.model.SeriesSelection; +import org.achartengine.renderer.DefaultRenderer; +import org.achartengine.renderer.XYMultipleSeriesRenderer; +import org.achartengine.tools.FitZoom; +import org.achartengine.tools.PanListener; +import org.achartengine.tools.Zoom; +import org.achartengine.tools.ZoomListener; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Build; +import android.os.Handler; +import android.view.MotionEvent; +import android.view.View; + +/** + * The view that encapsulates the graphical chart. + */ +public class GraphicalView extends View { + /** The chart to be drawn. */ + private AbstractChart mChart; + /** The chart renderer. */ + private DefaultRenderer mRenderer; + /** The view bounds. */ + private Rect mRect = new Rect(); + /** The user interface thread handler. */ + private Handler mHandler; + /** The zoom buttons rectangle. */ + private RectF mZoomR = new RectF(); + /** The zoom in icon. */ + private Bitmap zoomInImage; + /** The zoom out icon. */ + private Bitmap zoomOutImage; + /** The fit zoom icon. */ + private Bitmap fitZoomImage; + /** The zoom area size. */ + private int zoomSize = 50; + /** The zoom buttons background color. */ + private static final int ZOOM_BUTTONS_COLOR = Color.argb(175, 150, 150, 150); + /** The zoom in tool. */ + private Zoom mZoomIn; + /** The zoom out tool. */ + private Zoom mZoomOut; + /** The fit zoom tool. */ + private FitZoom mFitZoom; + /** The paint to be used when drawing the chart. */ + private Paint mPaint = new Paint(); + /** The touch handler. */ + private ITouchHandler mTouchHandler; + /** The old x coordinate. */ + private float oldX; + /** The old y coordinate. */ + private float oldY; + + /** + * Creates a new graphical view. + * + * @param context the context + * @param chart the chart to be drawn + */ + public GraphicalView(Context context, AbstractChart chart) { + super(context); + mChart = chart; + mHandler = new Handler(); + if (mChart instanceof XYChart) { + mRenderer = ((XYChart) mChart).getRenderer(); + } else { + mRenderer = ((RoundChart) mChart).getRenderer(); + } + if (mRenderer.isZoomButtonsVisible()) { + zoomInImage = BitmapFactory.decodeStream(GraphicalView.class + .getResourceAsStream("image/zoom_in.png")); + zoomOutImage = BitmapFactory.decodeStream(GraphicalView.class + .getResourceAsStream("image/zoom_out.png")); + fitZoomImage = BitmapFactory.decodeStream(GraphicalView.class + .getResourceAsStream("image/zoom-1.png")); + } + + if (mRenderer instanceof XYMultipleSeriesRenderer + && ((XYMultipleSeriesRenderer) mRenderer).getMarginsColor() == XYMultipleSeriesRenderer.NO_COLOR) { + ((XYMultipleSeriesRenderer) mRenderer).setMarginsColor(mPaint.getColor()); + } + if (mRenderer.isZoomEnabled() && mRenderer.isZoomButtonsVisible() + || mRenderer.isExternalZoomEnabled()) { + mZoomIn = new Zoom(mChart, true, mRenderer.getZoomRate()); + mZoomOut = new Zoom(mChart, false, mRenderer.getZoomRate()); + mFitZoom = new FitZoom(mChart); + } + int version = 7; + try { + version = Integer.valueOf(Build.VERSION.SDK); + } catch (Exception e) { + // do nothing + } + if (version < 7) { + mTouchHandler = new TouchHandlerOld(this, mChart); + } else { + mTouchHandler = new TouchHandler(this, mChart); + } + } + + /** + * Returns the current series selection object. + * + * @return the series selection + */ + public SeriesSelection getCurrentSeriesAndPoint() { + return mChart.getSeriesAndPointForScreenCoordinate(new Point(oldX, oldY)); + } + + /** + * Transforms the currently selected screen point to a real point. + * + * @param scale the scale + * @return the currently selected real point + */ + public double[] toRealPoint(int scale) { + if (mChart instanceof XYChart) { + XYChart chart = (XYChart) mChart; + return chart.toRealPoint(oldX, oldY, scale); + } + return null; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + canvas.getClipBounds(mRect); + int top = mRect.top; + int left = mRect.left; + int width = mRect.width(); + int height = mRect.height(); + if (mRenderer.isInScroll()) { + top = 0; + left = 0; + width = getMeasuredWidth(); + height = getMeasuredHeight(); + } + mChart.draw(canvas, left, top, width, height, mPaint); + if (mRenderer != null && mRenderer.isZoomEnabled() && mRenderer.isZoomButtonsVisible()) { + mPaint.setColor(ZOOM_BUTTONS_COLOR); + zoomSize = Math.max(zoomSize, Math.min(width, height) / 7); + mZoomR.set(left + width - zoomSize * 3, top + height - zoomSize * 0.775f, left + width, top + + height); + canvas.drawRoundRect(mZoomR, zoomSize / 3, zoomSize / 3, mPaint); + float buttonY = top + height - zoomSize * 0.625f; + canvas.drawBitmap(zoomInImage, left + width - zoomSize * 2.75f, buttonY, null); + canvas.drawBitmap(zoomOutImage, left + width - zoomSize * 1.75f, buttonY, null); + canvas.drawBitmap(fitZoomImage, left + width - zoomSize * 0.75f, buttonY, null); + } + } + + /** + * Sets the zoom rate. + * + * @param rate the zoom rate + */ + public void setZoomRate(float rate) { + if (mZoomIn != null && mZoomOut != null) { + mZoomIn.setZoomRate(rate); + mZoomOut.setZoomRate(rate); + } + } + + /** + * Do a chart zoom in. + */ + public void zoomIn() { + if (mZoomIn != null) { + mZoomIn.apply(Zoom.ZOOM_AXIS_XY); + repaint(); + } + } + + /** + * Do a chart zoom out. + */ + public void zoomOut() { + if (mZoomOut != null) { + mZoomOut.apply(Zoom.ZOOM_AXIS_XY); + repaint(); + } + } + + + + /** + * Do a chart zoom reset / fit zoom. + */ + public void zoomReset() { + if (mFitZoom != null) { + mFitZoom.apply(); + mZoomIn.notifyZoomResetListeners(); + repaint(); + } + } + + /** + * Adds a new zoom listener. + * + * @param listener zoom listener + */ + public void addZoomListener(ZoomListener listener, boolean onButtons, boolean onPinch) { + if (onButtons) { + if (mZoomIn != null) { + mZoomIn.addZoomListener(listener); + mZoomOut.addZoomListener(listener); + } + if (onPinch) { + mTouchHandler.addZoomListener(listener); + } + } + } + + /** + * Removes a zoom listener. + * + * @param listener zoom listener + */ + public synchronized void removeZoomListener(ZoomListener listener) { + if (mZoomIn != null) { + mZoomIn.removeZoomListener(listener); + mZoomOut.removeZoomListener(listener); + } + mTouchHandler.removeZoomListener(listener); + } + + /** + * Adds a new pan listener. + * + * @param listener pan listener + */ + public void addPanListener(PanListener listener) { + mTouchHandler.addPanListener(listener); + } + + /** + * Removes a pan listener. + * + * @param listener pan listener + */ + public void removePanListener(PanListener listener) { + mTouchHandler.removePanListener(listener); + } + + protected RectF getZoomRectangle() { + return mZoomR; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + // save the x and y so they can be used in the click and long press + // listeners + oldX = event.getX(); + oldY = event.getY(); + } + if (mRenderer != null && (mRenderer.isPanEnabled() || mRenderer.isZoomEnabled())) { + if (mTouchHandler.handleTouch(event)) { + return true; + } + } + return super.onTouchEvent(event); + } + + /** + * Schedule a view content repaint. + */ + public void repaint() { + mHandler.post(new Runnable() { + public void run() { + invalidate(); + } + }); + } + + /** + * Schedule a view content repaint, in the specified rectangle area. + * + * @param left the left position of the area to be repainted + * @param top the top position of the area to be repainted + * @param right the right position of the area to be repainted + * @param bottom the bottom position of the area to be repainted + */ + public void repaint(final int left, final int top, final int right, final int bottom) { + mHandler.post(new Runnable() { + public void run() { + invalidate(left, top, right, bottom); + } + }); + } + + /** + * Saves the content of the graphical view to a bitmap. + * + * @return the bitmap + */ + public Bitmap toBitmap() { + setDrawingCacheEnabled(false); + if (!isDrawingCacheEnabled()) { + setDrawingCacheEnabled(true); + } + if (mRenderer.isApplyBackgroundColor()) { + setDrawingCacheBackgroundColor(mRenderer.getBackgroundColor()); + } + setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH); + return getDrawingCache(true); + } + +} \ No newline at end of file diff --git a/android-libraries/achartengine/src/org/achartengine/ITouchHandler.java b/android-libraries/achartengine/src/org/achartengine/ITouchHandler.java new file mode 100644 index 00000000..4debe964 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/ITouchHandler.java @@ -0,0 +1,63 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine; + +import org.achartengine.tools.PanListener; +import org.achartengine.tools.ZoomListener; + +import android.view.MotionEvent; + +/** + * The interface to be implemented by the touch handlers. + */ +public interface ITouchHandler { + /** + * Handles the touch event. + * + * @param event the touch event + * @return true if the event was handled + */ + boolean handleTouch(MotionEvent event); + + /** + * Adds a new zoom listener. + * + * @param listener zoom listener + */ + void addZoomListener(ZoomListener listener); + + /** + * Removes a zoom listener. + * + * @param listener zoom listener + */ + void removeZoomListener(ZoomListener listener); + + /** + * Adds a new pan listener. + * + * @param listener pan listener + */ + void addPanListener(PanListener listener); + + /** + * Removes a pan listener. + * + * @param listener pan listener + */ + void removePanListener(PanListener listener); + +} \ No newline at end of file diff --git a/android-libraries/achartengine/src/org/achartengine/TouchHandler.java b/android-libraries/achartengine/src/org/achartengine/TouchHandler.java new file mode 100644 index 00000000..a06d05d6 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/TouchHandler.java @@ -0,0 +1,204 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine; + +import org.achartengine.chart.AbstractChart; +import org.achartengine.chart.RoundChart; +import org.achartengine.chart.XYChart; +import org.achartengine.renderer.DefaultRenderer; +import org.achartengine.tools.Pan; +import org.achartengine.tools.PanListener; +import org.achartengine.tools.Zoom; +import org.achartengine.tools.ZoomListener; + +import android.graphics.RectF; +import android.view.MotionEvent; + +/** + * The main handler of the touch events. + */ +public class TouchHandler implements ITouchHandler { + /** The chart renderer. */ + private DefaultRenderer mRenderer; + /** The old x coordinate. */ + private float oldX; + /** The old y coordinate. */ + private float oldY; + /** The old x2 coordinate. */ + private float oldX2; + /** The old y2 coordinate. */ + private float oldY2; + /** The zoom buttons rectangle. */ + private RectF zoomR = new RectF(); + /** The pan tool. */ + private Pan mPan; + /** The zoom for the pinch gesture. */ + private Zoom mPinchZoom; + /** The graphical view. */ + private GraphicalView graphicalView; + + /** + * Creates a new graphical view. + * + * @param view the graphical view + * @param chart the chart to be drawn + */ + public TouchHandler(GraphicalView view, AbstractChart chart) { + graphicalView = view; + zoomR = graphicalView.getZoomRectangle(); + if (chart instanceof XYChart) { + mRenderer = ((XYChart) chart).getRenderer(); + } else { + mRenderer = ((RoundChart) chart).getRenderer(); + } + if (mRenderer.isPanEnabled()) { + mPan = new Pan(chart); + } + if (mRenderer.isZoomEnabled()) { + mPinchZoom = new Zoom(chart, true, 1); + } + } + + /** + * Handles the touch event. + * + * @param event the touch event + */ + public boolean handleTouch(MotionEvent event) { + int action = event.getAction(); + if (mRenderer != null && action == MotionEvent.ACTION_MOVE) { + if (oldX >= 0 || oldY >= 0) { + float newX = event.getX(0); + float newY = event.getY(0); + if (event.getPointerCount() > 1 && (oldX2 >= 0 || oldY2 >= 0) && mRenderer.isZoomEnabled()) { + float newX2 = event.getX(1); + float newY2 = event.getY(1); + float newDeltaX = Math.abs(newX - newX2); + float newDeltaY = Math.abs(newY - newY2); + float oldDeltaX = Math.abs(oldX - oldX2); + float oldDeltaY = Math.abs(oldY - oldY2); + float zoomRate = 1; + + float tan1 = Math.abs(newY - oldY) / Math.abs(newX - oldX); + float tan2 = Math.abs(newY2 - oldY2) / Math.abs(newX2 - oldX2); + if ( tan1 <= 0.577 && tan2 <= 0.577) { + // horizontal pinch zoom, |deltaY| / |deltaX| is [0 ~ 0.577], 0.577 is the approximate value of tan(Pi/6) + zoomRate = newDeltaX / oldDeltaX; + if (zoomRate > 0.909 && zoomRate < 1.1) { + mPinchZoom.setZoomRate(zoomRate); + mPinchZoom.apply(Zoom.ZOOM_AXIS_X); + } + } else if ( tan1 >= 1.732 && tan2 >= 1.732 ) { + // pinch zoom vertically, |deltaY| / |deltaX| is [1.732 ~ infinity], 1.732 is the approximate value of tan(Pi/3) + zoomRate = newDeltaY / oldDeltaY; + if (zoomRate > 0.909 && zoomRate < 1.1) { + mPinchZoom.setZoomRate(zoomRate); + mPinchZoom.apply(Zoom.ZOOM_AXIS_Y); + } + } else if ( (tan1 > 0.577 && tan1 < 1.732) && (tan2 > 0.577 && tan2 < 1.732) ){ + // pinch zoom diagonally + if (Math.abs(newX - oldX) >= Math.abs(newY - oldY)) { + zoomRate = newDeltaX / oldDeltaX; + } else { + zoomRate = newDeltaY / oldDeltaY; + } + if (zoomRate > 0.909 && zoomRate < 1.1) { + mPinchZoom.setZoomRate(zoomRate); + mPinchZoom.apply(Zoom.ZOOM_AXIS_XY); + } + } + oldX2 = newX2; + oldY2 = newY2; + } else if (mRenderer.isPanEnabled()) { + mPan.apply(oldX, oldY, newX, newY); + oldX2 = 0; + oldY2 = 0; + } + oldX = newX; + oldY = newY; + graphicalView.repaint(); + return true; + } + } else if (action == MotionEvent.ACTION_DOWN) { + oldX = event.getX(0); + oldY = event.getY(0); + if (mRenderer != null && mRenderer.isZoomEnabled() && zoomR.contains(oldX, oldY)) { + if (oldX < zoomR.left + zoomR.width() / 3) { + graphicalView.zoomIn(); + } else if (oldX < zoomR.left + zoomR.width() * 2 / 3) { + graphicalView.zoomOut(); + } else { + graphicalView.zoomReset(); + } + return true; + } + } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { + oldX = 0; + oldY = 0; + oldX2 = 0; + oldY2 = 0; + if (action == MotionEvent.ACTION_POINTER_UP) { + oldX = -1; + oldY = -1; + } + } + return !mRenderer.isClickEnabled(); + } + + /** + * Adds a new zoom listener. + * + * @param listener zoom listener + */ + public void addZoomListener(ZoomListener listener) { + if (mPinchZoom != null) { + mPinchZoom.addZoomListener(listener); + } + } + + /** + * Removes a zoom listener. + * + * @param listener zoom listener + */ + public void removeZoomListener(ZoomListener listener) { + if (mPinchZoom != null) { + mPinchZoom.removeZoomListener(listener); + } + } + + /** + * Adds a new pan listener. + * + * @param listener pan listener + */ + public void addPanListener(PanListener listener) { + if (mPan != null) { + mPan.addPanListener(listener); + } + } + + /** + * Removes a pan listener. + * + * @param listener pan listener + */ + public void removePanListener(PanListener listener) { + if (mPan != null) { + mPan.removePanListener(listener); + } + } +} \ No newline at end of file diff --git a/android-libraries/achartengine/src/org/achartengine/TouchHandlerOld.java b/android-libraries/achartengine/src/org/achartengine/TouchHandlerOld.java new file mode 100644 index 00000000..38b4f227 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/TouchHandlerOld.java @@ -0,0 +1,137 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine; + +import org.achartengine.chart.AbstractChart; +import org.achartengine.chart.RoundChart; +import org.achartengine.chart.XYChart; +import org.achartengine.renderer.DefaultRenderer; +import org.achartengine.tools.Pan; +import org.achartengine.tools.PanListener; +import org.achartengine.tools.ZoomListener; + +import android.graphics.RectF; +import android.view.MotionEvent; + +/** + * A handler implementation for touch events for older platforms. + */ +public class TouchHandlerOld implements ITouchHandler { + /** The chart renderer. */ + private DefaultRenderer mRenderer; + /** The old x coordinate. */ + private float oldX; + /** The old y coordinate. */ + private float oldY; + /** The zoom buttons rectangle. */ + private RectF zoomR = new RectF(); + /** The pan tool. */ + private Pan mPan; + /** The graphical view. */ + private GraphicalView graphicalView; + + /** + * Creates an implementation of the old version of the touch handler. + * + * @param view the graphical view + * @param chart the chart to be drawn + */ + public TouchHandlerOld(GraphicalView view, AbstractChart chart) { + graphicalView = view; + zoomR = graphicalView.getZoomRectangle(); + if (chart instanceof XYChart) { + mRenderer = ((XYChart) chart).getRenderer(); + } else { + mRenderer = ((RoundChart) chart).getRenderer(); + } + if (mRenderer.isPanEnabled()) { + mPan = new Pan(chart); + } + } + + public boolean handleTouch(MotionEvent event) { + int action = event.getAction(); + if (mRenderer != null && action == MotionEvent.ACTION_MOVE) { + if (oldX >= 0 || oldY >= 0) { + float newX = event.getX(); + float newY = event.getY(); + if (mRenderer.isPanEnabled()) { + mPan.apply(oldX, oldY, newX, newY); + } + oldX = newX; + oldY = newY; + graphicalView.repaint(); + return true; + } + } else if (action == MotionEvent.ACTION_DOWN) { + oldX = event.getX(); + oldY = event.getY(); + if (mRenderer != null && mRenderer.isZoomEnabled() && zoomR.contains(oldX, oldY)) { + if (oldX < zoomR.left + zoomR.width() / 3) { + graphicalView.zoomIn(); + } else if (oldX < zoomR.left + zoomR.width() * 2 / 3) { + graphicalView.zoomOut(); + } else { + graphicalView.zoomReset(); + } + return true; + } + } else if (action == MotionEvent.ACTION_UP) { + oldX = 0; + oldY = 0; + } + return !mRenderer.isClickEnabled(); + } + + /** + * Adds a new zoom listener. + * + * @param listener zoom listener + */ + public void addZoomListener(ZoomListener listener) { + } + + /** + * Removes a zoom listener. + * + * @param listener zoom listener + */ + public void removeZoomListener(ZoomListener listener) { + } + + /** + * Adds a new pan listener. + * + * @param listener pan listener + */ + public void addPanListener(PanListener listener) { + if (mPan != null) { + mPan.addPanListener(listener); + } + } + + /** + * Removes a pan listener. + * + * @param listener pan listener + */ + public void removePanListener(PanListener listener) { + if (mPan != null) { + mPan.removePanListener(listener); + } + } + +} \ No newline at end of file diff --git a/android-libraries/achartengine/src/org/achartengine/chart/AbstractChart.java b/android-libraries/achartengine/src/org/achartengine/chart/AbstractChart.java new file mode 100644 index 00000000..f99a5c2a --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/chart/AbstractChart.java @@ -0,0 +1,475 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.chart; + +import java.io.Serializable; +import java.util.List; + +import org.achartengine.model.Point; +import org.achartengine.model.SeriesSelection; +import org.achartengine.renderer.DefaultRenderer; +import org.achartengine.renderer.SimpleSeriesRenderer; +import org.achartengine.renderer.XYMultipleSeriesRenderer; +import org.achartengine.renderer.XYMultipleSeriesRenderer.Orientation; +import org.achartengine.util.MathHelper; + +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.Rect; +import android.graphics.RectF; + +/** + * An abstract class to be implemented by the chart rendering classes. + */ +public abstract class AbstractChart implements Serializable { + /** + * The graphical representation of the chart. + * + * @param canvas the canvas to paint to + * @param x the top left x value of the view to draw to + * @param y the top left y value of the view to draw to + * @param width the width of the view to draw to + * @param height the height of the view to draw to + * @param paint the paint + */ + public abstract void draw(Canvas canvas, int x, int y, int width, int height, Paint paint); + + /** + * Draws the chart background. + * + * @param renderer the chart renderer + * @param canvas the canvas to paint to + * @param x the top left x value of the view to draw to + * @param y the top left y value of the view to draw to + * @param width the width of the view to draw to + * @param height the height of the view to draw to + * @param paint the paint used for drawing + * @param newColor if a new color is to be used + * @param color the color to be used + */ + protected void drawBackground(DefaultRenderer renderer, Canvas canvas, int x, int y, int width, + int height, Paint paint, boolean newColor, int color) { + if (renderer.isApplyBackgroundColor() || newColor) { + if (newColor) { + paint.setColor(color); + } else { + paint.setColor(renderer.getBackgroundColor()); + } + paint.setStyle(Style.FILL); + canvas.drawRect(x, y, x + width, y + height, paint); + } + } + + /** + * Draws the chart legend. + * + * @param canvas the canvas to paint to + * @param renderer the series renderer + * @param titles the titles to go to the legend + * @param left the left X value of the area to draw to + * @param right the right X value of the area to draw to + * @param y the y value of the area to draw to + * @param width the width of the area to draw to + * @param height the height of the area to draw to + * @param legendSize the legend size + * @param paint the paint to be used for drawing + * @param calculate if only calculating the legend size + * + * @return the legend height + */ + protected int drawLegend(Canvas canvas, DefaultRenderer renderer, String[] titles, int left, + int right, int y, int width, int height, int legendSize, Paint paint, boolean calculate) { + float size = 32; + if (renderer.isShowLegend()) { + float currentX = left; + float currentY = y + height - legendSize + size; + paint.setTextAlign(Align.LEFT); + paint.setTextSize(renderer.getLegendTextSize()); + int sLength = Math.min(titles.length, renderer.getSeriesRendererCount()); + for (int i = 0; i < sLength; i++) { + final float lineSize = getLegendShapeWidth(i); + String text = titles[i]; + if (titles.length == renderer.getSeriesRendererCount()) { + paint.setColor(renderer.getSeriesRendererAt(i).getColor()); + } else { + paint.setColor(Color.LTGRAY); + } + float[] widths = new float[text.length()]; + paint.getTextWidths(text, widths); + float sum = 0; + for (float value : widths) { + sum += value; + } + float extraSize = lineSize + 10 + sum; + float currentWidth = currentX + extraSize; + + if (i > 0 && getExceed(currentWidth, renderer, right, width)) { + currentX = left; + currentY += renderer.getLegendTextSize(); + size += renderer.getLegendTextSize(); + currentWidth = currentX + extraSize; + } + if (getExceed(currentWidth, renderer, right, width)) { + float maxWidth = right - currentX - lineSize - 10; + if (isVertical(renderer)) { + maxWidth = width - currentX - lineSize - 10; + } + int nr = paint.breakText(text, true, maxWidth, widths); + text = text.substring(0, nr) + "..."; + } + if (!calculate) { + drawLegendShape(canvas, renderer.getSeriesRendererAt(i), currentX, currentY, i, paint); + drawString(canvas, text, currentX + lineSize + 5, currentY + 5, paint); + } + currentX += extraSize; + } + } + return Math.round(size + renderer.getLegendTextSize()); + } + + /** + * Draw a multiple lines string. + * + * @param canvas the canvas to paint to + * @param text the text to be painted + * @param x the x value of the area to draw to + * @param y the y value of the area to draw to + * @param paint the paint to be used for drawing + */ + protected void drawString(Canvas canvas, String text, float x, float y, Paint paint) { + String[] lines = text.split("\n"); + Rect rect = new Rect(); + int yOff = 0; + for (int i = 0; i < lines.length; ++i) { + canvas.drawText(lines[i], x, y + yOff, paint); + paint.getTextBounds(lines[i], 0, lines[i].length(), rect); + yOff = yOff + rect.height() + 5; // space between lines is 5 + } + } + + /** + * Calculates if the current width exceeds the total width. + * + * @param currentWidth the current width + * @param renderer the renderer + * @param right the right side pixel value + * @param width the total width + * @return if the current width exceeds the total width + */ + protected boolean getExceed(float currentWidth, DefaultRenderer renderer, int right, int width) { + boolean exceed = currentWidth > right; + if (isVertical(renderer)) { + exceed = currentWidth > width; + } + return exceed; + } + + /** + * Checks if the current chart is rendered as vertical. + * + * @param renderer the renderer + * @return if the chart is rendered as a vertical one + */ + public boolean isVertical(DefaultRenderer renderer) { + return renderer instanceof XYMultipleSeriesRenderer + && ((XYMultipleSeriesRenderer) renderer).getOrientation() == Orientation.VERTICAL; + } + + /** + * Makes sure the fraction digit is not displayed, if not needed. + * + * @param label the input label value + * @return the label without the useless fraction digit + */ + protected String getLabel(double label) { + String text = ""; + if (label == Math.round(label)) { + text = Math.round(label) + ""; + } else { + text = label + ""; + } + return text; + } + + private static float[] calculateDrawPoints(float p1x, float p1y, float p2x, float p2y, + int screenHeight, int screenWidth) { + float drawP1x; + float drawP1y; + float drawP2x; + float drawP2y; + + if (p1y > screenHeight) { + // Intersection with the top of the screen + float m = (p2y - p1y) / (p2x - p1x); + drawP1x = (screenHeight - p1y + m * p1x) / m; + drawP1y = screenHeight; + + if (drawP1x < 0) { + // If Intersection is left of the screen we calculate the intersection + // with the left border + drawP1x = 0; + drawP1y = p1y - m * p1x; + } else if (drawP1x > screenWidth) { + // If Intersection is right of the screen we calculate the intersection + // with the right border + drawP1x = screenWidth; + drawP1y = m * screenWidth + p1y - m * p1x; + } + } else if (p1y < 0) { + float m = (p2y - p1y) / (p2x - p1x); + drawP1x = (-p1y + m * p1x) / m; + drawP1y = 0; + if (drawP1x < 0) { + drawP1x = 0; + drawP1y = p1y - m * p1x; + } else if (drawP1x > screenWidth) { + drawP1x = screenWidth; + drawP1y = m * screenWidth + p1y - m * p1x; + } + } else { + // If the point is in the screen use it + drawP1x = p1x; + drawP1y = p1y; + } + + if (p2y > screenHeight) { + float m = (p2y - p1y) / (p2x - p1x); + drawP2x = (screenHeight - p1y + m * p1x) / m; + drawP2y = screenHeight; + if (drawP2x < 0) { + drawP2x = 0; + drawP2y = p1y - m * p1x; + } else if (drawP2x > screenWidth) { + drawP2x = screenWidth; + drawP2y = m * screenWidth + p1y - m * p1x; + } + } else if (p2y < 0) { + float m = (p2y - p1y) / (p2x - p1x); + drawP2x = (-p1y + m * p1x) / m; + drawP2y = 0; + if (drawP2x < 0) { + drawP2x = 0; + drawP2y = p1y - m * p1x; + } else if (drawP2x > screenWidth) { + drawP2x = screenWidth; + drawP2y = m * screenWidth + p1y - m * p1x; + } + } else { + // If the point is in the screen use it + drawP2x = p2x; + drawP2y = p2y; + } + + return new float[] { drawP1x, drawP1y, drawP2x, drawP2y }; + } + + /** + * The graphical representation of a path. + * + * @param canvas the canvas to paint to + * @param points the points that are contained in the path to paint + * @param paint the paint to be used for painting + * @param circular if the path ends with the start point + */ + protected void drawPath(Canvas canvas, float[] points, Paint paint, boolean circular) { + Path path = new Path(); + int height = canvas.getHeight(); + int width = canvas.getWidth(); + + float[] tempDrawPoints; + if (points.length < 4) { + return; + } + tempDrawPoints = calculateDrawPoints(points[0], points[1], points[2], points[3], height, width); + path.moveTo(tempDrawPoints[0], tempDrawPoints[1]); + path.lineTo(tempDrawPoints[2], tempDrawPoints[3]); + + for (int i = 4; i < points.length; i += 2) { + if ((points[i - 1] < 0 && points[i + 1] < 0) + || (points[i - 1] > height && points[i + 1] > height)) { + continue; + } + tempDrawPoints = calculateDrawPoints(points[i - 2], points[i - 1], points[i], points[i + 1], + height, width); + if (!circular) { + path.moveTo(tempDrawPoints[0], tempDrawPoints[1]); + } + path.lineTo(tempDrawPoints[2], tempDrawPoints[3]); + } + if (circular) { + path.lineTo(points[0], points[1]); + } + canvas.drawPath(path, paint); + } + + /** + * Returns the legend shape width. + * + * @param seriesIndex the series index + * @return the legend shape width + */ + public abstract int getLegendShapeWidth(int seriesIndex); + + /** + * The graphical representation of the legend shape. + * + * @param canvas the canvas to paint to + * @param renderer the series renderer + * @param x the x value of the point the shape should be drawn at + * @param y the y value of the point the shape should be drawn at + * @param seriesIndex the series index + * @param paint the paint to be used for drawing + */ + public abstract void drawLegendShape(Canvas canvas, SimpleSeriesRenderer renderer, float x, + float y, int seriesIndex, Paint paint); + + /** + * Calculates the best text to fit into the available space. + * + * @param text the entire text + * @param width the width to fit the text into + * @param paint the paint + * @return the text to fit into the space + */ + private String getFitText(String text, float width, Paint paint) { + String newText = text; + int length = text.length(); + int diff = 0; + while (paint.measureText(newText) > width && diff < length) { + diff++; + newText = text.substring(0, length - diff) + "..."; + } + if (diff == length) { + newText = "..."; + } + return newText; + } + + /** + * Calculates the current legend size. + * + * @param renderer the renderer + * @param defaultHeight the default height + * @param extraHeight the added extra height + * @return the legend size + */ + protected int getLegendSize(DefaultRenderer renderer, int defaultHeight, float extraHeight) { + int legendSize = renderer.getLegendHeight(); + if (renderer.isShowLegend() && legendSize == 0) { + legendSize = defaultHeight; + } + if (!renderer.isShowLegend() && renderer.isShowLabels()) { + legendSize = (int) (renderer.getLabelsTextSize() * 4 / 3 + extraHeight); + } + return legendSize; + } + + /** + * Draws a text label. + * + * @param canvas the canvas + * @param labelText the label text + * @param renderer the renderer + * @param prevLabelsBounds the previous rendered label bounds + * @param centerX the round chart center on X axis + * @param centerY the round chart center on Y axis + * @param shortRadius the short radius for the round chart + * @param longRadius the long radius for the round chart + * @param currentAngle the current angle + * @param angle the label extra angle + * @param left the left side + * @param right the right side + * @param color the label color + * @param paint the paint + * @param line if a line to the label should be drawn + */ + protected void drawLabel(Canvas canvas, String labelText, DefaultRenderer renderer, + List prevLabelsBounds, int centerX, int centerY, float shortRadius, float longRadius, + float currentAngle, float angle, int left, int right, int color, Paint paint, boolean line) { + if (renderer.isShowLabels()) { + paint.setColor(color); + double rAngle = Math.toRadians(90 - (currentAngle + angle / 2)); + double sinValue = Math.sin(rAngle); + double cosValue = Math.cos(rAngle); + int x1 = Math.round(centerX + (float) (shortRadius * sinValue)); + int y1 = Math.round(centerY + (float) (shortRadius * cosValue)); + int x2 = Math.round(centerX + (float) (longRadius * sinValue)); + int y2 = Math.round(centerY + (float) (longRadius * cosValue)); + + float size = renderer.getLabelsTextSize(); + float extra = Math.max(size / 2, 10); + paint.setTextAlign(Align.LEFT); + if (x1 > x2) { + extra = -extra; + paint.setTextAlign(Align.RIGHT); + } + float xLabel = x2 + extra; + float yLabel = y2; + float width = right - xLabel; + if (x1 > x2) { + width = xLabel - left; + } + labelText = getFitText(labelText, width, paint); + float widthLabel = paint.measureText(labelText); + boolean okBounds = false; + while (!okBounds && line) { + boolean intersects = false; + int length = prevLabelsBounds.size(); + for (int j = 0; j < length && !intersects; j++) { + RectF prevLabelBounds = prevLabelsBounds.get(j); + if (prevLabelBounds.intersects(xLabel, yLabel, xLabel + widthLabel, yLabel + size)) { + intersects = true; + yLabel = Math.max(yLabel, prevLabelBounds.bottom); + } + } + okBounds = !intersects; + } + + if (line) { + y2 = (int) (yLabel - size / 2); + canvas.drawLine(x1, y1, x2, y2, paint); + canvas.drawLine(x2, y2, x2 + extra, y2, paint); + } else { + paint.setTextAlign(Align.CENTER); + } + canvas.drawText(labelText, xLabel, yLabel, paint); + if (line) { + prevLabelsBounds.add(new RectF(xLabel, yLabel, xLabel + widthLabel, yLabel + size)); + } + } + } + + public boolean isNullValue(double value) { + return Double.isNaN(value) || Double.isInfinite(value) || value == MathHelper.NULL_VALUE; + } + + /** + * Given screen coordinates, returns the series and point indexes of a chart + * element. If there is no chart element (line, point, bar, etc) at those + * coordinates, null is returned. + * + * @param screenPoint + * @return the series and point indexes + */ + public SeriesSelection getSeriesAndPointForScreenCoordinate(Point screenPoint) { + return null; + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/chart/BarChart.java b/android-libraries/achartengine/src/org/achartengine/chart/BarChart.java new file mode 100644 index 00000000..d5d0fb23 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/chart/BarChart.java @@ -0,0 +1,329 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.chart; + +import org.achartengine.model.XYMultipleSeriesDataset; +import org.achartengine.model.XYSeries; +import org.achartengine.renderer.SimpleSeriesRenderer; +import org.achartengine.renderer.XYMultipleSeriesRenderer; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.RectF; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.GradientDrawable.Orientation; + +/** + * The bar chart rendering class. + */ +public class BarChart extends XYChart { + /** The constant to identify this chart type. */ + public static final String TYPE = "Bar"; + /** The legend shape width. */ + private static final int SHAPE_WIDTH = 12; + /** The chart type. */ + protected Type mType = Type.DEFAULT; + + /** + * The bar chart type enum. + */ + public enum Type { + DEFAULT, STACKED; + } + + BarChart() { + } + + BarChart(Type type) { + mType = type; + } + + /** + * Builds a new bar chart instance. + * + * @param dataset the multiple series dataset + * @param renderer the multiple series renderer + * @param type the bar chart type + */ + public BarChart(XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer, Type type) { + super(dataset, renderer); + mType = type; + } + + @Override + protected ClickableArea[] clickableAreasForPoints(float[] points, double[] values, + float yAxisValue, int seriesIndex, int startIndex) { + int seriesNr = mDataset.getSeriesCount(); + int length = points.length; + ClickableArea[] ret = new ClickableArea[length / 2]; + float halfDiffX = getHalfDiffX(points, length, seriesNr); + for (int i = 0; i < length; i += 2) { + float x = points[i]; + float y = points[i + 1]; + if (mType == Type.STACKED) { + ret[i / 2] = new ClickableArea(new RectF(x - halfDiffX, y, x + halfDiffX, yAxisValue), + values[i], values[i + 1]); + } else { + float startX = x - seriesNr * halfDiffX + seriesIndex * 2 * halfDiffX; + ret[i / 2] = new ClickableArea(new RectF(startX, y, startX + 2 * halfDiffX, yAxisValue), + values[i], values[i + 1]); + } + } + return ret; + } + + /** + * The graphical representation of a series. + * + * @param canvas the canvas to paint to + * @param paint the paint to be used for drawing + * @param points the array of points to be used for drawing the series + * @param seriesRenderer the series renderer + * @param yAxisValue the minimum value of the y axis + * @param seriesIndex the index of the series currently being drawn + * @param startIndex the start index of the rendering points + */ + public void drawSeries(Canvas canvas, Paint paint, float[] points, + SimpleSeriesRenderer seriesRenderer, float yAxisValue, int seriesIndex, int startIndex) { + int seriesNr = mDataset.getSeriesCount(); + int length = points.length; + paint.setColor(seriesRenderer.getColor()); + paint.setStyle(Style.FILL); + float halfDiffX = getHalfDiffX(points, length, seriesNr); + for (int i = 0; i < length; i += 2) { + float x = points[i]; + float y = points[i + 1]; + drawBar(canvas, x, yAxisValue, x, y, halfDiffX, seriesNr, seriesIndex, paint); + } + paint.setColor(seriesRenderer.getColor()); + } + + /** + * Draws a bar. + * + * @param canvas the canvas + * @param xMin the X axis minimum + * @param yMin the Y axis minimum + * @param xMax the X axis maximum + * @param yMax the Y axis maximum + * @param halfDiffX half the size of a bar + * @param seriesNr the total number of series + * @param seriesIndex the current series index + * @param paint the paint + */ + protected void drawBar(Canvas canvas, float xMin, float yMin, float xMax, float yMax, + float halfDiffX, int seriesNr, int seriesIndex, Paint paint) { + int scale = mDataset.getSeriesAt(seriesIndex).getScaleNumber(); + if (mType == Type.STACKED) { + drawBar(canvas, xMin - halfDiffX, yMax, xMax + halfDiffX, yMin, scale, seriesIndex, paint); + } else { + float startX = xMin - seriesNr * halfDiffX + seriesIndex * 2 * halfDiffX; + drawBar(canvas, startX, yMax, startX + 2 * halfDiffX, yMin, scale, seriesIndex, paint); + } + } + + /** + * Draws a bar. + * + * @param canvas the canvas + * @param xMin the X axis minimum + * @param yMin the Y axis minimum + * @param xMax the X axis maximum + * @param yMax the Y axis maximum + * @param scale the scale index + * @param seriesIndex the current series index + * @param paint the paint + */ + private void drawBar(Canvas canvas, float xMin, float yMin, float xMax, float yMax, int scale, + int seriesIndex, Paint paint) { + SimpleSeriesRenderer renderer = mRenderer.getSeriesRendererAt(seriesIndex); + if (renderer.isGradientEnabled()) { + float minY = (float) toScreenPoint(new double[] { 0, renderer.getGradientStopValue() }, scale)[1]; + float maxY = (float) toScreenPoint(new double[] { 0, renderer.getGradientStartValue() }, + scale)[1]; + float gradientMinY = Math.max(minY, Math.min(yMin, yMax)); + float gradientMaxY = Math.min(maxY, Math.max(yMin, yMax)); + int gradientMinColor = renderer.getGradientStopColor(); + int gradientMaxColor = renderer.getGradientStartColor(); + int gradientStartColor = gradientMaxColor; + int gradientStopColor = gradientMinColor; + + if (yMin < minY) { + paint.setColor(gradientMinColor); + canvas.drawRect(Math.round(xMin), Math.round(yMin), Math.round(xMax), + Math.round(gradientMinY), paint); + } else { + gradientStopColor = getGradientPartialColor(gradientMinColor, gradientMaxColor, + (maxY - gradientMinY) / (maxY - minY)); + } + if (yMax > maxY) { + paint.setColor(gradientMaxColor); + canvas.drawRect(Math.round(xMin), Math.round(gradientMaxY), Math.round(xMax), + Math.round(yMax), paint); + } else { + gradientStartColor = getGradientPartialColor(gradientMaxColor, gradientMinColor, + (gradientMaxY - minY) / (maxY - minY)); + } + GradientDrawable gradient = new GradientDrawable(Orientation.BOTTOM_TOP, new int[] { + gradientStartColor, gradientStopColor }); + gradient.setBounds(Math.round(xMin), Math.round(gradientMinY), Math.round(xMax), + Math.round(gradientMaxY)); + gradient.draw(canvas); + } else { + if (Math.abs(yMin - yMax) < 1) { + if (yMin < yMax) { + yMax = yMin + 1; + } else { + yMax = yMin - 1; + } + } + canvas + .drawRect(Math.round(xMin), Math.round(yMin), Math.round(xMax), Math.round(yMax), paint); + } + } + + private int getGradientPartialColor(int minColor, int maxColor, float fraction) { + int alpha = Math.round(fraction * Color.alpha(minColor) + (1 - fraction) + * Color.alpha(maxColor)); + int r = Math.round(fraction * Color.red(minColor) + (1 - fraction) * Color.red(maxColor)); + int g = Math.round(fraction * Color.green(minColor) + (1 - fraction) * Color.green(maxColor)); + int b = Math.round(fraction * Color.blue(minColor) + (1 - fraction) * Color.blue((maxColor))); + return Color.argb(alpha, r, g, b); + } + + /** + * The graphical representation of the series values as text. + * + * @param canvas the canvas to paint to + * @param series the series to be painted + * @param renderer the series renderer + * @param paint the paint to be used for drawing + * @param points the array of points to be used for drawing the series + * @param seriesIndex the index of the series currently being drawn + * @param startIndex the start index of the rendering points + */ + protected void drawChartValuesText(Canvas canvas, XYSeries series, SimpleSeriesRenderer renderer, + Paint paint, float[] points, int seriesIndex, int startIndex) { + int seriesNr = mDataset.getSeriesCount(); + float halfDiffX = getHalfDiffX(points, points.length, seriesNr); + for (int i = 0; i < points.length; i += 2) { + int index = startIndex + i / 2; + double value = series.getY(index); + if (!isNullValue(value)) { + float x = points[i]; + if (mType == Type.DEFAULT) { + x += seriesIndex * 2 * halfDiffX - (seriesNr - 1.5f) * halfDiffX; + } + if (value >= 0) { + drawText(canvas, getLabel(value), x, points[i + 1] - renderer.getChartValuesSpacing(), + paint, 0); + } else { + drawText(canvas, getLabel(value), x, points[i + 1] + renderer.getChartValuesTextSize() + + renderer.getChartValuesSpacing() - 3, paint, 0); + } + } + } + } + + /** + * Returns the legend shape width. + * + * @param seriesIndex the series index + * @return the legend shape width + */ + public int getLegendShapeWidth(int seriesIndex) { + return SHAPE_WIDTH; + } + + /** + * The graphical representation of the legend shape. + * + * @param canvas the canvas to paint to + * @param renderer the series renderer + * @param x the x value of the point the shape should be drawn at + * @param y the y value of the point the shape should be drawn at + * @param seriesIndex the series index + * @param paint the paint to be used for drawing + */ + public void drawLegendShape(Canvas canvas, SimpleSeriesRenderer renderer, float x, float y, + int seriesIndex, Paint paint) { + float halfShapeWidth = SHAPE_WIDTH / 2; + canvas.drawRect(x, y - halfShapeWidth, x + SHAPE_WIDTH, y + halfShapeWidth, paint); + } + + /** + * Calculates and returns the half-distance in the graphical representation of + * 2 consecutive points. + * + * @param points the points + * @param length the points length + * @param seriesNr the series number + * @return the calculated half-distance value + */ + protected float getHalfDiffX(float[] points, int length, int seriesNr) { + int div = length; + if (length > 2) { + div = length - 2; + } + float halfDiffX = (points[length - 2] - points[0]) / div; + if (halfDiffX == 0) { + halfDiffX = 10; + } + + if (mType != Type.STACKED) { + halfDiffX /= seriesNr; + } + return (float) (halfDiffX / (getCoeficient() * (1 + mRenderer.getBarSpacing()))); + } + + /** + * Returns the value of a constant used to calculate the half-distance. + * + * @return the constant value + */ + protected float getCoeficient() { + return 1f; + } + + /** + * Returns if the chart should display the null values. + * + * @return if null values should be rendered + */ + protected boolean isRenderNullValues() { + return true; + } + + /** + * Returns the default axis minimum. + * + * @return the default axis minimum + */ + public double getDefaultMinimum() { + return 0; + } + + /** + * Returns the chart type identifier. + * + * @return the chart type + */ + public String getChartType() { + return TYPE; + } +} diff --git a/android-libraries/achartengine/src/org/achartengine/chart/BubbleChart.java b/android-libraries/achartengine/src/org/achartengine/chart/BubbleChart.java new file mode 100644 index 00000000..f3125713 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/chart/BubbleChart.java @@ -0,0 +1,146 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.chart; + +import org.achartengine.model.XYMultipleSeriesDataset; +import org.achartengine.model.XYValueSeries; +import org.achartengine.renderer.SimpleSeriesRenderer; +import org.achartengine.renderer.XYMultipleSeriesRenderer; +import org.achartengine.renderer.XYSeriesRenderer; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.RectF; + +/** + * The bubble chart rendering class. + */ +public class BubbleChart extends XYChart { + /** The constant to identify this chart type. */ + public static final String TYPE = "Bubble"; + /** The legend shape width. */ + private static final int SHAPE_WIDTH = 10; + /** The minimum bubble size. */ + private static final int MIN_BUBBLE_SIZE = 2; + /** The maximum bubble size. */ + private static final int MAX_BUBBLE_SIZE = 20; + + BubbleChart() { + } + + /** + * Builds a new bubble chart instance. + * + * @param dataset the multiple series dataset + * @param renderer the multiple series renderer + */ + public BubbleChart(XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer) { + super(dataset, renderer); + } + + /** + * The graphical representation of a series. + * + * @param canvas the canvas to paint to + * @param paint the paint to be used for drawing + * @param points the array of points to be used for drawing the series + * @param seriesRenderer the series renderer + * @param yAxisValue the minimum value of the y axis + * @param seriesIndex the index of the series currently being drawn + * @param startIndex the start index of the rendering points + */ + public void drawSeries(Canvas canvas, Paint paint, float[] points, + SimpleSeriesRenderer seriesRenderer, float yAxisValue, int seriesIndex, int startIndex) { + XYSeriesRenderer renderer = (XYSeriesRenderer) seriesRenderer; + paint.setColor(renderer.getColor()); + paint.setStyle(Style.FILL); + int length = points.length; + XYValueSeries series = (XYValueSeries) mDataset.getSeriesAt(seriesIndex); + double max = series.getMaxValue(); + double coef = MAX_BUBBLE_SIZE / max; + for (int i = 0; i < length; i += 2) { + double size = series.getValue(startIndex + i / 2) * coef + MIN_BUBBLE_SIZE; + drawCircle(canvas, paint, points[i], points[i + 1], (float) size); + } + } + + @Override + protected ClickableArea[] clickableAreasForPoints(float[] points, double[] values, + float yAxisValue, int seriesIndex, int startIndex) { + int length = points.length; + XYValueSeries series = (XYValueSeries) mDataset.getSeriesAt(seriesIndex); + double max = series.getMaxValue(); + double coef = MAX_BUBBLE_SIZE / max; + ClickableArea[] ret = new ClickableArea[length / 2]; + for (int i = 0; i < length; i += 2) { + double size = series.getValue(startIndex + i / 2) * coef + MIN_BUBBLE_SIZE; + ret[i / 2] = new ClickableArea(new RectF(points[i] - (float) size, points[i + 1] + - (float) size, points[i] + (float) size, points[i + 1] + (float) size), values[i], + values[i + 1]); + } + return ret; + } + + /** + * Returns the legend shape width. + * + * @param seriesIndex the series index + * @return the legend shape width + */ + public int getLegendShapeWidth(int seriesIndex) { + return SHAPE_WIDTH; + } + + /** + * The graphical representation of the legend shape. + * + * @param canvas the canvas to paint to + * @param renderer the series renderer + * @param x the x value of the point the shape should be drawn at + * @param y the y value of the point the shape should be drawn at + * @param seriesIndex the series index + * @param paint the paint to be used for drawing + */ + public void drawLegendShape(Canvas canvas, SimpleSeriesRenderer renderer, float x, float y, + int seriesIndex, Paint paint) { + paint.setStyle(Style.FILL); + drawCircle(canvas, paint, x + SHAPE_WIDTH, y, 3); + } + + /** + * The graphical representation of a circle point shape. + * + * @param canvas the canvas to paint to + * @param paint the paint to be used for drawing + * @param x the x value of the point the shape should be drawn at + * @param y the y value of the point the shape should be drawn at + * @param radius the bubble radius + */ + private void drawCircle(Canvas canvas, Paint paint, float x, float y, float radius) { + canvas.drawCircle(x, y, radius, paint); + } + + /** + * Returns the chart type identifier. + * + * @return the chart type + */ + public String getChartType() { + return TYPE; + } + +} \ No newline at end of file diff --git a/android-libraries/achartengine/src/org/achartengine/chart/ClickableArea.java b/android-libraries/achartengine/src/org/achartengine/chart/ClickableArea.java new file mode 100644 index 00000000..d2d306ca --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/chart/ClickableArea.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.chart; + +import android.graphics.RectF; + +public class ClickableArea { + private RectF rect; + private double x; + private double y; + + public ClickableArea(RectF rect, double x, double y) { + super(); + this.rect = rect; + this.x = x; + this.y = y; + } + + public RectF getRect() { + return rect; + } + + public double getX() { + return x; + } + + public double getY() { + return y; + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/chart/CombinedXYChart.java b/android-libraries/achartengine/src/org/achartengine/chart/CombinedXYChart.java new file mode 100644 index 00000000..d684c3a2 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/chart/CombinedXYChart.java @@ -0,0 +1,177 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.chart; + +import java.util.List; + +import org.achartengine.model.XYMultipleSeriesDataset; +import org.achartengine.model.XYSeries; +import org.achartengine.renderer.SimpleSeriesRenderer; +import org.achartengine.renderer.XYMultipleSeriesRenderer; +import org.achartengine.renderer.XYMultipleSeriesRenderer.Orientation; + +import android.graphics.Canvas; +import android.graphics.Paint; + +/** + * The combined XY chart rendering class. + */ +public class CombinedXYChart extends XYChart { + /** The embedded XY charts. */ + private XYChart[] mCharts; + /** The supported charts for being combined. */ + private Class[] xyChartTypes = new Class[] { TimeChart.class, LineChart.class, + CubicLineChart.class, BarChart.class, BubbleChart.class, ScatterChart.class, + RangeBarChart.class, RangeStackedBarChart.class }; + + /** + * Builds a new combined XY chart instance. + * + * @param dataset the multiple series dataset + * @param renderer the multiple series renderer + * @param types the XY chart types + */ + public CombinedXYChart(XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer, + String[] types) { + super(dataset, renderer); + int length = types.length; + mCharts = new XYChart[length]; + for (int i = 0; i < length; i++) { + try { + mCharts[i] = getXYChart(types[i]); + } catch (Exception e) { + // ignore + } + if (mCharts[i] == null) { + throw new IllegalArgumentException("Unknown chart type " + types[i]); + } else { + XYMultipleSeriesDataset newDataset = new XYMultipleSeriesDataset(); + newDataset.addSeries(dataset.getSeriesAt(i)); + XYMultipleSeriesRenderer newRenderer = new XYMultipleSeriesRenderer(); + // TODO: copy other parameters here + newRenderer.setBarSpacing(renderer.getBarSpacing()); + newRenderer.setPointSize(renderer.getPointSize()); + int scale = dataset.getSeriesAt(i).getScaleNumber(); + if (renderer.isMinXSet(scale)) { + newRenderer.setXAxisMin(renderer.getXAxisMin(scale)); + } + if (renderer.isMaxXSet(scale)) { + newRenderer.setXAxisMax(renderer.getXAxisMax(scale)); + } + if (renderer.isMinYSet(scale)) { + newRenderer.setYAxisMin(renderer.getYAxisMin(scale)); + } + if (renderer.isMaxYSet(scale)) { + newRenderer.setYAxisMax(renderer.getYAxisMax(scale)); + } + newRenderer.addSeriesRenderer(renderer.getSeriesRendererAt(i)); + mCharts[i].setDatasetRenderer(newDataset, newRenderer); + } + } + } + + /** + * Returns a chart instance based on the provided type. + * + * @param type the chart type + * @return an instance of a chart implementation + * @throws IllegalAccessException + * @throws InstantiationException + */ + private XYChart getXYChart(String type) throws IllegalAccessException, InstantiationException { + XYChart chart = null; + int length = xyChartTypes.length; + for (int i = 0; i < length && chart == null; i++) { + XYChart newChart = (XYChart) xyChartTypes[i].newInstance(); + if (type.equals(newChart.getChartType())) { + chart = newChart; + } + } + return chart; + } + + /** + * The graphical representation of a series. + * + * @param canvas the canvas to paint to + * @param paint the paint to be used for drawing + * @param points the array of points to be used for drawing the series + * @param seriesRenderer the series renderer + * @param yAxisValue the minimum value of the y axis + * @param seriesIndex the index of the series currently being drawn + * @param startIndex the start index of the rendering points + */ + public void drawSeries(Canvas canvas, Paint paint, float[] points, + SimpleSeriesRenderer seriesRenderer, float yAxisValue, int seriesIndex, int startIndex) { + mCharts[seriesIndex].setScreenR(getScreenR()); + mCharts[seriesIndex].setCalcRange(getCalcRange(mDataset.getSeriesAt(seriesIndex) + .getScaleNumber()), 0); + mCharts[seriesIndex].drawSeries(canvas, paint, points, seriesRenderer, yAxisValue, 0, + startIndex); + } + + @Override + protected ClickableArea[] clickableAreasForPoints(float[] points, double[] values, + float yAxisValue, int seriesIndex, int startIndex) { + return mCharts[seriesIndex].clickableAreasForPoints(points, values, yAxisValue, 0, startIndex); + } + + @Override + protected void drawSeries(XYSeries series, Canvas canvas, Paint paint, List pointsList, + SimpleSeriesRenderer seriesRenderer, float yAxisValue, int seriesIndex, Orientation or, + int startIndex) { + mCharts[seriesIndex].setScreenR(getScreenR()); + mCharts[seriesIndex].setCalcRange(getCalcRange(mDataset.getSeriesAt(seriesIndex) + .getScaleNumber()), 0); + mCharts[seriesIndex].drawSeries(series, canvas, paint, pointsList, seriesRenderer, yAxisValue, + 0, or, startIndex); + } + + /** + * Returns the legend shape width. + * + * @param seriesIndex the series index + * @return the legend shape width + */ + public int getLegendShapeWidth(int seriesIndex) { + return mCharts[seriesIndex].getLegendShapeWidth(0); + } + + /** + * The graphical representation of the legend shape. + * + * @param canvas the canvas to paint to + * @param renderer the series renderer + * @param x the x value of the point the shape should be drawn at + * @param y the y value of the point the shape should be drawn at + * @param seriesIndex the series index + * @param paint the paint to be used for drawing + */ + public void drawLegendShape(Canvas canvas, SimpleSeriesRenderer renderer, float x, float y, + int seriesIndex, Paint paint) { + mCharts[seriesIndex].drawLegendShape(canvas, renderer, x, y, 0, paint); + } + + /** + * Returns the chart type identifier. + * + * @return the chart type + */ + public String getChartType() { + return "Combined"; + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/chart/CubicLineChart.java b/android-libraries/achartengine/src/org/achartengine/chart/CubicLineChart.java new file mode 100644 index 00000000..2011318f --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/chart/CubicLineChart.java @@ -0,0 +1,120 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.chart; + +import org.achartengine.model.Point; +import org.achartengine.model.XYMultipleSeriesDataset; +import org.achartengine.renderer.XYMultipleSeriesRenderer; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; + +/** + * The interpolated (cubic) line chart rendering class. + */ +public class CubicLineChart extends LineChart { + /** The chart type. */ + public static final String TYPE = "Cubic"; + + private float firstMultiplier; + + private float secondMultiplier; + + private Point p1 = new Point(); + + private Point p2 = new Point(); + + private Point p3 = new Point(); + + public CubicLineChart() { + // default is to have first control point at about 33% of the distance, + firstMultiplier = 0.33f; + // and the next at 66% of the distance. + secondMultiplier = 1 - firstMultiplier; + } + + /** + * Builds a cubic line chart. + * + * @param dataset the dataset + * @param renderer the renderer + * @param smoothness smoothness determines how smooth the curve should be, + * range [0->0.5] super smooth, 0.5, means that it might not get + * close to control points if you have random data // less smooth, + * (close to 0) means that it will most likely touch all control // + * points + */ + public CubicLineChart(XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer, + float smoothness) { + super(dataset, renderer); + firstMultiplier = smoothness; + secondMultiplier = 1 - firstMultiplier; + } + + @Override + protected void drawPath(Canvas canvas, float[] points, Paint paint, boolean circular) { + Path p = new Path(); + float x = points[0]; + float y = points[1]; + p.moveTo(x, y); + + int length = points.length; + if (circular) { + length -= 4; + } + + for (int i = 0; i < length; i += 2) { + int nextIndex = i + 2 < length ? i + 2 : i; + int nextNextIndex = i + 4 < length ? i + 4 : nextIndex; + calc(points, p1, i, nextIndex, secondMultiplier); + p2.setX(points[nextIndex]); + p2.setY(points[nextIndex + 1]); + calc(points, p3, nextIndex, nextNextIndex, firstMultiplier); + // From last point, approaching x1/y1 and x2/y2 and ends up at x3/y3 + p.cubicTo(p1.getX(), p1.getY(), p2.getX(), p2.getY(), p3.getX(), p3.getY()); + } + if (circular) { + for (int i = length; i < length + 4; i += 2) { + p.lineTo(points[i], points[i + 1]); + } + p.lineTo(points[0], points[1]); + } + canvas.drawPath(p, paint); + } + + private void calc(float[] points, Point result, int index1, int index2, final float multiplier) { + float p1x = points[index1]; + float p1y = points[index1 + 1]; + float p2x = points[index2]; + float p2y = points[index2 + 1]; + + float diffX = p2x - p1x; // p2.x - p1.x; + float diffY = p2y - p1y; // p2.y - p1.y; + result.setX(p1x + (diffX * multiplier)); + result.setY(p1y + (diffY * multiplier)); + } + + /** + * Returns the chart type identifier. + * + * @return the chart type + */ + public String getChartType() { + return TYPE; + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/chart/DialChart.java b/android-libraries/achartengine/src/org/achartengine/chart/DialChart.java new file mode 100644 index 00000000..ebfcbbb1 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/chart/DialChart.java @@ -0,0 +1,236 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.chart; + +import org.achartengine.model.CategorySeries; +import org.achartengine.renderer.DefaultRenderer; +import org.achartengine.renderer.DialRenderer; +import org.achartengine.renderer.DialRenderer.Type; +import org.achartengine.util.MathHelper; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Paint.Style; + +/** + * The dial chart rendering class. + */ +public class DialChart extends RoundChart { + /** The radius of the needle. */ + private static final int NEEDLE_RADIUS = 10; + /** The series renderer. */ + private DialRenderer mRenderer; + + /** + * Builds a new dial chart instance. + * + * @param dataset the series dataset + * @param renderer the dial renderer + */ + public DialChart(CategorySeries dataset, DialRenderer renderer) { + super(dataset, renderer); + mRenderer = renderer; + } + + /** + * The graphical representation of the dial chart. + * + * @param canvas the canvas to paint to + * @param x the top left x value of the view to draw to + * @param y the top left y value of the view to draw to + * @param width the width of the view to draw to + * @param height the height of the view to draw to + * @param paint the paint + */ + @Override + public void draw(Canvas canvas, int x, int y, int width, int height, Paint paint) { + paint.setAntiAlias(mRenderer.isAntialiasing()); + paint.setStyle(Style.FILL); + paint.setTextSize(mRenderer.getLabelsTextSize()); + int legendSize = getLegendSize(mRenderer, height / 5, 0); + int left = x; + int top = y; + int right = x + width; + + int sLength = mDataset.getItemCount(); + String[] titles = new String[sLength]; + for (int i = 0; i < sLength; i++) { + titles[i] = mDataset.getCategory(i); + } + + if (mRenderer.isFitLegend()) { + legendSize = drawLegend(canvas, mRenderer, titles, left, right, y, width, height, legendSize, + paint, true); + } + int bottom = y + height - legendSize; + drawBackground(mRenderer, canvas, x, y, width, height, paint, false, DefaultRenderer.NO_COLOR); + + int mRadius = Math.min(Math.abs(right - left), Math.abs(bottom - top)); + int radius = (int) (mRadius * 0.35 * mRenderer.getScale()); + if (mCenterX == NO_VALUE) { + mCenterX = (left + right) / 2; + } + if (mCenterY == NO_VALUE) { + mCenterY = (bottom + top) / 2; + } + float shortRadius = radius * 0.9f; + float longRadius = radius * 1.1f; + double min = mRenderer.getMinValue(); + double max = mRenderer.getMaxValue(); + double angleMin = mRenderer.getAngleMin(); + double angleMax = mRenderer.getAngleMax(); + if (!mRenderer.isMinValueSet() || !mRenderer.isMaxValueSet()) { + int count = mRenderer.getSeriesRendererCount(); + for (int i = 0; i < count; i++) { + double value = mDataset.getValue(i); + if (!mRenderer.isMinValueSet()) { + min = Math.min(min, value); + } + if (!mRenderer.isMaxValueSet()) { + max = Math.max(max, value); + } + } + } + if (min == max) { + min = min * 0.5; + max = max * 1.5; + } + + paint.setColor(mRenderer.getLabelsColor()); + double minorTicks = mRenderer.getMinorTicksSpacing(); + double majorTicks = mRenderer.getMajorTicksSpacing(); + if (minorTicks == MathHelper.NULL_VALUE) { + minorTicks = (max - min) / 30; + } + if (majorTicks == MathHelper.NULL_VALUE) { + majorTicks = (max - min) / 10; + } + drawTicks(canvas, min, max, angleMin, angleMax, mCenterX, mCenterY, longRadius, radius, + minorTicks, paint, false); + drawTicks(canvas, min, max, angleMin, angleMax, mCenterX, mCenterY, longRadius, shortRadius, + majorTicks, paint, true); + + int count = mRenderer.getSeriesRendererCount(); + for (int i = 0; i < count; i++) { + double angle = getAngleForValue(mDataset.getValue(i), angleMin, angleMax, min, max); + paint.setColor(mRenderer.getSeriesRendererAt(i).getColor()); + boolean type = mRenderer.getVisualTypeForIndex(i) == Type.ARROW; + drawNeedle(canvas, angle, mCenterX, mCenterY, shortRadius, type, paint); + } + drawLegend(canvas, mRenderer, titles, left, right, y, width, height, legendSize, paint, false); + drawTitle(canvas, x, y, width, paint); + } + + /** + * Returns the angle for a specific chart value. + * + * @param value the chart value + * @param minAngle the minimum chart angle value + * @param maxAngle the maximum chart angle value + * @param min the minimum chart value + * @param max the maximum chart value + * @return the angle + */ + private double getAngleForValue(double value, double minAngle, double maxAngle, double min, + double max) { + double angleDiff = maxAngle - minAngle; + double diff = max - min; + return Math.toRadians(minAngle + (value - min) * angleDiff / diff); + } + + /** + * Draws the chart tick lines. + * + * @param canvas the canvas + * @param min the minimum chart value + * @param max the maximum chart value + * @param minAngle the minimum chart angle value + * @param maxAngle the maximum chart angle value + * @param centerX the center x value + * @param centerY the center y value + * @param longRadius the long radius + * @param shortRadius the short radius + * @param ticks the tick spacing + * @param paint the paint settings + * @param labels paint the labels + * @return the angle + */ + private void drawTicks(Canvas canvas, double min, double max, double minAngle, double maxAngle, + int centerX, int centerY, double longRadius, double shortRadius, double ticks, Paint paint, + boolean labels) { + for (double i = min; i <= max; i += ticks) { + double angle = getAngleForValue(i, minAngle, maxAngle, min, max); + double sinValue = Math.sin(angle); + double cosValue = Math.cos(angle); + int x1 = Math.round(centerX + (float) (shortRadius * sinValue)); + int y1 = Math.round(centerY + (float) (shortRadius * cosValue)); + int x2 = Math.round(centerX + (float) (longRadius * sinValue)); + int y2 = Math.round(centerY + (float) (longRadius * cosValue)); + canvas.drawLine(x1, y1, x2, y2, paint); + if (labels) { + paint.setTextAlign(Align.LEFT); + if (x1 <= x2) { + paint.setTextAlign(Align.RIGHT); + } + String text = i + ""; + if (Math.round(i) == (long) i) { + text = (long) i + ""; + } + canvas.drawText(text, x1, y1, paint); + } + } + } + + /** + * Returns the angle for a specific chart value. + * + * @param canvas the canvas + * @param angle the needle angle value + * @param centerX the center x value + * @param centerY the center y value + * @param radius the radius + * @param arrow if a needle or an arrow to be painted + * @param paint the paint settings + * @return the angle + */ + private void drawNeedle(Canvas canvas, double angle, int centerX, int centerY, double radius, + boolean arrow, Paint paint) { + double diff = Math.toRadians(90); + int needleSinValue = (int) (NEEDLE_RADIUS * Math.sin(angle - diff)); + int needleCosValue = (int) (NEEDLE_RADIUS * Math.cos(angle - diff)); + int needleX = (int) (radius * Math.sin(angle)); + int needleY = (int) (radius * Math.cos(angle)); + int needleCenterX = centerX + needleX; + int needleCenterY = centerY + needleY; + float[] points; + if (arrow) { + int arrowBaseX = centerX + (int) (radius * 0.85 * Math.sin(angle)); + int arrowBaseY = centerY + (int) (radius * 0.85 * Math.cos(angle)); + points = new float[] { arrowBaseX - needleSinValue, arrowBaseY - needleCosValue, + needleCenterX, needleCenterY, arrowBaseX + needleSinValue, arrowBaseY + needleCosValue }; + float width = paint.getStrokeWidth(); + paint.setStrokeWidth(5); + canvas.drawLine(centerX, centerY, needleCenterX, needleCenterY, paint); + paint.setStrokeWidth(width); + } else { + points = new float[] { centerX - needleSinValue, centerY - needleCosValue, needleCenterX, + needleCenterY, centerX + needleSinValue, centerY + needleCosValue }; + } + drawPath(canvas, points, paint, true); + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/chart/DoughnutChart.java b/android-libraries/achartengine/src/org/achartengine/chart/DoughnutChart.java new file mode 100644 index 00000000..ad67b076 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/chart/DoughnutChart.java @@ -0,0 +1,162 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.chart; + +import java.util.ArrayList; +import java.util.List; + +import org.achartengine.model.MultipleCategorySeries; +import org.achartengine.renderer.DefaultRenderer; +import org.achartengine.renderer.SimpleSeriesRenderer; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.RectF; + +/** + * The doughnut chart rendering class. + */ +public class DoughnutChart extends RoundChart { + /** The series dataset. */ + private MultipleCategorySeries mDataset; + /** A step variable to control the size of the legend shape. */ + private int mStep; + + /** + * Builds a new doughnut chart instance. + * + * @param dataset the series dataset + * @param renderer the series renderer + */ + public DoughnutChart(MultipleCategorySeries dataset, DefaultRenderer renderer) { + super(null, renderer); + mDataset = dataset; + } + + /** + * The graphical representation of the doughnut chart. + * + * @param canvas the canvas to paint to + * @param x the top left x value of the view to draw to + * @param y the top left y value of the view to draw to + * @param width the width of the view to draw to + * @param height the height of the view to draw to + * @param paint the paint + */ + @Override + public void draw(Canvas canvas, int x, int y, int width, int height, Paint paint) { + paint.setAntiAlias(mRenderer.isAntialiasing()); + paint.setStyle(Style.FILL); + paint.setTextSize(mRenderer.getLabelsTextSize()); + int legendSize = getLegendSize(mRenderer, height / 5, 0); + int left = x; + int top = y; + int right = x + width; + int cLength = mDataset.getCategoriesCount(); + String[] categories = new String[cLength]; + for (int category = 0; category < cLength; category++) { + categories[category] = mDataset.getCategory(category); + } + if (mRenderer.isFitLegend()) { + legendSize = drawLegend(canvas, mRenderer, categories, left, right, y, width, height, + legendSize, paint, true); + } + + int bottom = y + height - legendSize; + drawBackground(mRenderer, canvas, x, y, width, height, paint, false, DefaultRenderer.NO_COLOR); + mStep = SHAPE_WIDTH * 3 / 4; + + int mRadius = Math.min(Math.abs(right - left), Math.abs(bottom - top)); + double rCoef = 0.35 * mRenderer.getScale(); + double decCoef = 0.2 / cLength; + int radius = (int) (mRadius * rCoef); + if (mCenterX == NO_VALUE) { + mCenterX = (left + right) / 2; + } + if (mCenterY == NO_VALUE) { + mCenterY = (bottom + top) / 2; + } + float shortRadius = radius * 0.9f; + float longRadius = radius * 1.1f; + List prevLabelsBounds = new ArrayList(); + for (int category = 0; category < cLength; category++) { + int sLength = mDataset.getItemCount(category); + double total = 0; + String[] titles = new String[sLength]; + for (int i = 0; i < sLength; i++) { + total += mDataset.getValues(category)[i]; + titles[i] = mDataset.getTitles(category)[i]; + } + float currentAngle = mRenderer.getStartAngle(); + RectF oval = new RectF(mCenterX - radius, mCenterY - radius, mCenterX + radius, mCenterY + + radius); + for (int i = 0; i < sLength; i++) { + paint.setColor(mRenderer.getSeriesRendererAt(i).getColor()); + float value = (float) mDataset.getValues(category)[i]; + float angle = (float) (value / total * 360); + canvas.drawArc(oval, currentAngle, angle, true, paint); + drawLabel(canvas, mDataset.getTitles(category)[i], mRenderer, prevLabelsBounds, mCenterX, + mCenterY, shortRadius, longRadius, currentAngle, angle, left, right, + mRenderer.getLabelsColor(), paint, true); + currentAngle += angle; + } + radius -= (int) mRadius * decCoef; + shortRadius -= mRadius * decCoef - 2; + if (mRenderer.getBackgroundColor() != 0) { + paint.setColor(mRenderer.getBackgroundColor()); + } else { + paint.setColor(Color.WHITE); + } + paint.setStyle(Style.FILL); + oval = new RectF(mCenterX - radius, mCenterY - radius, mCenterX + radius, mCenterY + radius); + canvas.drawArc(oval, 0, 360, true, paint); + radius -= 1; + } + prevLabelsBounds.clear(); + drawLegend(canvas, mRenderer, categories, left, right, y, width, height, legendSize, paint, + false); + drawTitle(canvas, x, y, width, paint); + } + + /** + * Returns the legend shape width. + * + * @param seriesIndex the series index + * @return the legend shape width + */ + public int getLegendShapeWidth(int seriesIndex) { + return SHAPE_WIDTH; + } + + /** + * The graphical representation of the legend shape. + * + * @param canvas the canvas to paint to + * @param renderer the series renderer + * @param x the x value of the point the shape should be drawn at + * @param y the y value of the point the shape should be drawn at + * @param seriesIndex the series index + * @param paint the paint to be used for drawing + */ + public void drawLegendShape(Canvas canvas, SimpleSeriesRenderer renderer, float x, float y, + int seriesIndex, Paint paint) { + mStep--; + canvas.drawCircle(x + SHAPE_WIDTH - mStep, y, mStep, paint); + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/chart/LineChart.java b/android-libraries/achartengine/src/org/achartengine/chart/LineChart.java new file mode 100644 index 00000000..2c458986 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/chart/LineChart.java @@ -0,0 +1,175 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.chart; + +import org.achartengine.model.XYMultipleSeriesDataset; +import org.achartengine.renderer.SimpleSeriesRenderer; +import org.achartengine.renderer.XYMultipleSeriesRenderer; +import org.achartengine.renderer.XYSeriesRenderer; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.RectF; + +/** + * The line chart rendering class. + */ +public class LineChart extends XYChart { + /** The constant to identify this chart type. */ + public static final String TYPE = "Line"; + /** The legend shape width. */ + private static final int SHAPE_WIDTH = 30; + /** The scatter chart to be used to draw the data points. */ + private ScatterChart pointsChart; + + LineChart() { + } + + /** + * Builds a new line chart instance. + * + * @param dataset the multiple series dataset + * @param renderer the multiple series renderer + */ + public LineChart(XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer) { + super(dataset, renderer); + pointsChart = new ScatterChart(dataset, renderer); + } + + /** + * Sets the series and the renderer. + * + * @param dataset the series dataset + * @param renderer the series renderer + */ + protected void setDatasetRenderer(XYMultipleSeriesDataset dataset, + XYMultipleSeriesRenderer renderer) { + super.setDatasetRenderer(dataset, renderer); + pointsChart = new ScatterChart(dataset, renderer); + } + + /** + * The graphical representation of a series. + * + * @param canvas the canvas to paint to + * @param paint the paint to be used for drawing + * @param points the array of points to be used for drawing the series + * @param seriesRenderer the series renderer + * @param yAxisValue the minimum value of the y axis + * @param seriesIndex the index of the series currently being drawn + * @param startIndex the start index of the rendering points + */ + public void drawSeries(Canvas canvas, Paint paint, float[] points, + SimpleSeriesRenderer seriesRenderer, float yAxisValue, int seriesIndex, int startIndex) { + int length = points.length; + XYSeriesRenderer renderer = (XYSeriesRenderer) seriesRenderer; + float lineWidth = paint.getStrokeWidth(); + paint.setStrokeWidth(renderer.getLineWidth()); + if (renderer.isFillBelowLine()) { + paint.setColor(renderer.getFillBelowLineColor()); + int pLength = points.length; + float[] fillPoints = new float[pLength + 4]; + System.arraycopy(points, 0, fillPoints, 0, length); + fillPoints[0] = points[0] + 1; + fillPoints[length] = fillPoints[length - 2]; + fillPoints[length + 1] = yAxisValue; + fillPoints[length + 2] = fillPoints[0]; + fillPoints[length + 3] = fillPoints[length + 1]; + for (int i = 0; i < length + 4; i += 2) { + if (fillPoints[i + 1] < 0) { + fillPoints[i + 1] = 0; + } + } + paint.setStyle(Style.FILL); + drawPath(canvas, fillPoints, paint, true); + } + paint.setColor(seriesRenderer.getColor()); + paint.setStyle(Style.STROKE); + drawPath(canvas, points, paint, false); + paint.setStrokeWidth(lineWidth); + } + + @Override + protected ClickableArea[] clickableAreasForPoints(float[] points, double[] values, + float yAxisValue, int seriesIndex, int startIndex) { + int length = points.length; + ClickableArea[] ret = new ClickableArea[length / 2]; + for (int i = 0; i < length; i += 2) { + int selectableBuffer = mRenderer.getSelectableBuffer(); + ret[i / 2] = new ClickableArea(new RectF(points[i] - selectableBuffer, points[i + 1] + - selectableBuffer, points[i] + selectableBuffer, points[i + 1] + selectableBuffer), + values[i], values[i + 1]); + } + return ret; + } + + /** + * Returns the legend shape width. + * + * @param seriesIndex the series index + * @return the legend shape width + */ + public int getLegendShapeWidth(int seriesIndex) { + return SHAPE_WIDTH; + } + + /** + * The graphical representation of the legend shape. + * + * @param canvas the canvas to paint to + * @param renderer the series renderer + * @param x the x value of the point the shape should be drawn at + * @param y the y value of the point the shape should be drawn at + * @param seriesIndex the series index + * @param paint the paint to be used for drawing + */ + public void drawLegendShape(Canvas canvas, SimpleSeriesRenderer renderer, float x, float y, + int seriesIndex, Paint paint) { + canvas.drawLine(x, y, x + SHAPE_WIDTH, y, paint); + if (isRenderPoints(renderer)) { + pointsChart.drawLegendShape(canvas, renderer, x + 5, y, seriesIndex, paint); + } + } + + /** + * Returns if the chart should display the points as a certain shape. + * + * @param renderer the series renderer + */ + public boolean isRenderPoints(SimpleSeriesRenderer renderer) { + return ((XYSeriesRenderer) renderer).getPointStyle() != PointStyle.POINT; + } + + /** + * Returns the scatter chart to be used for drawing the data points. + * + * @return the data points scatter chart + */ + public ScatterChart getPointsChart() { + return pointsChart; + } + + /** + * Returns the chart type identifier. + * + * @return the chart type + */ + public String getChartType() { + return TYPE; + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/chart/PieChart.java b/android-libraries/achartengine/src/org/achartengine/chart/PieChart.java new file mode 100644 index 00000000..d656a95e --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/chart/PieChart.java @@ -0,0 +1,136 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.chart; + +import java.util.ArrayList; +import java.util.List; + +import org.achartengine.model.CategorySeries; +import org.achartengine.model.Point; +import org.achartengine.model.SeriesSelection; +import org.achartengine.renderer.DefaultRenderer; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.RectF; + +/** + * The pie chart rendering class. + */ +public class PieChart extends RoundChart { + /** Handles returning values when tapping on PieChart. */ + private PieMapper mPieMapper; + + /** + * Builds a new pie chart instance. + * + * @param dataset the series dataset + * @param renderer the series renderer + */ + public PieChart(CategorySeries dataset, DefaultRenderer renderer) { + super(dataset, renderer); + mPieMapper = new PieMapper(); + } + + /** + * The graphical representation of the pie chart. + * + * @param canvas the canvas to paint to + * @param x the top left x value of the view to draw to + * @param y the top left y value of the view to draw to + * @param width the width of the view to draw to + * @param height the height of the view to draw to + * @param paint the paint + */ + @Override + public void draw(Canvas canvas, int x, int y, int width, int height, Paint paint) { + paint.setAntiAlias(mRenderer.isAntialiasing()); + paint.setStyle(Style.FILL); + paint.setTextSize(mRenderer.getLabelsTextSize()); + int legendSize = getLegendSize(mRenderer, height / 5, 0); + int left = x; + int top = y; + int right = x + width; + int sLength = mDataset.getItemCount(); + double total = 0; + String[] titles = new String[sLength]; + for (int i = 0; i < sLength; i++) { + total += mDataset.getValue(i); + titles[i] = mDataset.getCategory(i); + } + if (mRenderer.isFitLegend()) { + legendSize = drawLegend(canvas, mRenderer, titles, left, right, y, width, height, legendSize, + paint, true); + } + int bottom = y + height - legendSize; + drawBackground(mRenderer, canvas, x, y, width, height, paint, false, DefaultRenderer.NO_COLOR); + + float currentAngle = mRenderer.getStartAngle(); + int mRadius = Math.min(Math.abs(right - left), Math.abs(bottom - top)); + int radius = (int) (mRadius * 0.35 * mRenderer.getScale()); + + if (mCenterX == NO_VALUE) { + mCenterX = (left + right) / 2; + } + if (mCenterY == NO_VALUE) { + mCenterY = (bottom + top) / 2; + } + + // Hook in clip detection after center has been calculated + mPieMapper.setDimensions(radius, mCenterX, mCenterY); + boolean loadPieCfg = !mPieMapper.areAllSegmentPresent(sLength); + if (loadPieCfg) { + mPieMapper.clearPieSegments(); + } + + float shortRadius = radius * 0.9f; + float longRadius = radius * 1.1f; + + RectF oval = new RectF(mCenterX - radius, mCenterY - radius, mCenterX + radius, mCenterY + + radius); + List prevLabelsBounds = new ArrayList(); + + for (int i = 0; i < sLength; i++) { + paint.setColor(mRenderer.getSeriesRendererAt(i).getColor()); + float value = (float) mDataset.getValue(i); + float angle = (float) (value / total * 360); + canvas.drawArc(oval, currentAngle, angle, true, paint); + drawLabel(canvas, mDataset.getCategory(i), mRenderer, prevLabelsBounds, mCenterX, mCenterY, + shortRadius, longRadius, currentAngle, angle, left, right, mRenderer.getLabelsColor(), + paint, true); + if (mRenderer.isDisplayValues()) { + drawLabel(canvas, getLabel(mDataset.getValue(i)), mRenderer, prevLabelsBounds, mCenterX, + mCenterY, shortRadius / 2, longRadius / 2, currentAngle, angle, left, right, + mRenderer.getLabelsColor(), paint, false); + } + + // Save details for getSeries functionality + if (loadPieCfg) { + mPieMapper.addPieSegment(i, value, currentAngle, angle); + } + currentAngle += angle; + } + prevLabelsBounds.clear(); + drawLegend(canvas, mRenderer, titles, left, right, y, width, height, legendSize, paint, false); + drawTitle(canvas, x, y, width, paint); + } + + public SeriesSelection getSeriesAndPointForScreenCoordinate(Point screenPoint) { + return mPieMapper.getSeriesAndPointForScreenCoordinate(screenPoint); + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/chart/PieMapper.java b/android-libraries/achartengine/src/org/achartengine/chart/PieMapper.java new file mode 100644 index 00000000..6e4b7150 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/chart/PieMapper.java @@ -0,0 +1,139 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.chart; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import org.achartengine.model.Point; +import org.achartengine.model.SeriesSelection; + +/** + * PieChart Segment Selection Management. + */ +public class PieMapper implements Serializable { + + private List mPieSegmentList = new ArrayList(); + + private int mPieChartRadius; + + private int mCenterX, mCenterY; + + /** + * Set PieChart location on screen. + * + * @param pieRadius + * @param centerX + * @param centerY + */ + public void setDimensions(int pieRadius, int centerX, int centerY) { + mPieChartRadius = pieRadius; + mCenterX = centerX; + mCenterY = centerY; + } + + /** + * If we have all PieChart Config then there is no point in reloading it + * + * @param datasetSize + * @return true if cfg for each segment is present + */ + public boolean areAllSegmentPresent(int datasetSize) { + return mPieSegmentList.size() == datasetSize; + } + + /** + * Add configuration for a PieChart Segment + * + * @param dataIndex + * @param value + * @param startAngle + * @param angle + */ + public void addPieSegment(int dataIndex, float value, float startAngle, float angle) { + mPieSegmentList.add(new PieSegment(dataIndex, value, startAngle, angle)); + } + + /** + * Clears the pie segments list. + */ + public void clearPieSegments() { + mPieSegmentList.clear(); + } + + /** + * Fetches angle relative to pie chart center point where 3 O'Clock is 0 and + * 12 O'Clock is 270degrees + * + * @param screenPoint + * @return angle in degress from 0-360. + */ + public double getAngle(Point screenPoint) { + double dx = screenPoint.getX() - mCenterX; + // Minus to correct for coord re-mapping + double dy = -(screenPoint.getY() - mCenterY); + + double inRads = Math.atan2(dy, dx); + + // We need to map to coord system when 0 degree is at 3 O'clock, 270 at 12 + // O'clock + if (inRads < 0) + inRads = Math.abs(inRads); + else + inRads = 2 * Math.PI - inRads; + + return Math.toDegrees(inRads); + } + + /** + * Checks if Point falls within PieChart + * + * @param screenPoint + * @return true if in PieChart + */ + public boolean isOnPieChart(Point screenPoint) { + // Using a bit of Pythagoras + // inside circle if (x-center_x)**2 + (y-center_y)**2 <= radius**2: + + double sqValue = (Math.pow(mCenterX - screenPoint.getX(), 2) + Math.pow( + mCenterY - screenPoint.getY(), 2)); + + double radiusSquared = mPieChartRadius * mPieChartRadius; + boolean isOnPieChart = sqValue <= radiusSquared; + return isOnPieChart; + } + + /** + * Fetches the SeriesSelection for the PieSegment selected. + * + * @param screenPoint - the user tap location + * @return null if screen point is not in PieChart or its config if it is + */ + public SeriesSelection getSeriesAndPointForScreenCoordinate(Point screenPoint) { + if (isOnPieChart(screenPoint)) { + double angleFromPieCenter = getAngle(screenPoint); + + for (PieSegment pieSeg : mPieSegmentList) { + if (pieSeg.isInSegment(angleFromPieCenter)) { + return new SeriesSelection(0, pieSeg.getDataIndex(), pieSeg.getValue(), + pieSeg.getValue()); + } + } + } + return null; + } +} diff --git a/android-libraries/achartengine/src/org/achartengine/chart/PieSegment.java b/android-libraries/achartengine/src/org/achartengine/chart/PieSegment.java new file mode 100644 index 00000000..0fb0a2e4 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/chart/PieSegment.java @@ -0,0 +1,70 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.chart; + +import java.io.Serializable; + +/** + * Holds An PieChart Segment + */ +public class PieSegment implements Serializable { + private float mStartAngle; + + private float mEndAngle; + + private int mDataIndex; + + private float mValue; + + public PieSegment(int dataIndex, float value, float startAngle, float angle) { + mStartAngle = startAngle; + mEndAngle = angle + startAngle; + mDataIndex = dataIndex; + mValue = value; + } + + /** + * Checks if angle falls in segment. + * + * @param angle + * @return true if in segment, false otherwise. + */ + public boolean isInSegment(double angle) { + return angle >= mStartAngle && angle <= mEndAngle; + } + + protected float getStartAngle() { + return mStartAngle; + } + + protected float getEndAngle() { + return mEndAngle; + } + + protected int getDataIndex() { + return mDataIndex; + } + + protected float getValue() { + return mValue; + } + + public String toString() { + return "mDataIndex=" + mDataIndex + ",mValue=" + mValue + ",mStartAngle=" + mStartAngle + + ",mEndAngle=" + mEndAngle; + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/chart/PointStyle.java b/android-libraries/achartengine/src/org/achartengine/chart/PointStyle.java new file mode 100644 index 00000000..29a2311a --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/chart/PointStyle.java @@ -0,0 +1,90 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.chart; + +/** + * The chart point style enumerator. + */ +public enum PointStyle { + X("x"), CIRCLE("circle"), TRIANGLE("triangle"), SQUARE("square"), DIAMOND("diamond"), POINT( + "point"); + + /** The point shape name. */ + private String mName; + + /** + * The point style enum constructor. + * + * @param name the name + */ + private PointStyle(String name) { + mName = name; + } + + /** + * Returns the point shape name. + * + * @return the point shape name + */ + public String getName() { + return mName; + } + + /** + * Returns the point shape name. + * + * @return the point shape name + */ + public String toString() { + return getName(); + } + + /** + * Return the point shape that has the provided symbol. + * + * @param name the point style name + * @return the point shape + */ + public static PointStyle getPointStyleForName(String name) { + PointStyle pointStyle = null; + PointStyle[] styles = values(); + int length = styles.length; + for (int i = 0; i < length && pointStyle == null; i++) { + if (styles[i].mName.equals(name)) { + pointStyle = styles[i]; + } + } + return pointStyle; + } + + /** + * Returns the point shape index based on the given name. + * + * @return the point shape index + */ + public static int getIndexForName(String name) { + int index = -1; + PointStyle[] styles = values(); + int length = styles.length; + for (int i = 0; i < length && index < 0; i++) { + if (styles[i].mName.equals(name)) { + index = i; + } + } + return Math.max(0, index); + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/chart/RangeBarChart.java b/android-libraries/achartengine/src/org/achartengine/chart/RangeBarChart.java new file mode 100644 index 00000000..105f509a --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/chart/RangeBarChart.java @@ -0,0 +1,145 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.chart; + +import org.achartengine.model.XYMultipleSeriesDataset; +import org.achartengine.model.XYSeries; +import org.achartengine.renderer.SimpleSeriesRenderer; +import org.achartengine.renderer.XYMultipleSeriesRenderer; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Style; + +/** + * The range bar chart rendering class. + */ +public class RangeBarChart extends BarChart { + /** The chart type. */ + public static final String TYPE = "RangeBar"; + + RangeBarChart() { + } + + RangeBarChart(Type type) { + super(type); + } + + /** + * Builds a new range bar chart instance. + * + * @param dataset the multiple series dataset + * @param renderer the multiple series renderer + * @param type the range bar chart type + */ + public RangeBarChart(XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer, Type type) { + super(dataset, renderer, type); + } + + /** + * The graphical representation of a series. + * + * @param canvas the canvas to paint to + * @param paint the paint to be used for drawing + * @param points the array of points to be used for drawing the series + * @param seriesRenderer the series renderer + * @param yAxisValue the minimum value of the y axis + * @param seriesIndex the index of the series currently being drawn + * @param startIndex the start index of the rendering points + */ + public void drawSeries(Canvas canvas, Paint paint, float[] points, + SimpleSeriesRenderer seriesRenderer, float yAxisValue, int seriesIndex, int startIndex) { + int seriesNr = mDataset.getSeriesCount(); + int length = points.length; + paint.setColor(seriesRenderer.getColor()); + paint.setStyle(Style.FILL); + float halfDiffX = getHalfDiffX(points, length, seriesNr); + int start = 0; + if (startIndex > 0) { + start = 2; + } + for (int i = start; i < length; i += 4) { + if (points.length > i + 3) { + float xMin = points[i]; + float yMin = points[i + 1]; + // xMin = xMax + float xMax = points[i + 2]; + float yMax = points[i + 3]; + drawBar(canvas, xMin, yMin, xMax, yMax, halfDiffX, seriesNr, seriesIndex, paint); + } + } + paint.setColor(seriesRenderer.getColor()); + } + + /** + * The graphical representation of the series values as text. + * + * @param canvas the canvas to paint to + * @param series the series to be painted + * @param renderer the series renderer + * @param paint the paint to be used for drawing + * @param points the array of points to be used for drawing the series + * @param seriesIndex the index of the series currently being drawn + * @param startIndex the start index of the rendering points + */ + protected void drawChartValuesText(Canvas canvas, XYSeries series, SimpleSeriesRenderer renderer, + Paint paint, float[] points, int seriesIndex, int startIndex) { + int seriesNr = mDataset.getSeriesCount(); + float halfDiffX = getHalfDiffX(points, points.length, seriesNr); + int start = 0; + if (startIndex > 0) { + start = 2; + } + for (int i = start; i < points.length; i += 4) { + int index = startIndex + i / 2; + float x = points[i]; + if (mType == Type.DEFAULT) { + x += seriesIndex * 2 * halfDiffX - (seriesNr - 1.5f) * halfDiffX; + } + + if (!isNullValue(series.getY(index + 1)) && points.length > i + 3) { + // draw the maximum value + drawText(canvas, getLabel(series.getY(index + 1)), x, + points[i + 3] - renderer.getChartValuesSpacing(), paint, 0); + } + if (!isNullValue(series.getY(index)) && points.length > i + 1) { + // draw the minimum value + drawText(canvas, getLabel(series.getY(index)), x, + points[i + 1] + renderer.getChartValuesTextSize() + renderer.getChartValuesSpacing() + - 3, paint, 0); + } + } + } + + /** + * Returns the value of a constant used to calculate the half-distance. + * + * @return the constant value + */ + protected float getCoeficient() { + return 0.5f; + } + + /** + * Returns the chart type identifier. + * + * @return the chart type + */ + public String getChartType() { + return TYPE; + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/chart/RangeStackedBarChart.java b/android-libraries/achartengine/src/org/achartengine/chart/RangeStackedBarChart.java new file mode 100644 index 00000000..4da4f006 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/chart/RangeStackedBarChart.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.chart; + +public class RangeStackedBarChart extends RangeBarChart { + /** The chart type. */ + public static final String TYPE = "RangeStackedBar"; + + RangeStackedBarChart() { + super(Type.STACKED); + } + + public String getChartType() { + return TYPE; + } +} \ No newline at end of file diff --git a/android-libraries/achartengine/src/org/achartengine/chart/RoundChart.java b/android-libraries/achartengine/src/org/achartengine/chart/RoundChart.java new file mode 100644 index 00000000..1a2121d4 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/chart/RoundChart.java @@ -0,0 +1,143 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.chart; + +import org.achartengine.model.CategorySeries; +import org.achartengine.renderer.DefaultRenderer; +import org.achartengine.renderer.SimpleSeriesRenderer; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Align; + +/** + * An abstract class to be extended by round like chart rendering classes. + */ +public abstract class RoundChart extends AbstractChart { + /** The legend shape width. */ + protected static final int SHAPE_WIDTH = 10; + /** The series dataset. */ + protected CategorySeries mDataset; + /** The series renderer. */ + protected DefaultRenderer mRenderer; + /** A no value constant. */ + protected static final int NO_VALUE = Integer.MAX_VALUE; + /** The chart center X axis. */ + protected int mCenterX = NO_VALUE; + /** The chart center y axis. */ + protected int mCenterY = NO_VALUE; + + /** + * Round chart. + * + * @param dataset the series dataset + * @param renderer the series renderer + */ + public RoundChart(CategorySeries dataset, DefaultRenderer renderer) { + mDataset = dataset; + mRenderer = renderer; + } + + /** + * The graphical representation of the round chart title. + * + * @param canvas the canvas to paint to + * @param x the top left x value of the view to draw to + * @param y the top left y value of the view to draw to + * @param width the width of the view to draw to + * @param paint the paint + */ + public void drawTitle(Canvas canvas, int x, int y, int width, Paint paint) { + if (mRenderer.isShowLabels()) { + paint.setColor(mRenderer.getLabelsColor()); + paint.setTextAlign(Align.CENTER); + paint.setTextSize(mRenderer.getChartTitleTextSize()); + drawString(canvas, mRenderer.getChartTitle(), x + width / 2, + y + mRenderer.getChartTitleTextSize(), paint); + } + } + + /** + * Returns the legend shape width. + * + * @param seriesIndex the series index + * @return the legend shape width + */ + public int getLegendShapeWidth(int seriesIndex) { + return SHAPE_WIDTH; + } + + /** + * The graphical representation of the legend shape. + * + * @param canvas the canvas to paint to + * @param renderer the series renderer + * @param x the x value of the point the shape should be drawn at + * @param y the y value of the point the shape should be drawn at + * @param seriesIndex the series index + * @param paint the paint to be used for drawing + */ + public void drawLegendShape(Canvas canvas, SimpleSeriesRenderer renderer, float x, float y, + int seriesIndex, Paint paint) { + canvas.drawRect(x, y - SHAPE_WIDTH / 2, x + SHAPE_WIDTH, y + SHAPE_WIDTH / 2, paint); + } + + /** + * Returns the renderer. + * + * @return the renderer + */ + public DefaultRenderer getRenderer() { + return mRenderer; + } + + /** + * Returns the center on X axis. + * + * @return the center on X axis + */ + public int getCenterX() { + return mCenterX; + } + + /** + * Returns the center on Y axis. + * + * @return the center on Y axis + */ + public int getCenterY() { + return mCenterY; + } + + /** + * Sets a new center on X axis. + * + * @param centerX center on X axis + */ + public void setCenterX(int centerX) { + mCenterX = centerX; + } + + /** + * Sets a new center on Y axis. + * + * @param centerY center on Y axis + */ + public void setCenterY(int centerY) { + mCenterY = centerY; + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/chart/ScatterChart.java b/android-libraries/achartengine/src/org/achartengine/chart/ScatterChart.java new file mode 100644 index 00000000..1d3b2a18 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/chart/ScatterChart.java @@ -0,0 +1,266 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.chart; + +import org.achartengine.model.XYMultipleSeriesDataset; +import org.achartengine.renderer.SimpleSeriesRenderer; +import org.achartengine.renderer.XYMultipleSeriesRenderer; +import org.achartengine.renderer.XYSeriesRenderer; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.RectF; + +/** + * The scatter chart rendering class. + */ +public class ScatterChart extends XYChart { + /** The constant to identify this chart type. */ + public static final String TYPE = "Scatter"; + /** The default point shape size. */ + private static final float SIZE = 3; + /** The legend shape width. */ + private static final int SHAPE_WIDTH = 10; + /** The point shape size. */ + private float size = SIZE; + + ScatterChart() { + } + + /** + * Builds a new scatter chart instance. + * + * @param dataset the multiple series dataset + * @param renderer the multiple series renderer + */ + public ScatterChart(XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer) { + super(dataset, renderer); + size = renderer.getPointSize(); + } + + // TODO: javadoc + protected void setDatasetRenderer(XYMultipleSeriesDataset dataset, + XYMultipleSeriesRenderer renderer) { + super.setDatasetRenderer(dataset, renderer); + size = renderer.getPointSize(); + } + + /** + * The graphical representation of a series. + * + * @param canvas the canvas to paint to + * @param paint the paint to be used for drawing + * @param points the array of points to be used for drawing the series + * @param seriesRenderer the series renderer + * @param yAxisValue the minimum value of the y axis + * @param seriesIndex the index of the series currently being drawn + * @param startIndex the start index of the rendering points + */ + public void drawSeries(Canvas canvas, Paint paint, float[] points, + SimpleSeriesRenderer seriesRenderer, float yAxisValue, int seriesIndex, int startIndex) { + XYSeriesRenderer renderer = (XYSeriesRenderer) seriesRenderer; + paint.setColor(renderer.getColor()); + if (renderer.isFillPoints()) { + paint.setStyle(Style.FILL); + } else { + paint.setStyle(Style.STROKE); + } + int length = points.length; + switch (renderer.getPointStyle()) { + case X: + for (int i = 0; i < length; i += 2) { + drawX(canvas, paint, points[i], points[i + 1]); + } + break; + case CIRCLE: + for (int i = 0; i < length; i += 2) { + drawCircle(canvas, paint, points[i], points[i + 1]); + } + break; + case TRIANGLE: + float[] path = new float[6]; + for (int i = 0; i < length; i += 2) { + drawTriangle(canvas, paint, path, points[i], points[i + 1]); + } + break; + case SQUARE: + for (int i = 0; i < length; i += 2) { + drawSquare(canvas, paint, points[i], points[i + 1]); + } + break; + case DIAMOND: + path = new float[8]; + for (int i = 0; i < length; i += 2) { + drawDiamond(canvas, paint, path, points[i], points[i + 1]); + } + break; + case POINT: + canvas.drawPoints(points, paint); + break; + } + } + + @Override + protected ClickableArea[] clickableAreasForPoints(float[] points, double[] values, + float yAxisValue, int seriesIndex, int startIndex) { + int length = points.length; + ClickableArea[] ret = new ClickableArea[length / 2]; + for (int i = 0; i < length; i += 2) { + int selectableBuffer = mRenderer.getSelectableBuffer(); + ret[i / 2] = new ClickableArea(new RectF(points[i] - selectableBuffer, points[i + 1] + - selectableBuffer, points[i] + selectableBuffer, points[i + 1] + selectableBuffer), + values[i], values[i + 1]); + } + return ret; + } + + /** + * Returns the legend shape width. + * + * @param seriesIndex the series index + * @return the legend shape width + */ + public int getLegendShapeWidth(int seriesIndex) { + return SHAPE_WIDTH; + } + + /** + * The graphical representation of the legend shape. + * + * @param canvas the canvas to paint to + * @param renderer the series renderer + * @param x the x value of the point the shape should be drawn at + * @param y the y value of the point the shape should be drawn at + * @param seriesIndex the series index + * @param paint the paint to be used for drawing + */ + public void drawLegendShape(Canvas canvas, SimpleSeriesRenderer renderer, float x, float y, + int seriesIndex, Paint paint) { + if (((XYSeriesRenderer) renderer).isFillPoints()) { + paint.setStyle(Style.FILL); + } else { + paint.setStyle(Style.STROKE); + } + switch (((XYSeriesRenderer) renderer).getPointStyle()) { + case X: + drawX(canvas, paint, x + SHAPE_WIDTH, y); + break; + case CIRCLE: + drawCircle(canvas, paint, x + SHAPE_WIDTH, y); + break; + case TRIANGLE: + drawTriangle(canvas, paint, new float[6], x + SHAPE_WIDTH, y); + break; + case SQUARE: + drawSquare(canvas, paint, x + SHAPE_WIDTH, y); + break; + case DIAMOND: + drawDiamond(canvas, paint, new float[8], x + SHAPE_WIDTH, y); + break; + case POINT: + canvas.drawPoint(x + SHAPE_WIDTH, y, paint); + break; + } + } + + /** + * The graphical representation of an X point shape. + * + * @param canvas the canvas to paint to + * @param paint the paint to be used for drawing + * @param x the x value of the point the shape should be drawn at + * @param y the y value of the point the shape should be drawn at + */ + private void drawX(Canvas canvas, Paint paint, float x, float y) { + canvas.drawLine(x - size, y - size, x + size, y + size, paint); + canvas.drawLine(x + size, y - size, x - size, y + size, paint); + } + + /** + * The graphical representation of a circle point shape. + * + * @param canvas the canvas to paint to + * @param paint the paint to be used for drawing + * @param x the x value of the point the shape should be drawn at + * @param y the y value of the point the shape should be drawn at + */ + private void drawCircle(Canvas canvas, Paint paint, float x, float y) { + canvas.drawCircle(x, y, size, paint); + } + + /** + * The graphical representation of a triangle point shape. + * + * @param canvas the canvas to paint to + * @param paint the paint to be used for drawing + * @param path the triangle path + * @param x the x value of the point the shape should be drawn at + * @param y the y value of the point the shape should be drawn at + */ + private void drawTriangle(Canvas canvas, Paint paint, float[] path, float x, float y) { + path[0] = x; + path[1] = y - size - size / 2; + path[2] = x - size; + path[3] = y + size; + path[4] = x + size; + path[5] = path[3]; + drawPath(canvas, path, paint, true); + } + + /** + * The graphical representation of a square point shape. + * + * @param canvas the canvas to paint to + * @param paint the paint to be used for drawing + * @param x the x value of the point the shape should be drawn at + * @param y the y value of the point the shape should be drawn at + */ + private void drawSquare(Canvas canvas, Paint paint, float x, float y) { + canvas.drawRect(x - size, y - size, x + size, y + size, paint); + } + + /** + * The graphical representation of a diamond point shape. + * + * @param canvas the canvas to paint to + * @param paint the paint to be used for drawing + * @param path the diamond path + * @param x the x value of the point the shape should be drawn at + * @param y the y value of the point the shape should be drawn at + */ + private void drawDiamond(Canvas canvas, Paint paint, float[] path, float x, float y) { + path[0] = x; + path[1] = y - size; + path[2] = x - size; + path[3] = y; + path[4] = x; + path[5] = y + size; + path[6] = x + size; + path[7] = y; + drawPath(canvas, path, paint, true); + } + + /** + * Returns the chart type identifier. + * + * @return the chart type + */ + public String getChartType() { + return TYPE; + } + +} \ No newline at end of file diff --git a/android-libraries/achartengine/src/org/achartengine/chart/TimeChart.java b/android-libraries/achartengine/src/org/achartengine/chart/TimeChart.java new file mode 100644 index 00000000..ba201de7 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/chart/TimeChart.java @@ -0,0 +1,226 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.chart; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import org.achartengine.model.XYMultipleSeriesDataset; +import org.achartengine.model.XYSeries; +import org.achartengine.renderer.XYMultipleSeriesRenderer; + +import android.graphics.Canvas; +import android.graphics.Paint; + +/** + * The time chart rendering class. + */ +public class TimeChart extends LineChart { + /** The constant to identify this chart type. */ + public static final String TYPE = "Time"; + /** The number of milliseconds in a day. */ + public static final long DAY = 24 * 60 * 60 * 1000; + /** The date format pattern to be used in formatting the X axis labels. */ + private String mDateFormat; + /** The starting point for labels. */ + private Double mStartPoint; + + TimeChart() { + } + + /** + * Builds a new time chart instance. + * + * @param dataset the multiple series dataset + * @param renderer the multiple series renderer + */ + public TimeChart(XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer) { + super(dataset, renderer); + } + + /** + * Returns the date format pattern to be used for formatting the X axis + * labels. + * + * @return the date format pattern for the X axis labels + */ + public String getDateFormat() { + return mDateFormat; + } + + /** + * Sets the date format pattern to be used for formatting the X axis labels. + * + * @param format the date format pattern for the X axis labels. If null, an + * appropriate default format will be used. + */ + public void setDateFormat(String format) { + mDateFormat = format; + } + + /** + * The graphical representation of the labels on the X axis. + * + * @param xLabels the X labels values + * @param xTextLabelLocations the X text label locations + * @param canvas the canvas to paint to + * @param paint the paint to be used for drawing + * @param left the left value of the labels area + * @param top the top value of the labels area + * @param bottom the bottom value of the labels area + * @param xPixelsPerUnit the amount of pixels per one unit in the chart labels + * @param minX the minimum value on the X axis in the chart + * @param maxX the maximum value on the X axis in the chart + */ + @Override + protected void drawXLabels(List xLabels, Double[] xTextLabelLocations, Canvas canvas, + Paint paint, int left, int top, int bottom, double xPixelsPerUnit, double minX, double maxX) { + int length = xLabels.size(); + if (length > 0) { + boolean showLabels = mRenderer.isShowLabels(); + boolean showGridY = mRenderer.isShowGridY(); + DateFormat format = getDateFormat(xLabels.get(0), xLabels.get(length - 1)); + for (int i = 0; i < length; i++) { + long label = Math.round(xLabels.get(i)); + float xLabel = (float) (left + xPixelsPerUnit * (label - minX)); + if (showLabels) { + paint.setColor(mRenderer.getXLabelsColor()); + canvas + .drawLine(xLabel, bottom, xLabel, bottom + mRenderer.getLabelsTextSize() / 3, paint); + drawText(canvas, format.format(new Date(label)), xLabel, + bottom + mRenderer.getLabelsTextSize() * 4 / 3, paint, mRenderer.getXLabelsAngle()); + } + if (showGridY) { + paint.setColor(mRenderer.getGridColor()); + canvas.drawLine(xLabel, bottom, xLabel, top, paint); + } + } + } + drawXTextLabels(xTextLabelLocations, canvas, paint, true, left, top, bottom, xPixelsPerUnit, + minX, maxX); + } + + /** + * Returns the date format pattern to be used, based on the date range. + * + * @param start the start date in milliseconds + * @param end the end date in milliseconds + * @return the date format + */ + private DateFormat getDateFormat(double start, double end) { + if (mDateFormat != null) { + SimpleDateFormat format = null; + try { + format = new SimpleDateFormat(mDateFormat); + return format; + } catch (Exception e) { + // do nothing here + } + } + DateFormat format = SimpleDateFormat.getDateInstance(SimpleDateFormat.MEDIUM); + double diff = end - start; + if (diff > DAY && diff < 5 * DAY) { + format = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.SHORT, SimpleDateFormat.SHORT); + } else if (diff < DAY) { + format = SimpleDateFormat.getTimeInstance(SimpleDateFormat.MEDIUM); + } + return format; + } + + /** + * Returns the chart type identifier. + * + * @return the chart type + */ + public String getChartType() { + return TYPE; + } + + protected List getXLabels(double min, double max, int count) { + final List result = new ArrayList(); + if (!mRenderer.isXRoundedLabels()) { + if (mDataset.getSeriesCount() > 0) { + XYSeries series = mDataset.getSeriesAt(0); + int length = series.getItemCount(); + int intervalLength = 0; + int startIndex = -1; + for (int i = 0; i < length; i++) { + double value = series.getX(i); + if (min <= value && value <= max) { + intervalLength++; + if (startIndex < 0) { + startIndex = i; + } + } + } + if (intervalLength < count) { + for (int i = startIndex; i < startIndex + intervalLength; i++) { + result.add(series.getX(i)); + } + } else { + float step = (float) intervalLength / count; + int intervalCount = 0; + for (int i = 0; i < length && intervalCount < count; i++) { + double value = series.getX(Math.round(i * step)); + if (min <= value && value <= max) { + result.add(value); + intervalCount++; + } + } + } + return result; + } else { + return super.getXLabels(min, max, count); + } + } + if (mStartPoint == null) { + mStartPoint = min - (min % DAY) + DAY + new Date(Math.round(min)).getTimezoneOffset() * 60 + * 1000; + } + if (count > 25) { + count = 25; + } + + + final double cycleMath = (max - min) / count; + if (cycleMath <= 0) { + return result; + } + double cycle = DAY; + + if (cycleMath <= DAY) { + while (cycleMath < cycle / 2) { + cycle = cycle / 2; + } + } else { + while (cycleMath > cycle) { + cycle = cycle * 2; + } + } + + double val = mStartPoint - Math.floor((mStartPoint - min) / cycle) * cycle; + int i = 0; + while (val < max && i++ <= count) { + result.add(val); + val += cycle; + } + + return result; + } +} diff --git a/android-libraries/achartengine/src/org/achartengine/chart/XYChart.java b/android-libraries/achartengine/src/org/achartengine/chart/XYChart.java new file mode 100644 index 00000000..6b531d60 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/chart/XYChart.java @@ -0,0 +1,905 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.chart; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.SortedMap; + +import org.achartengine.model.Point; +import org.achartengine.model.SeriesSelection; +import org.achartengine.model.XYMultipleSeriesDataset; +import org.achartengine.model.XYSeries; +import org.achartengine.renderer.BasicStroke; +import org.achartengine.renderer.DefaultRenderer; +import org.achartengine.renderer.SimpleSeriesRenderer; +import org.achartengine.renderer.XYMultipleSeriesRenderer; +import org.achartengine.renderer.XYMultipleSeriesRenderer.Orientation; +import org.achartengine.util.MathHelper; + +import android.graphics.Canvas; +import android.graphics.DashPathEffect; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Paint.Cap; +import android.graphics.Paint.Join; +import android.graphics.Paint.Style; +import android.graphics.PathEffect; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Typeface; + +/** + * The XY chart rendering class. + */ +public abstract class XYChart extends AbstractChart { + /** The multiple series dataset. */ + protected XYMultipleSeriesDataset mDataset; + /** The multiple series renderer. */ + protected XYMultipleSeriesRenderer mRenderer; + /** The current scale value. */ + private float mScale; + /** The current translate value. */ + private float mTranslate; + /** The canvas center point. */ + private Point mCenter; + /** The visible chart area, in screen coordinates. */ + private Rect mScreenR; + /** The calculated range. */ + private final Map mCalcRange = new HashMap(); + + /** + * The clickable areas for all points. The array index is the series index, + * and the RectF list index is the point index in that series. + */ + private Map> clickableAreas = new HashMap>(); + + protected XYChart() { + } + + /** + * Builds a new XY chart instance. + * + * @param dataset the multiple series dataset + * @param renderer the multiple series renderer + */ + public XYChart(XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer) { + mDataset = dataset; + mRenderer = renderer; + } + + // TODO: javadoc + protected void setDatasetRenderer(XYMultipleSeriesDataset dataset, + XYMultipleSeriesRenderer renderer) { + mDataset = dataset; + mRenderer = renderer; + } + + /** + * The graphical representation of the XY chart. + * + * @param canvas the canvas to paint to + * @param x the top left x value of the view to draw to + * @param y the top left y value of the view to draw to + * @param width the width of the view to draw to + * @param height the height of the view to draw to + * @param paint the paint + */ + public void draw(Canvas canvas, int x, int y, int width, int height, Paint paint) { + paint.setAntiAlias(mRenderer.isAntialiasing()); + int legendSize = getLegendSize(mRenderer, height / 5, mRenderer.getAxisTitleTextSize()); + int[] margins = mRenderer.getMargins(); + int left = x + margins[1]; + int top = y + margins[0]; + int right = x + width - margins[3]; + int sLength = mDataset.getSeriesCount(); + String[] titles = new String[sLength]; + for (int i = 0; i < sLength; i++) { + titles[i] = mDataset.getSeriesAt(i).getTitle(); + } + if (mRenderer.isFitLegend() && mRenderer.isShowLegend()) { + legendSize = drawLegend(canvas, mRenderer, titles, left, right, y, width, height, legendSize, + paint, true); + } + int bottom = y + height - margins[2] - legendSize; + if (mScreenR == null) { + mScreenR = new Rect(); + } + mScreenR.set(left, top, right, bottom); + drawBackground(mRenderer, canvas, x, y, width, height, paint, false, DefaultRenderer.NO_COLOR); + + if (paint.getTypeface() == null + || !paint.getTypeface().toString().equals(mRenderer.getTextTypefaceName()) + || paint.getTypeface().getStyle() != mRenderer.getTextTypefaceStyle()) { + paint.setTypeface(Typeface.create(mRenderer.getTextTypefaceName(), + mRenderer.getTextTypefaceStyle())); + } + Orientation or = mRenderer.getOrientation(); + if (or == Orientation.VERTICAL) { + right -= legendSize; + bottom += legendSize - 20; + } + int angle = or.getAngle(); + boolean rotate = angle == 90; + mScale = (float) (height) / width; + mTranslate = Math.abs(width - height) / 2; + if (mScale < 1) { + mTranslate *= -1; + } + mCenter = new Point((x + width) / 2, (y + height) / 2); + if (rotate) { + transform(canvas, angle, false); + } + + int maxScaleNumber = -Integer.MAX_VALUE; + for (int i = 0; i < sLength; i++) { + maxScaleNumber = Math.max(maxScaleNumber, mDataset.getSeriesAt(i).getScaleNumber()); + } + maxScaleNumber++; + if (maxScaleNumber < 0) { + return; + } + double[] minX = new double[maxScaleNumber]; + double[] maxX = new double[maxScaleNumber]; + double[] minY = new double[maxScaleNumber]; + double[] maxY = new double[maxScaleNumber]; + boolean[] isMinXSet = new boolean[maxScaleNumber]; + boolean[] isMaxXSet = new boolean[maxScaleNumber]; + boolean[] isMinYSet = new boolean[maxScaleNumber]; + boolean[] isMaxYSet = new boolean[maxScaleNumber]; + + for (int i = 0; i < maxScaleNumber; i++) { + minX[i] = mRenderer.getXAxisMin(i); + maxX[i] = mRenderer.getXAxisMax(i); + minY[i] = mRenderer.getYAxisMin(i); + maxY[i] = mRenderer.getYAxisMax(i); + isMinXSet[i] = mRenderer.isMinXSet(i); + isMaxXSet[i] = mRenderer.isMaxXSet(i); + isMinYSet[i] = mRenderer.isMinYSet(i); + isMaxYSet[i] = mRenderer.isMaxYSet(i); + if (mCalcRange.get(i) == null) { + mCalcRange.put(i, new double[4]); + } + } + double[] xPixelsPerUnit = new double[maxScaleNumber]; + double[] yPixelsPerUnit = new double[maxScaleNumber]; + for (int i = 0; i < sLength; i++) { + XYSeries series = mDataset.getSeriesAt(i); + int scale = series.getScaleNumber(); + if (series.getItemCount() == 0) { + continue; + } + if (!isMinXSet[scale]) { + double minimumX = series.getMinX(); + minX[scale] = Math.min(minX[scale], minimumX); + mCalcRange.get(scale)[0] = minX[scale]; + } + if (!isMaxXSet[scale]) { + double maximumX = series.getMaxX(); + maxX[scale] = Math.max(maxX[scale], maximumX); + mCalcRange.get(scale)[1] = maxX[scale]; + } + if (!isMinYSet[scale]) { + double minimumY = series.getMinY(); + minY[scale] = Math.min(minY[scale], (float) minimumY); + mCalcRange.get(scale)[2] = minY[scale]; + } + if (!isMaxYSet[scale]) { + double maximumY = series.getMaxY(); + maxY[scale] = Math.max(maxY[scale], (float) maximumY); + mCalcRange.get(scale)[3] = maxY[scale]; + } + } + for (int i = 0; i < maxScaleNumber; i++) { + if (maxX[i] - minX[i] != 0) { + xPixelsPerUnit[i] = (right - left) / (maxX[i] - minX[i]); + } + if (maxY[i] - minY[i] != 0) { + yPixelsPerUnit[i] = (float) ((bottom - top) / (maxY[i] - minY[i])); + } + } + + boolean hasValues = false; + // use a linked list for these reasons: + // 1) Avoid a large contiguous memory allocation + // 2) We don't need random seeking, only sequential reading/writing, so + // linked list makes sense + clickableAreas = new HashMap>(); + for (int i = 0; i < sLength; i++) { + XYSeries series = mDataset.getSeriesAt(i); + int scale = series.getScaleNumber(); + if (series.getItemCount() == 0) { + continue; + } + + hasValues = true; + SimpleSeriesRenderer seriesRenderer = mRenderer.getSeriesRendererAt(i); + + // int originalValuesLength = series.getItemCount(); + // int valuesLength = originalValuesLength; + // int length = valuesLength * 2; + + List points = new ArrayList(); + List values = new ArrayList(); + float yAxisValue = Math.min(bottom, (float) (bottom + yPixelsPerUnit[scale] * minY[scale])); + LinkedList clickableArea = new LinkedList(); + + clickableAreas.put(i, clickableArea); + + SortedMap range = series.getRange(minX[scale], maxX[scale], 1); + int startIndex = -1; + + for (Entry value : range.entrySet()) { + + double xValue = value.getKey(); + double yValue = value.getValue(); + if (startIndex < 0) { + startIndex = series.getIndexForKey(xValue); + } + + // points.add((float) (left + xPixelsPerUnit[scale] + // * (value.getKey().floatValue() - minX[scale]))); + // points.add((float) (bottom - yPixelsPerUnit[scale] + // * (value.getValue().floatValue() - minY[scale]))); + values.add(value.getKey()); + values.add(value.getValue()); + + if (!isNullValue(yValue)) { + points.add((float) (left + xPixelsPerUnit[scale] * (xValue - minX[scale]))); + points.add((float) (bottom - yPixelsPerUnit[scale] * (yValue - minY[scale]))); + } else if (isRenderNullValues()) { + points.add((float) (left + xPixelsPerUnit[scale] * (xValue - minX[scale]))); + points.add((float) (bottom - yPixelsPerUnit[scale] * (-minY[scale]))); + } else { + if (points.size() > 0) { + drawSeries(series, canvas, paint, points, seriesRenderer, yAxisValue, i, or, startIndex); + ClickableArea[] clickableAreasForSubSeries = clickableAreasForPoints( + MathHelper.getFloats(points), MathHelper.getDoubles(values), yAxisValue, i, + startIndex); + clickableArea.addAll(Arrays.asList(clickableAreasForSubSeries)); + points.clear(); + values.clear(); + } + clickableArea.add(null); + } + } + + if (points.size() > 0) { + drawSeries(series, canvas, paint, points, seriesRenderer, yAxisValue, i, or, startIndex); + ClickableArea[] clickableAreasForSubSeries = clickableAreasForPoints( + MathHelper.getFloats(points), MathHelper.getDoubles(values), yAxisValue, i, startIndex); + clickableArea.addAll(Arrays.asList(clickableAreasForSubSeries)); + } + } + + // draw stuff over the margins such as data doesn't render on these areas + drawBackground(mRenderer, canvas, x, bottom, width, height - bottom, paint, true, + mRenderer.getMarginsColor()); + drawBackground(mRenderer, canvas, x, y, width, margins[0], paint, true, + mRenderer.getMarginsColor()); + if (or == Orientation.HORIZONTAL) { + drawBackground(mRenderer, canvas, x, y, left - x, height - y, paint, true, + mRenderer.getMarginsColor()); + drawBackground(mRenderer, canvas, right, y, margins[3], height - y, paint, true, + mRenderer.getMarginsColor()); + } else if (or == Orientation.VERTICAL) { + drawBackground(mRenderer, canvas, right, y, width - right, height - y, paint, true, + mRenderer.getMarginsColor()); + drawBackground(mRenderer, canvas, x, y, left - x, height - y, paint, true, + mRenderer.getMarginsColor()); + } + + boolean showLabels = mRenderer.isShowLabels() && hasValues; + boolean showGridX = mRenderer.isShowGridX(); + boolean showCustomTextGrid = mRenderer.isShowCustomTextGrid(); + if (showLabels || showGridX) { + List xLabels = getValidLabels(getXLabels(minX[0], maxX[0], mRenderer.getXLabels())); + Map> allYLabels = getYLabels(minY, maxY, maxScaleNumber); + + int xLabelsLeft = left; + if (showLabels) { + paint.setColor(mRenderer.getXLabelsColor()); + paint.setTextSize(mRenderer.getLabelsTextSize()); + paint.setTextAlign(mRenderer.getXLabelsAlign()); + if (mRenderer.getXLabelsAlign() == Align.LEFT) { + xLabelsLeft += mRenderer.getLabelsTextSize() / 4; + } + } + drawXLabels(xLabels, mRenderer.getXTextLabelLocations(), canvas, paint, xLabelsLeft, top, + bottom, xPixelsPerUnit[0], minX[0], maxX[0]); + drawYLabels(allYLabels, canvas, paint, maxScaleNumber, left, right, bottom, yPixelsPerUnit, + minY); + + if (showLabels) { + paint.setColor(mRenderer.getLabelsColor()); + for (int i = 0; i < maxScaleNumber; i++) { + Align axisAlign = mRenderer.getYAxisAlign(i); + Double[] yTextLabelLocations = mRenderer.getYTextLabelLocations(i); + for (Double location : yTextLabelLocations) { + if (minY[i] <= location && location <= maxY[i]) { + float yLabel = (float) (bottom - yPixelsPerUnit[i] + * (location.doubleValue() - minY[i])); + String label = mRenderer.getYTextLabel(location, i); + paint.setColor(mRenderer.getYLabelsColor(i)); + paint.setTextAlign(mRenderer.getYLabelsAlign(i)); + if (or == Orientation.HORIZONTAL) { + if (axisAlign == Align.LEFT) { + canvas.drawLine(left + getLabelLinePos(axisAlign), yLabel, left, yLabel, paint); + drawText(canvas, label, left, yLabel - 2, paint, mRenderer.getYLabelsAngle()); + } else { + canvas.drawLine(right, yLabel, right + getLabelLinePos(axisAlign), yLabel, paint); + drawText(canvas, label, right, yLabel - 2, paint, mRenderer.getYLabelsAngle()); + } + + if (showCustomTextGrid) { + paint.setColor(mRenderer.getGridColor()); + canvas.drawLine(left, yLabel, right, yLabel, paint); + } + } else { + canvas.drawLine(right - getLabelLinePos(axisAlign), yLabel, right, yLabel, paint); + drawText(canvas, label, right + 10, yLabel - 2, paint, mRenderer.getYLabelsAngle()); + if (showCustomTextGrid) { + paint.setColor(mRenderer.getGridColor()); + canvas.drawLine(right, yLabel, left, yLabel, paint); + } + } + } + } + } + } + + if (showLabels) { + paint.setColor(mRenderer.getLabelsColor()); + float size = mRenderer.getAxisTitleTextSize(); + paint.setTextSize(size); + paint.setTextAlign(Align.CENTER); + if (or == Orientation.HORIZONTAL) { + drawText(canvas, mRenderer.getXTitle(), x + width / 2, + bottom + mRenderer.getLabelsTextSize() * 4 / 3 + size, paint, 0); + for (int i = 0; i < maxScaleNumber; i++) { + Align axisAlign = mRenderer.getYAxisAlign(i); + if (axisAlign == Align.LEFT) { + drawText(canvas, mRenderer.getYTitle(i), x + size, y + height / 2, paint, -90); + } else { + drawText(canvas, mRenderer.getYTitle(i), x + width, y + height / 2, paint, -90); + } + } + paint.setTextSize(mRenderer.getChartTitleTextSize()); + drawText(canvas, mRenderer.getChartTitle(), x + width / 2, + y + mRenderer.getChartTitleTextSize(), paint, 0); + } else if (or == Orientation.VERTICAL) { + drawText(canvas, mRenderer.getXTitle(), x + width / 2, y + height - size, paint, -90); + drawText(canvas, mRenderer.getYTitle(), right + 20, y + height / 2, paint, 0); + paint.setTextSize(mRenderer.getChartTitleTextSize()); + drawText(canvas, mRenderer.getChartTitle(), x + size, top + height / 2, paint, 0); + } + } + } + if (or == Orientation.HORIZONTAL) { + drawLegend(canvas, mRenderer, titles, left, right, y, width, height, legendSize, paint, false); + } else if (or == Orientation.VERTICAL) { + transform(canvas, angle, true); + drawLegend(canvas, mRenderer, titles, left, right, y, width, height, legendSize, paint, false); + transform(canvas, angle, false); + } + if (mRenderer.isShowAxes()) { + paint.setColor(mRenderer.getAxesColor()); + canvas.drawLine(left, bottom, right, bottom, paint); + boolean rightAxis = false; + for (int i = 0; i < maxScaleNumber && !rightAxis; i++) { + rightAxis = mRenderer.getYAxisAlign(i) == Align.RIGHT; + } + if (or == Orientation.HORIZONTAL) { + canvas.drawLine(left, top, left, bottom, paint); + if (rightAxis) { + canvas.drawLine(right, top, right, bottom, paint); + } + } else if (or == Orientation.VERTICAL) { + canvas.drawLine(right, top, right, bottom, paint); + } + } + if (rotate) { + transform(canvas, angle, true); + } + } + + protected List getXLabels(double min, double max, int count) { + return MathHelper.getLabels(min, max, count); + } + + protected Map> getYLabels(double[] minY, double[] maxY, int maxScaleNumber) { + Map> allYLabels = new HashMap>(); + for (int i = 0; i < maxScaleNumber; i++) { + allYLabels.put(i, + getValidLabels(MathHelper.getLabels(minY[i], maxY[i], mRenderer.getYLabels()))); + } + return allYLabels; + } + + protected Rect getScreenR() { + return mScreenR; + } + + protected void setScreenR(Rect screenR) { + mScreenR = screenR; + } + + private List getValidLabels(List labels) { + List result = new ArrayList(labels); + for (Double label : labels) { + if (label.isNaN()) { + result.remove(label); + } + } + return result; + } + + /** + * Draws the series. + * + * @param series the series + * @param canvas the canvas + * @param paint the paint object + * @param pointsList the points to be rendered + * @param seriesRenderer the series renderer + * @param yAxisValue the y axis value in pixels + * @param seriesIndex the series index + * @param or the orientation + * @param startIndex the start index of the rendering points + */ + protected void drawSeries(XYSeries series, Canvas canvas, Paint paint, List pointsList, + SimpleSeriesRenderer seriesRenderer, float yAxisValue, int seriesIndex, Orientation or, + int startIndex) { + BasicStroke stroke = seriesRenderer.getStroke(); + Cap cap = paint.getStrokeCap(); + Join join = paint.getStrokeJoin(); + float miter = paint.getStrokeMiter(); + PathEffect pathEffect = paint.getPathEffect(); + Style style = paint.getStyle(); + if (stroke != null) { + PathEffect effect = null; + if (stroke.getIntervals() != null) { + effect = new DashPathEffect(stroke.getIntervals(), stroke.getPhase()); + } + setStroke(stroke.getCap(), stroke.getJoin(), stroke.getMiter(), Style.FILL_AND_STROKE, + effect, paint); + } + float[] points = MathHelper.getFloats(pointsList); + drawSeries(canvas, paint, points, seriesRenderer, yAxisValue, seriesIndex, startIndex); + if (isRenderPoints(seriesRenderer)) { + ScatterChart pointsChart = getPointsChart(); + if (pointsChart != null) { + pointsChart.drawSeries(canvas, paint, points, seriesRenderer, yAxisValue, seriesIndex, + startIndex); + } + } + paint.setTextSize(seriesRenderer.getChartValuesTextSize()); + if (or == Orientation.HORIZONTAL) { + paint.setTextAlign(Align.CENTER); + } else { + paint.setTextAlign(Align.LEFT); + } + if (seriesRenderer.isDisplayChartValues()) { + paint.setTextAlign(seriesRenderer.getChartValuesTextAlign()); + drawChartValuesText(canvas, series, seriesRenderer, paint, points, seriesIndex, startIndex); + } + if (stroke != null) { + setStroke(cap, join, miter, style, pathEffect, paint); + } + } + + private void setStroke(Cap cap, Join join, float miter, Style style, PathEffect pathEffect, + Paint paint) { + paint.setStrokeCap(cap); + paint.setStrokeJoin(join); + paint.setStrokeMiter(miter); + paint.setPathEffect(pathEffect); + paint.setStyle(style); + } + + /** + * The graphical representation of the series values as text. + * + * @param canvas the canvas to paint to + * @param series the series to be painted + * @param renderer the series renderer + * @param paint the paint to be used for drawing + * @param points the array of points to be used for drawing the series + * @param seriesIndex the index of the series currently being drawn + * @param startIndex the start index of the rendering points + */ + protected void drawChartValuesText(Canvas canvas, XYSeries series, SimpleSeriesRenderer renderer, + Paint paint, float[] points, int seriesIndex, int startIndex) { + if (points.length > 1) { // there are more than one point + // record the first point's position + float previousPointX = points[0]; + float previousPointY = points[1]; + for (int k = 0; k < points.length; k += 2) { + if (k == 2) { // decide whether to display first two points' values or not + if (Math.abs(points[2]- points[0]) > 100 || Math.abs(points[3] - points[1]) > 100) { + // first point + drawText(canvas, getLabel(series.getY(startIndex)), points[0], points[1] + - renderer.getChartValuesSpacing(), paint, 0); + // second point + drawText(canvas, getLabel(series.getY(startIndex + 1)), points[2], points[3] + - renderer.getChartValuesSpacing(), paint, 0); + + previousPointX = points[2]; + previousPointY = points[3]; + } + } else if (k > 2) { + // compare current point's position with the previous point's, if they are not too close, display + if (Math.abs(points[k]- previousPointX) > 100 || Math.abs(points[k+1] - previousPointY) > 100) { + drawText(canvas, getLabel(series.getY(startIndex + k / 2)), points[k], points[k + 1] + - renderer.getChartValuesSpacing(), paint, 0); + previousPointX = points[k]; + previousPointY = points[k+1]; + } + } + } + } else { // if only one point, display it + for (int k = 0; k < points.length; k += 2) { + drawText(canvas, getLabel(series.getY(startIndex + k / 2)), points[k], points[k + 1] + - renderer.getChartValuesSpacing(), paint, 0); + } + } + } + + /** + * The graphical representation of a text, to handle both HORIZONTAL and + * VERTICAL orientations and extra rotation angles. + * + * @param canvas the canvas to paint to + * @param text the text to be rendered + * @param x the X axis location of the text + * @param y the Y axis location of the text + * @param paint the paint to be used for drawing + * @param extraAngle the text angle + */ + protected void drawText(Canvas canvas, String text, float x, float y, Paint paint, + float extraAngle) { + float angle = -mRenderer.getOrientation().getAngle() + extraAngle; + if (angle != 0) { + // canvas.scale(1 / mScale, mScale); + canvas.rotate(angle, x, y); + } + drawString(canvas, text, x, y, paint); + if (angle != 0) { + canvas.rotate(-angle, x, y); + // canvas.scale(mScale, 1 / mScale); + } + } + + /** + * Transform the canvas such as it can handle both HORIZONTAL and VERTICAL + * orientations. + * + * @param canvas the canvas to paint to + * @param angle the angle of rotation + * @param inverse if the inverse transform needs to be applied + */ + private void transform(Canvas canvas, float angle, boolean inverse) { + if (inverse) { + canvas.scale(1 / mScale, mScale); + canvas.translate(mTranslate, -mTranslate); + canvas.rotate(-angle, mCenter.getX(), mCenter.getY()); + } else { + canvas.rotate(angle, mCenter.getX(), mCenter.getY()); + canvas.translate(-mTranslate, mTranslate); + canvas.scale(mScale, 1 / mScale); + } + } + + /** + * The graphical representation of the labels on the X axis. + * + * @param xLabels the X labels values + * @param xTextLabelLocations the X text label locations + * @param canvas the canvas to paint to + * @param paint the paint to be used for drawing + * @param left the left value of the labels area + * @param top the top value of the labels area + * @param bottom the bottom value of the labels area + * @param xPixelsPerUnit the amount of pixels per one unit in the chart labels + * @param minX the minimum value on the X axis in the chart + * @param maxX the maximum value on the X axis in the chart + */ + protected void drawXLabels(List xLabels, Double[] xTextLabelLocations, Canvas canvas, + Paint paint, int left, int top, int bottom, double xPixelsPerUnit, double minX, double maxX) { + int length = xLabels.size(); + boolean showLabels = mRenderer.isShowLabels(); + boolean showGridY = mRenderer.isShowGridY(); + for (int i = 0; i < length; i++) { + double label = xLabels.get(i); + float xLabel = (float) (left + xPixelsPerUnit * (label - minX)); + if (showLabels) { + paint.setColor(mRenderer.getXLabelsColor()); + canvas.drawLine(xLabel, bottom, xLabel, bottom + mRenderer.getLabelsTextSize() / 3, paint); + drawText(canvas, getLabel(label), xLabel, bottom + mRenderer.getLabelsTextSize() * 4 / 3, + paint, mRenderer.getXLabelsAngle()); + } + if (showGridY) { + paint.setColor(mRenderer.getGridColor()); + canvas.drawLine(xLabel, bottom, xLabel, top, paint); + } + } + drawXTextLabels(xTextLabelLocations, canvas, paint, showLabels, left, top, bottom, + xPixelsPerUnit, minX, maxX); + } + + /** + * The graphical representation of the labels on the X axis. + * + * @param allYLabels the Y labels values + * @param canvas the canvas to paint to + * @param paint the paint to be used for drawing + * @param maxScaleNumber the maximum scale number + * @param left the left value of the labels area + * @param right the right value of the labels area + * @param bottom the bottom value of the labels area + * @param yPixelsPerUnit the amount of pixels per one unit in the chart labels + * @param minY the minimum value on the Y axis in the chart + */ + protected void drawYLabels(Map> allYLabels, Canvas canvas, Paint paint, + int maxScaleNumber, int left, int right, int bottom, double[] yPixelsPerUnit, double[] minY) { + Orientation or = mRenderer.getOrientation(); + boolean showGridX = mRenderer.isShowGridX(); + boolean showLabels = mRenderer.isShowLabels(); + for (int i = 0; i < maxScaleNumber; i++) { + paint.setTextAlign(mRenderer.getYLabelsAlign(i)); + List yLabels = allYLabels.get(i); + int length = yLabels.size(); + for (int j = 0; j < length; j++) { + double label = yLabels.get(j); + Align axisAlign = mRenderer.getYAxisAlign(i); + boolean textLabel = mRenderer.getYTextLabel(label, i) != null; + float yLabel = (float) (bottom - yPixelsPerUnit[i] * (label - minY[i])); + if (or == Orientation.HORIZONTAL) { + if (showLabels && !textLabel) { + paint.setColor(mRenderer.getYLabelsColor(i)); + if (axisAlign == Align.LEFT) { + canvas.drawLine(left + getLabelLinePos(axisAlign), yLabel, left, yLabel, paint); + drawText(canvas, getLabel(label), left, yLabel - 2, paint, + mRenderer.getYLabelsAngle()); + } else { + canvas.drawLine(right, yLabel, right + getLabelLinePos(axisAlign), yLabel, paint); + drawText(canvas, getLabel(label), right, yLabel - 2, paint, + mRenderer.getYLabelsAngle()); + } + } + if (showGridX) { + paint.setColor(mRenderer.getGridColor()); + canvas.drawLine(left, yLabel, right, yLabel, paint); + } + } else if (or == Orientation.VERTICAL) { + if (showLabels && !textLabel) { + paint.setColor(mRenderer.getYLabelsColor(i)); + canvas.drawLine(right - getLabelLinePos(axisAlign), yLabel, right, yLabel, paint); + drawText(canvas, getLabel(label), right + 10, yLabel - 2, paint, + mRenderer.getYLabelsAngle()); + } + if (showGridX) { + paint.setColor(mRenderer.getGridColor()); + canvas.drawLine(right, yLabel, left, yLabel, paint); + } + } + } + } + } + + /** + * The graphical representation of the text labels on the X axis. + * + * @param xTextLabelLocations the X text label locations + * @param canvas the canvas to paint to + * @param paint the paint to be used for drawing + * @param left the left value of the labels area + * @param top the top value of the labels area + * @param bottom the bottom value of the labels area + * @param xPixelsPerUnit the amount of pixels per one unit in the chart labels + * @param minX the minimum value on the X axis in the chart + * @param maxX the maximum value on the X axis in the chart + */ + protected void drawXTextLabels(Double[] xTextLabelLocations, Canvas canvas, Paint paint, + boolean showLabels, int left, int top, int bottom, double xPixelsPerUnit, double minX, + double maxX) { + boolean showCustomTextGrid = mRenderer.isShowCustomTextGrid(); + if (showLabels) { + paint.setColor(mRenderer.getXLabelsColor()); + for (Double location : xTextLabelLocations) { + if (minX <= location && location <= maxX) { + float xLabel = (float) (left + xPixelsPerUnit * (location.doubleValue() - minX)); + paint.setColor(mRenderer.getXLabelsColor()); + canvas + .drawLine(xLabel, bottom, xLabel, bottom + mRenderer.getLabelsTextSize() / 3, paint); + drawText(canvas, mRenderer.getXTextLabel(location), xLabel, + bottom + mRenderer.getLabelsTextSize() * 4 / 3, paint, mRenderer.getXLabelsAngle()); + if (showCustomTextGrid) { + paint.setColor(mRenderer.getGridColor()); + canvas.drawLine(xLabel, bottom, xLabel, top, paint); + } + } + } + } + } + + // TODO: docs + public XYMultipleSeriesRenderer getRenderer() { + return mRenderer; + } + + public XYMultipleSeriesDataset getDataset() { + return mDataset; + } + + public double[] getCalcRange(int scale) { + return mCalcRange.get(scale); + } + + public void setCalcRange(double[] range, int scale) { + mCalcRange.put(scale, range); + } + + public double[] toRealPoint(float screenX, float screenY) { + return toRealPoint(screenX, screenY, 0); + } + + public double[] toScreenPoint(double[] realPoint) { + return toScreenPoint(realPoint, 0); + } + + private int getLabelLinePos(Align align) { + int pos = 4; + if (align == Align.LEFT) { + pos = -pos; + } + return pos; + } + + /** + * Transforms a screen point to a real coordinates point. + * + * @param screenX the screen x axis value + * @param screenY the screen y axis value + * @return the real coordinates point + */ + public double[] toRealPoint(float screenX, float screenY, int scale) { + double realMinX = mRenderer.getXAxisMin(scale); + double realMaxX = mRenderer.getXAxisMax(scale); + double realMinY = mRenderer.getYAxisMin(scale); + double realMaxY = mRenderer.getYAxisMax(scale); + return new double[] { + (screenX - mScreenR.left) * (realMaxX - realMinX) / mScreenR.width() + realMinX, + (mScreenR.top + mScreenR.height() - screenY) * (realMaxY - realMinY) / mScreenR.height() + + realMinY }; + } + + public double[] toScreenPoint(double[] realPoint, int scale) { + double realMinX = mRenderer.getXAxisMin(scale); + double realMaxX = mRenderer.getXAxisMax(scale); + double realMinY = mRenderer.getYAxisMin(scale); + double realMaxY = mRenderer.getYAxisMax(scale); + if (!mRenderer.isMinXSet(scale) || !mRenderer.isMaxXSet(scale) || !mRenderer.isMinXSet(scale) + || !mRenderer.isMaxYSet(scale)) { + double[] calcRange = getCalcRange(scale); + realMinX = calcRange[0]; + realMaxX = calcRange[1]; + realMinY = calcRange[2]; + realMaxY = calcRange[3]; + } + return new double[] { + (realPoint[0] - realMinX) * mScreenR.width() / (realMaxX - realMinX) + mScreenR.left, + (realMaxY - realPoint[1]) * mScreenR.height() / (realMaxY - realMinY) + mScreenR.top }; + } + + public SeriesSelection getSeriesAndPointForScreenCoordinate(final Point screenPoint) { + if (clickableAreas != null) + for (int seriesIndex = clickableAreas.size() - 1; seriesIndex >= 0; seriesIndex--) { + // series 0 is drawn first. Then series 1 is drawn on top, and series 2 + // on top of that. + // we want to know what the user clicked on, so traverse them in the + // order they appear on the screen. + int pointIndex = 0; + if (clickableAreas.get(seriesIndex) != null) { + RectF rectangle; + for (ClickableArea area : clickableAreas.get(seriesIndex)) { + rectangle = area.getRect(); + if (rectangle != null && rectangle.contains(screenPoint.getX(), screenPoint.getY())) { + return new SeriesSelection(seriesIndex, pointIndex, area.getX(), area.getY()); + } + pointIndex++; + } + } + } + return super.getSeriesAndPointForScreenCoordinate(screenPoint); + } + + /** + * The graphical representation of a series. + * + * @param canvas the canvas to paint to + * @param paint the paint to be used for drawing + * @param points the array of points to be used for drawing the series + * @param seriesRenderer the series renderer + * @param yAxisValue the minimum value of the y axis + * @param seriesIndex the index of the series currently being drawn + * @param startIndex the start index of the rendering points + */ + public abstract void drawSeries(Canvas canvas, Paint paint, float[] points, + SimpleSeriesRenderer seriesRenderer, float yAxisValue, int seriesIndex, int startIndex); + + /** + * Returns the clickable areas for all passed points + * + * @param points the array of points + * @param values the array of values of each point + * @param yAxisValue the minimum value of the y axis + * @param seriesIndex the index of the series to which the points belong + * @return an array of rectangles with the clickable area + * @param startIndex the start index of the rendering points + */ + protected abstract ClickableArea[] clickableAreasForPoints(float[] points, double[] values, + float yAxisValue, int seriesIndex, int startIndex); + + /** + * Returns if the chart should display the null values. + * + * @return if null values should be rendered + */ + protected boolean isRenderNullValues() { + return false; + } + + /** + * Returns if the chart should display the points as a certain shape. + * + * @param renderer the series renderer + */ + public boolean isRenderPoints(SimpleSeriesRenderer renderer) { + return false; + } + + /** + * Returns the default axis minimum. + * + * @return the default axis minimum + */ + public double getDefaultMinimum() { + return MathHelper.NULL_VALUE; + } + + /** + * Returns the scatter chart to be used for drawing the data points. + * + * @return the data points scatter chart + */ + public ScatterChart getPointsChart() { + return null; + } + + /** + * Returns the chart type identifier. + * + * @return the chart type + */ + public abstract String getChartType(); + +} diff --git a/android-libraries/achartengine/src/org/achartengine/chart/package.html b/android-libraries/achartengine/src/org/achartengine/chart/package.html new file mode 100644 index 00000000..2c5fbec1 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/chart/package.html @@ -0,0 +1,6 @@ + +AChartEngine + +Provides the classes that handle the actual rendering / drawing of the charts, based on the provided model and renderer. + + \ No newline at end of file diff --git a/android-libraries/achartengine/src/org/achartengine/image/zoom-1.png b/android-libraries/achartengine/src/org/achartengine/image/zoom-1.png new file mode 100644 index 0000000000000000000000000000000000000000..8265f27840a14b4b275086df67af01c73a3a8420 GIT binary patch literal 1139 zcmV-(1dRKMP)iK~#9!wU*CoR96(oKljb}2AN4rGl?J?Uud%EMjJ&JKEVVG z!nl};q=TD^kVeR+1>N}r;}nZUq?AL44q-HL)Vj{^+{y=l%Wtq*5sw8X9PBZYGsVkw_$n#bU%_F%pRcb#-+llS$gz z+L)f6wq|E%zX*ch`+4NpV`KJXxh;Tlxtz~tvm82fh-5NJUtix}PRl|FK{lHu zm&+l9KnTI*%a^PEj*bp8nGBxiNLje2sXyh)6=tKT?j#U zcQ;BYwcv{TLv=2|x%z zJRV0Wg)s&pM0LJpN~!9aF@`9LkWvyw(LbvFdj*U0^YhhjU2Bcj8Yv}GN^-dzxm<2X zvaQ%K44ImmLMgRa6JU%Pe){w&zV8!;AzEvUF@#}A5CmwgQA!bpAz>JzltODw5CjB4 zK(SavYdu_3BFnN0g+k%$d_GTmdwW#|&+`yMaPQtdgb-9J6{M6q{BPgB<;jyL0EOLJ zg=5?H52sI`?!0^VF1BrBSr)c!BZSy-CQ7Ml%lPo&1J|!#XJTSv2+9UUeVdVy5ea+;%af{SP@!vXJ%)ntuV;yBK7U0vPNwY9Zh{xzVtw>Ncs zeEdS!^^)P?VQOk>C@n2T2!ZRm*tU&nntb^1f#=Vkv%0$KM5EE+gM))>vk4THp`oD% z*=+Ww$B!SEjE|2KkH?8bB2-mX5s$~IuC68)ixCco35Ub9w6tIthJ5qp&B(ob_kKJ* zJw3h=P$(3>({;URVq$`DI7~DerMkMBa5zjb7$gt~5C{Ya27^Q*5kjF5&CSj1?d{3^ z{r%4^%lhV101436)|PTzw`Xi@jPmkwqR}YvcpOdB2m}H=di02afdL;Pr6d#z(bUvL zI-SPzyq>D6s?@cBTrM|~N~Nf+ttAu+QBhHW5CSPBLj2h#Ap}AQA0nlst*wn@GKpar zBi8~f%hJ2MyU{d_NF;&~0?+gCJnt{gMcY-o5Q6^xew0%BrvN1YN~wnW`g(%FpdWl- zV89P>Wj1_3CX?~?`}gkyG~5J02ti3n2}&uHQh1){!#9vmD5V19lc$8k_fVVWkkZBr-|*xlVlDRp>l4?NFXSz21c zG))SH0+wY_C=`6%bzNN7^=+nUVi*RNWnozsi;Ii6uDfzAKuVd(WHMy4Sq#GRMv$L~knue4TP1F272qFCb6j#Q@ z#RZd-lWc8mEd#wbP4aJ2%B`cLqpv=G{3zSo+x_Mh%VijbU!HtE&)L}-Gcz-+uC6-3 z*Fg5hRpGj>CWKht-Q6A8+}xD)_4SmMmHEMo^*%p8XM1~_>FH@+zkclidEiUn6!`hJ zb)5iWLWpNl%ASW0AJWy;MPp+l00##Lyn6MDmoHxeECWx07rQWzN|_ygF>@YIfg RUN-;$002ovPDHLkV1jl}0RjL3 literal 0 HcmV?d00001 diff --git a/android-libraries/achartengine/src/org/achartengine/image/zoom_out.png b/android-libraries/achartengine/src/org/achartengine/image/zoom_out.png new file mode 100644 index 0000000000000000000000000000000000000000..d67a87de533e307cb0e95ab2623259d98eb1adae GIT binary patch literal 1074 zcmV-21kL-2P)ZcG>Ma%D2N{kj*D)b>fpvF=zwm5 z5E-mXbGYn7^T$p^GpsN-Jnrg5T1P7C|(AOa;G)2(2OS^S;-Oc{m-T zrPvDx4(A=t@1Aow=RWwKkErw^ola*8g~FI=nwsM{ZAvMCBVg&#qer>*_4ObB6JUIN z{ORWA=36P{z~JB@Po6v>9*^U?E`>sYwY4>7XJ>I7XTG_)`Q_^B>TmxV@a);M%<1Xr zdri}-Mn^|!Y;2^ux*8z_uIpmkHiltva&p4EckftUUUp)!*y#TLe#LA8Wo2Y!N*^MnBpeRY-ri0&o5l0I zf%^LTOhrJUP#DW(GBh^|6Wo(=@fdzCMD% zAkk$*6OgHj5^FtBYK%d*(n*+D6FRIvx1=Pk|6&0!b@mStg@CYELSdRfXg!!Xcw z-M7!q&f>c6QbmB2GMCHc$mjFux=yK7Lf7?A#57GxrIIhEX`<^o+uPgB%*+7fDh`@u zS%wgz`{3XpH8?nk>$+H$<*x_JvaoI2&!P+~7K^-k^@_8zvw2|Zu1Wq=O8NWo@$on3 z=jXD!yW4MGIjFAdetAlz5?5DOym|A6<>h4u_zuY5xhh=O4GJOVcXoEhK79BfTU%SH zsj2aUm+M_D7TMa`;`Qs-tgNg!KneI7xCDN^Z(S#WxDev4lyacIzn|XTUQ($P0Q>v< zEG{mxu&@9y54;5417BQU_5%18`02g}CkcE7i~-t>AvywFLpdzpl%ce8{Ia30zH0O({*&9N-ElJ|v`^m66S6?PM}( sKQ!Pb mCategories = new ArrayList(); + /** The series values. */ + private List mValues = new ArrayList(); + + /** + * Builds a new category series. + * + * @param title the series title + */ + public CategorySeries(String title) { + mTitle = title; + } + + /** + * Returns the series title. + * + * @return the series title + */ + public String getTitle() { + return mTitle; + } + + /** + * Adds a new value to the series + * + * @param value the new value + */ + public synchronized void add(double value) { + add(mCategories.size() + "", value); + } + + /** + * Adds a new value to the series. + * + * @param category the category + * @param value the new value + */ + public synchronized void add(String category, double value) { + mCategories.add(category); + mValues.add(value); + } + + /** + * Replaces the value at the specific index in the series. + * + * @param index the index in the series + * @param category the category + * @param value the new value + */ + public synchronized void set(int index, String category, double value) { + mCategories.set(index, category); + mValues.set(index, value); + } + + /** + * Removes an existing value from the series. + * + * @param index the index in the series of the value to remove + */ + public synchronized void remove(int index) { + mCategories.remove(index); + mValues.remove(index); + } + + /** + * Removes all the existing values from the series. + */ + public synchronized void clear() { + mCategories.clear(); + mValues.clear(); + } + + /** + * Returns the value at the specified index. + * + * @param index the index + * @return the value at the index + */ + public synchronized double getValue(int index) { + return mValues.get(index); + } + + /** + * Returns the category name at the specified index. + * + * @param index the index + * @return the category name at the index + */ + public synchronized String getCategory(int index) { + return mCategories.get(index); + } + + /** + * Returns the series item count. + * + * @return the series item count + */ + public synchronized int getItemCount() { + return mCategories.size(); + } + + /** + * Transforms the category series to an XY series. + * + * @return the XY series + */ + public XYSeries toXYSeries() { + XYSeries xySeries = new XYSeries(mTitle); + int k = 0; + for (double value : mValues) { + xySeries.add(++k, value); + } + return xySeries; + } +} diff --git a/android-libraries/achartengine/src/org/achartengine/model/MultipleCategorySeries.java b/android-libraries/achartengine/src/org/achartengine/model/MultipleCategorySeries.java new file mode 100644 index 00000000..992db4c1 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/model/MultipleCategorySeries.java @@ -0,0 +1,145 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.model; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * A series for the multiple category charts like the doughnut. + */ +public class MultipleCategorySeries implements Serializable { + /** The series title. */ + private String mTitle; + /** The series local keys. */ + private List mCategories = new ArrayList(); + /** The series name. */ + private List mTitles = new ArrayList(); + /** The series values. */ + private List mValues = new ArrayList(); + + /** + * Builds a new category series. + * + * @param title the series title + */ + public MultipleCategorySeries(String title) { + mTitle = title; + } + + /** + * Adds a new value to the series + * + * @param titles the titles to be used as labels + * @param values the new value + */ + public void add(String[] titles, double[] values) { + add(mCategories.size() + "", titles, values); + } + + /** + * Adds a new value to the series. + * + * @param category the category name + * @param titles the titles to be used as labels + * @param values the new value + */ + public void add(String category, String[] titles, double[] values) { + mCategories.add(category); + mTitles.add(titles); + mValues.add(values); + } + + /** + * Removes an existing value from the series. + * + * @param index the index in the series of the value to remove + */ + public void remove(int index) { + mCategories.remove(index); + mTitles.remove(index); + mValues.remove(index); + } + + /** + * Removes all the existing values from the series. + */ + public void clear() { + mCategories.clear(); + mTitles.clear(); + mValues.clear(); + } + + /** + * Returns the values at the specified index. + * + * @param index the index + * @return the value at the index + */ + public double[] getValues(int index) { + return mValues.get(index); + } + + /** + * Returns the category name at the specified index. + * + * @param index the index + * @return the category name at the index + */ + public String getCategory(int index) { + return mCategories.get(index); + } + + /** + * Returns the categories count. + * + * @return the categories count + */ + public int getCategoriesCount() { + return mCategories.size(); + } + + /** + * Returns the series item count. + * + * @param index the index + * @return the series item count + */ + public int getItemCount(int index) { + return mValues.get(index).length; + } + + /** + * Returns the series titles. + * + * @param index the index + * @return the series titles + */ + public String[] getTitles(int index) { + return mTitles.get(index); + } + + /** + * Transforms the category series to an XY series. + * + * @return the XY series + */ + public XYSeries toXYSeries() { + XYSeries xySeries = new XYSeries(mTitle); + return xySeries; + } +} diff --git a/android-libraries/achartengine/src/org/achartengine/model/Point.java b/android-libraries/achartengine/src/org/achartengine/model/Point.java new file mode 100644 index 00000000..4d2767cf --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/model/Point.java @@ -0,0 +1,52 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.model; + +import java.io.Serializable; + +/** + * A class to encapsulate the definition of a point. + */ +public final class Point implements Serializable { + /** The X axis coordinate value. */ + private float mX; + /** The Y axis coordinate value. */ + private float mY; + + public Point() { + } + + public Point(float x, float y) { + mX = x; + mY = y; + } + + public float getX() { + return mX; + } + + public float getY() { + return mY; + } + + public void setX(float x) { + mX = x; + } + + public void setY(float y) { + mY = y; + } +} diff --git a/android-libraries/achartengine/src/org/achartengine/model/RangeCategorySeries.java b/android-libraries/achartengine/src/org/achartengine/model/RangeCategorySeries.java new file mode 100644 index 00000000..c004fa10 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/model/RangeCategorySeries.java @@ -0,0 +1,111 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.model; + +import java.util.ArrayList; +import java.util.List; +/** + * A series for the range category charts like the range bar. + */ +public class RangeCategorySeries extends CategorySeries { + /** The series values. */ + private List mMaxValues = new ArrayList(); + /** + * Builds a new category series. + * + * @param title the series title + */ + public RangeCategorySeries(String title) { + super(title); + } + /** + * Adds new values to the series + * + * @param minValue the new minimum value + * @param maxValue the new maximum value + */ + public synchronized void add(double minValue, double maxValue) { + super.add(minValue); + mMaxValues.add(maxValue); + } + + /** + * Adds new values to the series. + * + * @param category the category + * @param minValue the new minimum value + * @param maxValue the new maximum value + */ + public synchronized void add(String category, double minValue, double maxValue) { + super.add(category, minValue); + mMaxValues.add(maxValue); + } + + /** + * Removes existing values from the series. + * + * @param index the index in the series of the values to remove + */ + public synchronized void remove(int index) { + super.remove(index); + mMaxValues.remove(index); + } + + /** + * Removes all the existing values from the series. + */ + public synchronized void clear() { + super.clear(); + mMaxValues.clear(); + } + + /** + * Returns the minimum value at the specified index. + * + * @param index the index + * @return the minimum value at the index + */ + public double getMinimumValue(int index) { + return getValue(index); + } + + /** + * Returns the maximum value at the specified index. + * + * @param index the index + * @return the maximum value at the index + */ + public double getMaximumValue(int index) { + return mMaxValues.get(index); + } + + /** + * Transforms the range category series to an XY series. + * + * @return the XY series + */ + public XYSeries toXYSeries() { + XYSeries xySeries = new XYSeries(getTitle()); + int length = getItemCount(); + for (int k = 0; k < length; k++) { + xySeries.add(k + 1, getMinimumValue(k)); + // the new fast XYSeries implementation doesn't allow 2 values at the same X, + // so I had to do a hack until I find a better solution + xySeries.add(k + 1.000001, getMaximumValue(k)); + } + return xySeries; + } +} diff --git a/android-libraries/achartengine/src/org/achartengine/model/SeriesSelection.java b/android-libraries/achartengine/src/org/achartengine/model/SeriesSelection.java new file mode 100644 index 00000000..4319c742 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/model/SeriesSelection.java @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.model; + +public class SeriesSelection { + private int mSeriesIndex; + + private int mPointIndex; + + private double mXValue; + + private double mValue; + + public SeriesSelection(int seriesIndex, int pointIndex, double xValue, double value) { + mSeriesIndex = seriesIndex; + mPointIndex = pointIndex; + mXValue = xValue; + mValue = value; + } + + public int getSeriesIndex() { + return mSeriesIndex; + } + + public int getPointIndex() { + return mPointIndex; + } + + public double getXValue() { + return mXValue; + } + + public double getValue() { + return mValue; + } +} \ No newline at end of file diff --git a/android-libraries/achartengine/src/org/achartengine/model/TimeSeries.java b/android-libraries/achartengine/src/org/achartengine/model/TimeSeries.java new file mode 100644 index 00000000..6b9a18e5 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/model/TimeSeries.java @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.model; + +import java.util.Date; + +/** + * A series for the date / time charts. + */ +public class TimeSeries extends XYSeries { + + /** + * Builds a new date / time series. + * + * @param title the series title + */ + public TimeSeries(String title) { + super(title); + } + + /** + * Adds a new value to the series. + * + * @param x the date / time value for the X axis + * @param y the value for the Y axis + */ + public synchronized void add(Date x, double y) { + super.add(x.getTime(), y); + } +} diff --git a/android-libraries/achartengine/src/org/achartengine/model/XYMultipleSeriesDataset.java b/android-libraries/achartengine/src/org/achartengine/model/XYMultipleSeriesDataset.java new file mode 100644 index 00000000..6ac6a6de --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/model/XYMultipleSeriesDataset.java @@ -0,0 +1,94 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.model; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * A series that includes 0 to many XYSeries. + */ +public class XYMultipleSeriesDataset implements Serializable { + /** The included series. */ + private List mSeries = new ArrayList(); + + /** + * Adds a new XY series to the list. + * + * @param series the XY series to ass + */ + public synchronized void addSeries(XYSeries series) { + mSeries.add(series); + } + + /** + * Adds a new XY series to the list. + * + * @param index the index in the series list + * @param series the XY series to ass + */ + public synchronized void addSeries(int index, XYSeries series) { + mSeries.add(index, series); + } + + /** + * Removes the XY series from the list. + * + * @param index the index in the series list of the series to remove + */ + public synchronized void removeSeries(int index) { + mSeries.remove(index); + } + + /** + * Removes the XY series from the list. + * + * @param series the XY series to be removed + */ + public synchronized void removeSeries(XYSeries series) { + mSeries.remove(series); + } + + /** + * Returns the XY series at the specified index. + * + * @param index the index + * @return the XY series at the index + */ + public synchronized XYSeries getSeriesAt(int index) { + return mSeries.get(index); + } + + /** + * Returns the XY series count. + * + * @return the XY series count + */ + public synchronized int getSeriesCount() { + return mSeries.size(); + } + + /** + * Returns an array of the XY series. + * + * @return the XY series array + */ + public synchronized XYSeries[] getSeries() { + return mSeries.toArray(new XYSeries[0]); + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/model/XYSeries.java b/android-libraries/achartengine/src/org/achartengine/model/XYSeries.java new file mode 100644 index 00000000..56063b77 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/model/XYSeries.java @@ -0,0 +1,255 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.model; + +import java.io.Serializable; +import java.util.Iterator; +import java.util.SortedMap; + +import org.achartengine.util.IndexXYMap; +import org.achartengine.util.MathHelper; +import org.achartengine.util.XYEntry; + +/** + * An XY series encapsulates values for XY charts like line, time, area, + * scatter... charts. + */ +public class XYSeries implements Serializable { + /** The series title. */ + private String mTitle; + /** A map to contain values for X and Y axes and index for each bundle */ + private final IndexXYMap mXY = new IndexXYMap(); + /** The minimum value for the X axis. */ + private double mMinX = MathHelper.NULL_VALUE; + /** The maximum value for the X axis. */ + private double mMaxX = -MathHelper.NULL_VALUE; + /** The minimum value for the Y axis. */ + private double mMinY = MathHelper.NULL_VALUE; + /** The maximum value for the Y axis. */ + private double mMaxY = -MathHelper.NULL_VALUE; + /** The scale number for this series. */ + private final int mScaleNumber; + + /** + * Builds a new XY series. + * + * @param title the series title. + */ + public XYSeries(String title) { + this(title, 0); + } + + /** + * Builds a new XY series. + * + * @param title the series title. + * @param scaleNumber the series scale number + */ + public XYSeries(String title, int scaleNumber) { + mTitle = title; + mScaleNumber = scaleNumber; + initRange(); + } + + public int getScaleNumber() { + return mScaleNumber; + } + + /** + * Initializes the range for both axes. + */ + private void initRange() { + mMinX = MathHelper.NULL_VALUE; + mMaxX = -MathHelper.NULL_VALUE; + mMinY = MathHelper.NULL_VALUE; + mMaxY = -MathHelper.NULL_VALUE; + int length = getItemCount(); + for (int k = 0; k < length; k++) { + double x = getX(k); + double y = getY(k); + updateRange(x, y); + } + } + + /** + * Updates the range on both axes. + * + * @param x the new x value + * @param y the new y value + */ + private void updateRange(double x, double y) { + mMinX = Math.min(mMinX, x); + mMaxX = Math.max(mMaxX, x); + mMinY = Math.min(mMinY, y); + mMaxY = Math.max(mMaxY, y); + } + + /** + * Returns the series title. + * + * @return the series title + */ + public String getTitle() { + return mTitle; + } + + /** + * Sets the series title. + * + * @param title the series title + */ + public void setTitle(String title) { + mTitle = title; + } + + /** + * Adds a new value to the series. + * + * @param x the value for the X axis + * @param y the value for the Y axis + */ + public synchronized void add(double x, double y) { + mXY.put(x, y); + updateRange(x, y); + } + + /** + * Removes an existing value from the series. + * + * @param index the index in the series of the value to remove + */ + public synchronized void remove(int index) { + XYEntry removedEntry = mXY.removeByIndex(index); + double removedX = removedEntry.getKey(); + double removedY = removedEntry.getValue(); + if (removedX == mMinX || removedX == mMaxX || removedY == mMinY || removedY == mMaxY) { + initRange(); + } + } + + /** + * Removes all the existing values from the series. + */ + public synchronized void clear() { + mXY.clear(); + initRange(); + } + + /** + * Returns the X axis value at the specified index. + * + * @param index the index + * @return the X value + */ + public synchronized double getX(int index) { + return mXY.getXByIndex(index); + } + + /** + * Returns the Y axis value at the specified index. + * + * @param index the index + * @return the Y value + */ + public synchronized double getY(int index) { + return mXY.getYByIndex(index); + } + + /** + * Returns submap of x and y values according to the given start and end + * + * @param start start x value + * @param stop stop x value + * @return a submap of x and y values + */ + public synchronized SortedMap getRange(double start, double stop, + int beforeAfterPoints) { + // we need to add one point before the start and one point after the end (if + // there are any) + // to ensure that line doesn't end before the end of the screen + + // this would be simply: start = mXY.lowerKey(start) but NavigableMap is + // available since API 9 + SortedMap headMap = mXY.headMap(start); + if (!headMap.isEmpty()) { + start = headMap.lastKey(); + } + + // this would be simply: end = mXY.higherKey(end) but NavigableMap is + // available since API 9 + // so we have to do this hack in order to support older versions + SortedMap tailMap = mXY.tailMap(stop); + if (!tailMap.isEmpty()) { + Iterator tailIterator = tailMap.keySet().iterator(); + Double next = tailIterator.next(); + if (tailIterator.hasNext()) { + stop = tailIterator.next(); + } else { + stop += next; + } + } + return mXY.subMap(start, stop); + } + + public int getIndexForKey(double key) { + return mXY.getIndexForKey(key); + } + + /** + * Returns the series item count. + * + * @return the series item count + */ + public synchronized int getItemCount() { + return mXY.size(); + } + + /** + * Returns the minimum value on the X axis. + * + * @return the X axis minimum value + */ + public double getMinX() { + return mMinX; + } + + /** + * Returns the minimum value on the Y axis. + * + * @return the Y axis minimum value + */ + public double getMinY() { + return mMinY; + } + + /** + * Returns the maximum value on the X axis. + * + * @return the X axis maximum value + */ + public double getMaxX() { + return mMaxX; + } + + /** + * Returns the maximum value on the Y axis. + * + * @return the Y axis maximum value + */ + public double getMaxY() { + return mMaxY; + } +} diff --git a/android-libraries/achartengine/src/org/achartengine/model/XYValueSeries.java b/android-libraries/achartengine/src/org/achartengine/model/XYValueSeries.java new file mode 100644 index 00000000..81aff28b --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/model/XYValueSeries.java @@ -0,0 +1,139 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.model; + +import java.util.ArrayList; +import java.util.List; + +import org.achartengine.util.MathHelper; + +/** + * An extension of the XY series which adds a third dimension. It is used for XY + * charts like bubble. + */ +public class XYValueSeries extends XYSeries { + /** A list to contain the series values. */ + private List mValue = new ArrayList(); + /** The minimum value. */ + private double mMinValue = MathHelper.NULL_VALUE; + /** The maximum value. */ + private double mMaxValue = -MathHelper.NULL_VALUE; + + /** + * Builds a new XY value series. + * + * @param title the series title. + */ + public XYValueSeries(String title) { + super(title); + } + + /** + * Adds a new value to the series. + * + * @param x the value for the X axis + * @param y the value for the Y axis + * @param value the value + */ + public synchronized void add(double x, double y, double value) { + super.add(x, y); + mValue.add(value); + updateRange(value); + } + + /** + * Initializes the values range. + */ + private void initRange() { + mMinValue = MathHelper.NULL_VALUE; + mMaxValue = MathHelper.NULL_VALUE; + int length = getItemCount(); + for (int k = 0; k < length; k++) { + updateRange(getValue(k)); + } + } + + /** + * Updates the values range. + * + * @param value the new value + */ + private void updateRange(double value) { + mMinValue = Math.min(mMinValue, value); + mMaxValue = Math.max(mMaxValue, value); + } + + /** + * Adds a new value to the series. + * + * @param x the value for the X axis + * @param y the value for the Y axis + */ + public synchronized void add(double x, double y) { + add(x, y, 0d); + } + + /** + * Removes an existing value from the series. + * + * @param index the index in the series of the value to remove + */ + public synchronized void remove(int index) { + super.remove(index); + double removedValue = mValue.remove(index); + if (removedValue == mMinValue || removedValue == mMaxValue) { + initRange(); + } + } + + /** + * Removes all the values from the series. + */ + public synchronized void clear() { + super.clear(); + mValue.clear(); + initRange(); + } + + /** + * Returns the value at the specified index. + * + * @param index the index + * @return the value + */ + public synchronized double getValue(int index) { + return mValue.get(index); + } + + /** + * Returns the minimum value. + * + * @return the minimum value + */ + public double getMinValue() { + return mMinValue; + } + + /** + * Returns the maximum value. + * + * @return the maximum value + */ + public double getMaxValue() { + return mMaxValue; + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/model/package.html b/android-libraries/achartengine/src/org/achartengine/model/package.html new file mode 100644 index 00000000..6135d29c --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/model/package.html @@ -0,0 +1,6 @@ + +AChartEngine + +Provides the classes that handle the data values (data model) to be used by displaying the charts. + + \ No newline at end of file diff --git a/android-libraries/achartengine/src/org/achartengine/package.html b/android-libraries/achartengine/src/org/achartengine/package.html new file mode 100644 index 00000000..b26cf259 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/package.html @@ -0,0 +1,6 @@ + +AChartEngine + +The main classes, including the ChartFactory, GraphicalActivity and GraphicalView. + + \ No newline at end of file diff --git a/android-libraries/achartengine/src/org/achartengine/renderer/BasicStroke.java b/android-libraries/achartengine/src/org/achartengine/renderer/BasicStroke.java new file mode 100644 index 00000000..3ac93d54 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/renderer/BasicStroke.java @@ -0,0 +1,107 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.renderer; + +import java.io.Serializable; + +import android.graphics.Paint.Cap; +import android.graphics.Paint.Join; + +/** + * A descriptor for the stroke style. + */ +public class BasicStroke implements Serializable { + /** The solid line style. */ + public static final BasicStroke SOLID = new BasicStroke(Cap.BUTT, Join.MITER, 4, null, 0); + /** The dashed line style. */ + public static final BasicStroke DASHED = new BasicStroke(Cap.ROUND, Join.BEVEL, 10, new float[] { + 10, 10 }, 1); + /** The dot line style. */ + public static final BasicStroke DOTTED = new BasicStroke(Cap.ROUND, Join.BEVEL, 5, new float[] { + 2, 10 }, 1); + /** The stroke cap. */ + private Cap mCap; + /** The stroke join. */ + private Join mJoin; + /** The stroke miter. */ + private float mMiter; + /** The path effect intervals. */ + private float[] mIntervals; + /** The path effect phase. */ + private float mPhase; + + /** + * Build a new basic stroke style. + * + * @param cap the stroke cap + * @param join the stroke join + * @param miter the stroke miter + * @param intervals the path effect intervals + * @param phase the path effect phase + */ + public BasicStroke(Cap cap, Join join, float miter, float[] intervals, float phase) { + mCap = cap; + mJoin = join; + mMiter = miter; + mIntervals = intervals; + } + + /** + * Returns the stroke cap. + * + * @return the stroke cap + */ + public Cap getCap() { + return mCap; + } + + /** + * Returns the stroke join. + * + * @return the stroke join + */ + public Join getJoin() { + return mJoin; + } + + /** + * Returns the stroke miter. + * + * @return the stroke miter + */ + public float getMiter() { + return mMiter; + } + + /** + * Returns the path effect intervals. + * + * @return the path effect intervals + */ + public float[] getIntervals() { + return mIntervals; + } + + /** + * Returns the path effect phase. + * + * @return the path effect phase + */ + public float getPhase() { + return mPhase; + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/renderer/DefaultRenderer.java b/android-libraries/achartengine/src/org/achartengine/renderer/DefaultRenderer.java new file mode 100644 index 00000000..4dbb2549 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/renderer/DefaultRenderer.java @@ -0,0 +1,750 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.renderer; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import android.graphics.Color; +import android.graphics.Typeface; + +/** + * An abstract renderer to be extended by the multiple series classes. + */ +public class DefaultRenderer implements Serializable { + /** The chart title. */ + private String mChartTitle = ""; + /** The chart title text size. */ + private float mChartTitleTextSize = 15; + /** A no color constant. */ + public static final int NO_COLOR = 0; + /** The default background color. */ + public static final int BACKGROUND_COLOR = Color.BLACK; + /** The default color for text. */ + public static final int TEXT_COLOR = Color.LTGRAY; + /** A text font for regular text, like the chart labels. */ + private static final Typeface REGULAR_TEXT_FONT = Typeface + .create(Typeface.SERIF, Typeface.NORMAL); + /** The typeface name for the texts. */ + private String mTextTypefaceName = REGULAR_TEXT_FONT.toString(); + /** The typeface style for the texts. */ + private int mTextTypefaceStyle = Typeface.NORMAL; + /** The chart background color. */ + private int mBackgroundColor; + /** If the background color is applied. */ + private boolean mApplyBackgroundColor; + /** If the axes are visible. */ + private boolean mShowAxes = true; + /** The axes color. */ + private int mAxesColor = TEXT_COLOR; + /** If the labels are visible. */ + private boolean mShowLabels = true; + /** The labels color. */ + private int mLabelsColor = TEXT_COLOR; + /** The labels text size. */ + private float mLabelsTextSize = 10; + /** If the legend is visible. */ + private boolean mShowLegend = true; + /** The legend text size. */ + private float mLegendTextSize = 12; + /** If the legend should size to fit. */ + private boolean mFitLegend = false; + /** If the X axis grid should be displayed. */ + private boolean mShowGridX = false; + /** If the Y axis grid should be displayed. */ + private boolean mShowGridY = false; + /** If the custom text grid should be displayed. */ + private boolean mShowCustomTextGrid = false; + /** The simple renderers that are included in this multiple series renderer. */ + private List mRenderers = new ArrayList(); + /** The antialiasing flag. */ + private boolean mAntialiasing = true; + /** The legend height. */ + private int mLegendHeight = 0; + /** The margins size. */ + private int[] mMargins = new int[] { 20, 30, 10, 20 }; + /** A value to be used for scaling the chart. */ + private float mScale = 1; + /** A flag for enabling the pan. */ + private boolean mPanEnabled = true; + /** A flag for enabling the zoom. */ + private boolean mZoomEnabled = true; + /** A flag for enabling the visibility of the zoom buttons. */ + private boolean mZoomButtonsVisible = false; + /** The zoom rate. */ + private float mZoomRate = 1.5f; + /** A flag for enabling the external zoom. */ + private boolean mExternalZoomEnabled = false; + /** The original chart scale. */ + private float mOriginalScale = mScale; + /** A flag for enabling the click on elements. */ + private boolean mClickEnabled = false; + /** The selectable radius around a clickable point. */ + private int selectableBuffer = 15; + /** If the chart should display the values (available for pie chart). */ + private boolean mDisplayValues; + + /** + * A flag to be set if the chart is inside a scroll and doesn't need to shrink + * when not enough space. + */ + private boolean mInScroll; + /** The start angle for circular charts such as pie, doughnut, etc. */ + private float mStartAngle = 0; + + /** + * Returns the chart title. + * + * @return the chart title + */ + public String getChartTitle() { + return mChartTitle; + } + + /** + * Sets the chart title. + * + * @param title the chart title + */ + public void setChartTitle(String title) { + mChartTitle = title; + } + + /** + * Returns the chart title text size. + * + * @return the chart title text size + */ + public float getChartTitleTextSize() { + return mChartTitleTextSize; + } + + /** + * Sets the chart title text size. + * + * @param textSize the chart title text size + */ + public void setChartTitleTextSize(float textSize) { + mChartTitleTextSize = textSize; + } + + /** + * Adds a simple renderer to the multiple renderer. + * + * @param renderer the renderer to be added + */ + public void addSeriesRenderer(SimpleSeriesRenderer renderer) { + mRenderers.add(renderer); + } + + /** + * Adds a simple renderer to the multiple renderer. + * + * @param index the index in the renderers list + * @param renderer the renderer to be added + */ + public void addSeriesRenderer(int index, SimpleSeriesRenderer renderer) { + mRenderers.add(index, renderer); + } + + /** + * Removes a simple renderer from the multiple renderer. + * + * @param renderer the renderer to be removed + */ + public void removeSeriesRenderer(SimpleSeriesRenderer renderer) { + mRenderers.remove(renderer); + } + + /** + * Removes all renderers from the multiple renderer. + */ + public void removeAllRenderers() { + mRenderers.clear(); + } + + /** + * Returns the simple renderer from the multiple renderer list. + * + * @param index the index in the simple renderers list + * @return the simple renderer at the specified index + */ + public SimpleSeriesRenderer getSeriesRendererAt(int index) { + return mRenderers.get(index); + } + + /** + * Returns the simple renderers count in the multiple renderer list. + * + * @return the simple renderers count + */ + public int getSeriesRendererCount() { + return mRenderers.size(); + } + + /** + * Returns an array of the simple renderers in the multiple renderer list. + * + * @return the simple renderers array + */ + public SimpleSeriesRenderer[] getSeriesRenderers() { + return mRenderers.toArray(new SimpleSeriesRenderer[0]); + } + + /** + * Returns the background color. + * + * @return the background color + */ + public int getBackgroundColor() { + return mBackgroundColor; + } + + /** + * Sets the background color. + * + * @param color the background color + */ + public void setBackgroundColor(int color) { + mBackgroundColor = color; + } + + /** + * Returns if the background color should be applied. + * + * @return the apply flag for the background color. + */ + public boolean isApplyBackgroundColor() { + return mApplyBackgroundColor; + } + + /** + * Sets if the background color should be applied. + * + * @param apply the apply flag for the background color + */ + public void setApplyBackgroundColor(boolean apply) { + mApplyBackgroundColor = apply; + } + + /** + * Returns the axes color. + * + * @return the axes color + */ + public int getAxesColor() { + return mAxesColor; + } + + /** + * Sets the axes color. + * + * @param color the axes color + */ + public void setAxesColor(int color) { + mAxesColor = color; + } + + /** + * Returns the labels color. + * + * @return the labels color + */ + public int getLabelsColor() { + return mLabelsColor; + } + + /** + * Sets the labels color. + * + * @param color the labels color + */ + public void setLabelsColor(int color) { + mLabelsColor = color; + } + + /** + * Returns the labels text size. + * + * @return the labels text size + */ + public float getLabelsTextSize() { + return mLabelsTextSize; + } + + /** + * Sets the labels text size. + * + * @param textSize the labels text size + */ + public void setLabelsTextSize(float textSize) { + mLabelsTextSize = textSize; + } + + /** + * Returns if the axes should be visible. + * + * @return the visibility flag for the axes + */ + public boolean isShowAxes() { + return mShowAxes; + } + + /** + * Sets if the axes should be visible. + * + * @param showAxes the visibility flag for the axes + */ + public void setShowAxes(boolean showAxes) { + mShowAxes = showAxes; + } + + /** + * Returns if the labels should be visible. + * + * @return the visibility flag for the labels + */ + public boolean isShowLabels() { + return mShowLabels; + } + + /** + * Sets if the labels should be visible. + * + * @param showLabels the visibility flag for the labels + */ + public void setShowLabels(boolean showLabels) { + mShowLabels = showLabels; + } + + /** + * Returns if the X axis grid should be visible. + * + * @return the visibility flag for the X axis grid + */ + public boolean isShowGridX() { + return mShowGridX; + } + + /** + * Returns if the Y axis grid should be visible. + * + * @return the visibility flag for the Y axis grid + */ + public boolean isShowGridY() { + return mShowGridY; + } + + /** + * Sets if the X axis grid should be visible. + * + * @param showGrid the visibility flag for the X axis grid + */ + public void setShowGridX(boolean showGrid) { + mShowGridX = showGrid; + } + + /** + * Sets if the Y axis grid should be visible. + * + * @param showGrid the visibility flag for the Y axis grid + */ + public void setShowGridY(boolean showGrid) { + mShowGridY = showGrid; + } + + /** + * Sets if the grid should be visible. + * + * @param showGrid the visibility flag for the grid + */ + public void setShowGrid(boolean showGrid) { + setShowGridX(showGrid); + setShowGridY(showGrid); + } + + /** + * Returns if the grid should be visible for custom X or Y labels. + * + * @return the visibility flag for the custom text grid + */ + public boolean isShowCustomTextGrid() { + return mShowCustomTextGrid; + } + + /** + * Sets if the grid for custom X or Y labels should be visible. + * + * @param showGrid the visibility flag for the custom text grid + */ + public void setShowCustomTextGrid(boolean showGrid) { + mShowCustomTextGrid = showGrid; + } + + /** + * Returns if the legend should be visible. + * + * @return the visibility flag for the legend + */ + public boolean isShowLegend() { + return mShowLegend; + } + + /** + * Sets if the legend should be visible. + * + * @param showLegend the visibility flag for the legend + */ + public void setShowLegend(boolean showLegend) { + mShowLegend = showLegend; + } + + /** + * Returns if the legend should size to fit. + * + * @return the fit behavior + */ + public boolean isFitLegend() { + return mFitLegend; + } + + /** + * Sets if the legend should size to fit. + * + * @param fit the fit behavior + */ + public void setFitLegend(boolean fit) { + mFitLegend = fit; + } + + /** + * Returns the text typeface name. + * + * @return the text typeface name + */ + public String getTextTypefaceName() { + return mTextTypefaceName; + } + + /** + * Returns the text typeface style. + * + * @return the text typeface style + */ + public int getTextTypefaceStyle() { + return mTextTypefaceStyle; + } + + /** + * Returns the legend text size. + * + * @return the legend text size + */ + public float getLegendTextSize() { + return mLegendTextSize; + } + + /** + * Sets the legend text size. + * + * @param textSize the legend text size + */ + public void setLegendTextSize(float textSize) { + mLegendTextSize = textSize; + } + + /** + * Sets the text typeface name and style. + * + * @param typefaceName the text typeface name + * @param style the text typeface style + */ + public void setTextTypeface(String typefaceName, int style) { + mTextTypefaceName = typefaceName; + mTextTypefaceStyle = style; + } + + /** + * Returns the antialiasing flag value. + * + * @return the antialiasing value + */ + public boolean isAntialiasing() { + return mAntialiasing; + } + + /** + * Sets the antialiasing value. + * + * @param antialiasing the antialiasing + */ + public void setAntialiasing(boolean antialiasing) { + mAntialiasing = antialiasing; + } + + /** + * Returns the value to be used for scaling the chart. + * + * @return the scale value + */ + public float getScale() { + return mScale; + } + + /** + * Returns the original value to be used for scaling the chart. + * + * @return the original scale value + */ + public float getOriginalScale() { + return mOriginalScale; + } + + /** + * Sets the value to be used for scaling the chart. It works on some charts + * like pie, doughnut, dial. + * + * @param scale the scale value + */ + public void setScale(float scale) { + mScale = scale; + } + + /** + * Returns the enabled state of the zoom. + * + * @return if zoom is enabled + */ + public boolean isZoomEnabled() { + return mZoomEnabled; + } + + /** + * Sets the enabled state of the zoom. + * + * @param enabled zoom enabled + */ + public void setZoomEnabled(boolean enabled) { + mZoomEnabled = enabled; + } + + /** + * Returns the visible state of the zoom buttons. + * + * @return if zoom buttons are visible + */ + public boolean isZoomButtonsVisible() { + return mZoomButtonsVisible; + } + + /** + * Sets the visible state of the zoom buttons. + * + * @param visible if the zoom buttons are visible + */ + public void setZoomButtonsVisible(boolean visible) { + mZoomButtonsVisible = visible; + } + + /** + * Returns the enabled state of the external (application implemented) zoom. + * + * @return if external zoom is enabled + */ + public boolean isExternalZoomEnabled() { + return mExternalZoomEnabled; + } + + /** + * Sets the enabled state of the external (application implemented) zoom. + * + * @param enabled external zoom enabled + */ + public void setExternalZoomEnabled(boolean enabled) { + mExternalZoomEnabled = enabled; + } + + /** + * Returns the zoom rate. + * + * @return the zoom rate + */ + public float getZoomRate() { + return mZoomRate; + } + + /** + * Returns the enabled state of the pan. + * + * @return if pan is enabled + */ + public boolean isPanEnabled() { + return mPanEnabled; + } + + /** + * Sets the enabled state of the pan. + * + * @param enabled pan enabled + */ + public void setPanEnabled(boolean enabled) { + mPanEnabled = enabled; + } + + /** + * Sets the zoom rate. + * + * @param rate the zoom rate + */ + public void setZoomRate(float rate) { + mZoomRate = rate; + } + + /** + * Returns the enabled state of the click. + * + * @return if click is enabled + */ + public boolean isClickEnabled() { + return mClickEnabled; + } + + /** + * Sets the enabled state of the click. + * + * @param enabled click enabled + */ + public void setClickEnabled(boolean enabled) { + mClickEnabled = enabled; + } + + /** + * Returns the selectable radius value around clickable points. + * + * @return the selectable radius + */ + public int getSelectableBuffer() { + return selectableBuffer; + } + + /** + * Sets the selectable radius value around clickable points. + * + * @param buffer the selectable radius + */ + public void setSelectableBuffer(int buffer) { + selectableBuffer = buffer; + } + + /** + * Returns the legend height. + * + * @return the legend height + */ + public int getLegendHeight() { + return mLegendHeight; + } + + /** + * Sets the legend height, in pixels. + * + * @param height the legend height + */ + public void setLegendHeight(int height) { + mLegendHeight = height; + } + + /** + * Returns the margin sizes. An array containing the margins in this order: + * top, left, bottom, right + * + * @return the margin sizes + */ + public int[] getMargins() { + return mMargins; + } + + /** + * Sets the margins, in pixels. + * + * @param margins an array containing the margin size values, in this order: + * top, left, bottom, right + */ + public void setMargins(int[] margins) { + mMargins = margins; + } + + /** + * Returns if the chart is inside a scroll view and doesn't need to shrink. + * + * @return if it is inside a scroll view + */ + public boolean isInScroll() { + return mInScroll; + } + + /** + * To be set if the chart is inside a scroll view and doesn't need to shrink + * when not enough space. + * + * @param inScroll if it is inside a scroll view + */ + public void setInScroll(boolean inScroll) { + mInScroll = inScroll; + } + + /** + * Returns the start angle for circular charts such as pie, doughnut. An angle + * of 0 degrees correspond to the geometric angle of 0 degrees (3 o'clock on a + * watch.) + * + * @return the start angle in degrees + */ + public float getStartAngle() { + return mStartAngle; + } + + /** + * Sets the start angle for circular charts such as pie, doughnut, etc. An + * angle of 0 degrees correspond to the geometric angle of 0 degrees (3 + * o'clock on a watch.) + * + * @param startAngle the start angle in degrees + */ + public void setStartAngle(float startAngle) { + mStartAngle = startAngle; + } + + /** + * Returns if the values should be displayed as text. + * + * @return if the values should be displayed as text + */ + public boolean isDisplayValues() { + return mDisplayValues; + } + + /** + * Sets if the values should be displayed as text (supported by pie chart). + * + * @param display if the values should be displayed as text + */ + public void setDisplayValues(boolean display) { + mDisplayValues = display; + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/renderer/DialRenderer.java b/android-libraries/achartengine/src/org/achartengine/renderer/DialRenderer.java new file mode 100644 index 00000000..1ed84619 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/renderer/DialRenderer.java @@ -0,0 +1,196 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.renderer; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.achartengine.util.MathHelper; + +/** + * Dial chart renderer. + */ +public class DialRenderer extends DefaultRenderer { + /** The start angle in the dial range. */ + private double mAngleMin = 330; + /** The end angle in the dial range. */ + private double mAngleMax = 30; + /** The start value in dial range. */ + private double mMinValue = MathHelper.NULL_VALUE; + /** The end value in dial range. */ + private double mMaxValue = -MathHelper.NULL_VALUE; + /** The spacing for the minor ticks. */ + private double mMinorTickSpacing = MathHelper.NULL_VALUE; + /** The spacing for the major ticks. */ + private double mMajorTickSpacing = MathHelper.NULL_VALUE; + /** An array of the renderers types (default is NEEDLE). */ + private List mVisualTypes = new ArrayList(); + + public enum Type { + NEEDLE, ARROW; + } + + /** + * Returns the start angle value of the dial. + * + * @return the angle start value + */ + public double getAngleMin() { + return mAngleMin; + } + + /** + * Sets the start angle value of the dial. + * + * @param min the dial angle start value + */ + public void setAngleMin(double min) { + mAngleMin = min; + } + + /** + * Returns the end angle value of the dial. + * + * @return the angle end value + */ + public double getAngleMax() { + return mAngleMax; + } + + /** + * Sets the end angle value of the dial. + * + * @param max the dial angle end value + */ + public void setAngleMax(double max) { + mAngleMax = max; + } + + /** + * Returns the start value to be rendered on the dial. + * + * @return the start value on dial + */ + public double getMinValue() { + return mMinValue; + } + + /** + * Sets the start value to be rendered on the dial. + * + * @param min the start value on the dial + */ + public void setMinValue(double min) { + mMinValue = min; + } + + /** + * Returns if the minimum dial value was set. + * + * @return the minimum dial value was set or not + */ + public boolean isMinValueSet() { + return mMinValue != MathHelper.NULL_VALUE; + } + + /** + * Returns the end value to be rendered on the dial. + * + * @return the end value on the dial + */ + public double getMaxValue() { + return mMaxValue; + } + + /** + * Sets the end value to be rendered on the dial. + * + * @param max the end value on the dial + */ + public void setMaxValue(double max) { + mMaxValue = max; + } + + /** + * Returns if the maximum dial value was set. + * + * @return the maximum dial was set or not + */ + public boolean isMaxValueSet() { + return mMaxValue != -MathHelper.NULL_VALUE; + } + + /** + * Returns the minor ticks spacing. + * + * @return the minor ticks spacing + */ + public double getMinorTicksSpacing() { + return mMinorTickSpacing; + } + + /** + * Sets the minor ticks spacing. + * + * @param spacing the minor ticks spacing + */ + public void setMinorTicksSpacing(double spacing) { + mMinorTickSpacing = spacing; + } + + /** + * Returns the major ticks spacing. + * + * @return the major ticks spacing + */ + public double getMajorTicksSpacing() { + return mMajorTickSpacing; + } + + /** + * Sets the major ticks spacing. + * + * @param spacing the major ticks spacing + */ + public void setMajorTicksSpacing(double spacing) { + mMajorTickSpacing = spacing; + } + + /** + * Returns the visual type at the specified index. + * + * @param index the index + * @return the visual type + */ + public Type getVisualTypeForIndex(int index) { + if (index < mVisualTypes.size()) { + return mVisualTypes.get(index); + } + return Type.NEEDLE; + } + + /** + * Sets the visual types. + * + * @param types the visual types + */ + public void setVisualTypes(Type[] types) { + mVisualTypes.clear(); + mVisualTypes.addAll(Arrays.asList(types)); + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/renderer/SimpleSeriesRenderer.java b/android-libraries/achartengine/src/org/achartengine/renderer/SimpleSeriesRenderer.java new file mode 100644 index 00000000..0763fc58 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/renderer/SimpleSeriesRenderer.java @@ -0,0 +1,235 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.renderer; + +import java.io.Serializable; + +import android.graphics.Color; +import android.graphics.Paint.Align; + +/** + * A simple series renderer. + */ +public class SimpleSeriesRenderer implements Serializable { + /** The series color. */ + private int mColor = Color.BLUE; + /** If the values should be displayed above the chart points. */ + private boolean mDisplayChartValues; + /** The chart values text size. */ + private float mChartValuesTextSize = 10; + /** The chart values text alignment. */ + private Align mChartValuesTextAlign = Align.CENTER; + /** The chart values spacing from the data point. */ + private float mChartValuesSpacing = 5f; + /** The stroke style. */ + private BasicStroke mStroke; + /** If gradient is enabled. */ + private boolean mGradientEnabled = false; + /** The gradient start value. */ + private double mGradientStartValue; + /** The gradient start color. */ + private int mGradientStartColor; + /** The gradient stop value. */ + private double mGradientStopValue; + /** The gradient stop color. */ + private int mGradientStopColor; + + /** + * Returns the series color. + * + * @return the series color + */ + public int getColor() { + return mColor; + } + + /** + * Sets the series color. + * + * @param color the series color + */ + public void setColor(int color) { + mColor = color; + } + + /** + * Returns if the chart point values should be displayed as text. + * + * @return if the chart point values should be displayed as text + */ + public boolean isDisplayChartValues() { + return mDisplayChartValues; + } + + /** + * Sets if the chart point values should be displayed as text. + * + * @param display if the chart point values should be displayed as text + */ + public void setDisplayChartValues(boolean display) { + mDisplayChartValues = display; + } + + /** + * Returns the chart values text size. + * + * @return the chart values text size + */ + public float getChartValuesTextSize() { + return mChartValuesTextSize; + } + + /** + * Sets the chart values text size. + * + * @param textSize the chart values text size + */ + public void setChartValuesTextSize(float textSize) { + mChartValuesTextSize = textSize; + } + + /** + * Returns the chart values text align. + * + * @return the chart values text align + */ + public Align getChartValuesTextAlign() { + return mChartValuesTextAlign; + } + + /** + * Sets the chart values text align. + * + * @param align the chart values text align + */ + public void setChartValuesTextAlign(Align align) { + mChartValuesTextAlign = align; + } + + /** + * Returns the chart values spacing from the data point. + * + * @return the chart values spacing + */ + public float getChartValuesSpacing() { + return mChartValuesSpacing; + } + + /** + * Sets the chart values spacing from the data point. + * + * @param spacing the chart values spacing (in pixels) from the chart data + * point + */ + public void setChartValuesSpacing(float spacing) { + mChartValuesSpacing = spacing; + } + + /** + * Returns the stroke style. + * + * @return the stroke style + */ + public BasicStroke getStroke() { + return mStroke; + } + + /** + * Sets the stroke style. + * + * @param stroke the stroke style + */ + public void setStroke(BasicStroke stroke) { + mStroke = stroke; + } + + /** + * Returns the gradient is enabled value. + * + * @return the gradient enabled + */ + public boolean isGradientEnabled() { + return mGradientEnabled; + } + + /** + * Sets the gradient enabled value. + * + * @param enabled the gradient enabled + */ + public void setGradientEnabled(boolean enabled) { + mGradientEnabled = enabled; + } + + /** + * Returns the gradient start value. + * + * @return the gradient start value + */ + public double getGradientStartValue() { + return mGradientStartValue; + } + + /** + * Returns the gradient start color. + * + * @return the gradient start color + */ + public int getGradientStartColor() { + return mGradientStartColor; + } + + /** + * Sets the gradient start value and color. + * + * @param start the gradient start value + * @param color the gradient start color + */ + public void setGradientStart(double start, int color) { + mGradientStartValue = start; + mGradientStartColor = color; + } + + /** + * Returns the gradient stop value. + * + * @return the gradient stop value + */ + public double getGradientStopValue() { + return mGradientStopValue; + } + + /** + * Returns the gradient stop color. + * + * @return the gradient stop color + */ + public int getGradientStopColor() { + return mGradientStopColor; + } + + /** + * Sets the gradient stop value and color. + * + * @param start the gradient stop value + * @param color the gradient stop color + */ + public void setGradientStop(double start, int color) { + mGradientStopValue = start; + mGradientStopColor = color; + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/renderer/XYMultipleSeriesRenderer.java b/android-libraries/achartengine/src/org/achartengine/renderer/XYMultipleSeriesRenderer.java new file mode 100644 index 00000000..64b34211 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/renderer/XYMultipleSeriesRenderer.java @@ -0,0 +1,1101 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.renderer; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.achartengine.util.MathHelper; + +import android.graphics.Color; +import android.graphics.Paint.Align; + +/** + * Multiple XY series renderer. + */ +public class XYMultipleSeriesRenderer extends DefaultRenderer { + /** The X axis title. */ + private String mXTitle = ""; + /** The Y axis title. */ + private String[] mYTitle; + /** The axis title text size. */ + private float mAxisTitleTextSize = 12; + /** The start value in the X axis range. */ + private double[] mMinX; + /** The end value in the X axis range. */ + private double[] mMaxX; + /** The start value in the Y axis range. */ + private double[] mMinY; + /** The end value in the Y axis range. */ + private double[] mMaxY; + /** The approximative number of labels on the x axis. */ + private int mXLabels = 5; + /** The approximative number of labels on the y axis. */ + private int mYLabels = 5; + /** The current orientation of the chart. */ + private Orientation mOrientation = Orientation.HORIZONTAL; + /** The X axis text labels. */ + private Map mXTextLabels = new HashMap(); + /** The Y axis text labels. */ + private Map> mYTextLabels = new LinkedHashMap>(); + /** A flag for enabling or not the pan on the X axis. */ + private boolean mPanXEnabled = true; + /** A flag for enabling or not the pan on the Y axis. */ + private boolean mPanYEnabled = true; + /** A flag for enabling or not the zoom on the X axis. */ + private boolean mZoomXEnabled = true; + /** A flag for enabling or not the zoom on the Y axis . */ + private boolean mZoomYEnabled = true; + /** The spacing between bars, in bar charts. */ + private double mBarSpacing = 0; + /** The margins colors. */ + private int mMarginsColor = NO_COLOR; + /** The pan limits. */ + private double[] mPanLimits; + /** The zoom limits. */ + private double[] mZoomLimits; + /** The X axis labels rotation angle. */ + private float mXLabelsAngle; + /** The Y axis labels rotation angle. */ + private float mYLabelsAngle; + /** The initial axis range. */ + private Map initialRange = new LinkedHashMap(); + /** The point size for charts displaying points. */ + private float mPointSize = 3; + /** The grid color. */ + private int mGridColor = Color.argb(75, 200, 200, 200); + /** The number of scales. */ + private int scalesCount; + /** The X axis labels alignment. */ + private Align xLabelsAlign = Align.CENTER; + /** The Y axis labels alignment. */ + private Align[] yLabelsAlign; + /** The Y axis alignment. */ + private Align[] yAxisAlign; + /** The X axis labels color. */ + private int mXLabelsColor = TEXT_COLOR; + /** The Y axis labels color. */ + private int[] mYLabelsColor = new int[] { TEXT_COLOR }; + /** + * If X axis value selection algorithm to be used. Only used by the time + * charts. + */ + private boolean mXRoundedLabels = true; + + /** + * An enum for the XY chart orientation of the X axis. + */ + public enum Orientation { + HORIZONTAL(0), VERTICAL(90); + /** The rotate angle. */ + private int mAngle = 0; + + private Orientation(int angle) { + mAngle = angle; + } + + /** + * Return the orientation rotate angle. + * + * @return the orientaion rotate angle + */ + public int getAngle() { + return mAngle; + } + } + + public XYMultipleSeriesRenderer() { + this(1); + } + + public XYMultipleSeriesRenderer(int scaleNumber) { + scalesCount = scaleNumber; + initAxesRange(scaleNumber); + } + + public void initAxesRange(int scales) { + mYTitle = new String[scales]; + yLabelsAlign = new Align[scales]; + yAxisAlign = new Align[scales]; + mYLabelsColor = new int[scales]; + mMinX = new double[scales]; + mMaxX = new double[scales]; + mMinY = new double[scales]; + mMaxY = new double[scales]; + for (int i = 0; i < scales; i++) { + mYLabelsColor[i] = TEXT_COLOR; + initAxesRangeForScale(i); + } + } + + public void initAxesRangeForScale(int i) { + mMinX[i] = MathHelper.NULL_VALUE; + mMaxX[i] = -MathHelper.NULL_VALUE; + mMinY[i] = MathHelper.NULL_VALUE; + mMaxY[i] = -MathHelper.NULL_VALUE; + double[] range = new double[] { mMinX[i], mMaxX[i], mMinY[i], mMaxY[i] }; + initialRange.put(i, range); + mYTitle[i] = ""; + mYTextLabels.put(i, new HashMap()); + yLabelsAlign[i] = Align.CENTER; + yAxisAlign[i] = Align.LEFT; + } + + /** + * Returns the current orientation of the chart X axis. + * + * @return the chart orientation + */ + public Orientation getOrientation() { + return mOrientation; + } + + /** + * Sets the current orientation of the chart X axis. + * + * @param orientation the chart orientation + */ + public void setOrientation(Orientation orientation) { + mOrientation = orientation; + } + + /** + * Returns the title for the X axis. + * + * @return the X axis title + */ + public String getXTitle() { + return mXTitle; + } + + /** + * Sets the title for the X axis. + * + * @param title the X axis title + */ + public void setXTitle(String title) { + mXTitle = title; + } + + /** + * Returns the title for the Y axis. + * + * @return the Y axis title + */ + public String getYTitle() { + return getYTitle(0); + } + + /** + * Returns the title for the Y axis. + * + * @param scale the renderer scale + * @return the Y axis title + */ + public String getYTitle(int scale) { + return mYTitle[scale]; + } + + /** + * Sets the title for the Y axis. + * + * @param title the Y axis title + */ + public void setYTitle(String title) { + setYTitle(title, 0); + } + + /** + * Sets the title for the Y axis. + * + * @param title the Y axis title + * @param scale the renderer scale + */ + public void setYTitle(String title, int scale) { + mYTitle[scale] = title; + } + + /** + * Returns the axis title text size. + * + * @return the axis title text size + */ + public float getAxisTitleTextSize() { + return mAxisTitleTextSize; + } + + /** + * Sets the axis title text size. + * + * @param textSize the chart axis text size + */ + public void setAxisTitleTextSize(float textSize) { + mAxisTitleTextSize = textSize; + } + + /** + * Returns the start value of the X axis range. + * + * @return the X axis range start value + */ + public double getXAxisMin() { + return getXAxisMin(0); + } + + /** + * Sets the start value of the X axis range. + * + * @param min the X axis range start value + */ + public void setXAxisMin(double min) { + setXAxisMin(min, 0); + } + + /** + * Returns if the minimum X value was set. + * + * @return the minX was set or not + */ + public boolean isMinXSet() { + return isMinXSet(0); + } + + /** + * Returns the end value of the X axis range. + * + * @return the X axis range end value + */ + public double getXAxisMax() { + return getXAxisMax(0); + } + + /** + * Sets the end value of the X axis range. + * + * @param max the X axis range end value + */ + public void setXAxisMax(double max) { + setXAxisMax(max, 0); + } + + /** + * Returns if the maximum X value was set. + * + * @return the maxX was set or not + */ + public boolean isMaxXSet() { + return isMaxXSet(0); + } + + /** + * Returns the start value of the Y axis range. + * + * @return the Y axis range end value + */ + public double getYAxisMin() { + return getYAxisMin(0); + } + + /** + * Sets the start value of the Y axis range. + * + * @param min the Y axis range start value + */ + public void setYAxisMin(double min) { + setYAxisMin(min, 0); + } + + /** + * Returns if the minimum Y value was set. + * + * @return the minY was set or not + */ + public boolean isMinYSet() { + return isMinYSet(0); + } + + /** + * Returns the end value of the Y axis range. + * + * @return the Y axis range end value + */ + public double getYAxisMax() { + return getYAxisMax(0); + } + + /** + * Sets the end value of the Y axis range. + * + * @param max the Y axis range end value + */ + public void setYAxisMax(double max) { + setYAxisMax(max, 0); + } + + /** + * Returns if the maximum Y value was set. + * + * @return the maxY was set or not + */ + public boolean isMaxYSet() { + return isMaxYSet(0); + } + + /** + * Returns the start value of the X axis range. + * + * @param scale the renderer scale + * @return the X axis range start value + */ + public double getXAxisMin(int scale) { + return mMinX[scale]; + } + + /** + * Sets the start value of the X axis range. + * + * @param min the X axis range start value + * @param scale the renderer scale + */ + public void setXAxisMin(double min, int scale) { + if (!isMinXSet(scale)) { + initialRange.get(scale)[0] = min; + } + mMinX[scale] = min; + } + + /** + * Returns if the minimum X value was set. + * + * @param scale the renderer scale + * @return the minX was set or not + */ + public boolean isMinXSet(int scale) { + return mMinX[scale] != MathHelper.NULL_VALUE; + } + + /** + * Returns the end value of the X axis range. + * + * @param scale the renderer scale + * @return the X axis range end value + */ + public double getXAxisMax(int scale) { + return mMaxX[scale]; + } + + /** + * Sets the end value of the X axis range. + * + * @param max the X axis range end value + * @param scale the renderer scale + */ + public void setXAxisMax(double max, int scale) { + if (!isMaxXSet(scale)) { + initialRange.get(scale)[1] = max; + } + mMaxX[scale] = max; + } + + /** + * Returns if the maximum X value was set. + * + * @param scale the renderer scale + * @return the maxX was set or not + */ + public boolean isMaxXSet(int scale) { + return mMaxX[scale] != -MathHelper.NULL_VALUE; + } + + /** + * Returns the start value of the Y axis range. + * + * @param scale the renderer scale + * @return the Y axis range end value + */ + public double getYAxisMin(int scale) { + return mMinY[scale]; + } + + /** + * Sets the start value of the Y axis range. + * + * @param min the Y axis range start value + * @param scale the renderer scale + */ + public void setYAxisMin(double min, int scale) { + if (!isMinYSet(scale)) { + initialRange.get(scale)[2] = min; + } + mMinY[scale] = min; + } + + /** + * Returns if the minimum Y value was set. + * + * @param scale the renderer scale + * @return the minY was set or not + */ + public boolean isMinYSet(int scale) { + return mMinY[scale] != MathHelper.NULL_VALUE; + } + + /** + * Returns the end value of the Y axis range. + * + * @param scale the renderer scale + * @return the Y axis range end value + */ + public double getYAxisMax(int scale) { + return mMaxY[scale]; + } + + /** + * Sets the end value of the Y axis range. + * + * @param max the Y axis range end value + * @param scale the renderer scale + */ + public void setYAxisMax(double max, int scale) { + if (!isMaxYSet(scale)) { + initialRange.get(scale)[3] = max; + } + mMaxY[scale] = max; + } + + /** + * Returns if the maximum Y value was set. + * + * @param scale the renderer scale + * @return the maxY was set or not + */ + public boolean isMaxYSet(int scale) { + return mMaxY[scale] != -MathHelper.NULL_VALUE; + } + + /** + * Returns the approximate number of labels for the X axis. + * + * @return the approximate number of labels for the X axis + */ + public int getXLabels() { + return mXLabels; + } + + /** + * Sets the approximate number of labels for the X axis. + * + * @param xLabels the approximate number of labels for the X axis + */ + public void setXLabels(int xLabels) { + mXLabels = xLabels; + } + + /** + * Adds a new text label for the specified X axis value. + * + * @param x the X axis value + * @param text the text label + * @deprecated use addXTextLabel instead + */ + public void addTextLabel(double x, String text) { + addXTextLabel(x, text); + } + + /** + * Adds a new text label for the specified X axis value. + * + * @param x the X axis value + * @param text the text label + */ + public void addXTextLabel(double x, String text) { + mXTextLabels.put(x, text); + } + + /** + * Returns the X axis text label at the specified X axis value. + * + * @param x the X axis value + * @return the X axis text label + */ + public String getXTextLabel(Double x) { + return mXTextLabels.get(x); + } + + /** + * Returns the X text label locations. + * + * @return the X text label locations + */ + public Double[] getXTextLabelLocations() { + return mXTextLabels.keySet().toArray(new Double[0]); + } + + /** + * Clears the existing text labels. + * + * @deprecated use clearXTextLabels instead + */ + public void clearTextLabels() { + clearXTextLabels(); + } + + /** + * Clears the existing text labels on the X axis. + */ + public void clearXTextLabels() { + mXTextLabels.clear(); + } + + /** + * If X axis labels should be rounded. + * + * @return if rounded time values to be used + */ + public boolean isXRoundedLabels() { + return mXRoundedLabels; + } + + /** + * Sets if X axis rounded time values to be used. + * + * @param rounded rounded values to be used + */ + public void setXRoundedLabels(boolean rounded) { + mXRoundedLabels = rounded; + } + + /** + * Adds a new text label for the specified Y axis value. + * + * @param y the Y axis value + * @param text the text label + */ + public void addYTextLabel(double y, String text) { + addYTextLabel(y, text, 0); + } + + /** + * Adds a new text label for the specified Y axis value. + * + * @param y the Y axis value + * @param text the text label + * @param scale the renderer scale + */ + public void addYTextLabel(double y, String text, int scale) { + mYTextLabels.get(scale).put(y, text); + } + + /** + * Returns the Y axis text label at the specified Y axis value. + * + * @param y the Y axis value + * @return the Y axis text label + */ + public String getYTextLabel(Double y) { + return getYTextLabel(y, 0); + } + + /** + * Returns the Y axis text label at the specified Y axis value. + * + * @param y the Y axis value + * @param scale the renderer scale + * @return the Y axis text label + */ + public String getYTextLabel(Double y, int scale) { + return mYTextLabels.get(scale).get(y); + } + + /** + * Returns the Y text label locations. + * + * @return the Y text label locations + */ + public Double[] getYTextLabelLocations() { + return getYTextLabelLocations(0); + } + + /** + * Returns the Y text label locations. + * + * @param scale the renderer scale + * @return the Y text label locations + */ + public Double[] getYTextLabelLocations(int scale) { + return mYTextLabels.get(scale).keySet().toArray(new Double[0]); + } + + /** + * Clears the existing text labels on the Y axis. + */ + public void clearYTextLabels() { + clearYTextLabels(0); + } + + /** + * Clears the existing text labels on the Y axis. + * + * @param scale the renderer scale + */ + public void clearYTextLabels(int scale) { + mYTextLabels.get(scale).clear(); + } + + /** + * Returns the approximate number of labels for the Y axis. + * + * @return the approximate number of labels for the Y axis + */ + public int getYLabels() { + return mYLabels; + } + + /** + * Sets the approximate number of labels for the Y axis. + * + * @param yLabels the approximate number of labels for the Y axis + */ + public void setYLabels(int yLabels) { + mYLabels = yLabels; + } + + /** + * Sets if the chart point values should be displayed as text. + * + * @param display if the chart point values should be displayed as text + * @deprecated use SimpleSeriesRenderer.setDisplayChartValues() instead + */ + public void setDisplayChartValues(boolean display) { + SimpleSeriesRenderer[] renderers = getSeriesRenderers(); + for (SimpleSeriesRenderer renderer : renderers) { + renderer.setDisplayChartValues(display); + } + } + + /** + * Sets the chart values text size. + * + * @param textSize the chart values text size + * @deprecated use SimpleSeriesRenderer.setChartValuesTextSize() instead + */ + public void setChartValuesTextSize(float textSize) { + SimpleSeriesRenderer[] renderers = getSeriesRenderers(); + for (SimpleSeriesRenderer renderer : renderers) { + renderer.setChartValuesTextSize(textSize); + } + } + + /** + * Returns the enabled state of the pan on at least one axis. + * + * @return if pan is enabled + */ + public boolean isPanEnabled() { + return isPanXEnabled() || isPanYEnabled(); + } + + /** + * Returns the enabled state of the pan on X axis. + * + * @return if pan is enabled on X axis + */ + public boolean isPanXEnabled() { + return mPanXEnabled; + } + + /** + * Returns the enabled state of the pan on Y axis. + * + * @return if pan is enabled on Y axis + */ + public boolean isPanYEnabled() { + return mPanYEnabled; + } + + /** + * Sets the enabled state of the pan. + * + * @param enabledX pan enabled on X axis + * @param enabledY pan enabled on Y axis + */ + public void setPanEnabled(boolean enabledX, boolean enabledY) { + mPanXEnabled = enabledX; + mPanYEnabled = enabledY; + } + + /** + * Returns the enabled state of the zoom on at least one axis. + * + * @return if zoom is enabled + */ + public boolean isZoomEnabled() { + return isZoomXEnabled() || isZoomYEnabled(); + } + + /** + * Returns the enabled state of the zoom on X axis. + * + * @return if zoom is enabled on X axis + */ + public boolean isZoomXEnabled() { + return mZoomXEnabled; + } + + /** + * Returns the enabled state of the zoom on Y axis. + * + * @return if zoom is enabled on Y axis + */ + public boolean isZoomYEnabled() { + return mZoomYEnabled; + } + + /** + * Sets the enabled state of the zoom. + * + * @param enabledX zoom enabled on X axis + * @param enabledY zoom enabled on Y axis + */ + public void setZoomEnabled(boolean enabledX, boolean enabledY) { + mZoomXEnabled = enabledX; + mZoomYEnabled = enabledY; + } + + /** + * Returns the spacing between bars, in bar charts. + * + * @return the spacing between bars + * @deprecated use getBarSpacing instead + */ + public double getBarsSpacing() { + return getBarSpacing(); + } + + /** + * Returns the spacing between bars, in bar charts. + * + * @return the spacing between bars + */ + public double getBarSpacing() { + return mBarSpacing; + } + + /** + * Sets the spacing between bars, in bar charts. Only available for bar + * charts. This is a coefficient of the bar width. For instance, if you want + * the spacing to be a half of the bar width, set this value to 0.5. + * + * @param spacing the spacing between bars coefficient + */ + public void setBarSpacing(double spacing) { + mBarSpacing = spacing; + } + + /** + * Returns the margins color. + * + * @return the margins color + */ + public int getMarginsColor() { + return mMarginsColor; + } + + /** + * Sets the color of the margins. + * + * @param color the margins color + */ + public void setMarginsColor(int color) { + mMarginsColor = color; + } + + /** + * Returns the grid color. + * + * @return the grid color + */ + public int getGridColor() { + return mGridColor; + } + + /** + * Sets the color of the grid. + * + * @param color the grid color + */ + public void setGridColor(int color) { + mGridColor = color; + } + + /** + * Returns the pan limits. + * + * @return the pan limits + */ + public double[] getPanLimits() { + return mPanLimits; + } + + /** + * Sets the pan limits as an array of 4 values. Setting it to null or a + * different size array will disable the panning limitation. Values: + * [panMinimumX, panMaximumX, panMinimumY, panMaximumY] + * + * @param panLimits the pan limits + */ + public void setPanLimits(double[] panLimits) { + mPanLimits = panLimits; + } + + /** + * Returns the zoom limits. + * + * @return the zoom limits + */ + public double[] getZoomLimits() { + return mZoomLimits; + } + + /** + * Sets the zoom limits as an array of 4 values. Setting it to null or a + * different size array will disable the zooming limitation. Values: + * [zoomMinimumX, zoomMaximumX, zoomMinimumY, zoomMaximumY] + * + * @param zoomLimits the zoom limits + */ + public void setZoomLimits(double[] zoomLimits) { + mZoomLimits = zoomLimits; + } + + /** + * Returns the rotation angle of labels for the X axis. + * + * @return the rotation angle of labels for the X axis + */ + public float getXLabelsAngle() { + return mXLabelsAngle; + } + + /** + * Sets the rotation angle (in degrees) of labels for the X axis. + * + * @param angle the rotation angle of labels for the X axis + */ + public void setXLabelsAngle(float angle) { + mXLabelsAngle = angle; + } + + /** + * Returns the rotation angle of labels for the Y axis. + * + * @return the approximate number of labels for the Y axis + */ + public float getYLabelsAngle() { + return mYLabelsAngle; + } + + /** + * Sets the rotation angle (in degrees) of labels for the Y axis. + * + * @param angle the rotation angle of labels for the Y axis + */ + public void setYLabelsAngle(float angle) { + mYLabelsAngle = angle; + } + + /** + * Returns the size of the points, for charts displaying points. + * + * @return the point size + */ + public float getPointSize() { + return mPointSize; + } + + /** + * Sets the size of the points, for charts displaying points. + * + * @param size the point size + */ + public void setPointSize(float size) { + mPointSize = size; + } + + public void setRange(double[] range) { + setRange(range, 0); + } + + /** + * Sets the axes range values. + * + * @param range an array having the values in this order: minX, maxX, minY, + * maxY + * @param scale the renderer scale + */ + public void setRange(double[] range, int scale) { + setXAxisMin(range[0], scale); + setXAxisMax(range[1], scale); + setYAxisMin(range[2], scale); + setYAxisMax(range[3], scale); + } + + public boolean isInitialRangeSet() { + return isInitialRangeSet(0); + } + + /** + * Returns if the initial range is set. + * + * @param scale the renderer scale + * @return the initial range was set or not + */ + public boolean isInitialRangeSet(int scale) { + return initialRange.get(scale) != null; + } + + /** + * Returns the initial range. + * + * @return the initial range + */ + public double[] getInitialRange() { + return getInitialRange(0); + } + + /** + * Returns the initial range. + * + * @param scale the renderer scale + * @return the initial range + */ + public double[] getInitialRange(int scale) { + return initialRange.get(scale); + } + + /** + * Sets the axes initial range values. This will be used in the zoom fit tool. + * + * @param range an array having the values in this order: minX, maxX, minY, + * maxY + */ + public void setInitialRange(double[] range) { + setInitialRange(range, 0); + } + + /** + * Sets the axes initial range values. This will be used in the zoom fit tool. + * + * @param range an array having the values in this order: minX, maxX, minY, + * maxY + * @param scale the renderer scale + */ + public void setInitialRange(double[] range, int scale) { + initialRange.put(scale, range); + } + + /** + * Returns the X axis labels color. + * + * @return the X axis labels color + */ + public int getXLabelsColor() { + return mXLabelsColor; + } + + /** + * Returns the Y axis labels color. + * + * @return the Y axis labels color + */ + public int getYLabelsColor(int scale) { + return mYLabelsColor[scale]; + } + + /** + * Sets the X axis labels color. + * + * @param color the X axis labels color + */ + public void setXLabelsColor(int color) { + mXLabelsColor = color; + } + + /** + * Sets the Y axis labels color. + * + * @param scale the renderer scale + * @param color the Y axis labels color + */ + public void setYLabelsColor(int scale, int color) { + mYLabelsColor[scale] = color; + } + + /** + * Returns the X axis labels alignment. + * + * @return X labels alignment + */ + public Align getXLabelsAlign() { + return xLabelsAlign; + } + + /** + * Sets the X axis labels alignment. + * + * @param align the X labels alignment + */ + public void setXLabelsAlign(Align align) { + xLabelsAlign = align; + } + + /** + * Returns the Y axis labels alignment. + * + * @param scale the renderer scale + * @return Y labels alignment + */ + public Align getYLabelsAlign(int scale) { + return yLabelsAlign[scale]; + } + + public void setYLabelsAlign(Align align) { + setYLabelsAlign(align, 0); + } + + public Align getYAxisAlign(int scale) { + return yAxisAlign[scale]; + } + + public void setYAxisAlign(Align align, int scale) { + yAxisAlign[scale] = align; + } + + /** + * Sets the Y axis labels alignment. + * + * @param align the Y labels alignment + */ + public void setYLabelsAlign(Align align, int scale) { + yLabelsAlign[scale] = align; + } + + public int getScalesCount() { + return scalesCount; + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/renderer/XYSeriesRenderer.java b/android-libraries/achartengine/src/org/achartengine/renderer/XYSeriesRenderer.java new file mode 100644 index 00000000..42e4808e --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/renderer/XYSeriesRenderer.java @@ -0,0 +1,128 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.renderer; + +import org.achartengine.chart.PointStyle; + +import android.graphics.Color; + +/** + * A renderer for the XY type series. + */ +public class XYSeriesRenderer extends SimpleSeriesRenderer { + /** If the chart points should be filled. */ + private boolean mFillPoints = false; + /** If the chart should be filled below its line. */ + private boolean mFillBelowLine = false; + /** The fill below the chart line color. */ + private int mFillColor = Color.argb(125, 0, 0, 200); + /** The point style. */ + private PointStyle mPointStyle = PointStyle.POINT; + /** The chart line width. */ + private float mLineWidth = 1; + + /** + * Returns if the chart should be filled below the line. + * + * @return the fill below line status + */ + public boolean isFillBelowLine() { + return mFillBelowLine; + } + + /** + * Sets if the line chart should be filled below its line. Filling below the + * line transforms a line chart into an area chart. + * + * @param fill the fill below line flag value + */ + public void setFillBelowLine(boolean fill) { + mFillBelowLine = fill; + } + + /** + * Returns if the chart points should be filled. + * + * @return the points fill status + */ + public boolean isFillPoints() { + return mFillPoints; + } + + /** + * Sets if the chart points should be filled. + * + * @param fill the points fill flag value + */ + public void setFillPoints(boolean fill) { + mFillPoints = fill; + } + + /** + * Returns the fill below line color. + * + * @return the fill below line color + */ + public int getFillBelowLineColor() { + return mFillColor; + } + + /** + * Sets the fill below the line color. + * + * @param color the fill below line color + */ + public void setFillBelowLineColor(int color) { + mFillColor = color; + } + + /** + * Returns the point style. + * + * @return the point style + */ + public PointStyle getPointStyle() { + return mPointStyle; + } + + /** + * Sets the point style. + * + * @param style the point style + */ + public void setPointStyle(PointStyle style) { + mPointStyle = style; + } + + /** + * Returns the chart line width. + * + * @return the line width + */ + public float getLineWidth() { + return mLineWidth; + } + + /** + * Sets the chart line width. + * + * @param lineWidth the line width + */ + public void setLineWidth(float lineWidth) { + mLineWidth = lineWidth; + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/renderer/package.html b/android-libraries/achartengine/src/org/achartengine/renderer/package.html new file mode 100644 index 00000000..c9db0a45 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/renderer/package.html @@ -0,0 +1,6 @@ + +AChartEngine + +Provides renderer classes that keep the chart rendering / drawing styles. + + \ No newline at end of file diff --git a/android-libraries/achartengine/src/org/achartengine/tools/AbstractTool.java b/android-libraries/achartengine/src/org/achartengine/tools/AbstractTool.java new file mode 100644 index 00000000..99841ed7 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/tools/AbstractTool.java @@ -0,0 +1,111 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.tools; + +import org.achartengine.chart.AbstractChart; +import org.achartengine.chart.XYChart; +import org.achartengine.renderer.XYMultipleSeriesRenderer; + +/** + * Abstract class for being extended by graphical view tools. + */ +public abstract class AbstractTool { + /** The chart. */ + protected AbstractChart mChart; + /** The renderer. */ + protected XYMultipleSeriesRenderer mRenderer; + + /** + * Abstract tool constructor. + * + * @param chart the chart + */ + public AbstractTool(AbstractChart chart) { + mChart = chart; + if (chart instanceof XYChart) { + mRenderer = ((XYChart) chart).getRenderer(); + } + } + + /** + * Returns the current chart range. + * + * @param scale the scale + * @return the chart range + */ + public double[] getRange(int scale) { + double minX = mRenderer.getXAxisMin(scale); + double maxX = mRenderer.getXAxisMax(scale); + double minY = mRenderer.getYAxisMin(scale); + double maxY = mRenderer.getYAxisMax(scale); + return new double[] { minX, maxX, minY, maxY }; + } + + /** + * Sets the range to the calculated one, if not already set. + * + * @param range the range + * @param scale the scale + */ + public void checkRange(double[] range, int scale) { + if (mChart instanceof XYChart) { + double[] calcRange = ((XYChart) mChart).getCalcRange(scale); + if (calcRange != null) { + if (!mRenderer.isMinXSet(scale)) { + range[0] = calcRange[0]; + mRenderer.setXAxisMin(range[0], scale); + } + if (!mRenderer.isMaxXSet(scale)) { + range[1] = calcRange[1]; + mRenderer.setXAxisMax(range[1], scale); + } + if (!mRenderer.isMinYSet(scale)) { + range[2] = calcRange[2]; + mRenderer.setYAxisMin(range[2], scale); + } + if (!mRenderer.isMaxYSet(scale)) { + range[3] = calcRange[3]; + mRenderer.setYAxisMax(range[3], scale); + } + } + } + } + + /** + * Sets a new range on the X axis. + * + * @param min the minimum value + * @param max the maximum value + * @param scale the scale + */ + protected void setXRange(double min, double max, int scale) { + mRenderer.setXAxisMin(min, scale); + mRenderer.setXAxisMax(max, scale); + } + + /** + * Sets a new range on the Y axis. + * + * @param min the minimum value + * @param max the maximum value + * @param scale the scale + */ + protected void setYRange(double min, double max, int scale) { + mRenderer.setYAxisMin(min, scale); + mRenderer.setYAxisMax(max, scale); + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/tools/FitZoom.java b/android-libraries/achartengine/src/org/achartengine/tools/FitZoom.java new file mode 100644 index 00000000..92f67b84 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/tools/FitZoom.java @@ -0,0 +1,78 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.tools; + +import org.achartengine.chart.AbstractChart; +import org.achartengine.chart.RoundChart; +import org.achartengine.chart.XYChart; +import org.achartengine.model.XYSeries; +import org.achartengine.renderer.DefaultRenderer; +import org.achartengine.util.MathHelper; + +public class FitZoom extends AbstractTool { + /** + * Builds an instance of the fit zoom tool. + * + * @param chart the XY chart + */ + public FitZoom(AbstractChart chart) { + super(chart); + } + + /** + * Apply the tool. + */ + public void apply() { + if (mChart instanceof XYChart) { + if (((XYChart) mChart).getDataset() == null) { + return; + } + int scales = mRenderer.getScalesCount(); + if (mRenderer.isInitialRangeSet()) { + for (int i = 0; i < scales; i++) { + if (mRenderer.isInitialRangeSet(i)) { + mRenderer.setRange(mRenderer.getInitialRange(i), i); + } + } + } else { + XYSeries[] series = ((XYChart) mChart).getDataset().getSeries(); + double[] range = null; + int length = series.length; + if (length > 0) { + for (int i = 0; i < scales; i++) { + range = new double[] { MathHelper.NULL_VALUE, -MathHelper.NULL_VALUE, + MathHelper.NULL_VALUE, -MathHelper.NULL_VALUE }; + for (int j = 0; j < length; j++) { + if (i == series[j].getScaleNumber()) { + range[0] = Math.min(range[0], series[j].getMinX()); + range[1] = Math.max(range[1], series[j].getMaxX()); + range[2] = Math.min(range[2], series[j].getMinY()); + range[3] = Math.max(range[3], series[j].getMaxY()); + } + } + double marginX = Math.abs(range[1] - range[0]) / 40; + double marginY = Math.abs(range[3] - range[2]) / 40; + mRenderer.setRange(new double[] { range[0] - marginX, range[1] + marginX, + range[2] - marginY, range[3] + marginY }, i); + } + } + } + } else { + DefaultRenderer renderer = ((RoundChart) mChart).getRenderer(); + renderer.setScale(renderer.getOriginalScale()); + } + } +} diff --git a/android-libraries/achartengine/src/org/achartengine/tools/Pan.java b/android-libraries/achartengine/src/org/achartengine/tools/Pan.java new file mode 100644 index 00000000..2d4ea28e --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/tools/Pan.java @@ -0,0 +1,163 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.tools; + +import java.util.ArrayList; +import java.util.List; + +import org.achartengine.chart.AbstractChart; +import org.achartengine.chart.RoundChart; +import org.achartengine.chart.XYChart; + +/** + * The pan tool. + */ +public class Pan extends AbstractTool { + /** The pan listeners. */ + private List mPanListeners = new ArrayList(); + /** Pan limits reached on the X axis. */ + private boolean limitsReachedX = false; + /** Pan limits reached on the X axis. */ + private boolean limitsReachedY = false; + + /** + * Builds and instance of the pan tool. + * + * @param chart the XY chart + */ + public Pan(AbstractChart chart) { + super(chart); + } + + /** + * Apply the tool. + * + * @param oldX the previous location on X axis + * @param oldY the previous location on Y axis + * @param newX the current location on X axis + * @param newY the current location on the Y axis + */ + public void apply(float oldX, float oldY, float newX, float newY) { + boolean notLimitedUp = true; + boolean notLimitedBottom = true; + boolean notLimitedLeft = true; + boolean notLimitedRight = true; + if (mChart instanceof XYChart) { + int scales = mRenderer.getScalesCount(); + double[] limits = mRenderer.getPanLimits(); + boolean limited = limits != null && limits.length == 4; + XYChart chart = (XYChart) mChart; + for (int i = 0; i < scales; i++) { + double[] range = getRange(i); + double[] calcRange = chart.getCalcRange(i); + if (limitsReachedX + && limitsReachedY + && (range[0] == range[1] && calcRange[0] == calcRange[1] || range[2] == range[3] + && calcRange[2] == calcRange[3])) { + return; + } + checkRange(range, i); + + double[] realPoint = chart.toRealPoint(oldX, oldY, i); + double[] realPoint2 = chart.toRealPoint(newX, newY, i); + double deltaX = realPoint[0] - realPoint2[0]; + double deltaY = realPoint[1] - realPoint2[1]; + double ratio = getAxisRatio(range); + if (chart.isVertical(mRenderer)) { + double newDeltaX = -deltaY * ratio; + double newDeltaY = deltaX / ratio; + deltaX = newDeltaX; + deltaY = newDeltaY; + } + if (mRenderer.isPanXEnabled()) { + if (limits != null) { + if (notLimitedLeft) { + notLimitedLeft = limits[0] <= range[0] + deltaX; + } + if (notLimitedRight) { + notLimitedRight = limits[1] >= range[1] + deltaX; + } + } + if (!limited || (notLimitedLeft && notLimitedRight)) { + setXRange(range[0] + deltaX, range[1] + deltaX, i); + limitsReachedX = false; + } else { + limitsReachedX = true; + } + } + if (mRenderer.isPanYEnabled()) { + if (limits != null) { + if (notLimitedBottom) { + notLimitedBottom = limits[2] <= range[2] + deltaY; + } + if (notLimitedUp) { + notLimitedUp = limits[3] >= range[3] + deltaY; + } + } + if (!limited || (notLimitedBottom && notLimitedUp)) { + setYRange(range[2] + deltaY, range[3] + deltaY, i); + limitsReachedY = false; + } else { + limitsReachedY = true; + } + } + } + } else { + RoundChart chart = (RoundChart) mChart; + chart.setCenterX(chart.getCenterX() + (int) (newX - oldX)); + chart.setCenterY(chart.getCenterY() + (int) (newY - oldY)); + } + notifyPanListeners(); + } + + /** + * Return the X / Y axis range ratio. + * + * @param range the axis range + * @return the ratio + */ + private double getAxisRatio(double[] range) { + return Math.abs(range[1] - range[0]) / Math.abs(range[3] - range[2]); + } + + /** + * Notify the pan listeners about a pan. + */ + private synchronized void notifyPanListeners() { + for (PanListener listener : mPanListeners) { + listener.panApplied(); + } + } + + /** + * Adds a new pan listener. + * + * @param listener pan listener + */ + public synchronized void addPanListener(PanListener listener) { + mPanListeners.add(listener); + } + + /** + * Removes a pan listener. + * + * @param listener pan listener + */ + public synchronized void removePanListener(PanListener listener) { + mPanListeners.add(listener); + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/tools/PanListener.java b/android-libraries/achartengine/src/org/achartengine/tools/PanListener.java new file mode 100644 index 00000000..d3d136c0 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/tools/PanListener.java @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.tools; + +/** + * A pan listener. + */ +public interface PanListener { + + /** + * Called when a pan change is triggered. + */ + void panApplied(); + +} diff --git a/android-libraries/achartengine/src/org/achartengine/tools/Zoom.java b/android-libraries/achartengine/src/org/achartengine/tools/Zoom.java new file mode 100644 index 00000000..0abee92c --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/tools/Zoom.java @@ -0,0 +1,189 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.tools; + +import java.util.ArrayList; +import java.util.List; + +import org.achartengine.chart.AbstractChart; +import org.achartengine.chart.RoundChart; +import org.achartengine.chart.XYChart; +import org.achartengine.renderer.DefaultRenderer; + +/** + * The zoom tool. + */ +public class Zoom extends AbstractTool { + /** A flag to be used to know if this is a zoom in or out. */ + private boolean mZoomIn; + /** The zoom rate. */ + private float mZoomRate; + /** The zoom listeners. */ + private List mZoomListeners = new ArrayList(); + /** Zoom limits reached on the X axis. */ + private boolean limitsReachedX = false; + /** Zoom limits reached on the Y axis. */ + private boolean limitsReachedY = false; + + /** Zoom on X axis and Y axis */ + public static final int ZOOM_AXIS_XY = 0; + /** Zoom on X axis independently */ + public static final int ZOOM_AXIS_X = 1; + /** Zoom on Y axis independently */ + public static final int ZOOM_AXIS_Y = 2; + + + /** + * Builds the zoom tool. + * + * @param chart the chart + * @param in zoom in or out + * @param rate the zoom rate + */ + public Zoom(AbstractChart chart, boolean in, float rate) { + super(chart); + mZoomIn = in; + setZoomRate(rate); + } + + /** + * Sets the zoom rate. + * + * @param rate + */ + public void setZoomRate(float rate) { + mZoomRate = rate; + } + + /** + * Apply the zoom. + */ + public void apply(int zoom_axis) { + if (mChart instanceof XYChart) { + int scales = mRenderer.getScalesCount(); + for (int i = 0; i < scales; i++) { + double[] range = getRange(i); + checkRange(range, i); + double[] limits = mRenderer.getZoomLimits(); + + double centerX = (range[0] + range[1]) / 2; + double centerY = (range[2] + range[3]) / 2; + double newWidth = range[1] - range[0]; + double newHeight = range[3] - range[2]; + double newXMin = centerX - newWidth / 2; + double newXMax = centerX + newWidth / 2; + double newYMin = centerY - newHeight / 2; + double newYMax = centerY + newHeight / 2; + + // if already reached last zoom, then it will always set to reached + if (i == 0) { + limitsReachedX = limits != null && (newXMin <= limits[0] || newXMax >= limits[1]); + limitsReachedY = limits != null && (newYMin <= limits[2] || newYMax >= limits[3]); + } + + if (mZoomIn) { + if (mRenderer.isZoomXEnabled() && // zoom in on X axis + (zoom_axis == ZOOM_AXIS_X || zoom_axis == ZOOM_AXIS_XY)) { + if (limitsReachedX && mZoomRate < 1) { + // ignore pinch zoom out once reached X limit + } else { + newWidth /= mZoomRate; + } + } + + if (mRenderer.isZoomYEnabled() && // zoom in on Y axis + (zoom_axis == ZOOM_AXIS_Y || zoom_axis == ZOOM_AXIS_XY)) { + if (limitsReachedY && mZoomRate < 1) { + } else { + newHeight /= mZoomRate; + } + } + } else { + if (mRenderer.isZoomXEnabled() && !limitsReachedX && // zoom out on X axis + (zoom_axis == ZOOM_AXIS_X || zoom_axis == ZOOM_AXIS_XY)) { + newWidth *= mZoomRate; + } + + if (mRenderer.isZoomYEnabled() && !limitsReachedY && // zoom out on Y axis + (zoom_axis == ZOOM_AXIS_Y || zoom_axis == ZOOM_AXIS_XY)) { + newHeight *= mZoomRate; + } + } + + if (mRenderer.isZoomXEnabled() && + (zoom_axis == ZOOM_AXIS_X || zoom_axis == ZOOM_AXIS_XY)) { + newXMin = centerX - newWidth / 2; + newXMax = centerX + newWidth / 2; + setXRange(newXMin, newXMax, i); + } + if (mRenderer.isZoomYEnabled() && + (zoom_axis == ZOOM_AXIS_Y || zoom_axis == ZOOM_AXIS_XY)) { + newYMin = centerY - newHeight / 2; + newYMax = centerY + newHeight / 2; + setYRange(newYMin, newYMax, i); + } + } + } else { + DefaultRenderer renderer = ((RoundChart) mChart).getRenderer(); + if (mZoomIn) { + renderer.setScale(renderer.getScale() * mZoomRate); + } else { + renderer.setScale(renderer.getScale() / mZoomRate); + } + } + notifyZoomListeners(new ZoomEvent(mZoomIn, mZoomRate)); + } + + + /** + * Notify the zoom listeners about a zoom change. + * + * @param e the zoom event + */ + private synchronized void notifyZoomListeners(ZoomEvent e) { + for (ZoomListener listener : mZoomListeners) { + listener.zoomApplied(e); + } + } + + /** + * Notify the zoom listeners about a zoom reset. + */ + public synchronized void notifyZoomResetListeners() { + for (ZoomListener listener : mZoomListeners) { + listener.zoomReset(); + } + } + + /** + * Adds a new zoom listener. + * + * @param listener zoom listener + */ + public synchronized void addZoomListener(ZoomListener listener) { + mZoomListeners.add(listener); + } + + /** + * Removes a zoom listener. + * + * @param listener zoom listener + */ + public synchronized void removeZoomListener(ZoomListener listener) { + mZoomListeners.add(listener); + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/tools/ZoomEvent.java b/android-libraries/achartengine/src/org/achartengine/tools/ZoomEvent.java new file mode 100644 index 00000000..bd8fb686 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/tools/ZoomEvent.java @@ -0,0 +1,56 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.tools; + + +/** + * A zoom event. + */ +public class ZoomEvent { + /** A flag to be used to know if this is a zoom in or out. */ + private boolean mZoomIn; + /** The zoom rate. */ + private float mZoomRate; + + /** + * Builds the zoom tool. + * + * @param in zoom in or out + * @param rate the zoom rate + */ + public ZoomEvent(boolean in, float rate) { + mZoomIn = in; + mZoomRate = rate; + } + + /** + * Returns the zoom type. + * + * @return true if zoom in, false otherwise + */ + public boolean isZoomIn() { + return mZoomIn; + } + + /** + * Returns the zoom rate. + * + * @return the zoom rate + */ + public float getZoomRate() { + return mZoomRate; + } +} diff --git a/android-libraries/achartengine/src/org/achartengine/tools/ZoomListener.java b/android-libraries/achartengine/src/org/achartengine/tools/ZoomListener.java new file mode 100644 index 00000000..4827483e --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/tools/ZoomListener.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.tools; + +/** + * A zoom listener. + */ +public interface ZoomListener { + + /** + * Called when a zoom change is triggered. + * @param e the zoom event + */ + void zoomApplied(ZoomEvent e); + + /** + * Called when a zoom reset is done. + */ + void zoomReset(); +} diff --git a/android-libraries/achartengine/src/org/achartengine/util/IndexXYMap.java b/android-libraries/achartengine/src/org/achartengine/util/IndexXYMap.java new file mode 100644 index 00000000..f9572622 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/util/IndexXYMap.java @@ -0,0 +1,108 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.TreeMap; + +/** + * This class requires sorted x values + */ +public class IndexXYMap extends TreeMap { + private final List indexList = new ArrayList(); + + private double maxXDifference = 0; + + public IndexXYMap() { + super(); + } + + public V put(K key, V value) { + indexList.add(key); + updateMaxXDifference(); + return super.put(key, value); + } + + private void updateMaxXDifference() { + if (indexList.size() < 2) { + maxXDifference = 0; + return; + } + + if (Math.abs((Double) indexList.get(indexList.size() - 1) + - (Double) indexList.get(indexList.size() - 2)) > maxXDifference) + maxXDifference = Math.abs((Double) indexList.get(indexList.size() - 1) + - (Double) indexList.get(indexList.size() - 2)); + } + + public double getMaxXDifference() { + return maxXDifference; + } + + public void clear() { + updateMaxXDifference(); + super.clear(); + indexList.clear(); + } + + /** + * Returns X-value according to the given index + * + * @param index + * @return the X value + */ + public K getXByIndex(int index) { + return indexList.get(index); + } + + /** + * Returns Y-value according to the given index + * + * @param index + * @return the Y value + */ + public V getYByIndex(int index) { + K key = indexList.get(index); + return this.get(key); + } + + /** + * Returns XY-entry according to the given index + * + * @param index + * @return the X and Y values + */ + public XYEntry getByIndex(int index) { + K key = indexList.get(index); + return new XYEntry(key, this.get(key)); + } + + /** + * Removes entry from map by index + * + * @param index + */ + public XYEntry removeByIndex(int index) { + K key = indexList.remove(index); + return new XYEntry(key, this.remove(key)); + } + + public int getIndexForKey(K key) { + return Collections.binarySearch(indexList, key, null); + } +} diff --git a/android-libraries/achartengine/src/org/achartengine/util/MathHelper.java b/android-libraries/achartengine/src/org/achartengine/util/MathHelper.java new file mode 100644 index 00000000..f5b893bf --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/util/MathHelper.java @@ -0,0 +1,174 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.util; + +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; + +/** + * Utility class for math operations. + */ +public class MathHelper { + /** A value that is used a null value. */ + public static final double NULL_VALUE = Double.MAX_VALUE; + /** + * A number formatter to be used to make sure we have a maximum number of + * fraction digits in the labels. + */ + private static final NumberFormat FORMAT = NumberFormat.getNumberInstance(); + + private MathHelper() { + // empty constructor + } + + /** + * Calculate the minimum and maximum values out of a list of doubles. + * + * @param values the input values + * @return an array with the minimum and maximum values + */ + public static double[] minmax(List values) { + if (values.size() == 0) { + return new double[2]; + } + double min = values.get(0); + double max = min; + int length = values.size(); + for (int i = 1; i < length; i++) { + double value = values.get(i); + min = Math.min(min, value); + max = Math.max(max, value); + } + return new double[] { min, max }; + } + + /** + * Computes a reasonable set of labels for a data interval and number of + * labels. + * + * @param start start value + * @param end final value + * @param approxNumLabels desired number of labels + * @return collection containing {start value, end value, increment} + */ + public static List getLabels(final double start, final double end, + final int approxNumLabels) { + FORMAT.setMaximumFractionDigits(5); + List labels = new ArrayList(); + double[] labelParams = computeLabels(start, end, approxNumLabels); + // when the start > end the inc will be negative so it will still work + int numLabels = 1 + (int) ((labelParams[1] - labelParams[0]) / labelParams[2]); + // we want the range to be inclusive but we don't want to blow up when + // looping for the case where the min and max are the same. So we loop + // on + // numLabels not on the values. + for (int i = 0; i < numLabels; i++) { + double z = labelParams[0] + i * labelParams[2]; + try { + // this way, we avoid a label value like 0.4000000000000000001 instead + // of 0.4 + z = FORMAT.parse(FORMAT.format(z)).doubleValue(); + } catch (ParseException e) { + // do nothing here + } + labels.add(z); + } + return labels; + } + + /** + * Computes a reasonable number of labels for a data range. + * + * @param start start value + * @param end final value + * @param approxNumLabels desired number of labels + * @return double[] array containing {start value, end value, increment} + */ + private static double[] computeLabels(final double start, final double end, + final int approxNumLabels) { + if (Math.abs(start - end) < 0.0000001f) { + return new double[] { start, start, 0 }; + } + double s = start; + double e = end; + boolean switched = false; + if (s > e) { + switched = true; + double tmp = s; + s = e; + e = tmp; + } + double xStep = roundUp(Math.abs(s - e) / approxNumLabels); + // Compute x starting point so it is a multiple of xStep. + double xStart = xStep * Math.ceil(s / xStep); + double xEnd = xStep * Math.floor(e / xStep); + if (switched) { + return new double[] { xEnd, xStart, -1.0 * xStep }; + } + return new double[] { xStart, xEnd, xStep }; + } + + /** + * Given a number, round up to the nearest power of ten times 1, 2, or 5. The + * argument must be strictly positive. + */ + private static double roundUp(final double val) { + int exponent = (int) Math.floor(Math.log10(val)); + double rval = val * Math.pow(10, -exponent); + if (rval > 5.0) { + rval = 10.0; + } else if (rval > 2.0) { + rval = 5.0; + } else if (rval > 1.0) { + rval = 2.0; + } + rval *= Math.pow(10, exponent); + return rval; + } + + /** + * Transforms a list of Float values into an array of float. + * + * @param values the list of Float + * @return the array of floats + */ + public static float[] getFloats(List values) { + int length = values.size(); + float[] result = new float[length]; + for (int i = 0; i < length; i++) { + result[i] = values.get(i).floatValue(); + } + return result; + } + + /** + * Transforms a list of Double values into an array of double. + * + * @param values the list of Double + * @return the array of doubles + */ + public static double[] getDoubles(List values) { + int length = values.size(); + double[] result = new double[length]; + for (int i = 0; i < length; i++) { + result[i] = values.get(i).doubleValue(); + } + return result; + } + +} diff --git a/android-libraries/achartengine/src/org/achartengine/util/XYEntry.java b/android-libraries/achartengine/src/org/achartengine/util/XYEntry.java new file mode 100644 index 00000000..53761a4d --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/util/XYEntry.java @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.achartengine.util; + +import java.util.Map.Entry; + +/** + * A map entry value encapsulating an XY point. + */ +public class XYEntry implements Entry { + private final K key; + + private V value; + + public XYEntry(K key, V value) { + this.key = key; + this.value = value; + } + + public K getKey() { + return key; + } + + public V getValue() { + return value; + } + + public V setValue(V object) { + this.value = object; + return this.value; + } +} \ No newline at end of file diff --git a/android-libraries/achartengine/src/org/achartengine/util/package.html b/android-libraries/achartengine/src/org/achartengine/util/package.html new file mode 100644 index 00000000..da92e913 --- /dev/null +++ b/android-libraries/achartengine/src/org/achartengine/util/package.html @@ -0,0 +1,6 @@ + +AChartEngine + +Utility classes that provide helper methods used by most of the other packages. + + \ No newline at end of file diff --git a/android/.classpath b/android/.classpath index 3288939d..1772b2ef 100644 --- a/android/.classpath +++ b/android/.classpath @@ -8,6 +8,5 @@ - diff --git a/android/libs/achartengine-1.0.0.jar b/android/libs/achartengine-1.0.0.jar deleted file mode 100644 index fe75dc3465cafd74d2bb0d24f64763a9b341f1ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 109717 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

1FaK=$7R&>P2N^z$#Yg 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ྷ{ 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

BP;&z6?GX`PD<&7zVs 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%0DKGxkH3ptni_fMu47`INtt)GNW$N$C0k6X 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 G(*FQ9ZfJD? diff --git a/android/project.properties b/android/project.properties index 0c951984..8202ea55 100644 --- a/android/project.properties +++ b/android/project.properties @@ -11,3 +11,4 @@ target=android-15 android.library.reference.1=../ActionBarSherlock android.library.reference.2=../TreeViewList +android.library.reference.3=../achartengine -- 2.47.2