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:box 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 seriesLineColor = new Color(0, 0, 0, 255)
Color seriesFillColor = new Color(255, 255, 255, 255)
Color medianColor = new Color(0, 0, 0, 255)
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 = 60.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)
setXMarkerInterval(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))
xAxMax += 1.0
if (series.arrayLength == 1 && n.samples == null)
{
highestY = hy
lowestY = ly
}
else
{
if (hy > highestY) highestY = hy
if (ly < lowestY) lowestY = 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 (highestY > yAxMax) yAxMax = highestY
if (lowestY < yAxMin) yAxMin = lowestY
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[], store 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(store 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
}
void 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)
dec fn[] = stcore.fiveNum(s[0].values)
if (fn[4] > highY) highY = fn[4]
if (fn[0] < lowY) lowY = fn[0]
}
}
highX += 1.0
}
//NOTE: we could now calculate end "endpoint" on each axis which is a multiple of their number interval
setXMinMax(lowX, highX)
setYMinMax(lowY, highY)
}
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
}
}
void drawBox(Canvas c, Sample s, Color color, int centreX, int barWidthPX)
{
dec thisX = 0.0
//get 5-number-summary, generate box and whiskers + median, and draw...
dec fn[] = stcore.fiveNum(s.values)
//this kind of graph plots the mean of the set
dec thisY = stcore.mean(s.values)
//top-left
int xy[] = getPlotPoint(thisX, fn[3])
//top-right
int xyTR[] = getPlotPoint(thisX+1.0, fn[3])
//bottom-left
int xyBL[] = getPlotPoint(thisX, fn[1])
//median
int xyMedian[] = getPlotPoint(thisX, fn[2])
//low whisker
int xyLowWhisker[] = getPlotPoint(thisX, fn[0])
//high whisker
int xyHighWhisker[] = getPlotPoint(thisX, fn[4])
//x-axis location
int xyXA[] = getPlotPoint(thisX, 0.0)
//box spanning 1st and 3rd quartiles (interquartile range)
c.rectOutline(new Rect2D(centreX - (barWidthPX / 2), xy[1], barWidthPX, xyBL[1] - xy[1], seriesLineColor))
if (xyBL[1] - xy[1] > 2) c.rect(new Rect2D(centreX - (barWidthPX / 2)+1, xy[1]+1, barWidthPX-2, (xyBL[1] - xy[1])-2, color))
//median line
c.line(new Line2D(centreX - (barWidthPX / 2), xyMedian[1], (centreX - (barWidthPX / 2) + barWidthPX)-1, xyMedian[1], medianColor))
//low whisker
c.line(new Line2D(centreX, xyBL[1], centreX, xyLowWhisker[1], seriesLineColor))
c.line(new Line2D(centreX - (barWidthPX / 2), xyLowWhisker[1], (centreX - (barWidthPX / 2) + barWidthPX)-1, xyLowWhisker[1], seriesLineColor))
//high whisker
c.line(new Line2D(centreX, xy[1], centreX, xyHighWhisker[1], seriesLineColor))
c.line(new Line2D(centreX - (barWidthPX / 2), xyHighWhisker[1], (centreX - (barWidthPX / 2) + barWidthPX)-1, xyHighWhisker[1], seriesLineColor))
//category label
//int textWidth = axisFont.getTextWidth(s.category)
//c.text(new Point2D(centreX - (textWidth/2), xyXA[1] + MARKER_LENGTH, axisColor), axisFont, samples[i].category)
}
void CategoryMulti:paint(Canvas c)
{
prepAxisLimits()
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 xSpacing = (getPlotPoint(1.0, 0.0)[0] - getPlotPoint(0.0, 0.0)[0]+1) - (barPad*2)
int totalBarWidthPX = ((barWidth / 100.0) * xSpacing) - series.arrayLength
int barWidthPX = totalBarWidthPX / series.arrayLength
dec thisX = 0.0
for (int i = 0; i < categories.arrayLength; i++)
{
int xyZ[] = getPlotPoint(thisX, 0.0)
int centreX = xyZ[0] + ((getPlotPoint(thisX+1.0, 0.0)[0] - xyZ[0]) / 2)
centreX -= totalBarWidthPX / 2
centreX += barWidthPX / 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)
{
drawBox(c, s[0], series[j].seriesColor, centreX, barWidthPX)
}
centreX += barWidthPX + 1
}
//category label
centreX = xyZ[0] + ((getPlotPoint(thisX+1.0, 0.0)[0] - xyZ[0]) / 2)
int textWidth = axisFont.getTextWidth(categories[i].string)
c.text(new Point2D(centreX - (textWidth/2), xyZ[1] + MARKER_LENGTH, axisColor), axisFont, categories[i].string)
thisX += 1.0
}
//axes and labels
drawAxes(c)
//legend
if (showLegend) drawLegend(c)
c.popSurface()
}
}