HomeForumSourceResearchGuide
Sign in to contribute to source. how it works
Component stats.chart.CategoryMulti:box by barry
expand copy to clipboardexpand
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[], 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
		}
	
	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()
		}
	
	}
Revision history
To propose a new revision to this entity, use dana source put -uc your/new/version.dn -n stats.chart.CategoryMulti:box -m "reason for update" -u yourUsername
Version 3 by barry
Version 2 by barry
Version 1 (this version) by barry
Notes for this version: Standard Library Initialisation