    #copyright ReportLab Inc. 2000-2001
#see license.txt for license details
#history http://cvs.sourceforge.net/cgi-bin/cvsweb.cgi/reportlab/graphics/charts/spider.py?cvsroot=reportlab
#$Header: /cvsroot/reportlab/reportlab/graphics/charts/spider.py,v 1.9 2003/06/19 17:33:00 rgbecker Exp $
# spider chart, also known as radar chart

"""Spider Chart

Normal use shows variation of 5-10 parameters against some 'norm' or target.
When there is more than one series, place the series with the largest
numbers first, as it will be overdrawn by each successive one.
"""
__version__=''' $Id: spider.py,v 1.9 2003/06/19 17:33:00 rgbecker Exp $ '''

import copy
from math import sin, cos, pi

from reportlab.lib import colors
from reportlab.lib.validators import isColor, isNumber, isListOfNumbersOrNone,\
                                    isListOfNumbers, isColorOrNone, isString,\
                                    isListOfStringsOrNone, OneOf, SequenceOf,\
                                    isBoolean, isListOfColors, isNumberOrNone,\
                                    isNoneOrListOfNoneOrStrings, isTextAnchor,\
                                    isNoneOrListOfNoneOrNumbers, isBoxAnchor,\
                                    isStringOrNone
from reportlab.lib.attrmap import *
from reportlab.pdfgen.canvas import Canvas
from reportlab.graphics.shapes import Group, Drawing, Line, Rect, Polygon, Ellipse, \
    Wedge, String, STATE_DEFAULTS
from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder
from reportlab.graphics.charts.areas import PlotArea
from piecharts import WedgeLabel
from reportlab.graphics.widgets.markers import makeMarker, uSymbol2Symbol

class StrandProperties(PropHolder):
    """This holds descriptive information about concentric 'strands'.

    Line style, whether filled etc.
    """

    _attrMap = AttrMap(
        strokeWidth = AttrMapValue(isNumber),
        fillColor = AttrMapValue(isColorOrNone),
        strokeColor = AttrMapValue(isColorOrNone),
        strokeDashArray = AttrMapValue(isListOfNumbersOrNone),
        fontName = AttrMapValue(isString),
        fontSize = AttrMapValue(isNumber),
        fontColor = AttrMapValue(isColorOrNone),
        labelRadius = AttrMapValue(isNumber),
        markers = AttrMapValue(isBoolean),
        markerType = AttrMapValue(isAnything),
        markerSize = AttrMapValue(isNumber),
        label_dx = AttrMapValue(isNumber),
        label_dy = AttrMapValue(isNumber),
        label_angle = AttrMapValue(isNumber),
        label_boxAnchor = AttrMapValue(isBoxAnchor),
        label_boxStrokeColor = AttrMapValue(isColorOrNone),
        label_boxStrokeWidth = AttrMapValue(isNumber),
        label_boxFillColor = AttrMapValue(isColorOrNone),
        label_strokeColor = AttrMapValue(isColorOrNone),
        label_strokeWidth = AttrMapValue(isNumber),
        label_text = AttrMapValue(isStringOrNone),
        label_leading = AttrMapValue(isNumberOrNone),
        label_width = AttrMapValue(isNumberOrNone),
        label_maxWidth = AttrMapValue(isNumberOrNone),
        label_height = AttrMapValue(isNumberOrNone),
        label_textAnchor = AttrMapValue(isTextAnchor),
        label_visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"),
        label_topPadding = AttrMapValue(isNumber,'padding at top of box'),
        label_leftPadding = AttrMapValue(isNumber,'padding at left of box'),
        label_rightPadding = AttrMapValue(isNumber,'padding at right of box'),
        label_bottomPadding = AttrMapValue(isNumber,'padding at bottom of box'),
        )

    def __init__(self):
        self.strokeWidth = 0
        self.fillColor = None
        self.strokeColor = STATE_DEFAULTS["strokeColor"]
        self.strokeDashArray = STATE_DEFAULTS["strokeDashArray"]
        self.fontName = STATE_DEFAULTS["fontName"]
        self.fontSize = STATE_DEFAULTS["fontSize"]
        self.fontColor = STATE_DEFAULTS["fillColor"]
        self.labelRadius = 1.2
        self.markers = 0
        self.markerType = None
        self.markerSize = 0
        self.label_dx = self.label_dy = self.label_angle = 0
        self.label_text = None
        self.label_topPadding = self.label_leftPadding = self.label_rightPadding = self.label_bottomPadding = 0
        self.label_boxAnchor = 'c'
        self.label_boxStrokeColor = None    #boxStroke
        self.label_boxStrokeWidth = 0.5 #boxStrokeWidth
        self.label_boxFillColor = None
        self.label_strokeColor = None
        self.label_strokeWidth = 0.1
        self.label_leading =    self.label_width = self.label_maxWidth = self.label_height = None
        self.label_textAnchor = 'start'
        self.label_visible = 1

class SpiderChart(PlotArea):
    _attrMap = AttrMap(BASE=PlotArea,
        data = AttrMapValue(None, desc='Data to be plotted, list of (lists of) numbers.'),
        labels = AttrMapValue(isListOfStringsOrNone, desc="optional list of labels to use for each data point"),
        startAngle = AttrMapValue(isNumber, desc="angle of first slice; like the compass, 0 is due North"),
        direction = AttrMapValue( OneOf('clockwise', 'anticlockwise'), desc="'clockwise' or 'anticlockwise'"),
        strands = AttrMapValue(None, desc="collection of strand descriptor objects"),
        )

    def __init__(self):
        PlotArea.__init__(self)

        self.data = [[10,12,14,16,14,12], [6,8,10,12,9,11]]
        self.labels = None  # or list of strings
        self.startAngle = 90
        self.direction = "clockwise"

        self.strands = TypedPropertyCollection(StrandProperties)
        self.strands[0].fillColor = colors.cornsilk
        self.strands[1].fillColor = colors.cyan


    def demo(self):
        d = Drawing(200, 100)

        sp = SpiderChart()
        sp.x = 50
        sp.y = 10
        sp.width = 100
        sp.height = 80
        sp.data = [[10,12,14,16,18,20],[6,8,4,6,8,10]]
        sp.labels = ['a','b','c','d','e','f']

        d.add(sp)
        return d

    def normalizeData(self, outer = 0.0):
        """Turns data into normalized ones where each datum is < 1.0,
        and 1.0 = maximum radius.  Adds 10% at outside edge by default"""
        data = self.data
        theMax = 0.0
        for row in data:
            for element in row:
                assert element >=0, "Cannot do spider plots of negative numbers!"
                if element > theMax:
                    theMax = element
        theMax = theMax * (1.0+outer)

        scaled = []
        for row in data:
            scaledRow = []
            for element in row:
                scaledRow.append(element / theMax)
            scaled.append(scaledRow)
        return scaled


    def draw(self):
        # normalize slice data
        g = self.makeBackground() or Group()

        xradius = self.width/2.0
        yradius = self.height/2.0
        self._radius = radius = min(xradius, yradius)
        centerx = self.x + xradius
        centery = self.y + yradius

        data = self.normalizeData()

        n = len(data[0])

        #labels
        if self.labels is None:
            labels = [''] * n
        else:
            labels = self.labels
            #there's no point in raising errors for less than enough errors if
            #we silently create all for the extreme case of no labels.
            i = n-len(labels)
            if i>0:
                labels = labels + ['']*i

        spokes = []
        csa = []
        angle = self.startAngle*pi/180
        direction = self.direction == "clockwise" and -1 or 1
        angleBetween = direction*(2 * pi)/n
        markers = self.strands.markers
        for i in xrange(n):
            car = cos(angle)*radius
            sar = sin(angle)*radius
            csa.append((car,sar,angle))
            spoke = Line(centerx, centery, centerx + car, centery + sar, strokeWidth = 0.5)
            #print 'added spoke (%0.2f, %0.2f) -> (%0.2f, %0.2f)' % (spoke.x1, spoke.y1, spoke.x2, spoke.y2)
            spokes.append(spoke)
            if labels:
                si = self.strands[i]
                text = si.label_text
                if text is None: text = labels[i]
                if text:
                    labelRadius = si.labelRadius
                    L = WedgeLabel()
                    L.x = centerx + labelRadius*car
                    L.y = centery + labelRadius*sar
                    L.boxAnchor = si.label_boxAnchor
                    L._pmv = angle*180/pi
                    L.dx = si.label_dx
                    L.dy = si.label_dy
                    L.angle = si.label_angle
                    L.boxAnchor = si.label_boxAnchor
                    L.boxStrokeColor = si.label_boxStrokeColor
                    L.boxStrokeWidth = si.label_boxStrokeWidth
                    L.boxFillColor = si.label_boxFillColor
                    L.strokeColor = si.label_strokeColor
                    L.strokeWidth = si.label_strokeWidth
                    L._text = text
                    L.leading = si.label_leading
                    L.width = si.label_width
                    L.maxWidth = si.label_maxWidth
                    L.height = si.label_height
                    L.textAnchor = si.label_textAnchor
                    L.visible = si.label_visible
                    L.topPadding = si.label_topPadding
                    L.leftPadding = si.label_leftPadding
                    L.rightPadding = si.label_rightPadding
                    L.bottomPadding = si.label_bottomPadding
                    L.fontName = si.fontName
                    L.fontSize = si.fontSize
                    L.fillColor = si.fontColor
                    spokes.append(L)
            angle = angle + angleBetween

        # now plot the polygons

        rowIdx = 0
        for row in data:
            # series plot
            points = []
            car, sar = csa[-1][:2]
            r = row[-1]
            points.append(centerx+car*r)
            points.append(centery+sar*r)
            for i in xrange(n):
                car, sar = csa[i][:2]
                r = row[i]
                points.append(centerx+car*r)
                points.append(centery+sar*r)

                # make up the 'strand'
                strand = Polygon(points)
                strand.fillColor = self.strands[rowIdx].fillColor
                strand.strokeColor = self.strands[rowIdx].strokeColor
                strand.strokeWidth = self.strands[rowIdx].strokeWidth
                strand.strokeDashArray = self.strands[rowIdx].strokeDashArray

                g.add(strand)

                # put in a marker, if it needs one
                if markers:
                    if hasattr(self.strands[rowIdx], 'markerType'):
                        uSymbol = self.strands[rowIdx].markerType
                    elif hasattr(self.strands, 'markerType'):
                        uSymbol = self.strands.markerType
                    else:
                        uSymbol = None
                    m_x =  centerx+car*r
                    m_y = centery+sar*r
                    m_size = self.strands[rowIdx].markerSize
                    m_fillColor = self.strands[rowIdx].fillColor
                    m_strokeColor = self.strands[rowIdx].strokeColor
                    m_strokeWidth = self.strands[rowIdx].strokeWidth
                    m_angle = 0
                    if type(uSymbol) is type(''):
                        symbol = makeMarker(uSymbol,
                                    size = m_size,
                                    x =  m_x,
                                    y = m_y,
                                    fillColor = m_fillColor,
                                    strokeColor = m_strokeColor,
                                    strokeWidth = m_strokeWidth,
                                    angle = m_angle,
                                    )
                    else:
                        symbol = uSymbol2Symbol(uSymbol,m_x,m_y,m_fillColor)
                        for k,v in (('size', m_size), ('fillColor', m_fillColor),
                                    ('x', m_x), ('y', m_y),
                                    ('strokeColor',m_strokeColor), ('strokeWidth',m_strokeWidth),
                                    ('angle',m_angle),):
                            try:
                                setattr(uSymbol,k,v)
                            except:
                                pass
                    g.add(symbol)

            rowIdx = rowIdx + 1

        # spokes go over strands
        for spoke in spokes:
            g.add(spoke)
        return g

def sample1():
    "Make a simple spider chart"

    d = Drawing(400, 400)

    pc = SpiderChart()
    pc.x = 50
    pc.y = 50
    pc.width = 300
    pc.height = 300
    pc.data = [[10,12,14,16,14,12], [6,8,10,12,9,15],[7,8,17,4,12,8,3]]
    pc.labels = ['a','b','c','d','e','f']
    pc.strands[2].fillColor=colors.palegreen

    d.add(pc)

    return d


def sample2():
    "Make a spider chart with markers, but no fill"

    d = Drawing(400, 400)

    pc = SpiderChart()
    pc.x = 50
    pc.y = 50
    pc.width = 300
    pc.height = 300
    pc.data = [[10,12,14,16,14,12], [6,8,10,12,9,15],[7,8,17,4,12,8,3]]
    pc.labels = ['U','V','W','X','Y','Z']
    pc.strands.strokeWidth = 2
    pc.strands[0].fillColor = None
    pc.strands[1].fillColor = None
    pc.strands[2].fillColor = None
    pc.strands[0].strokeColor = colors.red
    pc.strands[1].strokeColor = colors.blue
    pc.strands[2].strokeColor = colors.green
    pc.strands.markers = 1
    pc.strands.markerType = "FilledDiamond"
    pc.strands.markerSize = 6

    d.add(pc)

    return d


if __name__=='__main__':
    d = sample1()
    from reportlab.graphics.renderPDF import drawToFile
    drawToFile(d, 'spider.pdf')
    d = sample2()
    drawToFile(d, 'spider2.pdf')
    #print 'saved spider.pdf'
