uses data.String
data Sample {
dec values[]
char cat[]
}
data Series {
Sample samples[]
char name[]
char displayName[]
Color seriesColor
}
//marker length in pixels (the "ticks" on the axes)
const int MARKER_LENGTH = 3
const int MARKER_SIZE = 7
const int LEGEND_SQ_SIZE = 10
const int LEGEND_PAD_SIZE = 5
component provides CategoryMulti:barH requires ChartCore, stats.StatCore stcore, io.Output out, data.IntUtil iu, data.DecUtil du, ui.Font, data.query.Search search {
Series series[]
String categories[]
Color stdDevColor = new Color(0, 0, 0, 255)
Color axisColor = new Color(0, 0, 0, 255)
Color gridColor = new Color(220, 220, 220, 255)
Color bgColor = new Color(255, 255, 255, 255)
Font axisFont
Font labelFont
dec highestX
dec lowestX
dec highestY
dec lowestY
dec numberIntervalX = 1.0
dec markerIntervalX = 1.0
dec numberIntervalY
dec markerIntervalY
dec gridIntervalY
dec xAxMax
dec xAxMin
dec yAxMax
dec yAxMin
//bar width, in percentage of category area
dec barWidth = 50.0
int barPad = 0
bool showStdDev
bool showLegend
byte legendPosition = CategoryMulti.L_INSIDE
byte legendModX = 0
byte legendModY = 10
int originWidth = 0
int originHeight = 0
CategoryMulti:CategoryMulti()
{
super()
axisFont = new Font("SourceSansPro.ttf", 12)
setYMarkerInterval(1.0)
}
void CategoryMulti:setSize(int w, int h)
{
originWidth = w
originHeight = h
if (legendPosition == CategoryMulti.L_OUTSIDE)
{
int q = getLongestSeriesName() + LEGEND_SQ_SIZE + LEGEND_SQ_SIZE + LEGEND_SQ_SIZE
w -= q
}
super(w, h)
}
void CategoryMulti:addSeries(char name[], opt store Color color)
{
if (color == null) color = new Color(100, 200, 200, 255)
series = new Series[](series, new Series(null, name, name, color))
}
void CategoryMulti:addSample(char seriesName[], char cat[], dec yvalues[], opt bool redraw)
{
Series n = series.findFirst(Series.[name], new Series(name = seriesName))
if (n == null)
{
throw new Exception("unknown series '$seriesName'; series must first be added using addSeries()")
}
//update our highest and lowest X and Y points, to use in normalising coordinates
dec hy = stcore.max(yvalues)
dec ly = stcore.min(yvalues)
if (categories.find(String.[string], new String(cat)) == null)
categories = new String[](categories, new String(cat))
yAxMax += 1.0
if (series.arrayLength == 1 && n.samples == null)
{
highestX = hy
lowestX = ly
}
else
{
if (hy > highestX) highestX = hy
if (ly < lowestX) lowestX = ly
}
//add sample
n.samples = new Sample[](n.samples, new Sample(yvalues, cat))
// -- automated calculation of the highest and lowest points on the axis --
//update the axis endpoints, if the series values are now outside the bounds of whatever endpoints have been set
if (highestX > xAxMax) xAxMax = highestX
if (lowestX < xAxMin) xAxMin = lowestX
setXMinMax(xAxMin, xAxMax)
setYMinMax(yAxMin, yAxMax)
if (redraw) postRepaint()
}
void CategoryMulti:setYMinMax(dec min, dec max)
{
//TODO: disallow values that can't contain all of the graph points...
yAxMin = min
yAxMax = max
super(min, max)
}
void CategoryMulti:setSeriesColor(char seriesName[], Color c)
{
Series n = series.find(Series.[name], new Series(null, seriesName))[0]
n.seriesColor = c
}
void CategoryMulti:setSeriesName(char seriesName[], char displayName[])
{
Series n = series.find(Series.[name], new Series(null, seriesName))[0]
n.displayName = displayName
}
void CategoryMulti:showErrorBars(bool on)
{
showStdDev = on
}
void CategoryMulti:clampErrorBars(dec low, dec high)
{
}
void CategoryMulti:showLegend(bool on)
{
showLegend = on
}
void CategoryMulti:setLegendPosition(byte type, opt int x, int y)
{
legendPosition = type
if (isset x) legendModX = x
if (isset y) legendModY = y
setSize(originWidth, originHeight)
postRepaint()
}
void CategoryMulti:setAxisFont(Font f)
{
axisFont = f
super(f)
}
void CategoryMulti:setCatDisplayWidth(dec percent)
{
if (percent <= 0.0 || percent > 100.0)
throw new Exception("display width must be a percentage, between 1 and 100")
barWidth = percent
}
void CategoryMulti:setCatDisplayPadding(int pixels)
{
barPad = pixels
}
bool prepAxisLimits()
{
dec highX
dec lowX
dec highY
dec lowY
for (int i = 0; i < categories.arrayLength; i++)
{
dec barTop = 0.0
dec barLow = 0.0
for (int j = 0; j < series.arrayLength; j++)
{
Sample s[] = series[j].samples.find(Sample.[cat], new Sample(null, categories[i].string))
if (s != null)
{
//add it to the bar, by going on to the top or below the bottom, depending on whether it's positive or negative...
dec mean = stcore.mean(s[0].values)
if (showStdDev)
{
dec stdDev = stcore.stdDev(s[0].values)
if (mean - stdDev < lowX) lowX = mean - stdDev
if (mean + stdDev > highX) highX = mean + stdDev
}
else
{
if (mean > highX) highX = mean
if (mean < lowX) lowX = mean
}
}
}
highY += 1.0
}
if (lowX == highX || lowY == highY) return false
//NOTE: we could now calculate end "endpoint" on each axis which is a multiple of their number interval
setXMinMax(lowX, highX)
setYMinMax(lowY, highY)
return true
}
int getLongestSeriesName()
{
int result = 0
for (int i = 0; i < series.arrayLength; i++)
{
int wd = axisFont.getTextWidth(series[i].displayName)
if (wd > result) result = wd
}
return result
}
void drawLegend(Canvas c)
{
//calculate the longest series name, and offset the legend by this much from the right (plus the legend color square width)
int longestTxt = getLongestSeriesName()
int yPos = legendModY
int xStart
if (legendPosition == CategoryMulti.L_INSIDE || legendPosition == CategoryMulti.L_OUTSIDE)
xStart = (originWidth - (longestTxt + LEGEND_SQ_SIZE + LEGEND_SQ_SIZE + LEGEND_SQ_SIZE)) + legendModX
else if (legendPosition == CategoryMulti.L_ABSOLUTE)
xStart = legendModX
int textHeight = axisFont.getFontMetrics().height
int totalLegendHeight = ((textHeight + 5) * (series.arrayLength-1)) + textHeight
int totalLegendWidth = (longestTxt + LEGEND_SQ_SIZE + LEGEND_SQ_SIZE)
c.rect(new Rect2D(xStart - LEGEND_PAD_SIZE, yPos - LEGEND_PAD_SIZE, totalLegendWidth + (LEGEND_PAD_SIZE*2), totalLegendHeight + (LEGEND_PAD_SIZE*2), bgColor))
c.rectOutline(new Rect2D(xStart - LEGEND_PAD_SIZE, yPos - LEGEND_PAD_SIZE, totalLegendWidth + (LEGEND_PAD_SIZE*2), totalLegendHeight + (LEGEND_PAD_SIZE*2), axisColor))
yPos += (axisFont.getFontMetrics().descent)
for (int i = series.arrayLength - 1; i != INT_MAX; i--)
{
c.rect(new Rect2D(xStart, yPos, LEGEND_SQ_SIZE, LEGEND_SQ_SIZE, series[i].seriesColor))
int yOffset = (yPos + (LEGEND_SQ_SIZE / 2)) - (textHeight / 2)
c.text(new Point2D(xStart + LEGEND_SQ_SIZE + LEGEND_SQ_SIZE, yOffset, axisColor), axisFont, series[i].displayName)
yPos += textHeight + 5
}
}
int getMaxCatTextWidth()
{
int high = 0
for (int i = 0; i < categories.arrayLength; i++)
{
int textWidth = axisFont.getTextWidth(categories[i].string)
if (textWidth > high) high = textWidth
}
return high
}
void CategoryMulti:paint(Canvas c)
{
if (!prepAxisLimits())
{
throw new Exception("chart has no data to plot (min and max data point values are equal)")
}
setAxisLabelSpace(ChartCore.AXIS_Y, getMaxCatTextWidth())
Point pos = getPosition()
c.pushSurface(new Rect(pos.x, pos.y, originWidth, originHeight), 0, 0, 255)
preparePlotArea()
//background
c.rect(new Rect2D(0, 0, originWidth, originHeight, bgColor))
//grid lines, if any
drawGrid(c)
//data
int ySpacing = ((getPlotPoint(0.0, 0.0)[1] - getPlotPoint(0.0, 1.0)[1]+1) - barPad)
int totalBarHeightPX = (barWidth / 100.0) * ySpacing
int barHeightPX = totalBarHeightPX / series.arrayLength
dec thisY = 0.0
for (int i = 0; i < categories.arrayLength; i++)
{
int xyZ[] = getPlotPoint(0.0, thisY)
int centreY = xyZ[1] - ((xyZ[1] - getPlotPoint(0.0, thisY+1.0)[1]) / 2)
centreY -= totalBarHeightPX / 2
centreY += barHeightPX / 2
for (int j = 0; j < series.arrayLength; j++)
{
Sample s[] = series[j].samples.find(Sample.[cat], new Sample(null, categories[i].string))
if (s != null)
{
//add it to the bar, by going on to the top or below the bottom, depending on whether it's positive or negative...
dec mean = stcore.mean(s[0].values)
if (mean > 0.0)
{
int xyTop[] = getPlotPoint(0.0, thisY+1.0) //top-left
int xyBot[] = getPlotPoint(mean, thisY) //bottom-right
c.rect(new Rect2D(xyTop[0], (centreY - (barHeightPX/ 2))+barPad, xyBot[0] - xyTop[0], barHeightPX, series[j].seriesColor))
}
else
{
int xyTop[] = getPlotPoint(mean, thisY+1.0) //top-left
int xyBot[] = getPlotPoint(0.0, thisY) //bottom-right
c.rect(new Rect2D(xyTop[0], (centreY - (barHeightPX / 2))+barPad, xyBot[0] - xyTop[0], barHeightPX, series[j].seriesColor))
}
if (showStdDev)
{
dec thisX = mean
dec stdDev = stcore.stdDev(s[0].values)
if (stdDev != 0.0)
{
int xyHigh[] = getPlotPoint(thisX+stdDev, thisY)
int xyLow[] = getPlotPoint(thisX-stdDev, thisY)
c.line(new Line2D(xyHigh[0], centreY, xyLow[0], centreY, stdDevColor))
c.line(new Line2D(xyHigh[0], centreY-MARKER_SIZE/2, xyHigh[0], centreY+MARKER_SIZE/2, stdDevColor))
c.line(new Line2D(xyLow[0], centreY-MARKER_SIZE/2, xyLow[0], centreY+MARKER_SIZE/2, stdDevColor))
}
}
}
centreY += barHeightPX
}
//category label
centreY = xyZ[1] - ((xyZ[1] - getPlotPoint(0.0, thisY+1.0)[1]) / 2)
int textWidth = axisFont.getTextWidth(categories[i].string)
int textHeight = axisFont.getFontMetrics().height
c.text(new Point2D(getPlotArea().x - MARKER_LENGTH - textWidth, centreY - (textHeight / 2), axisColor), axisFont, categories[i].string)
thisY += 1.0
}
//axes and labels
drawAxes(c)
//legend
if (showLegend) drawLegend(c)
c.popSurface()
}
}