Tuesday, December 7, 2010

Bar Chart

Cros-posted at Technè - Blog de Tecnologia do C.E.S.A.R.

When developing a mobile application one must consider screen size fragmentation. If Apple have only two screen sizes for iPhone until now, this is the exception. All other manufacturers vary the screen sizes a lot.

To make developers life easier each technology provides widgets and ways to combine them. But there are times when you need an specific look and fell. What to do? Low level graphics. 2D, 3D, does not matter.

Even if you only have one target screen size it is a good practice to code using proportions. Avoid absolute values of x, y, width and height. Here we show how to do this with a BarChart and Java ME.

It is good to point that there already are components that do this. Our main point here is to present good low level visual programming.

First thing is to have a class for our data.


class ChartEntry {
String name;
float value;
public BarChartEntry(String name, float value) {
this.name = name;
this.value = value;
}
}

And another class to draw the data. Values on top of the bars and labels at the bottom. But if an entry has negative value we do the opposite and draw the value at the bottom.

public class BarChart {
private Vector entries = new Vector();
private float maxValue;
private float minValue;
private String widestName = "";
private int firstBar;

// Every time an entry is added we check for the limits
public void addEntry(BarChartEntry entry) {
entries.addElement(entry);
if (maxValue < entry.value) {
maxValue = entry.value;
}
if (minValue > entry.value) {
minValue = entry.value;
}
if (widestName.length() < entry.name.length()) {
widestName = entry.name;
}
}

public void nextBar () {
this.setFirstBar(this.firstBar +1);
}
public void previousBar () {
this.setFirstBar(this.firstBar -1);
}
private void setFirstBar(int firstBar) {
if (firstBar >= 0 && firstBar < this.entries.size())
this.firstBar = firstBar;
}

// similar to Graphics.drawRect.
// x and y is the top left corner of a rectangle
// w is for width and h is for height
public void draw(Graphics g, int x, int y, int w, int h) {
Font font = g.getFont();
// 2 = value and label strings drawn at top and bottom
int availableHeight = h - (2 * font.getHeight());
int barWidth = font.stringWidth(widestName);
// empty space between bars
int barMargin = w / 25;
int maxBars = (int) Math.floor(w / (barWidth + barMargin));
float valuesRange = maxValue - minValue;
int barOrigin = y + h - font.getHeight();
if (minValue < 0) {
// move origin line up
barOrigin = barOrigin
+ (int) ((availableHeight * minValue) / valuesRange);
}

g.drawLine(x, barOrigin, x + w, barOrigin);

// draw chart entries
BarChartEntry drawEntries [] = new BarChartEntry[this.entries.size()];
int barX = x;
int barCount = 0;
this.entries.copyInto(drawEntries);
for (int i = 0; i < maxBars && this.firstBar + i < drawEntries.length; i++) {
BarChartEntry chartEntry = drawEntries[this.firstBar + i];
// we get current bar height with a simple rule of three
// availableHeight -> valuesRange
// barHeight -> chartEntry.value
int barHeight = (int) ((availableHeight * chartEntry.value) / valuesRange);
int textCenterX = barX + barMargin + (barWidth / 2);
if (barHeight > 0) {
g.drawString(chartEntry.name, textCenterX, barOrigin,
Graphics.HCENTER | Graphics.TOP);
g.fillRect(barX + barMargin, barOrigin - barHeight, barWidth,
barHeight);
g.drawString(String.valueOf(chartEntry.value), textCenterX,
barOrigin - barHeight, Graphics.HCENTER
| Graphics.BOTTOM);
} else {
g.drawString(chartEntry.name, textCenterX, barOrigin,
Graphics.HCENTER | Graphics.BOTTOM);
g.fillRect(barX + barMargin, barOrigin, barWidth, -barHeight);
g.drawString(String.valueOf(chartEntry.value), textCenterX,
barOrigin - barHeight, Graphics.HCENTER | Graphics.TOP);
}
barX = barX + barMargin + barWidth;
barCount++;
}
}
}

Below is a Canvas that shows a bar chart and changes the first bar shown according to pressed keys.

public class BarChartCanvas extends Canvas {

BarChart barChart = new BarChart();

public BarChartCanvas() {
barChart.addEntry(new BarChartEntry("J", 8.0f));
barChart.addEntry(new BarChartEntry("FE", 10.0f));
barChart.addEntry(new BarChartEntry("MAR", 6.0f));
barChart.addEntry(new BarChartEntry("ABRI", 9.0f));
barChart.addEntry(new BarChartEntry("MAIO", -2.0f));
barChart.addEntry(new BarChartEntry("JUNHO", -4.0f));
barChart.addEntry(new BarChartEntry("JULHO", 6.0f));
barChart.addEntry(new BarChartEntry("AGOSTO", 9.0f));
}

protected void paint(Graphics g) {
g.setColor(0xffffff);
g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(0);
barChart.draw(g, 0, 0, getWidth(), getHeight());
}

protected void keyPressed(int keyCode) {
switch (keyCode) {
case Canvas.KEY_NUM2:
case Canvas.KEY_NUM4:
barChart.previousBar();
break;
case Canvas.KEY_NUM6:
case Canvas.KEY_NUM8:
barChart.nextBar();
break;
default:
switch (super.getGameAction(keyCode)) {
case Canvas.LEFT:
case Canvas.UP:
barChart.previousBar();
break;
case Canvas.RIGHT:
case Canvas.DOWN:
barChart.nextBar();
break;
}
break;
}
this.repaint();
}
}

And the MIDlet used to show this Canvas.

public class BarChartMIDlet extends MIDlet implements
CommandListener {

BarChartCanvas chartCanvas = new BarChartCanvas();

public BarChartMIDlet() {
chartCanvas.addCommand(new Command("Exit", Command.EXIT, 1));
chartCanvas.setCommandListener(this);
}

protected void destroyApp(boolean unconditional) { }

protected void pauseApp() { }

protected void startApp() {
Display.getDisplay(this).setCurrent(chartCanvas);
}

public void commandAction(Command c, Displayable d) {
this.notifyDestroyed();
}

}

Below are screens of this application running on DefaultCldcPhone1 emulator of Java Platform Micro Edition SDK 3.0.






As expected in landscape mode we can draw one more bar. We can change the screen orientation with menu View .. Orientation.
We hope this helps.

No comments: