docs/userguide/graph_widgets.py
author robin
Mon, 12 Sep 2022 09:33:45 +0100
changeset 4737 e39e4741759a
parent 4332 b9b7e096842f
permissions -rw-r--r--
nano bugfix in rl_addons/renderPM/_renderPM.c

#Copyright ReportLab Europe Ltd. 2000-2017
#see license.txt for license details
__version__='3.3.0'
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 slices, 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 slices - in
       fact we might not have created them yet. Does $pc.slices[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 slices 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.slices.labelRadius = 0.65
pc3.slices.fontName = "Helvetica-Bold"
pc3.slices.fontSize = 16
pc3.slices.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')