HomeForumSourceResearchGuide
Sign in to contribute to source. how it works
Component stats.chart.Category:box by barry
expand copy to clipboardexpand
data Sample {
	dec values[]
	char category[]
	}

//marker length in pixels (the "ticks" on the axes)
const int MARKER_LENGTH = 3

component provides Category:box requires ChartCore, stats.StatCore stcore, io.Output out, data.IntUtil iu, data.DecUtil du, ui.Font {
	
	Sample samples[]
	
	Color seriesColor = new Color(0, 0, 0, 255)
	Color seriesFillColor = new Color(255, 255, 255, 255)
	Color medianColor = new Color(200, 100, 100, 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
	
	//w/h in pixels
	int width
	int height
	
	Category:Category()
		{
		super()
		
		axisFont = new Font("SourceSansPro.ttf", 12)
		
		setXMarkerInterval(1.0)
		}
	
	void Category:addSample(char cat[], dec yvalues[], opt bool redraw)
		{
		//update our highest and lowest X and Y points, to use in normalising coordinates
		dec hy = stcore.max(yvalues)
		dec ly = stcore.min(yvalues)
		
		xAxMax += 1.0
		
		if (samples == null)
			{
			highestY = hy
			lowestY = ly
			}
			else
			{
			if (hy > highestY) highestY = hy
			if (ly < lowestY) lowestY = ly
			}
		
		//add sample
		samples = new Sample[](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 Category: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 Category:setSeriesColor(store Color c)
		{
		seriesColor = c
		}
	
	void Category:showErrorBars(bool on)
		{
		
		}
	
	void Category:clampErrorBars(dec low, dec high)
		{
		
		}
	
	void Category: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 Category:setCatDisplayPadding(int pixels)
		{
		barPad = pixels
		}
	
	void Category:paint(Canvas c)
		{
		Point pos = getPosition()
		WH wh = getSize()
		
		c.pushSurface(new Rect(pos.x, pos.y, wh.width, wh.height), 0, 0, 255)
		
		//TODO: calculate the 5num of every sample, and check if our chart is large enough to show the outlier whiskers
		// (by using setYMinMax())
		
		preparePlotArea()
		
		//background
		c.rect(new Rect2D(0, 0, wh.width, wh.height, bgColor))
		
		//grid lines, if any
		drawGrid(c)
		
		//data
		
		int xSpacing = getPlotPoint(1.0, 0.0)[0] - getPlotPoint(0.0, 0.0)[0]
		int barWidthPX = (barWidth / 100.0) * xSpacing
		
		dec thisX = 0.0
		for (int i = 0; i < samples.arrayLength; i++)
			{
			//get 5-number-summary, generate box and whiskers + median, and draw...
			dec fn[] = stcore.fiveNum(samples[i].values)
			
			//this kind of graph plots the mean of the set
			dec thisY = stcore.mean(samples[i].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)
			
			int centreX = xy[0] + ((xyTR[0] - xy[0]) / 2)
			
			//box spanning 1st and 3rd quartiles (interquartile range)
			c.rectOutline(new Rect2D(centreX - (barWidthPX / 2), xy[1], barWidthPX, xyBL[1] - xy[1], seriesColor))
			if (xyBL[1] - xy[1] > 2) c.rect(new Rect2D(centreX - (barWidthPX / 2)+1, xy[1]+1, barWidthPX-2, (xyBL[1] - xy[1])-2, seriesFillColor))
			
			//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], seriesColor))
			c.line(new Line2D(centreX - (barWidthPX / 2), xyLowWhisker[1], (centreX - (barWidthPX / 2) + barWidthPX)-1, xyLowWhisker[1], seriesColor))
			
			//high whisker
			c.line(new Line2D(centreX, xy[1], centreX, xyHighWhisker[1], seriesColor))
			c.line(new Line2D(centreX - (barWidthPX / 2), xyHighWhisker[1], (centreX - (barWidthPX / 2) + barWidthPX)-1, xyHighWhisker[1], seriesColor))
			
			//category label
			int textWidth = axisFont.getTextWidth(samples[i].category)
			c.text(new Point2D(centreX - (textWidth/2), xyXA[1] + MARKER_LENGTH, axisColor), axisFont, samples[i].category)
			
			thisX += 1.0
			}
		
		//axes and labels
		drawAxes(c)
		
		c.popSurface()
		}
	
	}
Revision history
To propose a new revision to this entity, use dana source put -uc your/new/version.dn -n stats.chart.Category:box -m "reason for update" -u yourUsername
Version 2 (this version) by barry
Notes for this version: Updates to prepare for upcoming compiler strictness changes in function parameter qualifier equivalence
Version 1 by barry