source: orange/orange/OrangeWidgets/Prototypes/OWTimeline.py @ 8042:ffcb93bc9028

Revision 8042:ffcb93bc9028, 27.1 KB checked in by markotoplak, 3 years ago (diff)

Hierarchical clustering: also catch RuntimeError when importing matplotlib (or the documentation could not be built on server).

Line 
1"""<name>Timeline</name>
2<description>Timeline Visualization</description>
3<icon>icons/Timeline.png</icon>
4<priority>30</priority>
5<contact>Janez Demsar (janez.demsar@fri.uni-lj.si)</contact>"""
6
7from OWWidget import *
8from OWGUI import *
9from OWDlgs import OWChooseImageSizeDlg
10import sip
11from math import ceil, log
12from random import Random
13
14class GraphicsViewWithScrollSync(QGraphicsView):
15    def __init__(self, *args, **argkw):
16        QGraphicsView.__init__(self, *args)
17        self.syncVertical = self.syncHorizontal = None
18        if argkw.get("forceScrollBars", True):
19            self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
20            self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
21        self.setAlignment(Qt.AlignLeft | Qt.AlignTop)
22        self.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing)
23               
24    def scrollContentsBy(self, dx, dy):
25        QGraphicsView.scrollContentsBy(self, dx, dy)
26        if dx and self.syncHorizontal:
27            self.syncHorizontal.horizontalScrollBar().setValue(self.horizontalScrollBar().value())
28        elif dy and self.syncVertical:
29            self.syncVertical.verticalScrollBar().setValue(self.verticalScrollBar().value())
30       
31       
32TM_YEAR, TM_MON, TM_DAY, TM_HOUR, TM_MIN, TM_SEC, TM_WDAY, TM_YDAY = range(8)
33
34def sym0(c, x, y):
35    c.addItem(QGraphicsLineItem(x-5, y-5, x+5, y+5))
36    c.addItem(QGraphicsLineItem(x+5, y-5, x-5, y+5))
37def sym1(c, x, y):
38    c.addItem(QGraphicsEllipseItem(x-5, y-5, 10, 10))
39def sym2(c, x, y):
40    c.addItem(QGraphicsLineItem(x-5, y+3.16, x+5, y+3.16))
41    c.addItem(QGraphicsLineItem(x-5, y+3.16, x, y-5.16))
42    c.addItem(QGraphicsLineItem(x, y-5.16, x+5, y+3.16))
43def sym3(c, x, y):
44    c.addItem(QGraphicsLineItem(x-5, y-3.16, x+5, y-3.16))
45    c.addItem(QGraphicsLineItem(x-5, y-3.16, x, y+5.16))
46    c.addItem(QGraphicsLineItem(x, y+5.45, x+5, y-3.16))
47def sym4(c, x, y):
48    c.addItem(QGraphicsLineItem(x-5, y-5, x+5, y-5))
49    c.addItem(QGraphicsLineItem(x+5, y-5, x+5, y+5))
50    c.addItem(QGraphicsLineItem(x+5, y+5, x-5, y+5))
51    c.addItem(QGraphicsLineItem(x-5, y+5, x-5, y-5))
52def sym5(c, x, y):
53    c.addItem(QGraphicsLineItem(x, y-5, x, y+5))
54    c.addItem(QGraphicsLineItem(x-4, y-5, x+4, y-5))
55    c.addItem(QGraphicsLineItem(x-4, y+5, x+4, y+5))
56def sym6(c, x, y):
57    c.addItem(QGraphicsLineItem(x-5, y-4, x+5, y+4))
58    c.addItem(QGraphicsLineItem(x-5, y+4, x+5, y-4))
59    c.addItem(QGraphicsLineItem(x, y+4, x, y-4))
60def sym7(c, x, y):
61    t = QGraphicsEllipseItem(x-6, y-3, 12, 12)
62    t.setSpanAngle(2880)
63    c.addItem(t)
64def sym8(c, x, y):
65    t = QGraphicsEllipseItem(x-6, y-8, 12, 12)
66    t.setStartAngle(2880)
67    t.setSpanAngle(2880)
68    c.addItem(t)
69def sym9(c, x, y):
70    c.addItem(QGraphicsLineItem(x-5, y-5, x, y+5))
71    c.addItem(QGraphicsLineItem(x, y-5, x, y+5))
72    c.addItem(QGraphicsLineItem(x, y-5, x+5, y+5))
73
74_font16 = QFont("", 13)
75def overflowSymbol(c, x, y, sym):
76    t = QGraphicsSimpleTextItem(sym)
77    t.setFont(_font16)
78    b = t.boundingRect()
79    t.setPos(x-b.width()/2, y-b.height()/2)
80    c.addItem(t)
81
82_rr = range(ord("A"), ord("Z")+1) + range(ord("0"), ord("9")+1) + range(ord("a"), ord("z")+1)
83symFunc = [sym0, sym1, sym2, sym3, sym4, sym5, sym6, sym7, sym8, sym9] + [lambda c, x, y, s=chr(s): overflowSymbol(c, x, y, s) for s in _rr]
84
85
86class LegendWindow(QDialog):
87    def __init__(self, legendcanvas):
88        QDialog.__init__(self)
89        self.setWindowTitle("Timeline Legend")
90        self.lay = QVBoxLayout()
91        self.setLayout(self.lay)
92        self.legendview = QGraphicsView(legendcanvas, self)
93        self.lay.addWidget(self.legendview)
94        self.legendview.setAlignment(Qt.AlignLeft | Qt.AlignTop)
95        self.position = None
96     
97    def closeEvent(self, ev):
98        self.position = self.pos()
99       
100    def showEvent(self, ev):
101        if self.position is not None:
102            self.move(self.position)
103
104    def resetBounds(self):
105        brh = int(self.legendview.scene().itemsBoundingRect().height())
106        self.legendview.resize(250, brh)
107        self.updateGeometry()
108        self.resize(280, min(450, brh+40))
109       
110class OWTimeline(OWWidget):
111    settingsList = ["lineDistance", "wrapTime", "normalizeEvent"]
112    contextHandlers = {"": DomainContextHandler("", ["groupAtt", "eventAtt", "timeAtt", "labelAtt", "timeScale", "timeInTicks", "customTimeRange", "jitteringAmount"])}
113
114    def __init__(self, parent=None, signalManager=None, name="Timeline"):
115        OWWidget.__init__(self, parent, signalManager, name)
116        self.inputs = [("Examples", ExampleTable, self.setData)]
117        self.outputs = []
118        self.lineDistance = 40
119        self.timeScale = 2
120        self.eventAtt = 0
121        self.timeAtt = 0
122        self.groupAtt = 0
123        self.labelAtt = 0
124        self.wrapTime = True
125        self.normalizeEvent = True
126        self.timeInTicks = True
127        self.jitteringAmount = 0
128        self.customTimeRange = ""
129#        self.showDensity = False
130        self.loadSettings()
131
132        self.icons = self.createAttributeIconDict()
133        self.data = None
134       
135        self.timeScales = [("Year", 3600*24*365.25, "%Y"), ("Month", 3600*24*30, "%B %Y"), ("Week", 3600*24*7, "%B %Y"),
136                            ("Day", 3600*24, "%B %d, %Y"), ("Hour", 3600, "%D %d, %Y @ %H:%M"),#("Minute", 60, "%D %d, %Y @ %H:%M"), ("Second", 1, "%D %d, %Y @ %H:%M:%S")
137                          ]
138        self.wrapScales = [[(str(i+1), 3600*24*sum([31, 28.25, 31, 30, 31, 30, 31, 31, 30, 31, 30][:i])) for i in range(12)],
139                           [(str(i+1), i*24*3600) for i in range(31)],
140                           [(str(i+1), i*24*3600) for i in range(31)],
141                           [(time.strftime("%a %b %d %H:%M:%S %Y", (0, 0, 0, 0, 0, 0, i, 0, 0)), i*24*3600) for i in range(7)],
142#                           [(time.asctime((0, 0, 0, 0, 0, 0, i, 0, 0)), i*24*3600) for i in range(7)],
143                           [(str(i), i*3600) for i in range(24)],
144                           [(str(i), i*60) for i in range(60)]
145                          ]
146       
147        self.jitteringAmounts = [0, 5, 10, 15, 20]
148               
149        b1 = OWGUI.widgetBox(self.controlArea, "Plotted Data", addSpace=True)
150        self.attrEventCombo = OWGUI.comboBox(b1, self, "eventAtt", label = "Symbol shape / Tick size", callback = self.updateDisplay, sendSelectedValue = True, valueType = str, emptyString="(Ticks)")
151        self.cbNormalize = OWGUI.checkBox(b1, self, "normalizeEvent", label = "Normalize values", callback = self.updateDisplay)
152#        self.cbDensity = OWGUI.checkBox(b1, self, "showDensity", label = "Show density (or average)", callback = self.updateDisplay)
153        OWGUI.separator(b1)
154        self.attrLabelCombo = OWGUI.comboBox(b1, self, "labelAtt", label = "Label", callback = self.updateDisplay, sendSelectedValue = True, valueType = str, emptyString="(None)")
155
156        b1 = OWGUI.widgetBox(self.controlArea, "Grouping", addSpace=True)
157        self.attrGroupCombo = OWGUI.comboBox(b1, self, "groupAtt", label = "Group by", callback = self.updateDisplay, sendSelectedValue = 1, valueType = str, emptyString="(None)", addSpace=True)
158        self.spDistance = OWGUI.spin(b1, self, "lineDistance", label = "Distance between groups", min=30, max=200, step=10, callback = self.updateDisplay, callbackOnReturn=True)
159
160        b1 = OWGUI.widgetBox(self.controlArea, "Time Scale")
161        self.attrTimeCombo = OWGUI.comboBox(b1, self, "timeAtt", label = "Time", callback = self.updateDisplay, sendSelectedValue = 1, valueType = str)
162        OWGUI.checkBox(b1, self, "timeInTicks", label="Time is in ticks (from Jan 1, 1970)", callback = self.updateDisplay)
163        bg = OWGUI.radioButtonsInBox(b1, self, "timeScale", [], label="Approximate amount of data per screen", callback = self.updateDisplay, addSpace=True)
164        self.rbTickedTimes = [OWGUI.appendRadioButton(bg, self, "timeScale", lab[0], callback = self.updateDisplay) for lab in self.timeScales]
165        OWGUI.appendRadioButton(bg, self, "timeScale", "Entire time range", callback = self.updateDisplay)
166        OWGUI.appendRadioButton(bg, self, "timeScale", "Custom", callback = self.updateDisplay)
167        OWGUI.lineEdit(OWGUI.indentedBox(bg), self, "customTimeRange", callback=self.updateDisplay, enterPlaceholder=True, validator=QDoubleValidator(self.controlArea))
168        OWGUI.separator(b1)
169        self.cbAggregate = OWGUI.checkBox(b1, self, "wrapTime", "Aggregate data within time scale", callback = self.updateDisplay)
170        OWGUI.separator(b1)
171        OWGUI.comboBox(b1, self, "jitteringAmount", label="Jittering", orientation=0, items = [("%i px" % i)if i else "None" for i in self.jitteringAmounts], callback=self.updateDisplay)
172       
173        OWGUI.rubber(self.controlArea)
174       
175        sip.delete(self.mainArea.layout())
176        self.layout = QGridLayout(self.mainArea)
177        self.layout.setHorizontalSpacing(2)
178        self.layout.setVerticalSpacing(2)
179
180        self.legendbox = OWGUI.widgetBox(self.mainArea, "Legend", addToLayout = False)
181        self.legendbox.setFixedHeight(80)
182        self.legendbox.setVisible(False)
183        self.mainArea.layout().addWidget(self.legendbox, 0, 0)
184
185        self.legendcanvas = QGraphicsScene(self)
186        self.legendview = QGraphicsView(self.legendcanvas, self.mainArea)
187        self.legendview.setAlignment(Qt.AlignLeft | Qt.AlignTop)
188        self.legendview.setSizePolicy(QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding))
189        self.legendview.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing)
190        self.legendbox.layout().addWidget(self.legendview)
191        self.legendWindow = LegendWindow(self.legendcanvas)
192       
193        self.legendLabel = OWGUI.widgetLabel(self.legendbox, "")
194        self.legendButton = OWGUI.button(self.legendbox, self, "Show Legend", callback=self.showLegend)
195
196        self.canvas = QGraphicsScene(self)
197        self.canvasview = GraphicsViewWithScrollSync(self.canvas, self.mainArea)
198        self.mainArea.layout().addWidget(self.canvasview, 1, 1)
199
200        self.groupscanvas = QGraphicsScene(self)
201        self.groupsview = GraphicsViewWithScrollSync(self.groupscanvas, self.mainArea)
202        self.mainArea.layout().addWidget(self.groupsview, 1, 0)
203        self.groupsview.setLayoutDirection(Qt.RightToLeft)
204       
205        self.timecanvas = QGraphicsScene(self)
206        self.timeview = GraphicsViewWithScrollSync(self.timecanvas, self.mainArea)
207        self.timeview.setFixedHeight(80)
208        self.mainArea.layout().addWidget(self.timeview, 0, 1)
209       
210        self.timeview.syncHorizontal = self.canvasview
211        self.canvasview.syncHorizontal = self.timeview
212        self.groupsview.syncVertical = self.canvasview
213        self.canvasview.syncVertical = self.groupsview
214
215        self.nSymbols = len(symFunc)
216                                       
217        self.resize(qApp.desktop().screenGeometry(self).width()-30, 600)
218   
219    def setData(self, data):
220        self.closeContext()
221        self.attrGroupCombo.clear()
222        self.attrEventCombo.clear()
223        self.attrTimeCombo.clear()
224        self.attrLabelCombo.clear()
225        self.data = data
226        if self.data:
227            discAttrs = [att for att in data.domain if att.varType==orange.VarTypes.Discrete]
228            contAttrs = [att for att in data.domain if att.varType==orange.VarTypes.Continuous]
229            if not discAttrs or not contAttrs:
230                self.error("The data needs to have at least one discrete and one continuous attribute")
231                self.data = None
232            else:
233                self.error("")
234                self.basStat = orange.DomainBasicAttrStat(data)
235                self.attrGroupCombo.addItem("(None)")
236                if discAttrs:
237                    for att in discAttrs:
238                        self.attrGroupCombo.addItem(self.icons[att.varType], att.name)
239                    self.groupAtt = discAttrs[0].name
240                else:
241                    self.groupAtt = ""
242
243                satt = None
244                for att, bas in zip(data.domain, self.basStat):
245                    self.attrTimeCombo.addItem(self.icons[att.varType], att.name)
246                    if not satt and bas and 946652400 <= bas.min <= bas.max <= 1577804400:
247                        satt = att
248                self.timeAtt = satt.name if satt else contAttrs[0].name
249       
250                self.attrEventCombo.addItem("(Ticks)")
251                self.attrLabelCombo.addItem("(None)")
252                overvalued = False
253                for att in data.domain:
254                    self.attrLabelCombo.addItem(self.icons[att.varType], att.name)
255                    if att.varType==orange.VarTypes.Discrete and len(att.values) > len(symFunc):
256                        overvalued = True
257                    elif att.varType in [orange.VarTypes.Discrete, orange.VarTypes.Continuous]:
258                        self.attrEventCombo.addItem(self.icons[att.varType], att.name)
259                if overvalued:
260                    self.warning(1, "Discrete features with more than %i different values are omitted\nfrom the list of possible symbol shapes." % len(symFunc))
261                else:
262                    self.warning(1, "")
263                self.eventAtt = self.labelAtt = ""
264       
265                self.openContext("", self.data)
266        self.updateDisplay()
267
268    def showLegend(self):
269        self.legendWindow.show()
270       
271    def closeEvent(self, ev):
272        self.legendWindow.close()
273   
274    def makeConsistent(self):
275        for tt in self.rbTickedTimes:
276            tt.setDisabled(not self.timeInTicks)
277        if not self.timeInTicks:
278            self.wrapTime = False 
279            if self.timeScale < len(self.timeScales):
280                self.timeScale = len(self.timeScales)
281        self.cbAggregate.setDisabled(self.timeScale>=len(self.timeScales))
282        if self.timeScale >= len(self.timeScales):
283            self.wrapTime = False 
284            if self.timeScale > len(self.timeScales) and not self.customTimeRange and self.data:
285                timePos = self.data.domain.index(self.timeAtt)
286                bas = self.basStat[timePos]
287                self.customTimeRange = str((bas.max-bas.min) or 1)
288
289    def updateDisplay(self):
290        self.canvas.clear()
291        self.timecanvas.clear()
292        self.groupscanvas.clear() 
293        self.legendcanvas.clear() 
294        if not self.data:
295            return
296
297        self.makeConsistent()
298       
299        font16 = QFont("", 13)
300        font12 = QFont("", 10)
301        font10 = QFont("", 8)
302       
303        timePos = self.data.domain.index(self.timeAtt)
304        if self.wrapTime:
305            timespan = self.timeScales[self.timeScale][1]
306            xFactor = 1000. / timespan
307        else: # ticks-based non-custom scale
308            bas = self.basStat[timePos]
309            minTime, maxTime = bas.min, bas.max
310            timespan = maxTime - minTime
311            if self.timeScale > len(self.timeScales):
312                xFactor = 1000. / (float(self.customTimeRange) or 1)
313            elif self.timeScale == len(self.timeScales):
314                xFactor = 1000. / timespan
315            else:
316                xFactor = 1000. / self.timeScales[self.timeScale][1] 
317           
318        if timespan*xFactor > 120000:
319            t = QGraphicsSimpleTextItem("Graph is too wide. Decrease the scale by choosing a larger time interval.")
320            t.setFont(font12)
321            t.setPos(10, 10)
322            self.canvas.addItem(t)
323            return
324       
325        jitter = self.jitteringAmounts[self.jitteringAmount]
326        randint = Random(0).randint
327               
328        self.setCursor(Qt.WaitCursor)
329        plotTicks = not self.eventAtt
330        if plotTicks:
331            self.cbNormalize.setDisabled(True)
332            self.legendbox.setVisible(False)
333            self.legendWindow.close()
334        else:
335            eventAttr = self.data.domain[self.eventAtt]
336            eventPos = self.data.domain.index(eventAttr)
337            discreteEvent = eventAttr.varType == orange.VarTypes.Discrete
338            self.cbNormalize.setDisabled(discreteEvent)
339            if discreteEvent:
340                self.legendbox.setVisible(True)
341                nEvents = len(eventAttr.values)
342                self.legendview.setVisible(nEvents <= 2)
343                self.legendButton.setVisible(nEvents > 2)
344                self.legendLabel.setVisible(nEvents > 2)
345                self.legendLabel.setText("There are %i different symbols" % nEvents)
346                if nEvents <= 2:
347                    self.legendWindow.close()
348                for i, val in enumerate(eventAttr.values):
349                    t = QGraphicsSimpleTextItem(str(val).decode("utf-8"))
350                    t.setFont(font12)
351                    if i < self.nSymbols:
352                        symFunc[i](self.legendcanvas, 0, 20*i+t.boundingRect().height()/2)
353                    else:
354                        overflowSymbol(self.legendcanvas, 0, 20*i, i)
355                    t.setPos(15, 20*i)
356                    self.legendcanvas.addItem(t)
357                br = self.legendcanvas.itemsBoundingRect()
358                self.legendcanvas.setSceneRect(-10,-5, br.width(), br.height())
359                self.legendWindow.resetBounds()
360            else:
361                self.legendbox.setVisible(False)
362                self.legendWindow.close()
363                bas = self.basStat[eventPos]
364                minCls, maxCls = bas.min, bas.max
365                if self.normalizeEvent:
366                    yFactor = (self.lineDistance-5) / ((maxCls-minCls) or 1)
367                else:
368                    yFactor = (self.lineDistance-5) / (max(abs(maxCls), abs(minCls)) or 1)
369
370        grouped = bool(self.groupAtt)
371        self.groupsview.setVisible(grouped)
372        if grouped:
373            groupPos = self.data.domain.index(self.groupAtt)
374            groupAttr = self.data.domain[groupPos]
375            filt = orange.Filter_sameValue(position=self.data.domain.index(groupAttr))
376            groupValues = groupAttr.values
377        else:
378            groupValues = [""]
379
380        labeled = bool(self.labelAtt)
381        if labeled:
382            labelPos = self.data.domain.index(self.labelAtt)
383            labelOff = -20 if plotTicks else (-22 if discreteEvent else 4) 
384
385        filt_unk = orange.Filter_isDefined(domain = self.data.domain)
386        filt_unk.check = [(i==timePos) or (not plotTicks and i==eventPos) or (grouped and i==groupPos) for i in range(len(filt_unk.check))]
387        data = filt_unk(self.data)
388        if len(data) != len(self.data):
389            self.warning(2, "Instances with unknown values of plotted data were removed.")
390        else:
391            self.warning(2, "")
392        maxw = 30
393        for index, group in enumerate(groupValues):
394#            if self.showDensity:
395#                density = [[] for i in eventAttr.values] if (discreteEvent and not plotTicks) else []
396            y = self.lineDistance * (1 + index)
397            self.canvas.addItem(QGraphicsLineItem(-15, y, xFactor*timespan + 15, y))
398            if grouped:
399                filt.value = index
400                thisAxis = data.filterref(filt)
401                t = QGraphicsSimpleTextItem(group.decode("utf-8"))
402                t.setFont(font12)
403                brect = t.boundingRect()
404                t.setPos(-10 - brect.width(), y - brect.height()/2)
405                self.groupscanvas.addItem(t)
406                maxw = max(maxw, brect.width())
407            else:
408                thisAxis = data
409
410            for ex in thisAxis:
411                if self.wrapTime:
412                    timetup = time.localtime(float(ex[timePos]))
413                    if self.timeScale == 0:
414                        x = xFactor * (86400*(timetup[TM_YDAY] + (timetup[TM_MON]>2 and timetup[TM_YEAR]%4 != 0)) + 3600*timetup[3] + 60*timetup[4] + timetup[5])
415                    elif self.timeScale == 1:
416                        x = xFactor * (86400*timetup[TM_DAY] + 3600*timetup[TM_HOUR] + 60*timetup[TM_MIN] + timetup[TM_SEC])
417                    elif self.timeScale == 2:
418                        x = xFactor * (86400*timetup[TM_WDAY] + 3600*timetup[TM_HOUR] + 60*timetup[TM_MIN] + timetup[TM_SEC])
419                    elif self.timeScale == 3:
420                        x = xFactor * (3600*timetup[TM_HOUR] + 60*timetup[TM_MIN] + timetup[TM_SEC])
421                    elif self.timeScale == 4:
422                        x = xFactor * (60*timetup[TM_MIN] + timetup[TM_SEC])
423                else:
424                    x = xFactor * (float(ex[timePos]) - minTime)
425
426                rx = x
427                if jitter:
428                    x += randint(-jitter, jitter)
429                   
430                if labeled:
431                    t = QGraphicsSimpleTextItem(str(ex[labelPos]).decode("utf-8"))
432                    t.setFont(font10)
433                    t.setPos(x - t.boundingRect().width()/2, y + labelOff)
434                    self.canvas.addItem(t)
435                   
436                if plotTicks:
437                    self.canvas.addItem(QGraphicsLineItem(x, y-3, x, y+3))
438#                    if self.showDensity:
439#                        density.append(rx)
440                elif discreteEvent:
441                    clsi = int(ex[eventPos])
442                    if clsi < self.nSymbols:
443                        symFunc[clsi](self.canvas, x, y)
444                    else:
445                        self.overflowSymbol(x, y, clsi)
446#                    if self.showDensity:
447#                        density[clsi].append(rx)
448                else:
449                    l = yFactor*(float(ex[eventPos])-minCls) if self.normalizeEvent else yFactor*float(ex[eventPos]) 
450                    self.canvas.addItem(QGraphicsLineItem(x, y, x, y-l))
451#                    if self.showDensity:
452#                        density.append((rx, l))
453                       
454#            if self.showDensity:
455#                if not discreteEvent or plotTicks:
456#                    import statc
457#                    loessCurve = statc.loess(sorted(density), 100, 0.2, 1)
458#                    q = QPolygonF()
459#                    for i, (x, l, foo) in enumerate(loessCurve):
460#                        q.insert(i, QPointF(x, y-l))
461#                    q.insert(i+1, QPointF(x, y))
462#                    q.insert(0, QPointF(loessCurve[0][0], y))
463#                    self.canvas.addItem(QGraphicsPolygonItem(q))
464
465        if self.timeScale >= len(self.timeScales):
466            if self.timeScale == len(self.timeScales):
467                cr = timespan
468            else:
469                cr = float(self.customTimeRange)
470            if cr:
471                decs = max(0, 2 + ceil(-log(cr)))
472                step = 10**decs
473                mt = round(minTime, decs)
474                while mt <= maxTime:
475                    t = QGraphicsSimpleTextItem("%.*f" % (decs, mt))
476                    t.setFont(font12)
477                    t.setPos(xFactor*(mt-minTime), 30)
478                    self.timecanvas.addItem(t)
479                    mt += cr/10                           
480        elif self.wrapTime:
481            for label, pos in self.wrapScales[self.timeScale]:
482                t = QGraphicsSimpleTextItem(label)
483                t.setFont(font12)
484                t.setPos(xFactor*pos, 30)
485                self.timecanvas.addItem(t)
486        else:
487            try:
488                realScale = self.timeScale if self.timeScale < 2 else self.timeScale-1
489                timeFmt = self.timeScales[self.timeScale][2]
490                mintup = time.localtime(minTime)
491                maxtup = time.localtime(maxTime)
492                starttime = time.mktime(mintup[:realScale+2] + ((0,)*9)[realScale+2:])
493                stoptime = time.mktime(maxtup[:realScale+2] + (0,0, 31, 24, 60, 60, 0, 0, 0)[realScale+2:])
494                prevReal = None
495                while starttime < stoptime:
496                    x = xFactor*(starttime-minTime)
497                   
498                    bk = time.localtime(starttime)
499                    if bk[realScale] != prevReal:
500                        t = QGraphicsSimpleTextItem(time.strftime(timeFmt, bk))
501                        t.setFont(font12)
502                        t.font().setBold(True)
503                        t.setPos(x, 0)
504                        self.timecanvas.addItem(t)
505                        self.timecanvas.addItem(QGraphicsLineItem(x-8, 0, x-8, 50))
506                       
507                    t = QGraphicsSimpleTextItem(str(bk[realScale+1]))
508                    t.setPos(x, 25)
509                    self.timecanvas.addItem(t)
510                    prevReal = bk[realScale]
511                    starttime = time.mktime(bk[:realScale+1] + (bk[realScale+1]+1, ) + bk[realScale+2:])
512            except:
513                self.timecanvas.clear()
514                t = QGraphicsSimpleTextItem("Error occurred while converting ticks to dates. Please verify that the time indeed represents valid ticks in seconds from January 1 1970.")
515                t.setFont(font12)
516                t.setPos(10, 10)
517                self.timecanvas.addItem(t)
518
519        swidth, sheight = 40+xFactor*timespan, self.lineDistance*(len(groupValues)+.5) 
520        self.groupscanvas.setSceneRect(-maxw-20, 0, maxw+20, sheight)
521        self.groupsview.setFixedWidth(min(250, maxw)+45)
522        if not plotTicks and discreteEvent:
523            self.legendbox.setFixedWidth(min(250, maxw)+45)
524        self.canvas.setSceneRect(-20, 0, swidth, sheight)
525        self.timecanvas.setSceneRect(-20, -5, swidth, 60)
526        self.canvasview.resetMatrix()
527        self.groupsview.resetMatrix()
528        self.timeview.resetMatrix()
529        self.setCursor(Qt.ArrowCursor)
530
531    def sendReport(self):
532        if not self.data:
533            return
534
535        self.reportData(self.data)
536        self.reportSettings("Visualization",
537                    [("Symbol shape/size", self.eventAtt),
538                     ("Normalized", OWGUI.YesNo[self.normalizeEvent]),
539                     ("Label", self.labelAtt),
540                     ("Grouping", self.groupAtt),
541                     ("Time", self.timeAtt),
542                     ("Time scale", self.timeScales[self.timeScale][0] if self.timeScale<len(self.timeScales) else ("Custom, "+self.customTimeRange if self.timeScale>len(self.timeScales) else "Entire range")),
543                     ("Time jittering", "%i pixels" % self.jitteringAmounts[self.jitteringAmount] if self.jitteringAmount else "None"),
544                     ("Aggregate data within time scale", OWGUI.YesNo[self.wrapTime]),
545                     ])
546
547        self.reportSection("Timeline")
548        groups, times, canvas = self.groupscanvas, self.timecanvas, self.canvas
549        if canvas.width()+groups.width() > 10000:
550            self.reportRaw("<p>Figure is too wide for the report.</p>")
551            return
552           
553        if self.eventAtt and self.data.domain[self.eventAtt].varType == orange.VarTypes.Discrete:
554            self.reportImage(lambda *x: OWChooseImageSizeDlg(self.legendcanvas).saveImage(*x))
555
556        painter = QPainter()
557        buffer = QPixmap(groups.width()+canvas.width(), times.height()+canvas.height())
558        painter.begin(buffer)
559        painter.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing)
560        painter.fillRect(buffer.rect(), QBrush(QColor(255, 255, 255)))
561        groups.render(painter, QRectF(0, times.height(), groups.width(), groups.height()))
562        times.render(painter, QRectF(groups.width(), 0, times.width(), times.height()))
563        canvas.render(painter, QRectF(groups.width(), times.height(), canvas.width(), canvas.height()))
564        painter.end()
565        self.reportImage(lambda filename: buffer.save(filename, os.path.splitext(filename)[1][1:]))
Note: See TracBrowser for help on using the repository browser.