/**
* Copyright (C) 2009, 2010 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 com.androsz.achartengine.chart;
import java.util.ArrayList;
import java.util.List;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.Typeface;
import com.androsz.achartengine.model.XYMultipleSeriesDataset;
import com.androsz.achartengine.model.XYSeries;
import com.androsz.achartengine.renderer.DefaultRenderer;
import com.androsz.achartengine.renderer.SimpleSeriesRenderer;
import com.androsz.achartengine.renderer.XYMultipleSeriesRenderer;
import com.androsz.achartengine.renderer.XYMultipleSeriesRenderer.Orientation;
import com.androsz.achartengine.util.MathHelper;
/**
* The XY chart rendering class.
*/
public abstract class XYChart extends AbstractChart {
/**
*
*/
private static final long serialVersionUID = 6699185573466470104L;
/** 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 PointF mCenter;
/** The visible chart area, in screen coordinates. */
private Rect screenR;
/** The calculated range. */
private final double[] calcRange = new double[4];
/**
* 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;
}
/**
* 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
*/
@Override
public void draw(Canvas canvas, int x, int y, int width, int height,
Paint paint) {
paint.setAntiAlias(mRenderer.isAntialiasing());
int legendSize = mRenderer.getLegendHeight();
if (mRenderer.isShowLegend() && legendSize == 0) {
legendSize = height / 5;
}
final int[] margins = mRenderer.getMargins();
final int left = x + margins[1];
final int top = y + margins[0];
int right = x + width - margins[3];
int bottom = y + height - margins[2] - legendSize;
if (screenR == null) {
screenR = new Rect();
}
screenR.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()));
}
final Orientation or = mRenderer.getOrientation();
if (or == Orientation.VERTICAL) {
right -= legendSize;
bottom += legendSize - 20;
}
final int angle = or.getAngle();
final boolean rotate = angle == 90;
mScale = (float) (height) / width;
mTranslate = Math.abs(width - height) / 2;
if (mScale < 1) {
mTranslate *= -1;
}
mCenter = new PointF((x + width) / 2, (y + height) / 2);
if (rotate) {
transform(canvas, angle, false);
}
double minX = mRenderer.getXAxisMin();
double maxX = mRenderer.getXAxisMax();
double minY = mRenderer.getYAxisMin();
double maxY = mRenderer.getYAxisMax();
final boolean isMinXSet = mRenderer.isMinXSet();
final boolean isMaxXSet = mRenderer.isMaxXSet();
final boolean isMinYSet = mRenderer.isMinYSet();
final boolean isMaxYSet = mRenderer.isMaxYSet();
double xPixelsPerUnit = 0;
double yPixelsPerUnit = 0;
final int sLength = mDataset.getSeriesCount();
final String[] titles = new String[sLength];
for (int i = 0; i < sLength; i++) {
final XYSeries series = mDataset.getSeriesAt(i);
titles[i] = series.getTitle();
if (series.getItemCount() == 0) {
continue;
}
if (!isMinXSet) {
final double minimumX = series.getMinX();
minX = Math.min(minX, minimumX);
calcRange[0] = minX;
}
if (!isMaxXSet) {
final double maximumX = series.getMaxX();
maxX = Math.max(maxX, maximumX);
calcRange[1] = maxX;
}
if (!isMinYSet) {
final double minimumY = series.getMinY();
minY = Math.min(minY, (float) minimumY);
calcRange[2] = minY;
}
if (!isMaxYSet) {
final double maximumY = series.getMaxY();
maxY = Math.max(maxY, (float) maximumY);
calcRange[3] = maxY;
}
}
if (maxX - minX != 0) {
xPixelsPerUnit = (right - left) / (maxX - minX);
}
if (maxY - minY != 0) {
yPixelsPerUnit = (float) ((bottom - top) / (maxY - minY));
}
boolean hasValues = false;
for (int i = 0; i < sLength; i++) {
final XYSeries series = mDataset.getSeriesAt(i);
if (series.getItemCount() == 0) {
continue;
}
hasValues = true;
final SimpleSeriesRenderer seriesRenderer = mRenderer
.getSeriesRendererAt(i);
final int originalValuesLength = series.getItemCount();
final int valuesLength = originalValuesLength;
final int length = valuesLength * 2;
final List<Float> points = new ArrayList<Float>();
for (int j = 0; j < length; j += 2) {
final int index = j / 2;
final double yValue = series.getY(index);
if (yValue != MathHelper.NULL_VALUE) {
points.add((float) (left + xPixelsPerUnit
* (series.getX(index) - minX)));
points.add((float) (bottom - yPixelsPerUnit
* (yValue - minY)));
} else {
if (points.size() > 0) {
drawSeries(
series,
canvas,
paint,
points,
seriesRenderer,
Math.min(
bottom,
(float) (bottom + yPixelsPerUnit * minY)),
i, or);
points.clear();
}
}
}
if (points.size() > 0) {
drawSeries(
series,
canvas,
paint,
points,
seriesRenderer,
Math.min(bottom, (float) (bottom + yPixelsPerUnit
* minY)), i, or);
}
}
// 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());
}
final boolean showLabels = mRenderer.isShowLabels() && hasValues;
final boolean showGrid = mRenderer.isShowGrid();
if (showLabels || showGrid) {
final List<Double> xLabels = MathHelper.getLabels(minX, maxX,
mRenderer.getXLabels());
final List<Double> yLabels = MathHelper.getLabels(minY, maxY,
mRenderer.getYLabels());
int xLabelsLeft = left;
if (showLabels) {
paint.setColor(mRenderer.getLabelsColor());
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, minX);
paint.setTextAlign(mRenderer.getYLabelsAlign());
final int length = yLabels.size();
for (int i = 0; i < length; i++) {
final double label = yLabels.get(i);
final float yLabel = (float) (bottom - yPixelsPerUnit
* (label - minY));
if (or == Orientation.HORIZONTAL) {
if (showLabels) {
paint.setColor(mRenderer.getLabelsColor());
canvas.drawLine(left - 4, yLabel, left, yLabel, paint);
drawText(canvas, getLabel(label), left - 2, yLabel - 2,
paint, mRenderer.getYLabelsAngle());
}
if (showGrid) {
paint.setColor(mRenderer.getGridColor());
canvas.drawLine(left, yLabel, right, yLabel, paint);
}
} else if (or == Orientation.VERTICAL) {
if (showLabels) {
paint.setColor(mRenderer.getLabelsColor());
canvas.drawLine(right + 4, yLabel, right, yLabel, paint);
drawText(canvas, getLabel(label), right + 10,
yLabel - 2, paint, mRenderer.getYLabelsAngle());
}
if (showGrid) {
paint.setColor(mRenderer.getGridColor());
canvas.drawLine(right, yLabel, left, yLabel, paint);
}
}
}
if (showLabels) {
paint.setColor(mRenderer.getLabelsColor());
final 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);
drawText(canvas, mRenderer.getYTitle(), x + size, 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);
} else if (or == Orientation.VERTICAL) {
transform(canvas, angle, true);
drawLegend(canvas, mRenderer, titles, left, right, y, width,
height, legendSize, paint);
transform(canvas, angle, false);
}
if (mRenderer.isShowAxes()) {
paint.setColor(mRenderer.getAxesColor());
canvas.drawLine(left, bottom, right, bottom, paint);
if (or == Orientation.HORIZONTAL) {
canvas.drawLine(left, top, left, bottom, paint);
} else if (or == Orientation.VERTICAL) {
canvas.drawLine(right, top, right, bottom, paint);
}
}
if (rotate) {
transform(canvas, angle, true);
}
}
/**
* The graphical representation of the series values as text.
*
* @param canvas
* the canvas to paint to
* @param series
* the series to be painted
* @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
*/
protected void drawChartValuesText(Canvas canvas, XYSeries series,
Paint paint, float[] points, int seriesIndex) {
for (int k = 0; k < points.length; k += 2) {
drawText(canvas, getLabel(series.getY(k / 2)), points[k],
points[k + 1] - 3.5f, paint, 0);
}
}
/**
* 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
*/
public abstract void drawSeries(Canvas canvas, Paint paint, float[] points,
SimpleSeriesRenderer seriesRenderer, float yAxisValue,
int seriesIndex);
private void drawSeries(XYSeries series, Canvas canvas, Paint paint,
List<Float> pointsList, SimpleSeriesRenderer seriesRenderer,
float yAxisValue, int seriesIndex, Orientation or) {
final float[] points = MathHelper.getFloats(pointsList);
drawSeries(canvas, paint, points, seriesRenderer, yAxisValue,
seriesIndex);
if (isRenderPoints(seriesRenderer)) {
final ScatterChart pointsChart = getPointsChart();
if (pointsChart != null) {
pointsChart.drawSeries(canvas, paint, points, seriesRenderer,
0, seriesIndex);
}
}
paint.setTextSize(mRenderer.getChartValuesTextSize());
if (or == Orientation.HORIZONTAL) {
paint.setTextAlign(Align.CENTER);
} else {
paint.setTextAlign(Align.LEFT);
}
if (mRenderer.isDisplayChartValues()) {
drawChartValuesText(canvas, series, paint, points, seriesIndex);
}
}
/**
* 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) {
final float angle = -mRenderer.getOrientation().getAngle() + extraAngle;
if (angle != 0) {
// canvas.scale(1 / mScale, mScale);
canvas.rotate(angle, x, y);
}
canvas.drawText(text, x, y, paint);
if (angle != 0) {
canvas.rotate(-angle, x, y);
// 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
*/
protected void drawXLabels(List<Double> xLabels,
Double[] xTextLabelLocations, Canvas canvas, Paint paint, int left,
int top, int bottom, double xPixelsPerUnit, double minX) {
final int length = xLabels.size();
final boolean showLabels = mRenderer.isShowLabels();
final boolean showGrid = mRenderer.isShowGrid();
for (int i = 0; i < length; i++) {
final double label = xLabels.get(i);
final float xLabel = (float) (left + xPixelsPerUnit
* (label - minX));
if (showLabels) {
paint.setColor(mRenderer.getLabelsColor());
canvas.drawLine(xLabel, bottom, xLabel,
bottom + mRenderer.getLabelsTextSize() / 3, paint);
drawText(canvas, getLabel(label), xLabel,
bottom + mRenderer.getLabelsTextSize() * 4 / 3, paint,
mRenderer.getXLabelsAngle());
}
if (showGrid) {
paint.setColor(mRenderer.getGridColor());
canvas.drawLine(xLabel, bottom, xLabel, top, paint);
}
}
if (showLabels) {
paint.setColor(mRenderer.getLabelsColor());
for (final Double location : xTextLabelLocations) {
final float xLabel = (float) (left + xPixelsPerUnit
* (location.doubleValue() - minX));
canvas.drawLine(xLabel, bottom, xLabel, bottom + 4, paint);
drawText(canvas, mRenderer.getXTextLabel(location), xLabel,
bottom + mRenderer.getLabelsTextSize(), paint,
mRenderer.getXLabelsAngle());
}
}
}
public double[] getCalcRange() {
return calcRange;
}
public XYMultipleSeriesDataset getDataset() {
return mDataset;
}
/**
* Returns the default axis minimum.
*
* @return the default axis minimum
*/
public double getDefaultMinimum() {
return MathHelper.NULL_VALUE;
}
/**
* 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;
}
/**
* Returns the scatter chart to be used for drawing the data points.
*
* @return the data points scatter chart
*/
public ScatterChart getPointsChart() {
return null;
}
// TODO: docs
public XYMultipleSeriesRenderer getRenderer() {
return mRenderer;
}
/**
* Returns if the chart should display the points as a certain shape.
*
* @param renderer
* the series renderer
*/
public boolean isRenderPoints(SimpleSeriesRenderer renderer) {
return false;
}
public PointF toRealPoint(float screenX, float screenY) {
final double realMinX = mRenderer.getXAxisMin();
final double realMaxX = mRenderer.getXAxisMax();
final double realMinY = mRenderer.getYAxisMin();
final double realMaxY = mRenderer.getYAxisMax();
return new PointF((float) ((screenX - screenR.left)
* (realMaxX - realMinX) / screenR.width() + realMinX),
(float) ((screenR.top + screenR.height() - screenY)
* (realMaxY - realMinY) / screenR.height() + realMinY));
}
public PointF toScreenPoint(PointF realPoint) {
final double realMinX = mRenderer.getXAxisMin();
final double realMaxX = mRenderer.getXAxisMax();
final double realMinY = mRenderer.getYAxisMin();
final double realMaxY = mRenderer.getYAxisMax();
return new PointF((float) ((realPoint.x - realMinX) * screenR.width()
/ (realMaxX - realMinX) + screenR.left),
(float) ((realMaxY - realPoint.y) * screenR.height()
/ (realMaxY - realMinY) + screenR.top));
}
/**
* 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.x, mCenter.y);
} else {
canvas.rotate(angle, mCenter.x, mCenter.y);
canvas.translate(-mTranslate, mTranslate);
canvas.scale(mScale, 1 / mScale);
}
}
}