diff --git a/CodenameOne/src/com/codename1/charts/views/AbstractChart.java b/CodenameOne/src/com/codename1/charts/views/AbstractChart.java index f239b07439..8697161a3f 100644 --- a/CodenameOne/src/com/codename1/charts/views/AbstractChart.java +++ b/CodenameOne/src/com/codename1/charts/views/AbstractChart.java @@ -41,7 +41,7 @@ public abstract class AbstractChart { private static final char[] stopCharCandidates = "!@#$%^&*()?><,./+-qwertyuiop[zxcvbnm,./\\|}{".toCharArray(); - private static float[] calculateDrawPoints(float p1x, float p1y, float p2x, float p2y, + protected static float[] calculateDrawPoints(float p1x, float p1y, float p2x, float p2y, int screenHeight, int screenWidth) { float drawP1x; float drawP1y; diff --git a/CodenameOne/src/com/codename1/charts/views/LineChart.java b/CodenameOne/src/com/codename1/charts/views/LineChart.java index 761fd5cb91..6ab5310c0c 100644 --- a/CodenameOne/src/com/codename1/charts/views/LineChart.java +++ b/CodenameOne/src/com/codename1/charts/views/LineChart.java @@ -1,33 +1,34 @@ -/** - * Copyright (C) 2009 - 2013 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.codename1.charts.views; - -import com.codename1.charts.compat.Canvas; -import com.codename1.charts.compat.Paint; -import com.codename1.charts.compat.Paint.Style; -import com.codename1.charts.models.XYMultipleSeriesDataset; -import com.codename1.charts.renderers.SimpleSeriesRenderer; -import com.codename1.charts.renderers.XYMultipleSeriesRenderer; -import com.codename1.charts.renderers.XYSeriesRenderer; -import com.codename1.charts.renderers.XYSeriesRenderer.FillOutsideLine; - -import java.util.ArrayList; -import java.util.List; - - +/** + * Copyright (C) 2009 - 2013 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.codename1.charts.views; + +import com.codename1.charts.compat.Canvas; +import com.codename1.charts.compat.Paint; +import com.codename1.charts.compat.Paint.Style; +import com.codename1.charts.models.XYMultipleSeriesDataset; +import com.codename1.charts.renderers.SimpleSeriesRenderer; +import com.codename1.charts.renderers.XYMultipleSeriesRenderer; +import com.codename1.charts.renderers.XYSeriesRenderer; +import com.codename1.charts.renderers.XYSeriesRenderer.FillOutsideLine; +import com.codename1.ui.geom.GeneralPath; + +import java.util.ArrayList; +import java.util.List; + + /** * Plots series of X/Y points using straight line segments. *

@@ -37,247 +38,336 @@ * to a Codename One form. */ 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 - */ - @Override - public void drawSeries(Canvas canvas, Paint paint, List points, XYSeriesRenderer renderer, - float yAxisValue, int seriesIndex, int startIndex) { - float lineWidth = paint.getStrokeWidth(); - paint.setStrokeWidth(renderer.getLineWidth()); - final FillOutsideLine[] fillOutsideLine = renderer.getFillOutsideLine(); - - for (FillOutsideLine fill : fillOutsideLine) { - if (fill.getType() != FillOutsideLine.Type.NONE) { - paint.setColor(fill.getColor()); - // TODO: find a way to do area charts without duplicating data - List fillPoints = new ArrayList(); - int[] range = fill.getFillRange(); - if (range == null) { - fillPoints.addAll(points); - } else { - if (points.size() > range[0] * 2 && points.size() > range[1] * 2) { - fillPoints.addAll(points.subList(range[0] * 2, range[1] * 2)); - } - } - - final float referencePoint; - - // switch on ENUM's generates reflection code that screws up J2ME - FillOutsideLine.Type tt = fill.getType(); - if (tt == FillOutsideLine.Type.BOUNDS_ALL || tt == FillOutsideLine.Type.BOUNDS_BELOW || tt == FillOutsideLine.Type.BOUNDS_ABOVE) { - referencePoint = yAxisValue; - } else { - if (tt == FillOutsideLine.Type.BELOW) { - referencePoint = canvas.getHeight(); - } else { - if (tt == FillOutsideLine.Type.ABOVE) { - referencePoint = 0; - } else { - throw new RuntimeException( - "You have added a new type of filling but have not implemented."); - } - } - } - /*switch (fill.getType()) { - case BOUNDS_ALL: - referencePoint = yAxisValue; - break; - case BOUNDS_BELOW: - referencePoint = yAxisValue; - break; - case BOUNDS_ABOVE: - referencePoint = yAxisValue; - break; - case BELOW: - referencePoint = canvas.getHeight(); - break; - case ABOVE: - referencePoint = 0; - break; - default: - throw new RuntimeException( - "You have added a new type of filling but have not implemented."); - }*/ - if (fill.getType() == FillOutsideLine.Type.BOUNDS_ABOVE - || fill.getType() == FillOutsideLine.Type.BOUNDS_BELOW) { - List boundsPoints = new ArrayList(); - boolean add = false; - int length = fillPoints.size(); - if (length > 0 && fill.getType() == FillOutsideLine.Type.BOUNDS_ABOVE - && fillPoints.get(1) < referencePoint - || fill.getType() == FillOutsideLine.Type.BOUNDS_BELOW - && fillPoints.get(1) > referencePoint) { - boundsPoints.add(fillPoints.get(0)); - boundsPoints.add(fillPoints.get(1)); - add = true; - } - - for (int i = 3; i < length; i += 2) { - float prevValue = fillPoints.get(i - 2); - float value = fillPoints.get(i); - - if (prevValue < referencePoint && value > referencePoint || prevValue > referencePoint - && value < referencePoint) { - float prevX = fillPoints.get(i - 3); - float x = fillPoints.get(i - 1); - boundsPoints.add(prevX + (x - prevX) * (referencePoint - prevValue) - / (value - prevValue)); - boundsPoints.add(referencePoint); - if (fill.getType() == FillOutsideLine.Type.BOUNDS_ABOVE && value > referencePoint - || fill.getType() == FillOutsideLine.Type.BOUNDS_BELOW && value < referencePoint) { - i += 2; - add = false; - } else { - boundsPoints.add(x); - boundsPoints.add(value); - add = true; - } - } else { - if (add || fill.getType() == FillOutsideLine.Type.BOUNDS_ABOVE - && value < referencePoint || fill.getType() == FillOutsideLine.Type.BOUNDS_BELOW - && value > referencePoint) { - boundsPoints.add(fillPoints.get(i - 1)); - boundsPoints.add(value); - } - } - } - - fillPoints.clear(); - fillPoints.addAll(boundsPoints); - } - int length = fillPoints.size(); - if (length > 0) { - fillPoints.set(0, fillPoints.get(0) + 1); - fillPoints.add(fillPoints.get(length - 2)); - fillPoints.add(referencePoint); - fillPoints.add(fillPoints.get(0)); - fillPoints.add(fillPoints.get(length + 1)); - for (int i = 0; i < length + 4; i += 2) { - if (fillPoints.get(i + 1) < 0) { - fillPoints.set(i + 1, 0f); - } - } - - paint.setStyle(Style.FILL); - drawPath(canvas, fillPoints, paint, true); - } - } - } - paint.setColor(renderer.getColor()); - paint.setStyle(Style.STROKE); - drawPath(canvas, points, paint, false); - paint.setStrokeWidth(lineWidth); - } - - @Override - protected ClickableArea[] clickableAreasForPoints(List points, List values, - float yAxisValue, int seriesIndex, int startIndex) { - int length = points.size(); - ClickableArea[] ret = new ClickableArea[length / 2]; - for (int i = 0; i < length; i += 2) { - int selectableBuffer = mRenderer.getSelectableBuffer(); - ret[i / 2] = new ClickableArea(PkgUtils.makeRect(points.get(i) - selectableBuffer, points.get(i + 1) - - selectableBuffer, points.get(i) + selectableBuffer, points.get(i + 1) - + selectableBuffer), values.get(i), values.get(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; - } - -} + /** 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 + */ + @Override + public void drawSeries(Canvas canvas, Paint paint, List points, XYSeriesRenderer renderer, + float yAxisValue, int seriesIndex, int startIndex) { + float lineWidth = paint.getStrokeWidth(); + paint.setStrokeWidth(renderer.getLineWidth()); + final FillOutsideLine[] fillOutsideLine = renderer.getFillOutsideLine(); + + for (FillOutsideLine fill : fillOutsideLine) { + if (fill.getType() != FillOutsideLine.Type.NONE) { + paint.setColor(fill.getColor()); + + final float referencePoint; + + // switch on ENUM's generates reflection code that screws up J2ME + FillOutsideLine.Type tt = fill.getType(); + if (tt == FillOutsideLine.Type.BOUNDS_ALL || tt == FillOutsideLine.Type.BOUNDS_BELOW || tt == FillOutsideLine.Type.BOUNDS_ABOVE) { + referencePoint = yAxisValue; + } else { + if (tt == FillOutsideLine.Type.BELOW) { + referencePoint = canvas.getHeight(); + } else { + if (tt == FillOutsideLine.Type.ABOVE) { + referencePoint = 0; + } else { + throw new RuntimeException( + "You have added a new type of filling but have not implemented."); + } + } + } + + int[] range = fill.getFillRange(); + + if (fill.getType() == FillOutsideLine.Type.BOUNDS_ABOVE + || fill.getType() == FillOutsideLine.Type.BOUNDS_BELOW) { + List fillPoints = new ArrayList(); + if (range == null) { + fillPoints.addAll(points); + } else { + if (points.size() > range[0] * 2 && points.size() > range[1] * 2) { + fillPoints.addAll(points.subList(range[0] * 2, range[1] * 2)); + } + } + + List boundsPoints = new ArrayList(); + boolean add = false; + int length = fillPoints.size(); + if (length > 0 && fill.getType() == FillOutsideLine.Type.BOUNDS_ABOVE + && fillPoints.get(1) < referencePoint + || fill.getType() == FillOutsideLine.Type.BOUNDS_BELOW + && fillPoints.get(1) > referencePoint) { + boundsPoints.add(fillPoints.get(0)); + boundsPoints.add(fillPoints.get(1)); + add = true; + } + + for (int i = 3; i < length; i += 2) { + float prevValue = fillPoints.get(i - 2); + float value = fillPoints.get(i); + + if (prevValue < referencePoint && value > referencePoint || prevValue > referencePoint + && value < referencePoint) { + float prevX = fillPoints.get(i - 3); + float x = fillPoints.get(i - 1); + boundsPoints.add(prevX + (x - prevX) * (referencePoint - prevValue) + / (value - prevValue)); + boundsPoints.add(referencePoint); + if (fill.getType() == FillOutsideLine.Type.BOUNDS_ABOVE && value > referencePoint + || fill.getType() == FillOutsideLine.Type.BOUNDS_BELOW && value < referencePoint) { + i += 2; + add = false; + } else { + boundsPoints.add(x); + boundsPoints.add(value); + add = true; + } + } else { + if (add || fill.getType() == FillOutsideLine.Type.BOUNDS_ABOVE + && value < referencePoint || fill.getType() == FillOutsideLine.Type.BOUNDS_BELOW + && value > referencePoint) { + boundsPoints.add(fillPoints.get(i - 1)); + boundsPoints.add(value); + } + } + } + + fillPoints.clear(); + fillPoints.addAll(boundsPoints); + length = fillPoints.size(); + if (length > 0) { + fillPoints.set(0, fillPoints.get(0) + 1); + fillPoints.add(fillPoints.get(length - 2)); + fillPoints.add(referencePoint); + fillPoints.add(fillPoints.get(0)); + fillPoints.add(fillPoints.get(length + 1)); + for (int i = 0; i < length + 4; i += 2) { + if (fillPoints.get(i + 1) < 0) { + fillPoints.set(i + 1, 0f); + } + } + + paint.setStyle(Style.FILL); + drawPath(canvas, fillPoints, paint, true); + } + } else { + paint.setStyle(Style.FILL); + drawFillPath(canvas, points, paint, referencePoint, range); + } + } + } + paint.setColor(renderer.getColor()); + paint.setStyle(Style.STROKE); + drawPath(canvas, points, paint, false); + paint.setStrokeWidth(lineWidth); + } + + private void drawFillPath(Canvas canvas, List points, Paint paint, float referencePoint, int[] range) { + GeneralPath path = new GeneralPath(); + int height = canvas.getHeight(); + int width = canvas.getWidth(); + + int startIndex = 0; + int endIndex = points.size(); + if (range != null) { + if (points.size() > range[0] * 2 && points.size() > range[1] * 2) { + startIndex = range[0] * 2; + endIndex = range[1] * 2; + } + } + + int length = endIndex - startIndex; + if (length < 2) return; + + // Logic from original: + // 1. fillPoints.set(0, fillPoints.get(0) + 1); + // 2. Add extra closing points + // 3. Clamp Y < 0 to 0 + + // We construct the path iteratively, mimicking drawPath logic but using our special iterator/values + + float startX = points.get(startIndex) + 1; + float startY = points.get(startIndex + 1); + if (startY < 0) startY = 0; + + // Since we need to form segments (p1, p2) for calculateDrawPoints + + float p1x, p1y, p2x, p2y; + float[] tempDrawPoints; + + if (length >= 4) { + p1x = startX; + p1y = startY; + p2x = points.get(startIndex + 2); + p2y = points.get(startIndex + 3); + if (p2y < 0) p2y = 0; + + tempDrawPoints = calculateDrawPoints(p1x, p1y, p2x, p2y, height, width); + path.moveTo(tempDrawPoints[0], tempDrawPoints[1]); + path.lineTo(tempDrawPoints[2], tempDrawPoints[3]); + + for (int i = 4; i < length; i += 2) { + p1x = points.get(startIndex + i - 2); + p1y = points.get(startIndex + i - 1); + if (p1y < 0) p1y = 0; + + p2x = points.get(startIndex + i); + p2y = points.get(startIndex + i + 1); + if (p2y < 0) p2y = 0; + + tempDrawPoints = calculateDrawPoints(p1x, p1y, p2x, p2y, height, width); + path.lineTo(tempDrawPoints[2], tempDrawPoints[3]); + } + } else { + // only 1 point? + path.moveTo(startX, startY); + } + + // Now add closing points. + // Last point was (points.get(endIndex-2), points.get(endIndex-1)) (clamped) + + // 1. To (LastX, RefY) + float lastX = points.get(endIndex - 2); + float nextX = lastX; + float nextY = referencePoint; + if (nextY < 0) nextY = 0; + + // We need previous point (p1) to calculate draw points segment. + p1x = points.get(endIndex - 2); // original X + p1y = points.get(endIndex - 1); // original Y + if (p1y < 0) p1y = 0; + + tempDrawPoints = calculateDrawPoints(p1x, p1y, nextX, nextY, height, width); + path.lineTo(tempDrawPoints[2], tempDrawPoints[3]); + + // 2. To (StartX, RefY) + // p1 is (LastX, RefY) + p1x = nextX; + p1y = nextY; + + nextX = startX; // This is StartX (modified +1) + nextY = referencePoint; + if (nextY < 0) nextY = 0; + + tempDrawPoints = calculateDrawPoints(p1x, p1y, nextX, nextY, height, width); + path.lineTo(tempDrawPoints[2], tempDrawPoints[3]); + + // 3. To (StartX, StartY) - Closing the loop + p1x = nextX; + p1y = nextY; + + nextX = startX; + nextY = startY; + + tempDrawPoints = calculateDrawPoints(p1x, p1y, nextX, nextY, height, width); + path.lineTo(tempDrawPoints[2], tempDrawPoints[3]); + + // Done + canvas.drawPath(path, paint); + } + + @Override + protected ClickableArea[] clickableAreasForPoints(List points, List values, + float yAxisValue, int seriesIndex, int startIndex) { + int length = points.size(); + ClickableArea[] ret = new ClickableArea[length / 2]; + for (int i = 0; i < length; i += 2) { + int selectableBuffer = mRenderer.getSelectableBuffer(); + ret[i / 2] = new ClickableArea(PkgUtils.makeRect(points.get(i) - selectableBuffer, points.get(i + 1) + - selectableBuffer, points.get(i) + selectableBuffer, points.get(i + 1) + + selectableBuffer), values.get(i), values.get(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; + } + +}