source: orange/docs/extend-widgets/rst/graphing.rst @ 11439:2a63a9963207

Revision 11439:2a63a9963207, 6.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 13 months ago (diff)

Small fixes to widget development documentation.

Line 
1###########################
2Graphing and Orange Widgets
3###########################
4
5The most fun widgets are of course those that include graphics. For
6this we either use control called canvas, which is Qt's general
7control for doing any graphics of choice (widgets for tree and heat map
8visualizations, for instance, use this), or use a special control for
9drawing data plots as provided in Qwt library and PyQwt
10interface. Here we look at the latter, and extend our learning curve
11widget with a control that plots the curve.
12
13*****
14Plots
15*****
16
17Let us construct a widget with a following appearance:
18
19.. image:: learningcurve-plot.png
20
21There are two new elements from our previous incarnation of
22a learning curve widget: a control with a list of classifiers, and a
23graph with a plot of learning curves. From a list of classifiers we
24can select those to be displayed in the plot.
25
26The widget still provides learning curve table, but this is now
27offered in a tabbed pane together with a graph. The code for
28definition of the tabbed pane, and initialization of the graph is::
29
30    # start of content (right) area
31    tabs = OWGUI.tabWidget(self.mainArea)
32
33    # graph widget
34    tab = OWGUI.createTabPage(tabs, "Graph")
35    self.graph = OWGraph(tab)
36    self.graph.setAxisAutoScale(QwtPlot.xBottom)
37    self.graph.setAxisAutoScale(QwtPlot.yLeft)
38    tab.layout().addWidget(self.graph)
39    self.setGraphGrid()
40
41:obj:`OWGrap` is a convenience subclass of QwtPlot and is imported from
42OWGraph module. For the graph, we use :obj:`setAxisAutoScale` to
43request that the axis are automatically set in regard to the data that
44is plotted in the graph. We plot the graph in using the following
45code::
46
47    def drawLearningCurve(self, learner):
48        if not self.data: return
49        curve = self.graph.addCurve(learner.name, xData=self.curvePoints, yData=learner.score, autoScale=True)
50
51        learner.curve = curve
52        self.setGraphStyle(learner)
53        self.graph.replot()
54
55This is simple. We store the curve returned from :obj:`addCurve` with a
56learner, and use a trick allowed in Orange that we can simply store
57this as a new attribute to the learning object. By default, Orange
58would give a warning of the type::
59
60    c:\Python23\Lib\site-packages\orange\OrangeWidgets\Test\OWLearningCurveC.py:269:
61     AttributeWarning: 'curve' is not a builtin attribute of 'kNNLearner'
62      setattr(learner, "curve", curve)
63
64but we surpress such warnings with a line::
65
66    warnings.filterwarnings("ignore", ".*builtin attribute.*", orange.AttributeWarning)
67
68
69.. warning::
70
71   This is a very bad design. Please do **not** store widget data in the
72   input objects.
73
74
75in the initialization part of the widget. In this way, each learner
76also stores the current scores, which is a list of numbers to be
77plotted in Qwt graph. The details on how the plot is set are dealt
78with in :obj:`setGraphStyle` function:` ::
79
80    def setGraphStyle(self, learner):
81        curve = learner.curve
82        if self.graphDrawLines:
83            curve.setStyle(QwtPlotCurve.Lines)
84        else:
85            curve.setStyle(QwtPlotCurve.NoCurve)
86        curve.setSymbol(QwtSymbol(QwtSymbol.Ellipse, \
87          QBrush(QColor(0,0,0)), QPen(QColor(0,0,0)),
88          QSize(self.graphPointSize, self.graphPointSize)))
89        curve.setPen(QPen(learner.color, 5))
90
91Notice that the color of the plot line that is specific to the
92learner is stored in its attribute :obj:`color`
93(:obj:`learner.color`). Who sets it and how? This we discuss in
94the following subsection.
95
96************************
97Colors in Orange Widgets
98************************
99
100Uniform assignment of colors across different widget is an
101important issue. When we plot the same data in different widgets, we
102expect that the color we used in a consistent way; for instance data
103instances of one class should be plotted in scatter plot and parallel
104axis plot using the same color. Developers are thus advised to use
105:obj:`ColorPaletteHSV`, which is provided as a method within
106:mod:`OWWidget` module. :obj:`ColorPaletteHSV` takes an
107integer as an attribute, and returns a list of corresponding number of
108colors. In our learning curve widget, we use it within a function that
109sets the list box with learners::
110
111    def updatellb(self):
112        self.blockSelectionChanges = 1
113        self.llb.clear()
114        colors = ColorPaletteHSV(len(self.learners))
115        for (i,lt) in enumerate(self.learners):
116            l = lt[1]
117            item = QListWidgetItem(ColorPixmap(colors[i]), l.name)
118            self.llb.addItem(item)
119            item.setSelected(l.isSelected)
120            l.color = colors[i]
121        self.blockSelectionChanges = 0
122
123The code above sets the items of the list box, where each item
124includes a learner and a small box in learner's color, which is in
125this widget also used as a sort of a legend for the graph. This box is
126returned by :obj:`ColorPixmap` function defined in
127:obj:`OWColorPalette.py`. Else, the classifier's list box control is
128defined in the initialization of the widget using::
129
130    self.cbox = OWGUI.widgetBox(self.controlArea, "Learners")
131    self.llb = OWGUI.listBox(self.cbox, self, "selectedLearners",
132                             selectionMode=QListWidget.MultiSelection,
133                             callback=self.learnerSelectionChanged)
134
135    self.llb.setMinimumHeight(50)
136    self.blockSelectionChanges = 0
137
138Now, what is this :obj:`blockSelectionChanges`? Any time
139user makes a selection change in list box of classifiers, we want to
140invoke the procedure called
141:obj:`learnerSelectionChanged`. But we want to perform
142actions there when changes in the list box are invoked from clicking
143by a user, and not by changing list box items from a program. This is
144why, every time we want :obj:`learnerSelectionChanged` not to
145perform its function, we set :obj:`self.blockSelectionChanges`
146to 1.
147
148In our widget, :obj:`learnerSelectionChanged` figures out
149if any curve should be removed from the graph (the user has just
150deselected the corresponding item in the list box) or added to the
151graph (the user just selected a learner)::
152
153    def learnerSelectionChanged(self):
154        if self.blockSelectionChanges:
155            return
156        for (i,lt) in enumerate(self.learners):
157            l = lt[1]
158            if l.isSelected != (i in self.selectedLearners):
159                if l.isSelected: # learner was deselected
160                    l.curve.detach()
161                else: # learner was selected
162                    self.drawLearningCurve(l)
163                self.graph.replot()
164            l.isSelected = i in self.selectedLearners
165
166The complete code of this widget is available :download:`here <OWLearningCurveC.py>`.
167This is almost like a typical
168widget that is include in a standard Orange distribution, with a
169typical size just under 300 lines. Just some final cosmetics is needed
170to make this widget a standard one, including setting some graph
171properties (like line and point sizes, grid line control, etc.) and
172saving the graph to an output file.
Note: See TracBrowser for help on using the repository browser.