#!/usr/local/bin/pythonw
# coding: iso-8859-1
# Copyright © 2008 by Amos Newcombe

'''Implement the Grid class for Graph.py.
'''

from math import floor, ceil, log
from operator import add

class Grid:
	'''A Grid instance embodies the list of coordinates at which gridlines will be drawn across the graph.
	
	To use this base class, pass a specific list to the constructor, alongside the data. Then the gridlines will be drawn at those coordinates. Subclasses of Grid implement algorithms for calculating the grid based on the data, if the client doesn't want to specify the grid explicitly. 
	
	Both Axes in a graph will have a Grid instance. A Grid instance with an empty list of coordinates means no grid will be drawn. You may want no grid for sparklines, or a minimalist style.
	'''
	
	def __init__(self, ldxData, lxGrid=None):
# 		print '%s(%s, %s)' % (self.__class__.__name__, lldxData, lxGrid)
		self.setDataExtremes(ldxData)
		self.setGrid(lxGrid or [])
	
	def setDataExtremes(self, ldxData):
		'''Find and store the extreme values of the data.
		
		If you use the Grid class explicitly, calculating your own grid, the DataExtremes methods will not be called. But they are here because many of the subclasses will need them.
		'''
# 		print 'setDataExtremes(%s)' % ldxData
		# By construction in Axis.setData(),
		# ldxData only includes the fields to be plotted on this axis.
		# First flatten the data -- from every field --
		lx = reduce(add, [[x for x in dx.values() if x != None] for dx in ldxData])
		self.tpxDataExtremes = (min(lx), max(lx)) # and Bob's your uncle!
# 		print 'data extremes:', self.tpxDataExtremes
	
	def getDataExtremes(self):
		'Recall the stored extreme values of the data.'
		return self.tpxDataExtremes
	
	def setGrid(self, lxGrid): self.lxGrid = lxGrid
	def getGrid(self): return self.lxGrid
	
	def __str__(self):
		'a compact string representation useful for debugging'
		return '%s(%s, %s) in %s' % (
			self.__class__.__name__, 
			list(self.getDataExtremes()), 
			self.getGrid(),
			list(self.getExtremes()),
		)
	
	def setExtremes(self, isDataIn=False):
		'''Find and store the extreme values of the grid, optionally including the extreme values of the data.
		
		Pass isDataIn = False (the default) to truncate the graph at the grid extremes, even if the data goes beyond them.
		
		Pass isDataIn = True to extend the graph to include all the data. This condition will be forced if only one grid line is to be drawn, to avoid a grid with a range of zero.
		'''
		lx = self.lxGrid[:]
		if len(lx) < 2: isDataIn = True # Force a positive-length range.
		# If we explicitly specify it, data extremes are included.
		if isDataIn: lx += list(self.getDataExtremes())
		self.tpxExtremes = (min(lx), max(lx))
		return self
	
	def getExtremes(self):
		'Recall the stored extreme values of the grid.'
		return self.tpxExtremes
	
	def getMin(self): return self.getExtremes()[0]
	def getMax(self): return self.getExtremes()[1]
	
	def getRange(self): return self.tpxExtremes[1] - self.tpxExtremes[0]

class Round125Grid(Grid):
	'''This Grid will give you a number of equally-spaced lines at round numbers across your data range.
	'''
	
	def __init__(self, ldxData, cMin=2):
		'''Specify a minimum number of gridlines in the second argument (cMin), and the grid will have that many, or a few more. 
		
		If what you get is too many, you can reduce the minimal "count" fractionally, so that in a given instance, cMin == 2 might give you 4 gridlines, cMin == 1.8 give you 3, and cMin == 1.5 give you 2.
		'''
		if cMin <= 0: raise ValueError, 'cMin == %s, but should be positive' % cMin
		Grid.__init__(self, ldxData, self.MakeGrid(float(cMin)))
	
	def MakeGrid(self, xMin):
		'''The common distance between adjacent grid lines (xGrid) is 1, 2, or 5 times a power of 10: the maximum such number that will allow at least the requested number of grid lines.
		
		How many actual gridlines do you get, based on the data and the requested number? It's complicated. But the actual number will always be greater than or equal to the requested number. The proof is outlined in the source code comments.
		'''
		xMinData, xMaxData = self.getDataExtremes()
		xDGrid = (xMaxData - xMinData) / xMin
		# xMin == (xMaxData - xMinData) / xDGrid
		nDGrid = int(floor(log(xDGrid, 10)))
		x = xDGrid / (10**nDGrid)
		# x only decreases, or remains the same, in these next 5 lines
		if  10 <= x: x = 1.; nDGrid += 1
		elif 5 <= x: x = 5.
		elif 2 <= x: x = 2.
		elif 1 <= x: x = 1.
		else       : x = 5.; nDGrid -= 1
		xDGrid = x * 10**nDGrid
		# xDGrid, proportional to x, is no larger than before, so ...
		# (xMaxData - xMinData) / xDGrid >= xMin
		nMin = int(floor(xMinData / xDGrid))
		nMax = int(ceil (xMaxData / xDGrid))
		# nMax - nMin >= (xMaxData - xMinData) / xDGrid >= xMin
		return [n * xDGrid for n in range(nMin, nMax+1)]

if __name__ == '__main__':
	
	from math import sin
	
	lx = [n*0.25 for n in range(14)]
	ldxData = [{'y': sin(x)} for x in lx]
	for dx in ldxData: print dx['y']
	print Grid(ldxData).setExtremes(True)
	print Round125Grid(ldxData).setExtremes()
