Ejemplo n.º 1
0
class HorizontalLineChart(LineChart):
    """Line chart with multiple lines.

    A line chart is assumed to have one category and one value axis.
    Despite its generic name this particular line chart class has
    a vertical value axis and a horizontal category one. It may
    evolve into individual horizontal and vertical variants (like
    with the existing bar charts).

    Available attributes are:

        x: x-position of lower-left chart origin
        y: y-position of lower-left chart origin
        width: chart width
        height: chart height

        useAbsolute: disables auto-scaling of chart elements (?)
        lineLabelNudge: distance of data labels to data points
        lineLabels: labels associated with data values
        lineLabelFormat: format string or callback function
        groupSpacing: space between categories

        joinedLines: enables drawing of lines

        strokeColor: color of chart lines (?)
        fillColor: color for chart background (?)
        lines: style list, used cyclically for data series

        valueAxis: value axis object
        categoryAxis: category axis object
        categoryNames: category names

        data: chart data, a list of data series of equal length
    """

    _attrMap = AttrMap(BASE=LineChart,
        useAbsolute = AttrMapValue(isNumber, desc='Flag to use absolute spacing values.',advancedUsage=1),
        lineLabelNudge = AttrMapValue(isNumber, desc='Distance between a data point and its label.',advancedUsage=1),
        lineLabels = AttrMapValue(None, desc='Handle to the list of data point labels.'),
        lineLabelFormat = AttrMapValue(None, desc='Formatting string or function used for data point labels.'),
        lineLabelArray = AttrMapValue(None, desc='explicit array of line label values, must match size of data if present.'),
        groupSpacing = AttrMapValue(isNumber, desc='? - Likely to disappear.'),
        joinedLines = AttrMapValue(isNumber, desc='Display data points joined with lines if true.'),
        lines = AttrMapValue(None, desc='Handle of the lines.'),
        valueAxis = AttrMapValue(None, desc='Handle of the value axis.'),
        categoryAxis = AttrMapValue(None, desc='Handle of the category axis.'),
        categoryNames = AttrMapValue(isListOfStringsOrNone, desc='List of category names.'),
        data = AttrMapValue(None, desc='Data to be plotted, list of (lists of) numbers.'),
        inFill = AttrMapValue(isBoolean, desc='Whether infilling should be done.',advancedUsage=1),
        reversePlotOrder = AttrMapValue(isBoolean, desc='If true reverse plot order.',advancedUsage=1),
        annotations = AttrMapValue(None, desc='list of callables, will be called with self, xscale, yscale.',advancedUsage=1),
        )

    def __init__(self):
        LineChart.__init__(self)

        # Allow for a bounding rectangle.
        self.strokeColor = None
        self.fillColor = None

        # Named so we have less recoding for the horizontal one :-)
        self.categoryAxis = XCategoryAxis()
        self.valueAxis = YValueAxis()

        # This defines two series of 3 points.  Just an example.
        self.data = [(100,110,120,130),
                     (70, 80, 80, 90)]
        self.categoryNames = ('North','South','East','West')

        self.lines = TypedPropertyCollection(LineChartProperties)
        self.lines.strokeWidth = 1
        self.lines[0].strokeColor = colors.red
        self.lines[1].strokeColor = colors.green
        self.lines[2].strokeColor = colors.blue

        # control spacing. if useAbsolute = 1 then
        # the next parameters are in points; otherwise
        # they are 'proportions' and are normalized to
        # fit the available space.
        self.useAbsolute = 0   #- not done yet
        self.groupSpacing = 1 #5

        self.lineLabels = TypedPropertyCollection(Label)
        self.lineLabelFormat = None
        self.lineLabelArray = None

        # This says whether the origin is above or below
        # the data point. +10 means put the origin ten points
        # above the data point if value > 0, or ten
        # points below if data value < 0.  This is different
        # to label dx/dy which are not dependent on the
        # sign of the data.
        self.lineLabelNudge = 10
        # If you have multiple series, by default they butt
        # together.

        # New line chart attributes.
        self.joinedLines = 1 # Connect items with straight lines.
        self.inFill = 0
        self.reversePlotOrder = 0


    def demo(self):
        """Shows basic use of a line chart."""

        drawing = Drawing(200, 100)

        data = [
                (13, 5, 20, 22, 37, 45, 19, 4),
                (14, 10, 21, 28, 38, 46, 25, 5)
                ]

        lc = HorizontalLineChart()

        lc.x = 20
        lc.y = 10
        lc.height = 85
        lc.width = 170
        lc.data = data
        lc.lines.symbol = makeMarker('Circle')

        drawing.add(lc)

        return drawing


    def calcPositions(self):
        """Works out where they go.

        Sets an attribute _positions which is a list of
        lists of (x, y) matching the data.
        """

        self._seriesCount = len(self.data)
        self._rowLength = max(list(map(len,self.data)))

        if self.useAbsolute:
            # Dimensions are absolute.
            normFactor = 1.0
        else:
            # Dimensions are normalized to fit.
            normWidth = self.groupSpacing
            availWidth = self.categoryAxis.scale(0)[1]
            normFactor = availWidth / normWidth
        self._normFactor = normFactor
        self._yzero = yzero = self.valueAxis.scale(0)
        self._hngs = hngs = 0.5 * self.groupSpacing * normFactor

        self._positions = []
        for rowNo in range(len(self.data)):
            lineRow = []
            for colNo in range(len(self.data[rowNo])):
                datum = self.data[rowNo][colNo]
                if datum is not None:
                    (groupX, groupWidth) = self.categoryAxis.scale(colNo)
                    x = groupX + hngs
                    y = yzero
                    height = self.valueAxis.scale(datum) - y
                    lineRow.append((x, y+height))
            self._positions.append(lineRow)


    def _innerDrawLabel(self, rowNo, colNo, x, y):
        "Draw a label for a given item in the list."

        labelFmt = self.lineLabelFormat
        labelValue = self.data[rowNo][colNo]

        if labelFmt is None:
            labelText = None
        elif type(labelFmt) is str:
            if labelFmt == 'values':
                try:
                    labelText = self.lineLabelArray[rowNo][colNo]
                except:
                    labelText = None
            else:
                labelText = labelFmt % labelValue
        elif hasattr(labelFmt,'__call__'):
            labelText = labelFmt(labelValue)
        else:
            raise ValueError("Unknown formatter type %s, expected string or function"%labelFmt)

        if labelText:
            label = self.lineLabels[(rowNo, colNo)]
            if not label.visible: return
            # Make sure labels are some distance off the data point.
            if y > 0:
                label.setOrigin(x, y + self.lineLabelNudge)
            else:
                label.setOrigin(x, y - self.lineLabelNudge)
            label.setText(labelText)
        else:
            label = None
        return label

    def drawLabel(self, G, rowNo, colNo, x, y):
        '''Draw a label for a given item in the list.
        G must have an add method'''
        G.add(self._innerDrawLabel(rowNo,colNo,x,y))

    def makeLines(self):
        g = Group()

        labelFmt = self.lineLabelFormat
        P = list(range(len(self._positions)))
        if self.reversePlotOrder: P.reverse()
        inFill = self.inFill
        if inFill:
            inFillY = self.categoryAxis._y
            inFillX0 = self.valueAxis._x
            inFillX1 = inFillX0 + self.categoryAxis._length
            inFillG = getattr(self,'_inFillG',g)
        yzero = self._yzero

        # Iterate over data rows.
        for rowNo in P:
            row = self._positions[rowNo]
            styleCount = len(self.lines)
            styleIdx = rowNo % styleCount
            rowStyle = self.lines[styleIdx]
            rowColor = rowStyle.strokeColor
            dash = getattr(rowStyle, 'strokeDashArray', None)
            lineStyle = getattr(rowStyle,'lineStyle',None)

            if hasattr(rowStyle, 'strokeWidth'):
                strokeWidth = rowStyle.strokeWidth
            elif hasattr(self.lines, 'strokeWidth'):
                strokeWidth = self.lines.strokeWidth
            else:
                strokeWidth = None

            # Iterate over data columns.
            if lineStyle=='bar':
                barWidth = getattr(rowStyle,'barWidth',Percentage(50))
                fillColor = getattr(rowStyle,'fillColor',rowColor)
                if isinstance(barWidth,Percentage):
                    hbw = self._hngs*barWidth*0.01
                else:
                    hbw = barWidth*0.5
                for colNo in range(len(row)):
                    x,y = row[colNo]
                    g.add(Rect(x-hbw,min(y,yzero),2*hbw,abs(y-yzero),strokeWidth=strokeWidth,strokeColor=rowColor,fillColor=fillColor))
            elif self.joinedLines or lineStyle=='joinedLine':
                points = []
                for colNo in range(len(row)):
                    points += row[colNo]
                if inFill:
                    points = points + [inFillX1,inFillY,inFillX0,inFillY]
                    inFillG.add(Polygon(points,fillColor=rowColor,strokeColor=rowColor,strokeWidth=0.1))
                else:
                    line = PolyLine(points,strokeColor=rowColor,strokeLineCap=0,strokeLineJoin=1)
                    if strokeWidth:
                        line.strokeWidth = strokeWidth
                    if dash:
                        line.strokeDashArray = dash
                    g.add(line)

            if hasattr(rowStyle, 'symbol'):
                uSymbol = rowStyle.symbol
            elif hasattr(self.lines, 'symbol'):
                uSymbol = self.lines.symbol
            else:
                uSymbol = None

            if uSymbol:
                for colNo in range(len(row)):
                    x1, y1 = row[colNo]
                    symbol = uSymbol2Symbol(uSymbol,x1,y1,rowStyle.strokeColor)
                    if symbol: g.add(symbol)

            # Draw item labels.
            for colNo in range(len(row)):
                x1, y1 = row[colNo]
                self.drawLabel(g, rowNo, colNo, x1, y1)

        return g

    def draw(self):
        "Draws itself."

        vA, cA = self.valueAxis, self.categoryAxis
        vA.setPosition(self.x, self.y, self.height)
        if vA: vA.joinAxis = cA
        if cA: cA.joinAxis = vA
        vA.configure(self.data)

        # If zero is in chart, put x axis there, otherwise
        # use bottom.
        xAxisCrossesAt = vA.scale(0)
        if ((xAxisCrossesAt > self.y + self.height) or (xAxisCrossesAt < self.y)):
            y = self.y
        else:
            y = xAxisCrossesAt

        cA.setPosition(self.x, y, self.width)
        cA.configure(self.data)

        self.calcPositions()

        g = Group()
        g.add(self.makeBackground())
        if self.inFill:
            self._inFillG = Group()
            g.add(self._inFillG)

        g.add(cA)
        g.add(vA)
        cAdgl = getattr(cA,'drawGridLast',False)
        vAdgl = getattr(vA,'drawGridLast',False)
        if not cAdgl: cA.makeGrid(g,parent=self,dim=vA.getGridDims)
        if not vAdgl: vA.makeGrid(g,parent=self,dim=cA.getGridDims)
        g.add(self.makeLines())
        if cAdgl: cA.makeGrid(g,parent=self,dim=vA.getGridDims)
        if vAdgl: vA.makeGrid(g,parent=self,dim=cA.getGridDims)
        for a in getattr(self,'annotations',()): g.add(a(self,cA.scale,vA.scale))
        return g
Ejemplo n.º 2
0
class LinePlot(AbstractLineChart):
    """Line plot with multiple lines.

    Both x- and y-axis are value axis (so there are no seperate
    X and Y versions of this class).
    """
    _attrMap = AttrMap(BASE=PlotArea,
        reversePlotOrder = AttrMapValue(isBoolean, desc='If true reverse plot order.',advancedUsage=1),
        lineLabelNudge = AttrMapValue(isNumber, desc='Distance between a data point and its label.',advancedUsage=1),
        lineLabels = AttrMapValue(None, desc='Handle to the list of data point labels.'),
        lineLabelFormat = AttrMapValue(None, desc='Formatting string or function used for data point labels.'),
        lineLabelArray = AttrMapValue(None, desc='explicit array of line label values, must match size of data if present.'),
        joinedLines = AttrMapValue(isNumber, desc='Display data points joined with lines if true.'),
        strokeColor = AttrMapValue(isColorOrNone, desc='Color used for background border of plot area.'),
        fillColor = AttrMapValue(isColorOrNone, desc='Color used for background interior of plot area.'),
        lines = AttrMapValue(None, desc='Handle of the lines.'),
        xValueAxis = AttrMapValue(None, desc='Handle of the x axis.'),
        yValueAxis = AttrMapValue(None, desc='Handle of the y axis.'),
        data = AttrMapValue(None, desc='Data to be plotted, list of (lists of) x/y tuples.'),
        annotations = AttrMapValue(None, desc='list of callables, will be called with self, xscale, yscale.',advancedUsage=1),
        behindAxes = AttrMapValue(isBoolean, desc='If true use separate line group.',advancedUsage=1),
        gridFirst = AttrMapValue(isBoolean, desc='If true use draw grids before axes.',advancedUsage=1),
        )

    def __init__(self):
        PlotArea.__init__(self)
        self.reversePlotOrder = 0

        self.xValueAxis = XValueAxis()
        self.yValueAxis = YValueAxis()

        # this defines two series of 3 points.  Just an example.
        self.data = [
            ((1,1), (2,2), (2.5,1), (3,3), (4,5)),
            ((1,2), (2,3), (2.5,2), (3,4), (4,6))
            ]

        self.lines = TypedPropertyCollection(LinePlotProperties)
        self.lines.strokeWidth = 1
        self.lines[0].strokeColor = colors.red
        self.lines[1].strokeColor = colors.blue

        self.lineLabels = TypedPropertyCollection(Label)
        self.lineLabelFormat = None
        self.lineLabelArray = None

        # this says whether the origin is inside or outside
        # the bar - +10 means put the origin ten points
        # above the tip of the bar if value > 0, or ten
        # points inside if bar value < 0.  This is different
        # to label dx/dy which are not dependent on the
        # sign of the data.
        self.lineLabelNudge = 10
        # if you have multiple series, by default they butt
        # together.

        # New line chart attributes.
        self.joinedLines = 1 # Connect items with straight lines.

        #private attributes
        self._inFill = None
        self.annotations = []
        self.behindAxes = 0
        self.gridFirst = 0

    def demo(self):
        """Shows basic use of a line chart."""

        drawing = Drawing(400, 200)

        data = [
            ((1,1), (2,2), (2.5,1), (3,3), (4,5)),
            ((1,2), (2,3), (2.5,2), (3.5,5), (4,6))
            ]

        lp = LinePlot()

        lp.x = 50
        lp.y = 50
        lp.height = 125
        lp.width = 300
        lp.data = data
        lp.joinedLines = 1
        lp.lineLabelFormat = '%2.0f'
        lp.strokeColor = colors.black

        lp.lines[0].strokeColor = colors.red
        lp.lines[0].symbol = makeMarker('FilledCircle')
        lp.lines[1].strokeColor = colors.blue
        lp.lines[1].symbol = makeMarker('FilledDiamond')

        lp.xValueAxis.valueMin = 0
        lp.xValueAxis.valueMax = 5
        lp.xValueAxis.valueStep = 1

        lp.yValueAxis.valueMin = 0
        lp.yValueAxis.valueMax = 7
        lp.yValueAxis.valueStep = 1

        drawing.add(lp)

        return drawing


    def calcPositions(self):
        """Works out where they go.

        Sets an attribute _positions which is a list of
        lists of (x, y) matching the data.
        """

        self._seriesCount = len(self.data)
        self._rowLength = max(map(len,self.data))

        self._positions = []
        for rowNo in range(len(self.data)):
            line = []
            for colNo in range(len(self.data[rowNo])):
                datum = self.data[rowNo][colNo] # x,y value
                if type(datum[0]) == type(''):
                    x = self.xValueAxis.scale(mktime(mkTimeTuple(datum[0])))
                else:
                    x = self.xValueAxis.scale(datum[0])
                y = self.yValueAxis.scale(datum[1])
                line.append((x, y))
            self._positions.append(line)

    def _innerDrawLabel(self, rowNo, colNo, x, y):
        "Draw a label for a given item in the list."

        labelFmt = self.lineLabelFormat
        labelValue = self.data[rowNo][colNo][1] ###

        if labelFmt is None:
            labelText = None
        elif type(labelFmt) is StringType:
            if labelFmt == 'values':
                labelText = self.lineLabelArray[rowNo][colNo]
            else:
                labelText = labelFmt % labelValue
        elif hasattr(labelFmt,'__call__'):
            labelText = labelFmt(labelValue)
        else:
            raise ValueError("Unknown formatter type %s, expected string or function"%labelFmt)

        if labelText:
            label = self.lineLabels[(rowNo, colNo)]
            if not label.visible: return
            #hack to make sure labels are outside the bar
            if y > 0:
                label.setOrigin(x, y + self.lineLabelNudge)
            else:
                label.setOrigin(x, y - self.lineLabelNudge)
            label.setText(labelText)
        else:
            label = None
        return label

    def drawLabel(self, G, rowNo, colNo, x, y):
        '''Draw a label for a given item in the list.
        G must have an add method'''
        G.add(self._innerDrawLabel(rowNo,colNo,x,y))

    def makeLines(self):
        g = Group()
        bubblePlot = getattr(self,'_bubblePlot',None)
        if bubblePlot:
            yA = self.yValueAxis
            xA = self.xValueAxis
            bubbleR = min(yA._bubbleRadius,xA._bubbleRadius)
            bubbleMax = xA._bubbleMax

        labelFmt = self.lineLabelFormat

        P = range(len(self._positions))
        if self.reversePlotOrder: P.reverse()
        inFill = getattr(self,'_inFill',None)
        styleCount = len(self.lines)
        if inFill or [rowNo for rowNo in P if getattr(self.lines[rowNo%styleCount],'inFill',False)]:
            inFillY = self.xValueAxis._y
            inFillX0 = self.yValueAxis._x
            inFillX1 = inFillX0 + self.xValueAxis._length
            inFillG = getattr(self,'_inFillG',g)
        lG = getattr(self,'_lineG',g)
        # Iterate over data rows.
        for rowNo in P:
            row = self._positions[rowNo]
            rowStyle = self.lines[rowNo % styleCount]
            rowColor = getattr(rowStyle,'strokeColor',None)
            dash = getattr(rowStyle, 'strokeDashArray', None)

            if hasattr(rowStyle, 'strokeWidth'):
                width = rowStyle.strokeWidth
            elif hasattr(self.lines, 'strokeWidth'):
                width = self.lines.strokeWidth
            else:
                width = None

            # Iterate over data columns.
            if self.joinedLines:
                points = []
                for xy in row:
                    points = points + [xy[0], xy[1]]
                if inFill or getattr(rowStyle,'inFill',False):
                    fpoints = [inFillX0,inFillY] + points + [inFillX1,inFillY]
                    filler = getattr(rowStyle, 'filler', None)
                    if filler:
                        filler.fill(self,inFillG,rowNo,rowColor,fpoints)
                    else:
                        inFillG.add(Polygon(fpoints,fillColor=rowColor,strokeColor=rowColor,strokeWidth=width or 0.1))
                if inFill in (None,0,2):
                    line = PolyLine(points,strokeColor=rowColor,strokeLineCap=0,strokeLineJoin=1)
                    if width:
                        line.strokeWidth = width
                    if dash:
                        line.strokeDashArray = dash
                    lG.add(line)

            if hasattr(rowStyle, 'symbol'):
                uSymbol = rowStyle.symbol
            elif hasattr(self.lines, 'symbol'):
                uSymbol = self.lines.symbol
            else:
                uSymbol = None

            if uSymbol:
                j = -1
                if bubblePlot: drow = self.data[rowNo]
                for xy in row:
                    j += 1
                    symbol = uSymbol2Symbol(uSymbol,xy[0],xy[1],rowColor)
                    if symbol:
                        if bubblePlot:
                            symbol.size = bubbleR*(drow[j][2]/bubbleMax)**0.5
                        g.add(symbol)

            # Draw data labels.
            for colNo in range(len(row)):
                x1, y1 = row[colNo]
                self.drawLabel(g, rowNo, colNo, x1, y1)

            shader = getattr(rowStyle, 'shader', None)
            if shader: shader.shade(self,g,rowNo,rowColor,row)

        return g

    def draw(self):
        yA = self.yValueAxis
        xA = self.xValueAxis
        if getattr(self,'_bubblePlot',None):
            yA._bubblePlot = xA._bubblePlot = 1
        yA.setPosition(self.x, self.y, self.height)
        if yA: yA.joinAxis = xA
        if xA: xA.joinAxis = yA
        yA.configure(self.data)

        # if zero is in chart, put x axis there, otherwise use bottom.
        xAxisCrossesAt = yA.scale(0)
        if ((xAxisCrossesAt > self.y + self.height) or (xAxisCrossesAt < self.y)):
            y = self.y
        else:
            y = xAxisCrossesAt

        xA.setPosition(self.x, y, self.width)
        xA.configure(self.data)
        self.calcPositions()
        g = Group()
        g.add(self.makeBackground())
        if self._inFill or self.behindAxes:
            xA._joinToAxis()
            if self._inFill:
                self._inFillG = Group()
                g.add(self._inFillG)
            if self.behindAxes:
                self._lineG = Group()
                g.add(self._lineG)
        if self.gridFirst:
            xA.makeGrid(g,parent=self,dim=yA.getGridDims)
            yA.makeGrid(g,parent=self,dim=xA.getGridDims)
        g.add(xA)
        g.add(yA)
        if not self.gridFirst:
            xAdgl = getattr(xA,'drawGridLast',False)
            yAdgl = getattr(yA,'drawGridLast',False)
            if not xAdgl: xA.makeGrid(g,parent=self,dim=yA.getGridDims)
            if not yAdgl: yA.makeGrid(g,parent=self,dim=xA.getGridDims)
        annotations = getattr(self,'annotations',[])
        for a in annotations:
            if getattr(a,'beforeLines',None):
                g.add(a(self,xA.scale,yA.scale))
        g.add(self.makeLines())
        if not self.gridFirst:
            if xAdgl: xA.makeGrid(g,parent=self,dim=yA.getGridDims)
            if yAdgl: yA.makeGrid(g,parent=self,dim=xA.getGridDims)
        for a in annotations:
            if not getattr(a,'beforeLines',None):
                g.add(a(self,xA.scale,yA.scale))
        return g

    def addCrossHair(self,name,xv,yv,strokeColor=colors.black,strokeWidth=1,beforeLines=True):
        from reportlab.graphics.shapes import Group, Line
        annotations = [a for a in getattr(self,'annotations',[]) if getattr(a,'name',None)!=name]
        def annotation(self,xScale,yScale):
            x = xScale(xv)
            y = yScale(yv)
            g = Group()
            xA = xScale.im_self #the x axis
            g.add(Line(xA._x,y,xA._x+xA._length,y,strokeColor=strokeColor,strokeWidth=strokeWidth))
            yA = yScale.im_self #the y axis
            g.add(Line(x,yA._y,x,yA._y+yA._length,strokeColor=strokeColor,strokeWidth=strokeWidth))
            return g
        annotation.beforeLines = beforeLines
        annotations.append(annotation)
        self.annotations = annotations
Ejemplo n.º 3
0
class LinePlot(AbstractLineChart):
    """Line plot with multiple lines.

    Both x- and y-axis are value axis (so there are no seperate
    X and Y versions of this class).
    """
    _attrMap = AttrMap(
        BASE=PlotArea,
        reversePlotOrder=AttrMapValue(isBoolean,
                                      desc='If true reverse plot order.',
                                      advancedUsage=1),
        lineLabelNudge=AttrMapValue(
            isNumber,
            desc='Distance between a data point and its label.',
            advancedUsage=1),
        lineLabels=AttrMapValue(
            None, desc='Handle to the list of data point labels.'),
        lineLabelFormat=AttrMapValue(
            None,
            desc='Formatting string or function used for data point labels.'),
        lineLabelArray=AttrMapValue(
            None,
            desc=
            'explicit array of line label values, must match size of data if present.'
        ),
        joinedLines=AttrMapValue(
            isNumber, desc='Display data points joined with lines if true.'),
        strokeColor=AttrMapValue(
            isColorOrNone,
            desc='Color used for background border of plot area.'),
        fillColor=AttrMapValue(
            isColorOrNone,
            desc='Color used for background interior of plot area.'),
        lines=AttrMapValue(None, desc='Handle of the lines.'),
        xValueAxis=AttrMapValue(None, desc='Handle of the x axis.'),
        yValueAxis=AttrMapValue(None, desc='Handle of the y axis.'),
        data=AttrMapValue(
            None, desc='Data to be plotted, list of (lists of) x/y tuples.'),
        annotations=AttrMapValue(
            None,
            desc='list of callables, will be called with self, xscale, yscale.',
            advancedUsage=1),
        behindAxes=AttrMapValue(isBoolean,
                                desc='If true use separate line group.',
                                advancedUsage=1),
        gridFirst=AttrMapValue(isBoolean,
                               desc='If true use draw grids before axes.',
                               advancedUsage=1),
    )

    def __init__(self):
        PlotArea.__init__(self)
        self.reversePlotOrder = 0

        self.xValueAxis = XValueAxis()
        self.yValueAxis = YValueAxis()

        # this defines two series of 3 points.  Just an example.
        self.data = [((1, 1), (2, 2), (2.5, 1), (3, 3), (4, 5)),
                     ((1, 2), (2, 3), (2.5, 2), (3, 4), (4, 6))]

        self.lines = TypedPropertyCollection(LinePlotProperties)
        self.lines.strokeWidth = 1
        self.lines[0].strokeColor = colors.red
        self.lines[1].strokeColor = colors.blue

        self.lineLabels = TypedPropertyCollection(Label)
        self.lineLabelFormat = None
        self.lineLabelArray = None

        # this says whether the origin is inside or outside
        # the bar - +10 means put the origin ten points
        # above the tip of the bar if value > 0, or ten
        # points inside if bar value < 0.  This is different
        # to label dx/dy which are not dependent on the
        # sign of the data.
        self.lineLabelNudge = 10
        # if you have multiple series, by default they butt
        # together.

        # New line chart attributes.
        self.joinedLines = 1  # Connect items with straight lines.

        #private attributes
        self._inFill = None
        self.annotations = []
        self.behindAxes = 0
        self.gridFirst = 0

    def demo(self):
        """Shows basic use of a line chart."""

        drawing = Drawing(400, 200)

        data = [((1, 1), (2, 2), (2.5, 1), (3, 3), (4, 5)),
                ((1, 2), (2, 3), (2.5, 2), (3.5, 5), (4, 6))]

        lp = LinePlot()

        lp.x = 50
        lp.y = 50
        lp.height = 125
        lp.width = 300
        lp.data = data
        lp.joinedLines = 1
        lp.lineLabelFormat = '%2.0f'
        lp.strokeColor = colors.black

        lp.lines[0].strokeColor = colors.red
        lp.lines[0].symbol = makeMarker('FilledCircle')
        lp.lines[1].strokeColor = colors.blue
        lp.lines[1].symbol = makeMarker('FilledDiamond')

        lp.xValueAxis.valueMin = 0
        lp.xValueAxis.valueMax = 5
        lp.xValueAxis.valueStep = 1

        lp.yValueAxis.valueMin = 0
        lp.yValueAxis.valueMax = 7
        lp.yValueAxis.valueStep = 1

        drawing.add(lp)

        return drawing

    def calcPositions(self):
        """Works out where they go.

        Sets an attribute _positions which is a list of
        lists of (x, y) matching the data.
        """

        self._seriesCount = len(self.data)
        self._rowLength = max(map(len, self.data))

        self._positions = []
        for rowNo in range(len(self.data)):
            line = []
            for colNo in range(len(self.data[rowNo])):
                datum = self.data[rowNo][colNo]  # x,y value
                if type(datum[0]) == type(''):
                    x = self.xValueAxis.scale(mktime(mkTimeTuple(datum[0])))
                else:
                    x = self.xValueAxis.scale(datum[0])
                y = self.yValueAxis.scale(datum[1])
                line.append((x, y))
            self._positions.append(line)

    def _innerDrawLabel(self, rowNo, colNo, x, y):
        "Draw a label for a given item in the list."

        labelFmt = self.lineLabelFormat
        labelValue = self.data[rowNo][colNo][1]  ###

        if labelFmt is None:
            labelText = None
        elif type(labelFmt) is StringType:
            if labelFmt == 'values':
                labelText = self.lineLabelArray[rowNo][colNo]
            else:
                labelText = labelFmt % labelValue
        elif hasattr(labelFmt, '__call__'):
            labelText = labelFmt(labelValue)
        else:
            raise ValueError(
                "Unknown formatter type %s, expected string or function" %
                labelFmt)

        if labelText:
            label = self.lineLabels[(rowNo, colNo)]
            if not label.visible: return
            #hack to make sure labels are outside the bar
            if y > 0:
                label.setOrigin(x, y + self.lineLabelNudge)
            else:
                label.setOrigin(x, y - self.lineLabelNudge)
            label.setText(labelText)
        else:
            label = None
        return label

    def drawLabel(self, G, rowNo, colNo, x, y):
        '''Draw a label for a given item in the list.
        G must have an add method'''
        G.add(self._innerDrawLabel(rowNo, colNo, x, y))

    def makeLines(self):
        g = Group()
        bubblePlot = getattr(self, '_bubblePlot', None)
        if bubblePlot:
            yA = self.yValueAxis
            xA = self.xValueAxis
            bubbleR = min(yA._bubbleRadius, xA._bubbleRadius)
            bubbleMax = xA._bubbleMax

        labelFmt = self.lineLabelFormat

        P = range(len(self._positions))
        if self.reversePlotOrder: P.reverse()
        inFill = getattr(self, '_inFill', None)
        styleCount = len(self.lines)
        if inFill or [
                rowNo for rowNo in P
                if getattr(self.lines[rowNo % styleCount], 'inFill', False)
        ]:
            inFillY = self.xValueAxis._y
            inFillX0 = self.yValueAxis._x
            inFillX1 = inFillX0 + self.xValueAxis._length
            inFillG = getattr(self, '_inFillG', g)
        lG = getattr(self, '_lineG', g)
        # Iterate over data rows.
        for rowNo in P:
            row = self._positions[rowNo]
            rowStyle = self.lines[rowNo % styleCount]
            rowColor = getattr(rowStyle, 'strokeColor', None)
            dash = getattr(rowStyle, 'strokeDashArray', None)

            if hasattr(rowStyle, 'strokeWidth'):
                width = rowStyle.strokeWidth
            elif hasattr(self.lines, 'strokeWidth'):
                width = self.lines.strokeWidth
            else:
                width = None

            # Iterate over data columns.
            if self.joinedLines:
                points = []
                for xy in row:
                    points += [xy[0], xy[1]]
                if inFill or getattr(rowStyle, 'inFill', False):
                    fpoints = [inFillX0, inFillY
                               ] + points + [inFillX1, inFillY]
                    filler = getattr(rowStyle, 'filler', None)
                    if filler:
                        filler.fill(self, inFillG, rowNo, rowColor, fpoints)
                    else:
                        inFillG.add(
                            Polygon(fpoints,
                                    fillColor=rowColor,
                                    strokeColor=rowColor,
                                    strokeWidth=width or 0.1))
                if inFill in (None, 0, 2):
                    line = PolyLine(points,
                                    strokeColor=rowColor,
                                    strokeLineCap=0,
                                    strokeLineJoin=1)
                    if width:
                        line.strokeWidth = width
                    if dash:
                        line.strokeDashArray = dash
                    lG.add(line)

            if hasattr(rowStyle, 'symbol'):
                uSymbol = rowStyle.symbol
            elif hasattr(self.lines, 'symbol'):
                uSymbol = self.lines.symbol
            else:
                uSymbol = None

            if uSymbol:
                j = -1
                if bubblePlot: drow = self.data[rowNo]
                for xy in row:
                    j += 1
                    symbol = uSymbol2Symbol(uSymbol, xy[0], xy[1], rowColor)
                    if symbol:
                        if bubblePlot:
                            symbol.size = bubbleR * (drow[j][2] /
                                                     bubbleMax)**0.5
                        g.add(symbol)

            # Draw data labels.
            for colNo in range(len(row)):
                x1, y1 = row[colNo]
                self.drawLabel(g, rowNo, colNo, x1, y1)

            shader = getattr(rowStyle, 'shader', None)
            if shader: shader.shade(self, g, rowNo, rowColor, row)

        return g

    def draw(self):
        yA = self.yValueAxis
        xA = self.xValueAxis
        if getattr(self, '_bubblePlot', None):
            yA._bubblePlot = xA._bubblePlot = 1
        yA.setPosition(self.x, self.y, self.height)
        if yA: yA.joinAxis = xA
        if xA: xA.joinAxis = yA
        yA.configure(self.data)

        # if zero is in chart, put x axis there, otherwise use bottom.
        xAxisCrossesAt = yA.scale(0)
        if ((xAxisCrossesAt > self.y + self.height)
                or (xAxisCrossesAt < self.y)):
            y = self.y
        else:
            y = xAxisCrossesAt

        xA.setPosition(self.x, y, self.width)
        xA.configure(self.data)
        self.calcPositions()
        g = Group()
        g.add(self.makeBackground())
        if self._inFill or self.behindAxes:
            xA._joinToAxis()
            if self._inFill:
                self._inFillG = Group()
                g.add(self._inFillG)
            if self.behindAxes:
                self._lineG = Group()
                g.add(self._lineG)
        if self.gridFirst:
            xA.makeGrid(g, parent=self, dim=yA.getGridDims)
            yA.makeGrid(g, parent=self, dim=xA.getGridDims)
        g.add(xA.draw())
        g.add(yA.draw())
        xAex = xA.visibleAxis and (xA._y, ) or ()
        yAex = yA.visibleAxis and (yA._x, ) or ()
        if not self.gridFirst:
            xAdgl = getattr(xA, 'drawGridLast', False)
            yAdgl = getattr(yA, 'drawGridLast', False)
            if not xAdgl:
                xA.makeGrid(g, parent=self, dim=yA.getGridDims, exclude=yAex)
            if not yAdgl:
                yA.makeGrid(g, parent=self, dim=xA.getGridDims, exclude=xAex)
        annotations = getattr(self, 'annotations', [])
        for a in annotations:
            if getattr(a, 'beforeLines', None):
                g.add(a(self, xA.scale, yA.scale))
        g.add(self.makeLines())
        if not self.gridFirst:
            if xAdgl:
                xA.makeGrid(g, parent=self, dim=yA.getGridDims, exclude=yAex)
            if yAdgl:
                yA.makeGrid(g, parent=self, dim=xA.getGridDims, exclude=xAex)
        for a in annotations:
            if not getattr(a, 'beforeLines', None):
                g.add(a(self, xA.scale, yA.scale))
        return g

    def addCrossHair(self,
                     name,
                     xv,
                     yv,
                     strokeColor=colors.black,
                     strokeWidth=1,
                     beforeLines=True):
        from reportlab.graphics.shapes import Group, Line
        annotations = [
            a for a in getattr(self, 'annotations', [])
            if getattr(a, 'name', None) != name
        ]

        def annotation(self, xScale, yScale):
            x = xScale(xv)
            y = yScale(yv)
            g = Group()
            xA = xScale.im_self  #the x axis
            g.add(
                Line(xA._x,
                     y,
                     xA._x + xA._length,
                     y,
                     strokeColor=strokeColor,
                     strokeWidth=strokeWidth))
            yA = yScale.im_self  #the y axis
            g.add(
                Line(x,
                     yA._y,
                     x,
                     yA._y + yA._length,
                     strokeColor=strokeColor,
                     strokeWidth=strokeWidth))
            return g

        annotation.beforeLines = beforeLines
        annotations.append(annotation)
        self.annotations = annotations
Ejemplo n.º 4
0
class HorizontalLineChart(LineChart):
    """Line chart with multiple lines.

    A line chart is assumed to have one category and one value axis.
    Despite its generic name this particular line chart class has
    a vertical value axis and a horizontal category one. It may
    evolve into individual horizontal and vertical variants (like
    with the existing bar charts).

    Available attributes are:

        x: x-position of lower-left chart origin
        y: y-position of lower-left chart origin
        width: chart width
        height: chart height

        useAbsolute: disables auto-scaling of chart elements (?)
        lineLabelNudge: distance of data labels to data points
        lineLabels: labels associated with data values
        lineLabelFormat: format string or callback function
        groupSpacing: space between categories

        joinedLines: enables drawing of lines

        strokeColor: color of chart lines (?)
        fillColor: color for chart background (?)
        lines: style list, used cyclically for data series

        valueAxis: value axis object
        categoryAxis: category axis object
        categoryNames: category names

        data: chart data, a list of data series of equal length
    """

    _attrMap = AttrMap(BASE=LineChart,
        useAbsolute = AttrMapValue(isNumber, desc='Flag to use absolute spacing values.',advancedUsage=1),
        lineLabelNudge = AttrMapValue(isNumber, desc='Distance between a data point and its label.',advancedUsage=1),
        lineLabels = AttrMapValue(None, desc='Handle to the list of data point labels.'),
        lineLabelFormat = AttrMapValue(None, desc='Formatting string or function used for data point labels.'),
        lineLabelArray = AttrMapValue(None, desc='explicit array of line label values, must match size of data if present.'),
        groupSpacing = AttrMapValue(isNumber, desc='? - Likely to disappear.'),
        joinedLines = AttrMapValue(isNumber, desc='Display data points joined with lines if true.'),
        lines = AttrMapValue(None, desc='Handle of the lines.'),
        valueAxis = AttrMapValue(None, desc='Handle of the value axis.'),
        categoryAxis = AttrMapValue(None, desc='Handle of the category axis.'),
        categoryNames = AttrMapValue(isListOfStringsOrNone, desc='List of category names.'),
        data = AttrMapValue(None, desc='Data to be plotted, list of (lists of) numbers.'),
        inFill = AttrMapValue(isBoolean, desc='Whether infilling should be done.',advancedUsage=1),
        reversePlotOrder = AttrMapValue(isBoolean, desc='If true reverse plot order.',advancedUsage=1),
        annotations = AttrMapValue(None, desc='list of callables, will be called with self, xscale, yscale.',advancedUsage=1),
        )

    def __init__(self):
        LineChart.__init__(self)

        # Allow for a bounding rectangle.
        self.strokeColor = None
        self.fillColor = None

        # Named so we have less recoding for the horizontal one :-)
        self.categoryAxis = XCategoryAxis()
        self.valueAxis = YValueAxis()

        # This defines two series of 3 points.  Just an example.
        self.data = [(100,110,120,130),
                     (70, 80, 80, 90)]
        self.categoryNames = ('North','South','East','West')

        self.lines = TypedPropertyCollection(LineChartProperties)
        self.lines.strokeWidth = 1
        self.lines[0].strokeColor = colors.red
        self.lines[1].strokeColor = colors.green
        self.lines[2].strokeColor = colors.blue

        # control spacing. if useAbsolute = 1 then
        # the next parameters are in points; otherwise
        # they are 'proportions' and are normalized to
        # fit the available space.
        self.useAbsolute = 0   #- not done yet
        self.groupSpacing = 1 #5

        self.lineLabels = TypedPropertyCollection(Label)
        self.lineLabelFormat = None
        self.lineLabelArray = None

        # This says whether the origin is above or below
        # the data point. +10 means put the origin ten points
        # above the data point if value > 0, or ten
        # points below if data value < 0.  This is different
        # to label dx/dy which are not dependent on the
        # sign of the data.
        self.lineLabelNudge = 10
        # If you have multiple series, by default they butt
        # together.

        # New line chart attributes.
        self.joinedLines = 1 # Connect items with straight lines.
        self.inFill = 0
        self.reversePlotOrder = 0


    def demo(self):
        """Shows basic use of a line chart."""

        drawing = Drawing(200, 100)

        data = [
                (13, 5, 20, 22, 37, 45, 19, 4),
                (14, 10, 21, 28, 38, 46, 25, 5)
                ]

        lc = HorizontalLineChart()

        lc.x = 20
        lc.y = 10
        lc.height = 85
        lc.width = 170
        lc.data = data
        lc.lines.symbol = makeMarker('Circle')

        drawing.add(lc)

        return drawing


    def calcPositions(self):
        """Works out where they go.

        Sets an attribute _positions which is a list of
        lists of (x, y) matching the data.
        """

        self._seriesCount = len(self.data)
        self._rowLength = max(list(map(len,self.data)))

        if self.useAbsolute:
            # Dimensions are absolute.
            normFactor = 1.0
        else:
            # Dimensions are normalized to fit.
            normWidth = self.groupSpacing
            availWidth = self.categoryAxis.scale(0)[1]
            normFactor = availWidth / normWidth

        self._positions = []
        for rowNo in range(len(self.data)):
            lineRow = []
            for colNo in range(len(self.data[rowNo])):
                datum = self.data[rowNo][colNo]
                if datum is not None:
                    (groupX, groupWidth) = self.categoryAxis.scale(colNo)
                    x = groupX + (0.5 * self.groupSpacing * normFactor)
                    y = self.valueAxis.scale(0)
                    height = self.valueAxis.scale(datum) - y
                    lineRow.append((x, y+height))
            self._positions.append(lineRow)


    def _innerDrawLabel(self, rowNo, colNo, x, y):
        "Draw a label for a given item in the list."

        labelFmt = self.lineLabelFormat
        labelValue = self.data[rowNo][colNo]

        if labelFmt is None:
            labelText = None
        elif type(labelFmt) is str:
            if labelFmt == 'values':
                try:
                    labelText = self.lineLabelArray[rowNo][colNo]
                except:
                    labelText = None
            else:
                labelText = labelFmt % labelValue
        elif hasattr(labelFmt,'__call__'):
            labelText = labelFmt(labelValue)
        else:
            raise ValueError("Unknown formatter type %s, expected string or function"%labelFmt)

        if labelText:
            label = self.lineLabels[(rowNo, colNo)]
            if not label.visible: return
            # Make sure labels are some distance off the data point.
            if y > 0:
                label.setOrigin(x, y + self.lineLabelNudge)
            else:
                label.setOrigin(x, y - self.lineLabelNudge)
            label.setText(labelText)
        else:
            label = None
        return label

    def drawLabel(self, G, rowNo, colNo, x, y):
        '''Draw a label for a given item in the list.
        G must have an add method'''
        G.add(self._innerDrawLabel(rowNo,colNo,x,y))

    def makeLines(self):
        g = Group()

        labelFmt = self.lineLabelFormat
        P = list(range(len(self._positions)))
        if self.reversePlotOrder: P.reverse()
        inFill = self.inFill
        if inFill:
            inFillY = self.categoryAxis._y
            inFillX0 = self.valueAxis._x
            inFillX1 = inFillX0 + self.categoryAxis._length
            inFillG = getattr(self,'_inFillG',g)

        # Iterate over data rows.
        for rowNo in P:
            row = self._positions[rowNo]
            styleCount = len(self.lines)
            styleIdx = rowNo % styleCount
            rowStyle = self.lines[styleIdx]
            rowColor = rowStyle.strokeColor
            dash = getattr(rowStyle, 'strokeDashArray', None)

            if hasattr(self.lines[styleIdx], 'strokeWidth'):
                strokeWidth = self.lines[styleIdx].strokeWidth
            elif hasattr(self.lines, 'strokeWidth'):
                strokeWidth = self.lines.strokeWidth
            else:
                strokeWidth = None

            # Iterate over data columns.
            if self.joinedLines:
                points = []
                for colNo in range(len(row)):
                    points += row[colNo]
                if inFill:
                    points = points + [inFillX1,inFillY,inFillX0,inFillY]
                    inFillG.add(Polygon(points,fillColor=rowColor,strokeColor=rowColor,strokeWidth=0.1))
                else:
                    line = PolyLine(points,strokeColor=rowColor,strokeLineCap=0,strokeLineJoin=1)
                    if strokeWidth:
                        line.strokeWidth = strokeWidth
                    if dash:
                        line.strokeDashArray = dash
                    g.add(line)

            if hasattr(self.lines[styleIdx], 'symbol'):
                uSymbol = self.lines[styleIdx].symbol
            elif hasattr(self.lines, 'symbol'):
                uSymbol = self.lines.symbol
            else:
                uSymbol = None

            if uSymbol:
                for colNo in range(len(row)):
                    x1, y1 = row[colNo]
                    symbol = uSymbol2Symbol(uSymbol,x1,y1,rowStyle.strokeColor)
                    if symbol: g.add(symbol)

            # Draw item labels.
            for colNo in range(len(row)):
                x1, y1 = row[colNo]
                self.drawLabel(g, rowNo, colNo, x1, y1)

        return g

    def draw(self):
        "Draws itself."

        vA, cA = self.valueAxis, self.categoryAxis
        vA.setPosition(self.x, self.y, self.height)
        if vA: vA.joinAxis = cA
        if cA: cA.joinAxis = vA
        vA.configure(self.data)

        # If zero is in chart, put x axis there, otherwise
        # use bottom.
        xAxisCrossesAt = vA.scale(0)
        if ((xAxisCrossesAt > self.y + self.height) or (xAxisCrossesAt < self.y)):
            y = self.y
        else:
            y = xAxisCrossesAt

        cA.setPosition(self.x, y, self.width)
        cA.configure(self.data)

        self.calcPositions()

        g = Group()
        g.add(self.makeBackground())
        if self.inFill:
            self._inFillG = Group()
            g.add(self._inFillG)

        g.add(cA)
        g.add(vA)
        cAdgl = getattr(cA,'drawGridLast',False)
        vAdgl = getattr(vA,'drawGridLast',False)
        if not cAdgl: cA.makeGrid(g,parent=self,dim=vA.getGridDims)
        if not vAdgl: vA.makeGrid(g,parent=self,dim=cA.getGridDims)
        g.add(self.makeLines())
        if cAdgl: cA.makeGrid(g,parent=self,dim=vA.getGridDims)
        if vAdgl: vA.makeGrid(g,parent=self,dim=cA.getGridDims)
        for a in getattr(self,'annotations',()): g.add(a(self,cA.scale,vA.scale))
        return g
Ejemplo n.º 5
0
class LinePlot(PlotArea):
    """Line plot with multiple lines.

    Both x- and y-axis are value axis (so there are no seperate
    X and Y versions of this class).
    """

    _attrMap = AttrMap(
        BASE=PlotArea,
        reversePlotOrder=AttrMapValue(isBoolean, desc="If true reverse plot order."),
        lineLabelNudge=AttrMapValue(isNumber, desc="Distance between a data point and its label."),
        lineLabels=AttrMapValue(None, desc="Handle to the list of data point labels."),
        lineLabelFormat=AttrMapValue(None, desc="Formatting string or function used for data point labels."),
        lineLabelArray=AttrMapValue(
            None, desc="explicit array of line label values, must match size of data if present."
        ),
        joinedLines=AttrMapValue(isNumber, desc="Display data points joined with lines if true."),
        strokeColor=AttrMapValue(isColorOrNone, desc="Color used for background border of plot area."),
        fillColor=AttrMapValue(isColorOrNone, desc="Color used for background interior of plot area."),
        lines=AttrMapValue(None, desc="Handle of the lines."),
        xValueAxis=AttrMapValue(None, desc="Handle of the x axis."),
        yValueAxis=AttrMapValue(None, desc="Handle of the y axis."),
        data=AttrMapValue(None, desc="Data to be plotted, list of (lists of) x/y tuples."),
        annotations=AttrMapValue(None, desc="list of callables, will be called with self, xscale, yscale."),
    )

    def __init__(self):
        PlotArea.__init__(self)
        self.reversePlotOrder = 0

        self.xValueAxis = XValueAxis()
        self.yValueAxis = YValueAxis()

        # this defines two series of 3 points.  Just an example.
        self.data = [((1, 1), (2, 2), (2.5, 1), (3, 3), (4, 5)), ((1, 2), (2, 3), (2.5, 2), (3, 4), (4, 6))]

        self.lines = TypedPropertyCollection(LinePlotProperties)
        self.lines.strokeWidth = 1
        self.lines[0].strokeColor = colors.red
        self.lines[1].strokeColor = colors.blue

        self.lineLabels = TypedPropertyCollection(Label)
        self.lineLabelFormat = None
        self.lineLabelArray = None

        # this says whether the origin is inside or outside
        # the bar - +10 means put the origin ten points
        # above the tip of the bar if value > 0, or ten
        # points inside if bar value < 0.  This is different
        # to label dx/dy which are not dependent on the
        # sign of the data.
        self.lineLabelNudge = 10
        # if you have multiple series, by default they butt
        # together.

        # New line chart attributes.
        self.joinedLines = 1  # Connect items with straight lines.

        # private attributes
        self._inFill = None

    def demo(self):
        """Shows basic use of a line chart."""

        drawing = Drawing(400, 200)

        data = [((1, 1), (2, 2), (2.5, 1), (3, 3), (4, 5)), ((1, 2), (2, 3), (2.5, 2), (3.5, 5), (4, 6))]

        lp = LinePlot()

        lp.x = 50
        lp.y = 50
        lp.height = 125
        lp.width = 300
        lp.data = data
        lp.joinedLines = 1
        lp.lineLabelFormat = "%2.0f"
        lp.strokeColor = colors.black

        lp.lines[0].strokeColor = colors.red
        lp.lines[0].symbol = makeMarker("FilledCircle")
        lp.lines[1].strokeColor = colors.blue
        lp.lines[1].symbol = makeMarker("FilledDiamond")

        lp.xValueAxis.valueMin = 0
        lp.xValueAxis.valueMax = 5
        lp.xValueAxis.valueStep = 1

        lp.yValueAxis.valueMin = 0
        lp.yValueAxis.valueMax = 7
        lp.yValueAxis.valueStep = 1

        drawing.add(lp)

        return drawing

    def calcPositions(self):
        """Works out where they go.

        Sets an attribute _positions which is a list of
        lists of (x, y) matching the data.
        """

        self._seriesCount = len(self.data)
        self._rowLength = max(map(len, self.data))

        self._positions = []
        for rowNo in range(len(self.data)):
            line = []
            for colNo in range(len(self.data[rowNo])):
                datum = self.data[rowNo][colNo]  # x,y value
                if type(datum[0]) == type(""):
                    x = self.xValueAxis.scale(mktime(mkTimeTuple(datum[0])))
                else:
                    x = self.xValueAxis.scale(datum[0])
                y = self.yValueAxis.scale(datum[1])
                line.append((x, y))
            self._positions.append(line)

    def _innerDrawLabel(self, rowNo, colNo, x, y):
        "Draw a label for a given item in the list."

        labelFmt = self.lineLabelFormat
        labelValue = self.data[rowNo][colNo][1]  ###

        if labelFmt is None:
            labelText = None
        elif type(labelFmt) is StringType:
            if labelFmt == "values":
                labelText = self.lineLabelArray[rowNo][colNo]
            else:
                labelText = labelFmt % labelValue
        elif type(labelFmt) is FunctionType:
            labelText = labelFmt(labelValue)
        elif isinstance(labelFmt, Formatter):
            labelText = labelFmt(labelValue)
        else:
            msg = "Unknown formatter type %s, expected string or function"
            raise Exception, msg % labelFmt

        if labelText:
            label = self.lineLabels[(rowNo, colNo)]
            # hack to make sure labels are outside the bar
            if y > 0:
                label.setOrigin(x, y + self.lineLabelNudge)
            else:
                label.setOrigin(x, y - self.lineLabelNudge)
            label.setText(labelText)
        else:
            label = None
        return label

    def drawLabel(self, G, rowNo, colNo, x, y):
        """Draw a label for a given item in the list.
        G must have an add method"""
        G.add(self._innerDrawLabel(rowNo, colNo, x, y))

    def makeLines(self):
        g = Group()
        bubblePlot = getattr(self, "_bubblePlot", None)
        if bubblePlot:
            yA = self.yValueAxis
            xA = self.xValueAxis
            bubbleR = min(yA._bubbleRadius, xA._bubbleRadius)
            bubbleMax = xA._bubbleMax

        labelFmt = self.lineLabelFormat

        P = range(len(self._positions))
        if self.reversePlotOrder:
            P.reverse()
        inFill = getattr(self, "_inFill", None)
        if inFill:
            inFillY = self.xValueAxis._y
            inFillX0 = self.yValueAxis._x
            inFillX1 = inFillX0 + self.xValueAxis._length
            inFillG = getattr(self, "_inFillG", g)
        # Iterate over data rows.
        styleCount = len(self.lines)
        for rowNo in P:
            row = self._positions[rowNo]
            rowStyle = self.lines[rowNo % styleCount]
            rowColor = rowStyle.strokeColor
            dash = getattr(rowStyle, "strokeDashArray", None)

            if hasattr(rowStyle, "strokeWidth"):
                width = rowStyle.strokeWidth
            elif hasattr(self.lines, "strokeWidth"):
                width = self.lines.strokeWidth
            else:
                width = None

            # Iterate over data columns.
            if self.joinedLines:
                points = []
                for xy in row:
                    points = points + [xy[0], xy[1]]
                if inFill:
                    points = points + [inFillX1, inFillY, inFillX0, inFillY]
                    filler = getattr(rowStyle, "filler", None)
                    if filler:
                        filler.fill(self, inFillG, rowNo, rowColor, points)
                    else:
                        inFillG.add(Polygon(points, fillColor=rowColor, strokeColor=rowColor, strokeWidth=0.1))
                else:
                    line = PolyLine(points, strokeColor=rowColor, strokeLineCap=0, strokeLineJoin=1)
                    if width:
                        line.strokeWidth = width
                    if dash:
                        line.strokeDashArray = dash
                    g.add(line)

            if hasattr(rowStyle, "symbol"):
                uSymbol = rowStyle.symbol
            elif hasattr(self.lines, "symbol"):
                uSymbol = self.lines.symbol
            else:
                uSymbol = None

            if uSymbol:
                j = -1
                if bubblePlot:
                    drow = self.data[rowNo]
                for xy in row:
                    j += 1
                    symbol = uSymbol2Symbol(uSymbol, xy[0], xy[1], rowColor)
                    if symbol:
                        if bubblePlot:
                            symbol.size = bubbleR * (drow[j][2] / bubbleMax) ** 0.5
                        g.add(symbol)

            # Draw data labels.
            for colNo in range(len(row)):
                x1, y1 = row[colNo]
                self.drawLabel(g, rowNo, colNo, x1, y1)

            shader = getattr(rowStyle, "shader", None)
            if shader:
                shader.shade(self, g, rowNo, rowColor, row)

        return g

    def makeSwatchSample(self, rowNo, x, y, width, height):
        styleCount = len(self.lines)
        styleIdx = rowNo % styleCount
        rowColor = self.lines[styleIdx].strokeColor

        if self.joinedLines:
            dash = getattr(self.lines[styleIdx], "strokeDashArray", getattr(self.lines, "strokeDashArray", None))
            strokeWidth = getattr(
                self.lines[styleIdx], "strokeWidth", getattr(self.lines[styleIdx], "strokeWidth", None)
            )
            L = Line(x, y, x + width, y + height, strokeColor=rowColor, strokeLineCap=0)
            if strokeWidth:
                L.strokeWidth = strokeWidth
            if dash:
                L.strokeDashArray = dash
        else:
            L = None

        if hasattr(self.lines[styleIdx], "symbol"):
            S = self.lines[styleIdx].symbol
        elif hasattr(self.lines, "symbol"):
            S = self.lines.symbol
        else:
            S = None

        if S:
            S = uSymbol2Symbol(S, x + width / 2.0, y + height / 2.0, rowColor)
        if S and L:
            g = Group()
            g.add(S)
            g.add(L)
            return g
        return S or L

    def draw(self):
        yA = self.yValueAxis
        xA = self.xValueAxis
        if getattr(self, "_bubblePlot", None):
            yA._bubblePlot = xA._bubblePlot = 1
        yA.setPosition(self.x, self.y, self.height)
        if yA:
            yA.joinAxis = xA
        if xA:
            xA.joinAxis = yA
        yA.configure(self.data)

        # if zero is in chart, put x axis there, otherwise use bottom.
        xAxisCrossesAt = yA.scale(0)
        if (xAxisCrossesAt > self.y + self.height) or (xAxisCrossesAt < self.y):
            y = self.y
        else:
            y = xAxisCrossesAt

        xA.setPosition(self.x, y, self.width)
        xA.configure(self.data)
        self.calcPositions()
        g = Group()
        g.add(self.makeBackground())
        if self._inFill:
            self._inFillG = Group()
            g.add(self._inFillG)
        g.add(xA)
        g.add(yA)
        yA.gridStart = xA._x
        yA.gridEnd = xA._x + xA._length
        xA.gridStart = yA._y
        xA.gridEnd = yA._y + yA._length
        xA.makeGrid(g, parent=self)
        yA.makeGrid(g, parent=self)
        g.add(self.makeLines())
        for a in getattr(self, "annotations", ()):
            g.add(a(self, xA.scale, yA.scale))
        return g