/*********************************************************************************
* TotalCross Software Development Kit *
* Copyright (C) 2000-2012 SuperWaba Ltda. *
* All Rights Reserved *
* *
* This library and virtual machine is distributed in the hope that it will *
* be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. *
* *
* This file is covered by the GNU LESSER GENERAL PUBLIC LICENSE VERSION 3.0 *
* A copy of this license is located in file license.txt at the root of this *
* SDK or can be downloaded here: *
* http://www.gnu.org/licenses/lgpl-3.0.txt *
* *
*********************************************************************************/
package totalcross.ui.chart;
import totalcross.sys.*;
import totalcross.ui.*;
import totalcross.ui.font.Font;
import totalcross.ui.gfx.*;
import totalcross.util.Vector;
/** The base class of all chart class.
* @see ColumnChart
* @see LineChart
* @see PieChart
* @see XYChart
* @since TotalCross 1.0
*/
public class Chart extends Control
{
/** Sample gray color to be used in the chart, taken from <a href='http://www.mulinblog.com/a-color-palette-optimized-for-data-visualization'>here</a>. */
public static final int COLOR1 = 0x4D4D4D; // (gray)
/** Sample blue color to be used in the chart, taken from <a href='http://www.mulinblog.com/a-color-palette-optimized-for-data-visualization'>here</a>. */
public static final int COLOR2 = 0x5DA5DA; // (blue)
/** Sample orange color to be used in the chart, taken from <a href='http://www.mulinblog.com/a-color-palette-optimized-for-data-visualization'>here</a>. */
public static final int COLOR3 = 0xFAA43A; // (orange)
/** Sample green color to be used in the chart, taken from <a href='http://www.mulinblog.com/a-color-palette-optimized-for-data-visualization'>here</a>. */
public static final int COLOR4 = 0x60BD68; // (green)
/** Sample pink color to be used in the chart, taken from <a href='http://www.mulinblog.com/a-color-palette-optimized-for-data-visualization'>here</a>. */
public static final int COLOR5 = 0xF17CB0; // (pink)
/** Sample brown color to be used in the chart, taken from <a href='http://www.mulinblog.com/a-color-palette-optimized-for-data-visualization'>here</a>. */
public static final int COLOR6 = 0xB2912F; // (brown)
/** Sample purple color to be used in the chart, taken from <a href='http://www.mulinblog.com/a-color-palette-optimized-for-data-visualization'>here</a>. */
public static final int COLOR7 = 0xB276B2; // (purple)
/** Sample yellow color to be used in the chart, taken from <a href='http://www.mulinblog.com/a-color-palette-optimized-for-data-visualization'>here</a>. */
public static final int COLOR8 = 0xDECF3F; // (yellow)
/** Sample red color to be used in the chart, taken from <a href='http://www.mulinblog.com/a-color-palette-optimized-for-data-visualization'>here</a>. */
public static final int COLOR9 = 0xF15854; // (red)
/** Indicates a 3D chart. Default is 2D.
* @see #type
*/
public static final int IS_3D = 1; // guich@tc110_20
/** Indicates a horizontal gradient.
* @see #type
*/
public static final int GRADIENT_HORIZONTAL = 2; // guich@tc110_20
/** Indicates a vertical gradient.
* @see #type
*/
public static final int GRADIENT_VERTICAL = 4; // guich@tc110_20
/** Indicates a dark gradient. The default is a bright gradient.
* @see #type
*/
public static final int GRADIENT_DARK = 8; // guich@tc110_20
/** Indicates to invert the gradient direction.
* @see #type
*/
public static final int GRADIENT_INVERT = 16; // guich@tc110_20
/** The chart type. The values can be OR'ed together. It applies to Column and Pie charts.
* For example:
* <pre>
* chart.type = Chart.IS_3D | Chart.GRADIENT_HORIZONTAL | Chart.GRADIENT_DARK;
* </pre>
* @see #IS_3D
* @see #GRADIENT_HORIZONTAL
* @see #GRADIENT_VERTICAL
* @see #GRADIENT_DARK
* @see #GRADIENT_INVERT
*/
public int type;
/** This chart's title */
protected String title;
/** The series that are currently added to this chart */
public Vector series = new Vector();
/** The value for the origin of the X axis */
public double xAxisMinValue;
/** The value for the end of the X axis */
public double xAxisMaxValue;
/** The number of subdivisions of the X axis */
public int xAxisSteps;
/** The categories of the X axis (may be null if X is a value axis) */
protected String[] xAxisCategories;
/** The value for the origin of the Y axis */
protected double yAxisMinValue;
/** The value for the end of the Y axis */
protected double yAxisMaxValue=100; // guich@tc100b5_56: initialize to something if the user don't call setYAxis
/** The number of subdivisions of the Y axis */
protected int yAxisSteps=10; // guich@tc100b5_56
/** The relative screen position of the X axis origin */
public int xAxisX1;
/** The relative screen position of the X axis end */
public int xAxisX2;
/** The relative screen position of the Y axis origin */
public int yAxisY1;
/** The relative screen position of the Y axis end */
public int yAxisY2;
/** This chart's empty border dimensions */
public Insets border = new Insets();
/** Flag to indicate whether the title must be painted */
public boolean showTitle;
/** Flag to indicate whether the legend must be painted */
public boolean showLegend;
/** Flag to indicate whether the categories must be painted */
public boolean showCategories;
/**
* Flag to indicate whether the categories must be painted on the next tick.<br>
* The field {@link #showCategories} must also be set to true.<br>
* <br>
* Special values for categories on tick:
* <ul>
* <li>"|" - Paints a line using the axis fore color.
* <li>":" - Paints a dotted line using the axis fore and back colors.
* </ul>
*
* @see #showCategories
*/
public boolean showCategoriesOnTick;
/** The index where the category mark is shown. */
public int categoryMarkIndex=-1;
/** The color for the category mark. */
public int categoryMarkColor = Color.RED;
/** Flag to indicate whether the y values must be painted */
public boolean showYValues; // guich@tc110_75
/** Flag to indicate whether the X grids must be painted */
public boolean showVGrids;
/** Flag to indicate whether the Y grids must be painted */
public boolean showHGrids;
/** The number of decimal places to show the data of the X axis. Defaults to 3. */
public int xDecimalPlaces = 3;
/** The number of decimal places to show the data of the Y axis. Defaults to 3. */
public int yDecimalPlaces = 3;
/** The position of the legend. Can be one of: RIGHT (default), LEFT, TOP, BOTTOM. */
public int legendPosition = RIGHT;
/** Perspective distance for the Legend. */
public int legendPerspective = 3;
/** Flag that indicates if the axis must be drawn. Defaults to true. */
protected boolean drawAxis=true;
/** Shows the first y value or not. */
public boolean showFirstYValue = true;
/** Color used in the axis lines. */
public int axisForeColor; // black
/** Color used in the axis lines. */
public int axisBackColor = -1;
/** Color used in the axis text (x,y values). */
public int axisText; // black
/** Values that may be shown with the legend. */
protected String[] legendValues; // guich@tc110_78
protected Rect clientRect = new Rect();
private String[] seriesNames = new String[0];
private Insets ci = new Insets();
/** Set to true to let a ChartData be snapped at the bottom. */
public boolean snapToBottom;
/** Set to true to let a ChartData be snapped at the top. */
public boolean snapToTop;
/** Defines a value that will be used as the y-value width.
* @since TotalCross 2.0
*/
public int yValuesSize;
/** The text color for the legend. */
public int legendTextColor; // black
public int fillColor2 = -1;
public int use2ndColorEveryXColumns = 1;
public boolean onlyShowCategories;
public int columnW;
/**
* Sets this chart's title
* @param title the new title
*/
public void setTitle(String title)
{
this.title = title;
}
/**
* @return this chart's title
*/
public String getTitle()
{
return title;
}
/**
* Sets the X axis as a value axis given the minimum (its origin) and
* the maximum values to be used
* @param min the minimum value (the axis origin)
* @param max the maximum value (the axis end)
* @param steps the number of subdivisions of the axis
*/
public void setXAxis(double min, double max, int steps)
{
// xAxisCategories = null; // disable categories
xAxisMinValue = min;
xAxisMaxValue = max;
xAxisSteps = steps;
}
/**
* Sets the X axis as a category axis
* @param categories the categories' names
*/
public void setXAxis(String[] categories)
{
xAxisCategories = categories;
xAxisMinValue = 0;
xAxisMaxValue = categories.length;
xAxisSteps = categories.length;
}
/**
* Sets the Y axis given the minimum (its origin) and the maximum values
* to be used
* @param min the minimum value (the axis origin)
* @param max the maximum value (the axis end)
* @param steps the number of subdivisions of the axis
*/
public void setYAxis(double min, double max, int steps)
{
yAxisMinValue = Math.min(min,max);
yAxisMaxValue = Math.max(min,max);
yAxisSteps = steps;
}
private int getTotalSize(String []names)
{
int t = 0;
for (int i =0; i < names.length; i++)
t += fm.stringWidth(names[i]);
return t;
}
protected void getCustomInsets(Insets r)
{
}
public static final int UNSET = -9999999;
int markPos = UNSET;
/**
* Draws the chart's basic features.
* @param g The graphics object.
*/
protected boolean draw(Graphics g)
{
boolean is3d = (type & IS_3D) != 0;
int sMaxLen = 0;
int sCount = series.size();
if (sCount != seriesNames.length)
seriesNames = new String[sCount];
for (int i = 0; i < sCount; i ++)
seriesNames[i] = ((Series) series.items[i]).name;
boolean drawTitle = showTitle && title != null;
boolean drawCategories = showCategories && xAxisCategories != null;
boolean drawLegend = showLegend && sCount > 0;
int top = snapToTop ? 0 : border.top;
int left = border.left;
int bottom = snapToBottom ? 0 : border.bottom;
int right = border.right;
double incY = (yAxisMaxValue - yAxisMinValue) / yAxisSteps;
boolean lr = legendPosition == LEFT || legendPosition == RIGHT;
int sqWH = fmH - 6, sqOff = (fmH - sqWH) / 2,xx;
if (drawTitle && !snapToTop)
top += fmH;
if (drawLegend)
{
sMaxLen += lr ? fm.getMaxWidth(seriesNames, 0, sCount) : (sqWH + 4) * sCount + getTotalSize(seriesNames);
if (legendValues != null)
sMaxLen += lr ? fm.getMaxWidth(legendValues, 0, sCount) : getTotalSize(legendValues);
int ww = sMaxLen + 32;
if (is3d)
ww += Math.abs(legendPerspective);
switch (legendPosition)
{
case RIGHT:
right += ww;
break;
case LEFT:
if (yValuesSize <= 0)
left += ww;
break;
case TOP:
top += fmH+6;
if (is3d)
top += Math.abs(legendPerspective);
break;
case BOTTOM:
bottom += fmH+6;
if (is3d)
bottom += Math.abs(legendPerspective);
break;
}
}
ci.top = ci.bottom = ci.left = ci.right = 0;
getCustomInsets(ci);
top += snapToTop ? 0 : ci.top;
bottom += snapToBottom ? 0 : ci.bottom;
left += ci.left;
right += ci.right;
if (showYValues)
{
int yvalW = yValuesSize;
for (double v = yAxisMinValue; v <= yAxisMaxValue; v += incY)
yvalW = Math.max(yvalW , fm.stringWidth(Convert.toCurrencyString(v,yDecimalPlaces)));
left += yvalW+3;
top += snapToTop ? 0 : fm.ascent/2;
bottom += snapToBottom ? 0 : fm.ascent/2;
if (drawCategories)
bottom += fmH/2-1;
}
else
if (drawCategories)
bottom += snapToBottom ? 0 : fmH - 3;
xAxisX1 = left + 3;
if (xAxisX1 < 0 || xAxisX1 >= width) // validate
return false;
xAxisX2 = width - right - 1;
if (xAxisX2 < 0 || xAxisX2 >= width || xAxisX2 <= xAxisX1) // validate
return false;
yAxisY1 = height - bottom - (snapToBottom ? 0 : 4);
if (!onlyShowCategories && (yAxisY1 < 0 || yAxisY1 > height)) // return false;
return false;
yAxisY2 = top;
if (!onlyShowCategories && (yAxisY2 < 0 || yAxisY2 >= height || yAxisY2 >= yAxisY1)) // validate
return false;
double inc = (xAxisMaxValue - xAxisMinValue) / xAxisSteps;
double val = xAxisMinValue;
columnW = getXValuePos(val+inc) - getXValuePos(val);
g.foreColor = axisForeColor;
if (!transparentBackground)
{
g.backColor = backColor;
g.fillRect(0,0,width,height);
if (axisBackColor != -1 && (drawCategories || showYValues))
{
g.backColor = axisBackColor;
g.fillRect(xAxisX1,yAxisY2,xAxisX2-xAxisX1,yAxisY1-yAxisY2);
}
}
if (!onlyShowCategories && drawAxis)
{
g.drawLine(xAxisX1, yAxisY1, xAxisX2, yAxisY1); // draw X axis
g.drawLine(xAxisX1, yAxisY2, xAxisX1, yAxisY1); // draw Y axis
}
int lastPos = UNSET;
markPos = UNSET;
if (fillColor2 != -1)
{
double x0 = val + inc * use2ndColorEveryXColumns;
g.backColor = fillColor2;
for (int j = 1, n = xAxisSteps; j <= n; j+=2, x0 += inc * use2ndColorEveryXColumns * 2) // vertical lines
g.fillRect(xx = getXValuePos(x0),yAxisY2,getXValuePos(x0+inc*use2ndColorEveryXColumns)-xx,yAxisY1-yAxisY2);
}
val = xAxisMinValue;
for (int i = 0; i <= xAxisSteps; i++, val += inc)
{
int pos = getXValuePos(val);
if (!onlyShowCategories && drawAxis && !snapToBottom) g.drawLine(pos, yAxisY1, pos, yAxisY1 + 3);
if (!onlyShowCategories && showVGrids && pos != xAxisX1) // draw vertical grids
g.drawDots(pos, yAxisY1, pos, yAxisY2);
if (drawCategories && i < xAxisCategories.length) // draw category
{
final int tW = (xAxisCategories[i] == null || xAxisCategories[i].length() == 0)
? 0
: fm.stringWidth(xAxisCategories[i]);
if (tW > 0)
{
if (showCategoriesOnTick)
{
final int p = pos - (tW / 2);
final int y1 = height - (fmH * 3 / 4);
if ("|".equals(xAxisCategories[i]))
g.drawLine(pos, y1, pos, y1 + fmH);
else if (":".equals(xAxisCategories[i]))
{
g.backColor = axisBackColor;
g.drawDots(pos, y1, pos, y1 + fmH);
}
else if (p > lastPos)
{
g.foreColor = axisText;
g.drawText(xAxisCategories[i], p, onlyShowCategories ? (height-fmH)/2 : yAxisY1, textShadowColor != -1, textShadowColor);
g.foreColor = axisForeColor;
lastPos = p + tW + fmH;
}
if (categoryMarkIndex == i)
markPos = pos;
pos += ((getXValuePos(val + inc) - pos) - tW) / 2;
}
else
{
pos += ((getXValuePos(val + inc) - pos) - tW) / 2;
g.foreColor = axisText;
g.drawText(xAxisCategories[i], pos, yAxisY1, textShadowColor != -1, textShadowColor);
g.foreColor = axisForeColor;
}
}
}
}
if (onlyShowCategories)
return true;
if (drawCategories && categoryMarkIndex >= 0 && markPos != UNSET)
{
g.foreColor = categoryMarkColor;
g.drawLine(markPos-1,yAxisY1,markPos-1,yAxisY2);
g.drawLine(markPos,yAxisY1,markPos,yAxisY2);
g.drawLine(markPos+1,yAxisY1,markPos+1,yAxisY2);
}
val = yAxisMinValue;
// adjust the number of steps depending on the number's height
int ySteps = yAxisSteps;
while (ySteps > 1 && showYValues && (getYValuePos(incY)-getYValuePos(incY*2)) < fm.ascent)
{
ySteps--;
incY = (yAxisMaxValue - yAxisMinValue) / ySteps;
}
for (int i = 0; i <= ySteps; i ++, val += incY)
{
int pos = getYValuePos(val);
if (i == 0 && !showFirstYValue)
;
else
if ((!snapToBottom || i != 0) && (!snapToTop || i != ySteps))
{
if (drawAxis) g.drawLine(xAxisX1, pos, xAxisX1 - 3, pos);
if (showYValues)
{
String s = Convert.toCurrencyString(val,yDecimalPlaces);
g.foreColor = axisText;
g.drawText(s, xAxisX1 - fm.stringWidth(s) - 3, pos-fmH/2, textShadowColor != -1, textShadowColor);
g.foreColor = axisForeColor;
}
}
if (showHGrids && pos != yAxisY1) // draw horizontal grids
g.drawDots(xAxisX1, pos, xAxisX2, pos);
}
if (drawTitle)
{
Font fp = this.font;
Font fb = fp.asBold();
g.setFont(fb);
g.drawText(title, (width - fb.fm.stringWidth(title)) / 2, 0, textShadowColor != -1, textShadowColor);
g.setFont(fp); // return to previous font
}
if (drawLegend)
{
int x,y,w,h;
if (lr)
{
w = sMaxLen + sqWH + 10;
h = fmH * sCount;
x = legendPosition == RIGHT ? xAxisX2 + 5 : 0;
y = yAxisY2 + (yAxisY1 - yAxisY2 - h) / 2;
}
else
{
w = sMaxLen + 6;
h = fmH + 2;
x = (this.width-w)/2;
if (legendPosition == TOP)
y = drawTitle ? fmH+3 : 0;
else
y = this.height - (fmH + 2 + (is3d ? Math.abs(legendPerspective):0));
}
if (is3d)
{
if (legendPerspective < 0 && lr)
x -= legendPerspective;
g.backColor = Color.interpolate(backColor,0);
g.fillRect(x + legendPerspective, y + Math.abs(legendPerspective), w, h);
}
g.backColor = backColor;
g.fillRect(x, y, w, h);
g.drawRect(x, y, w, h);
x += snapToBottom || snapToTop ? 0 : 3;
g.foreColor = legendTextColor;
int halfBlankWidth = fm.stringWidth(" ") / 2;
for (int i = 0; i < sCount; i ++)
{
Series se = (Series) series.items[i];
if (se.dot != null)
{
if (se.legendDot == null)
try {se.legendDot = se.dot.smoothScaledFixedAspectRatio(sqWH - halfBlankWidth,true);} catch (Exception e) {se.legendDot = se.dot;}
g.drawImage(se.legendDot,x + halfBlankWidth,y+sqOff + 1);
}
else
{
g.backColor = se.color;
g.fillRect(x, y + sqOff, sqWH, sqWH);
g.drawRect(x, y + sqOff, sqWH, sqWH);
}
String s = seriesNames[i];
if (legendValues != null)
s = s.concat(legendValues[i]);
g.drawText(s, x + halfBlankWidth + sqWH + halfBlankWidth, y, textShadowColor != -1, textShadowColor);
if (lr)
y += fmH;
else
x += fm.stringWidth(s)+4+sqWH;
}
g.backColor = backColor; // back to original back color
}
clientRect.set(left, top, this.width - right-left, this.height - top-bottom);
return true;
}
/**
* Draws a box to display a text in the chart area
* @param text the text to be displayed
* @param refX the X position of the anchor point
* @param refY the Y position of the anchor point
*/
protected void drawTextBox(Graphics g, int refX, int refY, String text)
{
Font f = this.font;
int boxW = f.fm.stringWidth(text) + 4;
int boxH = f.fm.height + 2;
int boxX = refX;
int boxY = refY - boxH - 2;
int dif = (boxX + boxW) - width;
if (dif > 0)
boxX -= dif;
dif = (boxY + boxH) - height;
if (dif > 0)
boxY -= dif;
if (boxX < 0)
boxX = 0;
if (boxY < 0)
boxY = 0;
g.foreColor = Color.BLACK;
g.backColor = backColor;
g.fillRect(boxX, boxY, boxW, boxH);
g.drawRect(boxX, boxY, boxW, boxH);
g.drawText(text, boxX + 2, boxY + 1, textShadowColor != -1, textShadowColor);
}
/**
* Calculates the screen position of a value in the X axis according to
* its current minimum and maximum values. This method assumes that the
* X axis has the same dimensions that it assumed on the last call to
* <code>draw</code>
* @param value the value
* @return the screen position of the value
* @see #draw(Graphics)
*/
public int getXValuePos(double value)
{
return xAxisX1 + (int)Math.round((value - xAxisMinValue) / (xAxisMaxValue - xAxisMinValue) * (xAxisX2 - xAxisX1));
}
/**
* Calculates the screen position of a value in the Y axis according to
* its current minimum and maximum values. This method assumes that the
* Y axis has the same dimensions that it assumed on the last call to
* <code>draw</code>
* @param value the value
* @return the screen position of the value
* @see #draw(Graphics)
*/
public int getYValuePos(double value)
{
return yAxisY1 - (int)Math.round((value - yAxisMinValue) / (yAxisMaxValue - yAxisMinValue) * (yAxisY1 - yAxisY2));
}
}