/*********************************************************************************
* 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.chart.Series;
import totalcross.ui.event.*;
import totalcross.ui.font.*;
import totalcross.ui.gfx.*;
/** A simple pie chart.
* <br><br>
* The values do not have to be in percentage; the percentage is computed based on the series values.
* If the user clicks on the slice, a popup shows the corresponding value.
* By setting the legendValueSuffix to "%", the value displayed will be the percentage instead of the
* serie's value.
*/
public class PieChart extends Chart
{
/** Specifies the distance that the selected pie will be placed from the rest of the pie. Defaults to fmH. */
public int distanceOfSelectedPie = Font.NORMAL_SIZE;
/** Specifies the selected pie. */
public int selectedSeries=-1;
/** The suffix used in the legend to display the values. E.G.: "%". Defaults to blank.
*/
public String legendValueSuffix = "";
/** Set to true to show the values in the legend. */
public boolean showValuesOnLegend;
/** Perspective horizontal distance. */
public int perspectiveH = Font.NORMAL_SIZE/2;
/** Perspective vertical distance. */
public int perspectiveV = Font.NORMAL_SIZE/2;
/** GAO: keeps track of the currently selected slice */
public int selectedSlice = -1;
/** GAO: if true, then offset selected pie slice, for visual indicator that its been selected */
public boolean offsetSelectedSlice = true;
private ToolTip tip;
private int lastPenX,lastPenY;
private static Coord c = new Coord();
private Rect rect = new Rect();
private double sum;
private int currentSelection=-1;
private int xx,yy,rr;
/**
* Creates a new Pie chart.
* The yDecimalPlaces defines the number of decimal places used to display the value in the legend.
*/
public PieChart()
{
drawAxis = false;
setXAxis(0, 100, 1);
setYAxis(0, 100, 1);
tip = new ToolTip(this, "");
tip.setBackColor(Color.WHITE);
tip.millisDelay = 50;
tip.borderColor = 0;
}
protected void getCustomInsets(Insets r)
{
if ((type & IS_3D) != 0)
{
if (perspectiveV > 0)
r.bottom = perspectiveV;
else
r.top = -perspectiveV;
if (perspectiveH < 0)
r.left = -perspectiveH;
else
r.right = perspectiveH;
}
if (currentSelection != -1)
r.bottom += distanceOfSelectedPie;
}
public void onPaint(Graphics g)
{
// compute sum and the values
sum = 0;
int sCount = series.size();
if (showValuesOnLegend && (legendValues == null || legendValues.length != sCount))
legendValues = new String[sCount];
for (int i = 0; i < sCount; i ++) // for each series
{
double v = ((Series)series.items[i]).yValues[0];
sum += v;
if (showValuesOnLegend)
legendValues[i] = " "+Convert.toCurrencyString(v,yDecimalPlaces) + legendValueSuffix;
}
if (!draw(g)) // draw axis, title, etc
return;
// Update points
int xx = clientRect.x + clientRect.width/2;
int yy = clientRect.y + clientRect.height/2;
int rr = Math.min(clientRect.width, clientRect.height)/2 - distanceOfSelectedPie;
if (rr > 0)
{
this.rr = rr;
if ((type & IS_3D) != 0)
drawPie(g, xx+perspectiveH, yy+perspectiveV, rr, true);
drawPie(g, xx, yy, rr, false);
}
}
private void drawPie(Graphics g, int xx, int yy, int rr, boolean is3d)
{
if (sum == 0) // juliana@168: an empty chart was drawing spurious lines.
return;
g.foreColor = 0;
int sCount = series.size();
double last=0,current;
this.xx = xx;
this.yy = yy;
for (int i = 0; i < sCount; i++) // for each series
{
// juliana@268: it is necessary to save the old positions to correctly offset the selected pie.
xx = this.xx;
yy = this.yy;
Series s = (Series) series.items[i];
int color = i == currentSelection ? Color.darker(s.color,32) : is3d ? Color.darker(s.color) : s.color;
//if (is3d) color = Color.interpolate(backColor,color);
double v = s.yValues[0];
current = last+(v*360/sum);
if (i == selectedSeries)
{
int half = (int)(last+(v/2*360/sum));
g.getAnglePoint(xx, yy, distanceOfSelectedPie, distanceOfSelectedPie, half, c);
xx = c.x;
yy = c.y;
}
if (last == current)
;
else
if ((type & GRADIENT_VERTICAL) != 0)
{
int fade = (type & GRADIENT_DARK) != 0 ? Color.darker(color,128) : Color.brighter(color,128);
g.backColor = is3d ? color : ((type & GRADIENT_INVERT) != 0) ? color : fade;
g.foreColor = is3d ? g.backColor : ((type & GRADIENT_INVERT) != 0) ? fade : color;
g.fillPieGradient(xx, yy, rr, last, current);
if (!is3d)
{
g.foreColor = 0;
g.drawPie(xx, yy, rr, last, current);
}
}
else
{
g.foreColor = is3d || sum == v ? color : 0; // fixed color when only 1 serie has value > 0
g.backColor = color;
g.fillPie(xx, yy, rr, last, current);
Window.safeUpdateScreen();
}
last = current;
}
}
public void onEvent(Event e)
{
switch (e.type)
{
case PenEvent.PEN_DOWN:
case PenEvent.PEN_DRAG:
{
PenEvent pe = (PenEvent)e;
lastPenX = pe.x;
lastPenY = pe.y;
break;
}
case ControlEvent.PRESSED:
if (e.target == tip)
{
if (currentSelection != -1)
setTipText((Series)series.items[currentSelection]);
else
{
// get the angle
int deltax = lastPenX - xx;
int deltay = lastPenY - yy;
int distance = (int)Math.sqrt(deltax*deltax + deltay*deltay);
double tan;
try {tan = (double)deltay / (double)deltax;} catch (ArithmeticException e1) {tan = 0;} // guich@tc123_4: prevent divide by 0 to close the program
double degree = Math.atan(tan) * 180 / Math.PI;
if (degree < 0) degree = -degree;
if (deltax >= 0)
{
if (deltay > 0) degree = 360 - degree;
}
else
if (deltay < 0)
degree = 180 - degree;
else
degree += 180;
// find the slice that contains this angle
if (sum == 0) break;
int sCount = series.size(),i;
double last=0,current;
for (i = 0; i < sCount; i++) // for each series
{
Series s = (Series) series.items[i];
double v = s.yValues[0];
current = last+(v*360/sum);
if (last <= degree && degree <= current)
{
int r = i == selectedSeries ? (rr + distanceOfSelectedPie) : rr;
if (r < distance) // outside the pie?
i = sCount; // don't show anything
else {
setTipText(s);
if (offsetSelectedSlice)
this.selectedSeries = i; // make the slice user tapped on be the 'selected' one
selectedSlice = i; // field so after receiving a Pie event, can retrieve selected slice
}
break;
}
last = current;
}
if (i == sCount) // not found?
tip.setText("");
}
}
break;
case ControlEvent.FOCUS_IN:
lastPenX = width/8-10;
lastPenY = 0-10;
currentSelection = -1; // don't change!
Window.needsPaint = true;
break;
case ControlEvent.FOCUS_OUT:
currentSelection = -1;
Window.needsPaint = true;
break;
case KeyEvent.SPECIAL_KEY_PRESS:
{
KeyEvent ke = (KeyEvent)e;
if (ke.key == SpecialKeys.ACTION || ke.key == SpecialKeys.ENTER)
{
parent.setHighlighting();
tip.penUp(null);
}
break;
}
}
}
private void setTipText(Series s)
{
tip.setText(Convert.toCurrencyString(legendValueSuffix.indexOf('%') >= 0 ? (s.yValues[0]/sum*100) : s.yValues[0],yDecimalPlaces) + legendValueSuffix);
Rect r = getAbsoluteRect();
rect.set(r.x+lastPenX+10,r.y+lastPenY+10,0,0);
tip.setControlRect(rect);
}
public void onFontChanged()
{
tip.setFont(this.font);
}
}