图表
11.3 图表¶
这其中的大部分动机是为了创建一个灵活的图表包。 本节介绍了我们图表模型背后的思想、设计目标以及图表包中已有的组件。
设计目标¶
以下是一些设计目标:
使简单的顶层使用变得非常简单
应该能够以最少的代码行创建一个简单的图表,并通过合理的自动设置使其"做出正确的事情"。 上面的饼图代码片段就做到了这一点。如果一个真正的图表有许多子组件,除非您想自定义它们的行为, 否则您仍然不需要与它们交互。
允许精确定位
在出版和平面设计中,一个绝对的要求是控制每个元素的位置和样式。 我们将尽量提供以固定大小和绘图比例来指定事物的属性,而不是自动调整大小。 因此,当您将 y 轴标签的字体变大时,"内部绘图矩形"不会神奇地改变, 即使这意味着您的标签可能会溢出图表矩形的左边缘。预览图表并选择适合的大小和间距是您的工作。
有些事情确实需要自动化。例如,如果您想将 N 个柱子放入 200 点的空间中, 并且事先不知道 N 的值,我们将柱间距指定为柱宽的百分比而不是点数大小, 让图表自行计算。这仍然是确定性的和可控的。
单独或成组控制子元素
我们使用智能集合类,让您可以自定义一组事物,或者只自定义其中的一个。 例如,您可以在我们的实验性饼图中这样做:
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] 实际上会惰性地创建一个小对象,用于保存有关该切片的信息;
如果在绘制时存在第四个切片,该对象将用于格式化它。
只暴露您应该更改的内容
从统计角度来看,在上面的示例中让您直接调整某个饼图切片的角度是错误的, 因为角度是由数据决定的。因此,并非所有内容都会通过公共属性暴露出来。 可能存在"后门"让您在真正需要时绕过这个限制,或者提供高级功能的方法, 但一般来说属性将是正交的。
基于组合和组件
图表由可重用的子组件构建而成。图例是一个容易理解的例子。 如果您需要一种特殊类型的图例(例如圆形色块),您应该继承标准图例组件。 然后您可以这样做……
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...
……或者创建/修改您自己的图表或绘图类,默认创建其中一个。 这对于时间序列图表也非常相关,因为 x 轴可以有多种样式。
顶层图表类将创建许多这样的组件,然后调用方法或设置私有属性来告诉它们 高度和位置——所有这些都应该自动为您完成并且您无法自定义的内容。 我们正在建模组件应该是什么样子,并将在达成共识后在这里发布它们的 API。
多图表
组件方法的一个推论是您可以创建包含多个图表或自定义数据图形的图。 我们最喜欢的一个目标示例是由用户贡献到我们画廊中的天气预报图; 我们希望使创建此类绘图变得容易,将构建块连接到它们的图例, 并以一致的方式输入数据。
(如果您想查看图像,可以在我们的网站上找到 此处)
概述¶
图表或绘图是放置在绘图上的一个对象;它本身不是绘图。 因此您可以控制它的位置,将多个图表放在同一个绘图上,或添加注释。
图表有两个轴;轴可以是值轴或类别轴。轴又有一个 Labels 属性, 让您可以配置所有文本标签或单独配置每一个。 大多数因图表而异的配置细节都与轴属性或轴标签相关。
对象通过上一节讨论的接口暴露属性;这些都是可选的, 目的是让最终用户配置外观。图表正常工作所必须设置的项以及图表与其组件之间的 必要通信,是通过方法来处理的。
您可以继承任何图表组件并用您的替代品替换原始组件, 前提是您实现了必要的方法和属性。
11.4 标签¶
标签是附加到某个图表元素的文本字符串。 它们用于轴上、标题或轴旁边,或附加到单个数据点上。 标签可以包含换行符,但只能使用一种字体。
标签的文本和"原点"通常由其父对象设置。它们通过方法而不是属性来访问。 因此,X 轴决定每个刻度标签的"参考点"和每个标签的数字或日期文本。 但是,最终用户可以直接设置标签(或标签集合)的属性, 以影响其相对于此原点的位置和所有格式。
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)
在上面的绘图中,标签是相对于绿色圆点定义的。 文本框的东北角应该在原点下方十个点处,并围绕该角旋转 45 度。
目前标签具有以下属性,我们相信这些属性足以满足我们迄今为止见过的所有图表:
表 - 标签属性
要查看更多具有不同属性组合的 Label 对象示例,
请查看 tests 文件夹中的 ReportLab 测试套件,
运行脚本 test_charts_textlabels.py 并查看它生成的 PDF 文档!
11.5 轴¶
我们识别两种基本的轴类型——值轴和类别轴。 两者都有水平和垂直两种变体。 两者都可以被子类化以创建非常特定类型的轴。 例如,如果您在时间序列应用中有复杂的规则来决定显示哪些日期, 或者需要不规则的缩放,您可以覆盖轴并创建一个新的。
轴负责确定从数据坐标到图像坐标的映射;根据图表的请求变换点; 绘制自身及其刻度线、网格线和轴标签。
此绘图显示了两个轴,每种类型一个,它们是直接创建的,没有引用任何图表:
以下是创建它们的代码:
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)
请记住,通常您不需要直接创建轴; 使用标准图表时,它会附带现成的轴。 方法是图表用来配置轴并处理几何关系的方式。 不过,我们将在下面详细讨论它们。 与我们描述的轴正交对应的轴基本上具有相同的属性, 除了那些涉及刻度的属性。
XCategoryAxis 类¶
类别轴实际上没有缩放功能;它只是将自身划分为等大小的区间。
它比值轴更简单。
图表(或程序员)通过 setPosition(x, y, length) 方法设置其位置。
下一阶段是向其展示数据以便它可以配置自身。
这对类别轴来说很简单——它只需计算其中一个数据系列中的数据点数量。
reversed 属性(如果为 1)表示类别应该反转。
绘制绘图时,轴可以通过其 scale() 方法为图表提供帮助,
告诉图表给定类别在页面上的起始和结束位置。
我们还没有看到让人们覆盖类别宽度或位置的需求。
XCategoryAxis 具有以下可编辑属性:
表 - XCategoryAxis 属性
YValueAxis¶
图中的左轴是一个 YValueAxis。 值轴与类别轴的不同之处在于,沿其长度的每个点对应于图表空间中的一个 y 值。 轴的工作是配置自身,并根据请求将 Y 值从图表空间转换为点, 以协助父图表进行绘制。
setPosition(x, y, length) 和 configure(data) 的工作方式与类别轴完全相同。
如果您没有完全指定最大值、最小值和刻度间隔,那么 configure() 会导致轴选择合适的值。
配置完成后,值轴可以使用 scale() 方法将 y 数据值转换为绘图空间。
例如:
>>> 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
>>>
默认情况下,最高数据点与轴的顶部对齐,最低数据点与轴的底部对齐, 轴为其刻度点选择"漂亮的整数"。您可以使用以下属性覆盖这些设置。
表 - YValueAxis 属性
valueSteps 属性让您可以明确指定刻度线的位置,
因此您不必遵循规则间隔。因此,您可以借助几个辅助函数绘制月末和月末日期,
而不需要特殊的时间序列图表类。
以下代码展示了如何创建具有特殊刻度间隔的简单 XValueAxis。
请确保在调用 configure 方法之前设置 valueSteps 属性!
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)
除了这些属性之外,所有轴类都有三个描述如何将两个轴相互连接的属性。
同样,这只有在您定义自己的图表或想要修改使用此类轴的现有图表的外观时才有意义。
这些属性在这里只是简要列出,但您可以在模块
reportlab/graphics/axes.py 中找到大量示例函数供您研究……
一个轴通过在第一个轴上调用方法 joinToAxis(otherAxis, mode, pos) 连接到另一个轴,
其中 mode 和 pos 分别是由 joinAxisMode 和 joinAxisPos 描述的属性。
'points' 表示使用绝对值,'value' 表示使用相对值
(两者都由 joinAxisPos 属性指示)沿轴方向。
表 - 轴连接属性
11.6 柱状图¶
本节介绍当前的 VerticalBarChart 类,它使用了前面介绍的坐标轴和标签。
我们认为这是朝着正确方向迈出的一步,但远非最终版本。
请注意,与我们交流的人大约各占一半,对于应该称其为"垂直"还是"水平"柱状图存在分歧。
我们选择了这个名称,因为"Vertical"出现在"Bar"旁边,
所以我们将其理解为柱条(而非类别轴)是垂直的。
像往常一样,我们从一个示例开始:
# 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)
这段代码的大部分内容都是关于设置坐标轴和标签的,我们已经在前面介绍过了。
以下是 VerticalBarChart 类的顶层属性:
Table - VerticalBarChart properties
从这个表格中我们可以推断,在上面的代码中添加以下行应该将柱条组之间的间距加倍
(groupSpacing 属性的默认值为五个点),我们还应该看到同一组柱条之间有
一些微小的间距(barSpacing)。
bc.groupSpacing = 10
bc.barSpacing = 2.5
事实上,这正是我们在上面的代码中添加这些行后看到的结果。 请注意,单个柱条的宽度也发生了变化。 这是因为柱条之间增加的间距必须从某个地方"取出", 因为图表的总宽度保持不变。
柱条标签会自动显示在负值柱条下端 下方, 以及正值柱条上端 上方。
垂直柱状图也支持堆叠柱条。
您可以通过将 categoryAxis 上的 style 属性设置为 'stacked'
来启用此布局。
bc.categoryAxis.style = 'stacked'
以下是之前图表的数值以堆叠样式排列的示例。
11.7 折线图¶
我们将"折线图"(Line Charts)视为本质上与"柱状图"(Bar Charts)相同, 只是用线条代替了柱条。 两者共享相同的类别轴/值轴对。 这与"折线图"(Line Plots)不同,后者的两个轴都是 值轴。
以下代码及其输出将作为一个简单的示例。
后面会有更多解释。
目前,您也可以运行工具 reportlab/lib/graphdocpy.py(不带任何参数),
并在生成的 PDF 文档中搜索折线图的示例来学习。
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 线图¶
下面我们展示一个更复杂的折线图示例, 该示例还使用了一些实验性功能,例如在每个数据点处放置线标记。
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)
表 - LinePlot 属性
11.9 饼图¶
像往常一样,我们从一个示例开始:
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)
属性在下面介绍。 饼图有一个 'slices' 集合,我们在同一个表格中记录楔形属性。
表 - Pie 属性
自定义标签¶
每个切片标签可以通过修改 slices 集合中
以 label_ 为前缀的属性来单独自定义。
例如,pc.slices[2].label_angle = 10 会更改
第三个标签的角度。
在使用这些自定义属性之前,你需要
通过以下方式禁用简单标签:pc.simplesLabels = 0
表 - Pie.slices 标签自定义属性
侧边标签¶
如果将 sideLabels 属性设为 true,则切片的标签 将被放置在两列中,饼图左右各一列,饼图的起始角度将自动设置。 右列的锚点设置为 'start',左列的锚点设置为 'end'。 饼图边缘到任一列边缘的距离由 sideLabelsOffset 属性决定, 该属性是饼图宽度的一个分数。 如果更改了 xradius,饼图可能会与标签重叠,因此 我们建议将 xradius 保持为 None。 下面是一个示例。
如果你将 sideLabels 设为 True,那么某些属性 将变得多余,例如 pointerLabelMode。 此外,sideLabelsOffset 只有在 sideLabels 设为 true 时才会影响饼图。
一些问题¶
如果切片太多,指针可能会交叉。
此外,尽管设置了 checkLabelOverlap,如果标签 对应于不相邻的切片,标签仍然可能重叠。
11.10 图例¶
目前可以找到各种初步的图例类,但需要进行清理
以与图表模型的其余部分保持一致。
图例是指定图表颜色和线条样式的天然位置;
我们提议每个图表创建时都带有一个不可见的 legend 属性。
然后可以通过以下方式指定颜色:
myChart.legend.defaultColors = [red, green, blue]
也可以定义一组共享同一图例的图表:
myLegend = Legend()
myLegend.defaultColor = [red, green.....] #yuck!
myLegend.columns = 2
# etc.
chart1.legend = myLegend
chart2.legend = myLegend
chart3.legend = myLegend
TODO: 这可行吗?与直接指定图表颜色相比,这种复杂化是否可接受?
遗留问题¶
有几个问题几乎已经解决,但现在将它们 真正公开还为时过早。 不过,以下是正在进行的工作列表:
-
颜色规范 - 目前图表有一个未公开的属性
defaultColors,它提供了一个颜色循环列表, 使得每个数据系列都有自己的颜色。 目前,如果你引入图例,你需要确保它共享 相同的颜色列表。 最有可能的是,这将被替换为一种方案,用于指定 一种包含每个数据系列不同属性值的图例。 这个图例然后也可以被多个图表共享,但本身不需要可见。 -
其他图表类型 - 当当前设计变得更加稳定后, 我们预计会添加条形图的变体,以处理百分比条形图 以及此处展示的并排变体。
展望¶
处理所有图表类型需要一些时间。 我们预计首先完成条形图和饼图,然后 试验实现更通用的图表。
X-Y 图¶
大多数其他图表涉及两个值轴,并以某种形式直接绘制 x-y 数据。
数据系列可以绘制为线条、标记符号、两者兼有,
或自定义图形(如开-高-低-收图形)。
它们都共享缩放和轴/标题格式化的概念。
在某个阶段,一个例程将遍历数据系列,并
在给定的 x-y 位置对数据点"执行某些操作"。
给定一个基本的折线图,只需覆盖一个方法(例如
drawSeries())就可以非常容易地派生出
自定义图表类型。
标记自定义与自定义形状¶
知名的绘图软件包如 Excel、Mathematica 等提供 了各种标记类型以添加到图表中。 我们可以做得更好 - 你可以编写任何你想要的图表控件, 然后告诉图表将其作为标记使用。
组合图表¶
组合多种图表类型非常容易。 你只需在同一个矩形中绘制多个图表(条形图、折线图等), 根据需要抑制坐标轴。 例如,一个图表可以在左侧轴上关联一条表示苏格兰 15 年伤寒病例数的折线,同时在右侧轴上显示一组 表示通货膨胀率的条形图。 如果有人能提醒我们这个示例的出处,我们将 注明来源,并乐意展示这个著名的图表作为示例。
交互式编辑器¶
Graphics 包的一个原则是使其图形组件的所有"有趣" 属性都可以通过设置相应公共属性的值来访问和修改。 这使得构建一个类似 GUI 编辑器的工具变得非常有吸引力, 帮助你交互式地完成这些操作。
ReportLab 使用 Tkinter 工具包构建了这样一个工具, 它可以加载描述绘图的纯 Python 代码,并记录你的 属性编辑操作。 然后,这个"更改历史"被用来创建该图表子类的代码, 可以立即保存并像任何其他图表一样使用,或作为 另一个交互式编辑会话的新起点。
不过,这仍在开发中,发布的条件 还需要进一步明确。
杂项¶
本文并未详尽地介绍所有图表类。
这些类正在不断开发中。
要查看当前发行版中的确切内容,请使用
graphdocpy.py 工具。
默认情况下,它将在 reportlab/graphics 上运行,并生成一份
完整报告。
(如果你想在其他模块或包上运行它,
graphdocpy.py -h 会打印帮助信息,告诉你
如何操作。)
这就是在"记录控件"一节中提到的工具。