Charts
11.3 Charts¶
The motivation for much of this is to create a flexible chart package. This section presents a treatment of the ideas behind our charting model, what the design goals are and what components of the chart package already exist.
Design Goals¶
Here are some of the design goals:
*Make simple top-level use really simple *
It should be possible to create a simple chart with minimum lines of code, yet have it 'do the right things' with sensible automatic settings. The pie chart snippets above do this. If a real chart has many subcomponents, you still should not need to interact with them unless you want to customize what they do.
*Allow precise positioning *
An absolute requirement in publishing and graphic design is to control the placing and style of every element. We will try to have properties that specify things in fixed sizes and proportions of the drawing, rather than having automatic resizing. Thus, the 'inner plot rectangle' will not magically change when you make the font size of the y labels bigger, even if this means your labels can spill out of the left edge of the chart rectangle. It is your job to preview the chart and choose sizes and spaces which will work.
Some things do need to be automatic. For example, if you want to fit N bars into a 200 point space and don't know N in advance, we specify bar separation as a percentage of the width of a bar rather than a point size, and let the chart work it out. This is still deterministic and controllable.
Control child elements individually or as a group
We use smart collection classes that let you customize a group of things, or just one of them. For example you can do this in our experimental pie chart:
d = Drawing(400,200)
pc = Pie()
pc.x = 150
pc.y = 50
pc.data = [10,20,30,40,50,60]
pc.labels = ['a','b','c','d','e','f']
pc.slices.strokeWidth=0.5
pc.slices[3].popout = 20
pc.slices[3].strokeWidth = 2
pc.slices[3].strokeDashArray = [2,2]
pc.slices[3].labelRadius = 1.75
pc.slices[3].fontColor = colors.red
d.add(pc, '')
pc.slices[3] actually lazily creates a little object which holds information about the slice in question; this will be used to format a fourth slice at draw-time if there is one.
*Only expose things you should change *
It would be wrong from a statistical viewpoint to let you directly adjust the angle of one of the pie slices in the above example, since that is determined by the data. So not everything will be exposed through the public properties. There may be 'back doors' to let you violate this when you really need to, or methods to provide advanced functionality, but in general properties will be orthogonal.
*Composition and component based *
Charts are built out of reusable child widgets. A Legend is an easy-to-grasp example. If you need a specialized type of legend (e.g. circular colour swatches), you should subclass the standard Legend widget. Then you could either do something like...
c = MyChartWithLegend()
c.legend = MyNewLegendClass() # just change it
c.legend.swatchRadius = 5 # set a property only relevant to the new one
c.data = [10,20,30] # and then configure as usual...
...or create/modify your own chart or drawing class which creates one of these by default. This is also very relevant for time series charts, where there can be many styles of x axis.
Top level chart classes will create a number of such components, and then either call methods or set private properties to tell them their height and position - all the stuff which should be done for you and which you cannot customise. We are working on modelling what the components should be and will publish their APIs here as a consensus emerges.
*Multiples *
A corollary of the component approach is that you can create diagrams with multiple charts, or custom data graphics. Our favourite example of what we are aiming for is the weather report in our gallery contributed by a user; we'd like to make it easy to create such drawings, hook the building blocks up to their legends, and feed that data in a consistent way.
(If you want to see the image, it is available on our website here)
Overview¶
A chart or plot is an object which is placed on a drawing; it is not itself a drawing. You can thus control where it goes, put several on the same drawing, or add annotations.
Charts have two axes; axes may be Value or Category axes. Axes in turn have a Labels property which lets you configure all text labels or each one individually. Most of the configuration details which vary from chart to chart relate to axis properties, or axis labels.
Objects expose properties through the interfaces discussed in the previous section; these are all optional and are there to let the end user configure the appearance. Things which must be set for a chart to work, and essential communication between a chart and its components, are handled through methods.
You can subclass any chart component and use your replacement instead of the original provided you implement the essential methods and properties.
11.4 Labels¶
A label is a string of text attached to some chart element. They are used on axes, for titles or alongside axes, or attached to individual data points. Labels may contain newline characters, but only one font.
The text and 'origin' of a label are typically set by its parent object. They are accessed by methods rather than properties. Thus, the X axis decides the 'reference point' for each tickmark label and the numeric or date text for each label. However, the end user can set properties of the label (or collection of labels) directly to affect its position relative to this origin and all of its formatting.
from reportlab.graphics import shapes
from reportlab.graphics.charts.textlabels import Label
d = Drawing(200, 100)
# mark the origin of the label
d.add(Circle(100,90, 5, fillColor=colors.green))
lab = Label()
lab.setOrigin(100,90)
lab.boxAnchor = 'ne'
lab.angle = 45
lab.dx = 0
lab.dy = -20
lab.boxStrokeColor = colors.green
lab.setText('Some
Multi-Line
Label')
d.add(lab)
In the drawing above, the label is defined relative to the green blob. The text box should have its north-east corner ten points down from the origin, and be rotated by 45 degrees about that corner.
At present labels have the following properties, which we believe are sufficient for all charts we have seen to date:
Table - Label properties
To see many more examples of Label objects with different
combinations of properties, please have a look into the
ReportLab test suite in the folder tests, run the
script test_charts_textlabels.py and look at the PDF document
it generates!
11.5 Axes¶
We identify two basic kinds of axes - Value and Category ones. Both come in horizontal and vertical flavors. Both can be subclassed to make very specific kinds of axis. For example, if you have complex rules for which dates to display in a time series application, or want irregular scaling, you override the axis and make a new one.
Axes are responsible for determining the mapping from data to image coordinates; transforming points on request from the chart; drawing themselves and their tickmarks, gridlines and axis labels.
This drawing shows two axes, one of each kind, which have been created directly without reference to any chart:
Here is the code that created them:
from reportlab.graphics import shapes
from reportlab.graphics.charts.axes import XCategoryAxis,YValueAxis
drawing = Drawing(400, 200)
data = [(10, 20, 30, 40), (15, 22, 37, 42)]
xAxis = XCategoryAxis()
xAxis.setPosition(75, 75, 300)
xAxis.configure(data)
xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni']
xAxis.labels.boxAnchor = 'n'
xAxis.labels[3].dy = -15
xAxis.labels[3].angle = 30
xAxis.labels[3].fontName = 'Times-Bold'
yAxis = YValueAxis()
yAxis.setPosition(50, 50, 125)
yAxis.configure(data)
drawing.add(xAxis)
drawing.add(yAxis)
Remember that, usually, you won't have to create axes directly; when using a standard chart, it comes with ready-made axes. The methods are what the chart uses to configure it and take care of the geometry. However, we will talk through them in detail below. The orthogonally dual axes to those we describe have essentially the same properties, except for those refering to ticks.
XCategoryAxis class¶
A Category Axis doesn't really have a scale; it just divides itself
into equal-sized buckets.
It is simpler than a value axis.
The chart (or programmer) sets its location with the method
setPosition(x, y, length).
The next stage is to show it the data so that it can configure
itself.
This is easy for a category axis - it just counts the number of
data points in one of the data series. The reversed attribute (if 1)
indicates that the categories should be reversed.
When the drawing is drawn, the axis can provide some help to the
chart with its scale() method, which tells the chart where
a given category begins and ends on the page.
We have not yet seen any need to let people override the widths
or positions of categories.
An XCategoryAxis has the following editable properties:
Table - XCategoryAxis properties
YValueAxis¶
The left axis in the diagram is a YValueAxis. A Value Axis differs from a Category Axis in that each point along its length corresponds to a y value in chart space. It is the job of the axis to configure itself, and to convert Y values from chart space to points on demand to assist the parent chart in plotting.
setPosition(x, y, length) and configure(data) work exactly as
for a category axis.
If you have not fully specified the maximum, minimum and tick
interval, then configure() results in the axis choosing suitable
values.
Once configured, the value axis can convert y data values to drawing
space with the scale() method.
Thus:
>>> yAxis = YValueAxis()
>>> yAxis.setPosition(50, 50, 125)
>>> data = [(10, 20, 30, 40),(15, 22, 37, 42)]
>>> yAxis.configure(data)
>>> yAxis.scale(10) # should be bottom of chart
50.0
>>> yAxis.scale(40) # should be near the top
167.1875
>>>
By default, the highest data point is aligned with the top of the axis, the lowest with the bottom of the axis, and the axis choose 'nice round numbers' for its tickmark points. You may override these settings with the properties below.
Table - YValueAxis properties
The valueSteps property lets you explicitly specify the
tick mark locations, so you don't have to follow regular intervals.
Hence, you can plot month ends and month end dates with a couple of
helper functions, and without needing special time series chart
classes.
The following code show how to create a simple XValueAxis with special
tick intervals. Make sure to set the valueSteps attribute before calling
the configure method!
from reportlab.graphics.shapes import Drawing
from reportlab.graphics.charts.axes import XValueAxis
drawing = Drawing(400, 100)
data = [(10, 20, 30, 40)]
xAxis = XValueAxis()
xAxis.setPosition(75, 50, 300)
xAxis.valueSteps = [10, 15, 20, 30, 35, 40]
xAxis.configure(data)
xAxis.labels.boxAnchor = 'n'
drawing.add(xAxis)
In addition to these properties, all axes classes have three
properties describing how to join two of them to each other.
Again, this is interesting only if you define your own charts
or want to modify the appearance of an existing chart using
such axes.
These properties are listed here only very briefly for now,
but you can find a host of sample functions in the module
reportlab/graphics/axes.py which you can examine...
One axis is joined to another, by calling the method
joinToAxis(otherAxis, mode, pos) on the first axis,
with mode and pos being the properties described by
joinAxisMode and joinAxisPos, respectively.
'points' means to use an absolute value, and 'value'
to use a relative value (both indicated by the the
joinAxisPos property) along the axis.
Table - Axes joining properties
11.6 Bar Charts¶
This describes our current VerticalBarChart class, which uses the
axes and labels above.
We think it is step in the right direction but is is
far from final.
Note that people we speak to are divided about 50/50 on whether to
call this a 'Vertical' or 'Horizontal' bar chart.
We chose this name because 'Vertical' appears next to 'Bar', so
we take it to mean that the bars rather than the category axis
are vertical.
As usual, we will start with an example:
# code to produce the above chart
from reportlab.graphics.shapes import Drawing
from reportlab.graphics.charts.barcharts import VerticalBarChart
drawing = Drawing(400, 200)
data = [
(13, 5, 20, 22, 37, 45, 19, 4),
(14, 6, 21, 23, 38, 46, 20, 5)
]
bc = VerticalBarChart()
bc.x = 50
bc.y = 50
bc.height = 125
bc.width = 300
bc.data = data
bc.strokeColor = colors.black
bc.valueAxis.valueMin = 0
bc.valueAxis.valueMax = 50
bc.valueAxis.valueStep = 10
bc.categoryAxis.labels.boxAnchor = 'ne'
bc.categoryAxis.labels.dx = 8
bc.categoryAxis.labels.dy = -2
bc.categoryAxis.labels.angle = 30
bc.categoryAxis.categoryNames = ['Jan-99','Feb-99','Mar-99',
'Apr-99','May-99','Jun-99','Jul-99','Aug-99']
drawing.add(bc)
Most of this code is concerned with setting up the axes and
labels, which we have already covered.
Here are the top-level properties of the VerticalBarChart class:
Table - VerticalBarChart properties
From this table we deduce that adding the following lines to our code
above should double the spacing between bar groups (the groupSpacing
attribute has a default value of five points) and we should also see
some tiny space between bars of the same group (barSpacing).
bc.groupSpacing = 10
bc.barSpacing = 2.5
And, in fact, this is exactly what we can see after adding these lines to the code above. Notice how the width of the individual bars has changed as well. This is because the space added between the bars has to be 'taken' from somewhere as the total chart width stays unchanged.
Bars labels are automatically displayed for negative values below the lower end of the bar for positive values above the upper end of the other ones.
Stacked bars are also supported for vertical bar graphs.
You enable this layout for your chart by setting the style
attribute to 'stacked' on the categoryAxis.
bc.categoryAxis.style = 'stacked'
Here is an example of the previous chart values arranged in the stacked style.
11.7 Line Charts¶
We consider "Line Charts" to be essentially the same as "Bar Charts", but with lines instead of bars. Both share the same pair of Category/Value axes pairs. This is in contrast to "Line Plots", where both axes are Value axes.
The following code and its output shall serve as a simple
example.
More explanation will follow.
For the time being you can also study the output of running
the tool reportlab/lib/graphdocpy.py withough any arguments
and search the generated PDF document for examples of
Line Charts.
from reportlab.graphics.charts.linecharts import HorizontalLineChart
drawing = Drawing(400, 200)
data = [
(13, 5, 20, 22, 37, 45, 19, 4),
(5, 20, 46, 38, 23, 21, 6, 14)
]
lc = HorizontalLineChart()
lc.x = 50
lc.y = 50
lc.height = 125
lc.width = 300
lc.data = data
lc.joinedLines = 1
catNames = 'Jan Feb Mar Apr May Jun Jul Aug'.split(' ')
lc.categoryAxis.categoryNames = catNames
lc.categoryAxis.labels.boxAnchor = 'n'
lc.valueAxis.valueMin = 0
lc.valueAxis.valueMax = 60
lc.valueAxis.valueStep = 15
lc.lines[0].strokeWidth = 2
lc.lines[1].strokeWidth = 1.5
drawing.add(lc)
Table - HorizontalLineChart properties
11.8 Line Plots¶
Below we show a more complex example of a Line Plot that also uses some experimental features like line markers placed at each data point.
from reportlab.graphics.charts.lineplots import LinePlot
from reportlab.graphics.widgets.markers import makeMarker
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.lines[0].symbol = makeMarker('FilledCircle')
lp.lines[1].symbol = makeMarker('Circle')
lp.lineLabelFormat = '%2.0f'
lp.strokeColor = colors.black
lp.xValueAxis.valueMin = 0
lp.xValueAxis.valueMax = 5
lp.xValueAxis.valueSteps = [1, 2, 2.5, 3, 4, 5]
lp.xValueAxis.labelTextFormat = '%2.1f'
lp.yValueAxis.valueMin = 0
lp.yValueAxis.valueMax = 7
lp.yValueAxis.valueSteps = [1, 2, 3, 5, 6]
drawing.add(lp)
Table - LinePlot properties
11.9 Pie Charts¶
As usual, we will start with an example:
from reportlab.graphics.charts.piecharts import Pie
d = Drawing(200, 100)
pc = Pie()
pc.x = 65
pc.y = 15
pc.width = 70
pc.height = 70
pc.data = [10,20,30,40,50,60]
pc.labels = ['a','b','c','d','e','f']
pc.slices.strokeWidth=0.5
pc.slices[3].popout = 10
pc.slices[3].strokeWidth = 2
pc.slices[3].strokeDashArray = [2,2]
pc.slices[3].labelRadius = 1.75
pc.slices[3].fontColor = colors.red
d.add(pc)
Properties are covered below. The pie has a 'slices' collection and we document wedge properties in the same table.
Table - Pie properties
Customizing Labels¶
Each slide label can be customized individually by changing
the properties prefixed by label_ in the collection slices.
For example pc.slices[2].label_angle = 10 changes the angle
of the third label.
Before being able to use these customization properties, you need
to disable simple labels with: pc.simplesLabels = 0
Table - Pie.slices label customization properties
Side Labels¶
If the sideLabels attribute is set to true, then the labels of the slices are placed in two columns, one on either side of the pie and the start angle of the pie will be set automatically. The anchor of the right hand column is set to 'start' and the anchor of the left hand column is set to 'end'. The distance from the edge of the pie from the edge of either column is decided by the sideLabelsOffset attribute, which is a fraction of the width of the pie. If xradius is changed, the pie can overlap the labels, and so we advise leaving xradius as None. There is an example below.
If you have sideLabels set to True, then some of the attributes become redundant, such as pointerLabelMode. Also sideLabelsOffset only changes the piechart if sideLabels is set to true.
Some issues¶
The pointers can cross if there are too many slices.
Also the labels can overlap despite checkLabelOverlap if they correspond to slices that are not adjacent.
11.10 Legends¶
Various preliminary legend classes can be found but need a
cleanup to be consistent with the rest of the charting
model.
Legends are the natural place to specify the colors and line
styles of charts; we propose that each chart is created with
a legend attribute which is invisible.
One would then do the following to specify colors:
myChart.legend.defaultColors = [red, green, blue]
One could also define a group of charts sharing the same legend:
myLegend = Legend()
myLegend.defaultColor = [red, green.....] #yuck!
myLegend.columns = 2
# etc.
chart1.legend = myLegend
chart2.legend = myLegend
chart3.legend = myLegend
TODO: Does this work? Is it an acceptable complication over specifying chart colors directly?
Remaining Issues¶
There are several issues that are almost solved, but for which is is a bit too early to start making them really public. Nevertheless, here is a list of things that are under way:
-
Color specification - right now the chart has an undocumented property
defaultColors, which provides a list of colors to cycle through, such that each data series gets its own color. Right now, if you introduce a legend, you need to make sure it shares the same list of colors. Most likely, this will be replaced with a scheme to specify a kind of legend containing attributes with different values for each data series. This legend can then also be shared by several charts, but need not be visible itself. -
Additional chart types - when the current design will have become more stable, we expect to add variants of bar charts to deal with percentile bars as well as the side-by-side variant seen here.
Outlook¶
It will take some time to deal with the full range of chart types. We expect to finalize bars and pies first and to produce trial implementations of more general plots, thereafter.
X-Y Plots¶
Most other plots involve two value axes and directly plotting
x-y data in some form.
The series can be plotted as lines, marker symbols, both, or
custom graphics such as open-high-low-close graphics.
All share the concepts of scaling and axis/title formatting.
At a certain point, a routine will loop over the data series and
'do something' with the data points at given x-y locations.
Given a basic line plot, it should be very easy to derive a
custom chart type just by overriding a single method - say,
drawSeries().
Marker customisation and custom shapes¶
Well known plotting packages such as excel, Mathematica and Excel offer ranges of marker types to add to charts. We can do better - you can write any kind of chart widget you want and just tell the chart to use it as an example.
Combination plots¶
Combining multiple plot types is really easy. You can just draw several charts (bar, line or whatever) in the same rectangle, suppressing axes as needed. So a chart could correlate a line with Scottish typhoid cases over a 15 year period on the left axis with a set of bars showing inflation rates on the right axis. If anyone can remind us where this example came from we'll attribute it, and happily show the well-known graph as an example.
Interactive editors¶
One principle of the Graphics package is to make all 'interesting' properties of its graphic components accessible and changeable by setting apropriate values of corresponding public attributes. This makes it very tempting to build a tool like a GUI editor that that helps you with doing that interactively.
ReportLab has built such a tool using the Tkinter toolkit that loads pure Python code describing a drawing and records your property editing operations. This "change history" is then used to create code for a subclass of that chart, say, that can be saved and used instantly just like any other chart or as a new starting point for another interactive editing session.
This is still work in progress, though, and the conditions for releasing this need to be further elaborated.
Misc.¶
This has not been an exhaustive look at all the chart classes.
Those classes are constantly being worked on.
To see exactly what is in the current distribution, use the
graphdocpy.py utility.
By default, it will run on reportlab/graphics, and produce a full
report.
(If you want to run it on other modules or packages,
graphdocpy.py -h prints a help message that will tell you
how.)
This is the tool that was mentioned in the section on 'Documenting Widgets'.