#Copyright ReportLab Europe Ltd. 2000-2008
#see license.txt for license details
__version__=''' $Id: graph_widgets.py 3375 2009-01-16 18:23:19Z jonas $ '''
from tools.docco.rl_doc_utils import *
from reportlab.graphics.shapes import *
from reportlab.graphics.widgets import signsandsymbols
heading2("Widgets")
disc("""
We now describe widgets and how they relate to shapes.
Using many examples it is shown how widgets make reusable
graphics components.
""")
heading3("Shapes vs. Widgets")
disc("""Up until now, Drawings have been 'pure data'. There is no code in them
to actually do anything, except assist the programmer in checking and
inspecting the drawing. In fact, that's the cornerstone of the whole
concept and is what lets us achieve portability - a renderer only
needs to implement the primitive shapes.""")
disc("""We want to build reusable graphic objects, including a powerful chart
library. To do this we need to reuse more tangible things than
rectangles and circles. We should be able to write objects for other
to reuse - arrows, gears, text boxes, UML diagram nodes, even fully
fledged charts.""")
disc("""
The Widget standard is a standard built on top of the shapes module.
Anyone can write new widgets, and we can build up libraries of them.
Widgets support the $getProperties()$ and $setProperties()$ methods,
so you can inspect and modify as well as document them in a uniform
way.
""")
bullet("A widget is a reusable shape ")
bullet("""it can be initialized with no arguments
when its $draw()$ method is called it creates a primitive Shape or a
Group to represent itself""")
bullet("""It can have any parameters you want, and they can drive the way it is
drawn""")
bullet("""it has a $demo()$ method which should return an attractively drawn
example of itself in a 200x100 rectangle. This is the cornerstone of
the automatic documentation tools. The $demo()$ method should also have
a well written docstring, since that is printed too!""")
disc("""Widgets run contrary to the idea that a drawing is just a bundle of
shapes; surely they have their own code? The way they work is that a
widget can convert itself to a group of primitive shapes. If some of
its components are themselves widgets, they will get converted too.
This happens automatically during rendering; the renderer will not see
your chart widget, but just a collection of rectangles, lines and
strings. You can also explicitly 'flatten out' a drawing, causing all
widgets to be converted to primitives.""")
heading3("Using a Widget")
disc("""
Let's imagine a simple new widget.
We will use a widget to draw a face, then show how it was implemented.""")
eg("""
>>> from reportlab.lib import colors
>>> from reportlab.graphics import shapes
>>> from reportlab.graphics import widgetbase
>>> from reportlab.graphics import renderPDF
>>> d = shapes.Drawing(200, 100)
>>> f = widgetbase.Face()
>>> f.skinColor = colors.yellow
>>> f.mood = "sad"
>>> d.add(f)
>>> renderPDF.drawToFile(d, 'face.pdf', 'A Face')
""")
from reportlab.graphics import widgetbase
d = Drawing(200, 120)
f = widgetbase.Face()
f.x = 50
f.y = 10
f.skinColor = colors.yellow
f.mood = "sad"
d.add(f)
draw(d, 'A sample widget')
disc("""
Let's see what properties it has available, using the $setProperties()$
method we have seen earlier:
""")
eg("""
>>> f.dumpProperties()
eyeColor = Color(0.00,0.00,1.00)
mood = sad
size = 80
skinColor = Color(1.00,1.00,0.00)
x = 10
y = 10
>>>
""")
disc("""
One thing which seems strange about the above code is that we did not
set the size or position when we made the face.
This is a necessary trade-off to allow a uniform interface for
constructing widgets and documenting them - they cannot require
arguments in their $__init__()$ method.
Instead, they are generally designed to fit in a 200 x 100
window, and you move or resize them by setting properties such as
x, y, width and so on after creation.
""")
disc("""
In addition, a widget always provides a $demo()$ method.
Simple ones like this always do something sensible before setting
properties, but more complex ones like a chart would not have any
data to plot.
The documentation tool calls $demo()$ so that your fancy new chart
class can create a drawing showing what it can do.
""")
disc("""
Here are a handful of simple widgets available in the module
<i>signsandsymbols.py</i>:
""")
from reportlab.graphics.shapes import Drawing
from reportlab.graphics.widgets import signsandsymbols
d = Drawing(230, 230)
ne = signsandsymbols.NoEntry()
ds = signsandsymbols.DangerSign()
fd = signsandsymbols.FloppyDisk()
ns = signsandsymbols.NoSmoking()
ne.x, ne.y = 10, 10
ds.x, ds.y = 120, 10
fd.x, fd.y = 10, 120
ns.x, ns.y = 120, 120
d.add(ne)
d.add(ds)
d.add(fd)
d.add(ns)
draw(d, 'A few samples from signsandsymbols.py')
disc("""
And this is the code needed to generate them as seen in the drawing above:
""")
eg("""
from reportlab.graphics.shapes import Drawing
from reportlab.graphics.widgets import signsandsymbols
d = Drawing(230, 230)
ne = signsandsymbols.NoEntry()
ds = signsandsymbols.DangerSign()
fd = signsandsymbols.FloppyDisk()
ns = signsandsymbols.NoSmoking()
ne.x, ne.y = 10, 10
ds.x, ds.y = 120, 10
fd.x, fd.y = 10, 120
ns.x, ns.y = 120, 120
d.add(ne)
d.add(ds)
d.add(fd)
d.add(ns)
""")
heading3("Compound Widgets")
disc("""Let's imagine a compound widget which draws two faces side by side.
This is easy to build when you have the Face widget.""")
eg("""
>>> tf = widgetbase.TwoFaces()
>>> tf.faceOne.mood
'happy'
>>> tf.faceTwo.mood
'sad'
>>> tf.dumpProperties()
faceOne.eyeColor = Color(0.00,0.00,1.00)
faceOne.mood = happy
faceOne.size = 80
faceOne.skinColor = None
faceOne.x = 10
faceOne.y = 10
faceTwo.eyeColor = Color(0.00,0.00,1.00)
faceTwo.mood = sad
faceTwo.size = 80
faceTwo.skinColor = None
faceTwo.x = 100
faceTwo.y = 10
>>>
""")
disc("""The attributes 'faceOne' and 'faceTwo' are deliberately exposed so you
can get at them directly. There could also be top-level attributes,
but there aren't in this case.""")
heading3("Verifying Widgets")
disc("""The widget designer decides the policy on verification, but by default
they work like shapes - checking every assignment - if the designer
has provided the checking information.""")
heading3("Implementing Widgets")
disc("""We tried to make it as easy to implement widgets as possible. Here's
the code for a Face widget which does not do any type checking:""")
eg("""
class Face(Widget):
\"\"\"This draws a face with two eyes, mouth and nose.\"\"\"
def __init__(self):
self.x = 10
self.y = 10
self.size = 80
self.skinColor = None
self.eyeColor = colors.blue
self.mood = 'happy'
def draw(self):
s = self.size # abbreviate as we will use this a lot
g = shapes.Group()
g.transform = [1,0,0,1,self.x, self.y]
# background
g.add(shapes.Circle(s * 0.5, s * 0.5, s * 0.5,
fillColor=self.skinColor))
# CODE OMITTED TO MAKE MORE SHAPES
return g
""")
disc("""We left out all the code to draw the shapes in this document, but you
can find it in the distribution in $widgetbase.py$.""")
disc("""By default, any attribute without a leading underscore is returned by
setProperties. This is a deliberate policy to encourage consistent
coding conventions.""")
disc("""Once your widget works, you probably want to add support for
verification. This involves adding a dictionary to the class called
$_verifyMap$, which map from attribute names to 'checking functions'.
The $widgetbase.py$ module defines a bunch of checking functions with names
like $isNumber$, $isListOfShapes$ and so on. You can also simply use $None$,
which means that the attribute must be present but can have any type.
And you can and should write your own checking functions. We want to
restrict the "mood" custom attribute to the values "happy", "sad" or
"ok". So we do this:""")
eg("""
class Face(Widget):
\"\"\"This draws a face with two eyes. It exposes a
couple of properties to configure itself and hides
all other details\"\"\"
def checkMood(moodName):
return (moodName in ('happy','sad','ok'))
_verifyMap = {
'x': shapes.isNumber,
'y': shapes.isNumber,
'size': shapes.isNumber,
'skinColor':shapes.isColorOrNone,
'eyeColor': shapes.isColorOrNone,
'mood': checkMood
}
""")
disc("""This checking will be performed on every attribute assignment; or, if
$config.shapeChecking$ is off, whenever you call $myFace.verify()$.""")
heading3("Documenting Widgets")
disc("""
We are working on a generic tool to document any Python package or
module; this is already checked into ReportLab and will be used to
generate a reference for the ReportLab package.
When it encounters widgets, it adds extra sections to the
manual including:""")
bullet("the doc string for your widget class ")
bullet("the code snippet from your <i>demo()</i> method, so people can see how to use it")
bullet("the drawing produced by the <i>demo()</i> method ")
bullet("the property dump for the widget in the drawing. ")
disc("""
This tool will mean that we can have guaranteed up-to-date
documentation on our widgets and charts, both on the web site
and in print; and that you can do the same for your own widgets,
too!
""")
heading3("Widget Design Strategies")
disc("""We could not come up with a consistent architecture for designing
widgets, so we are leaving that problem to the authors! If you do not
like the default verification strategy, or the way
$setProperties/getProperties$ works, you can override them yourself.""")
disc("""For simple widgets it is recommended that you do what we did above:
select non-overlapping properties, initialize every property on
$__init__$ and construct everything when $draw()$ is called. You can
instead have $__setattr__$ hooks and have things updated when certain
attributes are set. Consider a pie chart. If you want to expose the
individual wedges, you might write code like this:""")
eg("""
from reportlab.graphics.charts import piecharts
pc = piecharts.Pie()
pc.defaultColors = [navy, blue, skyblue] #used in rotation
pc.data = [10,30,50,25]
pc.slices[7].strokeWidth = 5
""")
#removed 'pc.backColor = yellow' from above code example
disc("""The last line is problematic as we have only created four wedges - in
fact we might not have created them yet. Does $pc.wedges[7]$ raise an
error? Is it a prescription for what should happen if a seventh wedge
is defined, used to override the default settings? We dump this
problem squarely on the widget author for now, and recommend that you
get a simple one working before exposing 'child objects' whose
existence depends on other properties' values :-)""")
disc("""We also discussed rules by which parent widgets could pass properties
to their children. There seems to be a general desire for a global way
to say that 'all wedges get their lineWidth from the lineWidth of
their parent' without a lot of repetitive coding. We do not have a
universal solution, so again leave that to widget authors. We hope
people will experiment with push-down, pull-down and pattern-matching
approaches and come up with something nice. In the meantime, we
certainly can write monolithic chart widgets which work like the ones
in, say, Visual Basic and Delphi.""")
disc("""For now have a look at the following sample code using an early
version of a pie chart widget and the output it generates:""")
eg("""
from reportlab.lib.colors import *
from reportlab.graphics import shapes,renderPDF
from reportlab.graphics.charts.piecharts import Pie
d = Drawing(400,200)
d.add(String(100,175,"Without labels", textAnchor="middle"))
d.add(String(300,175,"With labels", textAnchor="middle"))
pc = Pie()
pc.x = 25
pc.y = 50
pc.data = [10,20,30,40,50,60]
pc.slices[0].popout = 5
d.add(pc, 'pie1')
pc2 = Pie()
pc2.x = 150
pc2.y = 50
pc2.data = [10,20,30,40,50,60]
pc2.labels = ['a','b','c','d','e','f']
d.add(pc2, 'pie2')
pc3 = Pie()
pc3.x = 275
pc3.y = 50
pc3.data = [10,20,30,40,50,60]
pc3.labels = ['a','b','c','d','e','f']
pc3.wedges.labelRadius = 0.65
pc3.wedges.fontName = "Helvetica-Bold"
pc3.wedges.fontSize = 16
pc3.wedges.fontColor = colors.yellow
d.add(pc3, 'pie3')
""")
# Hack to force a new paragraph before the todo() :-(
disc("")
from reportlab.lib.colors import *
from reportlab.graphics import shapes,renderPDF
from reportlab.graphics.charts.piecharts import Pie
d = Drawing(400,200)
d.add(String(100,175,"Without labels", textAnchor="middle"))
d.add(String(300,175,"With labels", textAnchor="middle"))
pc = Pie()
pc.x = 25
pc.y = 50
pc.data = [10,20,30,40,50,60]
pc.slices[0].popout = 5
d.add(pc, 'pie1')
pc2 = Pie()
pc2.x = 150
pc2.y = 50
pc2.data = [10,20,30,40,50,60]
pc2.labels = ['a','b','c','d','e','f']
d.add(pc2, 'pie2')
pc3 = Pie()
pc3.x = 275
pc3.y = 50
pc3.data = [10,20,30,40,50,60]
pc3.labels = ['a','b','c','d','e','f']
pc3.slices.labelRadius = 0.65
pc3.slices.fontName = "Helvetica-Bold"
pc3.slices.fontSize = 16
pc3.slices.fontColor = colors.yellow
d.add(pc3, 'pie3')
draw(d, 'Some sample Pies')
|