]>
Commit | Line | Data |
---|---|---|
be05b434 RD |
1 | import wx |
2 | import math | |
3 | import random | |
4 | ||
5 | class RadarGraph(wx.Window): | |
6 | """ | |
7 | A simple radar graph that plots a collection of values in the | |
8 | range of 0-100 onto a polar coordinate system designed to easily | |
9 | show outliers, etc. You might use this kind of graph to monitor | |
10 | some sort of resource allocation metrics, and a quick glance at | |
11 | the graph can tell you when conditions are good (within some | |
12 | accepted tolerance level) or approaching critical levels (total | |
13 | resource consumption). | |
14 | """ | |
15 | def __init__(self, parent, title, labels): | |
16 | wx.Window.__init__(self, parent) | |
17 | self.title = title | |
18 | self.labels = labels | |
19 | self.data = [0.0] * len(labels) | |
20 | self.titleFont = wx.Font(14, wx.SWISS, wx.NORMAL, wx.BOLD) | |
21 | self.labelFont = wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL) | |
22 | ||
23 | self.InitBuffer() | |
24 | ||
25 | self.Bind(wx.EVT_SIZE, self.OnSize) | |
26 | self.Bind(wx.EVT_PAINT, self.OnPaint) | |
27 | ||
28 | ||
29 | def OnSize(self, evt): | |
30 | # When the window size changes we need a new buffer. | |
31 | self.InitBuffer() | |
32 | ||
33 | ||
34 | def OnPaint(self, evt): | |
35 | # This automatically Blits self.buffer to a wx.PaintDC when | |
36 | # the dc is destroyed, and so nothing else needs done. | |
37 | dc = wx.BufferedPaintDC(self, self.buffer) | |
38 | ||
39 | ||
40 | def InitBuffer(self): | |
41 | # Create the buffer bitmap to be the same size as the window, | |
42 | # then draw our graph to it. Since we use wx.BufferedDC | |
43 | # whatever is drawn to the buffer is also drawn to the window. | |
44 | w, h = self.GetClientSize() | |
45 | self.buffer = wx.EmptyBitmap(w, h) | |
46 | dc = wx.BufferedDC(wx.ClientDC(self), self.buffer) | |
47 | self.DrawGraph(dc) | |
48 | ||
49 | ||
50 | def GetData(self): | |
51 | return self.data | |
52 | ||
53 | def SetData(self, newData): | |
54 | assert len(newData) == len(self.data) | |
55 | self.data = newData[:] | |
56 | ||
57 | # The data has changed, so update the buffer and the window | |
58 | dc = wx.BufferedDC(wx.ClientDC(self), self.buffer) | |
59 | self.DrawGraph(dc) | |
60 | ||
61 | ||
62 | def PolarToCartesian(self, radius, angle, cx, cy): | |
63 | x = radius * math.cos(math.radians(angle)) | |
64 | y = radius * math.sin(math.radians(angle)) | |
65 | return (cx+x, cy-y) | |
66 | ||
67 | ||
68 | def DrawGraph(self, dc): | |
69 | spacer = 10 | |
70 | scaledmax = 150.0 | |
71 | ||
72 | dc.SetBackground(wx.Brush(self.GetBackgroundColour())) | |
73 | dc.Clear() | |
74 | dw, dh = dc.GetSize() | |
75 | ||
76 | # Find out where to draw the title and do it | |
77 | dc.SetFont(self.titleFont) | |
78 | tw, th = dc.GetTextExtent(self.title) | |
79 | dc.DrawText(self.title, (dw-tw)/2, spacer) | |
80 | ||
81 | # find the center of the space below the title | |
82 | th = th + 2*spacer | |
83 | cx = dw/2 | |
84 | cy = (dh-th)/2 + th | |
85 | ||
86 | # calculate a scale factor to use for drawing the graph based | |
87 | # on the minimum available width or height | |
88 | mindim = min(cx, (dh-th)/2) | |
89 | scale = mindim/scaledmax | |
90 | ||
91 | # draw the graph axis and "bulls-eye" with rings at scaled 25, | |
92 | # 50, 75 and 100 positions | |
93 | dc.SetPen(wx.Pen("black", 1)) | |
94 | dc.SetBrush(wx.TRANSPARENT_BRUSH) | |
95 | dc.DrawCircle(cx,cy, 25*scale) | |
96 | dc.DrawCircle(cx,cy, 50*scale) | |
97 | dc.DrawCircle(cx,cy, 75*scale) | |
98 | dc.DrawCircle(cx,cy, 100*scale) | |
99 | ||
100 | dc.SetPen(wx.Pen("black", 2)) | |
101 | dc.DrawLine(cx-110*scale, cy, cx+110*scale, cy) | |
102 | dc.DrawLine(cx, cy-110*scale, cx, cy+110*scale) | |
103 | ||
104 | # Now find the coordinates for each data point, draw the | |
105 | # labels, and find the max data point | |
106 | dc.SetFont(self.labelFont) | |
107 | maxval = 0 | |
108 | angle = 0 | |
109 | polypoints = [] | |
110 | for i, label in enumerate(self.labels): | |
111 | val = self.data[i] | |
112 | point = self.PolarToCartesian(val*scale, angle, cx, cy) | |
113 | polypoints.append(point) | |
114 | x, y = self.PolarToCartesian(125*scale, angle, cx,cy) | |
115 | dc.DrawText(label, x, y) | |
116 | if val > maxval: | |
117 | maxval = val | |
118 | angle = angle + 360/len(self.labels) | |
119 | ||
120 | # Set the brush color based on the max value (green is good, | |
121 | # red is bad) | |
122 | c = "forest green" | |
123 | if maxval > 70: | |
124 | c = "yellow" | |
125 | if maxval > 95: | |
126 | c = "red" | |
127 | ||
128 | # Finally, draw the plot data as a filled polygon | |
129 | dc.SetBrush(wx.Brush(c)) | |
130 | dc.SetPen(wx.Pen("navy", 3)) | |
131 | dc.DrawPolygon(polypoints) | |
132 | ||
133 | ||
134 | ||
135 | class TestFrame(wx.Frame): | |
136 | def __init__(self): | |
137 | wx.Frame.__init__(self, None, title="Double Buffered Drawing", | |
138 | size=(480,480)) | |
139 | self.plot = RadarGraph(self, "Sample 'Radar' Plot", | |
140 | ["A", "B", "C", "D", "E", "F", "G", "H"]) | |
141 | ||
142 | # Set some random initial data values | |
143 | data = [] | |
144 | for d in self.plot.GetData(): | |
145 | data.append(random.randint(0, 75)) | |
146 | self.plot.SetData(data) | |
147 | ||
148 | # Create a timer to update the data values | |
149 | self.Bind(wx.EVT_TIMER, self.OnTimeout) | |
150 | self.timer = wx.Timer(self) | |
151 | self.timer.Start(500) | |
152 | ||
153 | ||
154 | def OnTimeout(self, evt): | |
155 | # simulate the positive or negative growth of each data value | |
156 | data = [] | |
157 | for d in self.plot.GetData(): | |
158 | val = d + random.uniform(-5, 5) | |
159 | if val < 0: | |
160 | val = 0 | |
161 | if val > 110: | |
162 | val = 110 | |
163 | data.append(val) | |
164 | self.plot.SetData(data) | |
165 | ||
166 | ||
167 | app = wx.PySimpleApp() | |
168 | frm = TestFrame() | |
169 | frm.Show() | |
170 | app.MainLoop() |