HomeForumSourceResearchGuide
Sign in to contribute to source. how it works
Component stats.chart.CategoryMulti:barH 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: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[], 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
		}
	
	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()
		}
	
	}
Revision history
To propose a new revision to this entity, use dana source put -uc your/new/version.dn -n stats.chart.CategoryMulti:barH -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